Completed
Push — sync/georgestephanis/r157268-w... ( c36fea )
by George
09:38
created

Grunion_Contact_Form_Plugin::__construct()   D

Complexity

Conditions 10
Paths 16

Size

Total Lines 102
Code Lines 63

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 10
eloc 63
nc 16
nop 0
dl 0
loc 102
rs 4.8196
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
/*
4
Plugin Name: Grunion Contact Form
5
Description: Add a contact form to any post, page or text widget.  Emails will be sent to the post's author by default, or any email address you choose.  As seen on WordPress.com.
6
Plugin URI: http://automattic.com/#
7
AUthor: Automattic, Inc.
8
Author URI: http://automattic.com/
9
Version: 2.4
10
License: GPLv2 or later
11
*/
12
13
define( 'GRUNION_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
14
define( 'GRUNION_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
15
16
if ( is_admin() ) {
17
	require_once GRUNION_PLUGIN_DIR . 'admin.php';
18
}
19
20
add_action( 'rest_api_init', 'grunion_contact_form_require_endpoint' );
21
function grunion_contact_form_require_endpoint() {
22
	require_once GRUNION_PLUGIN_DIR . 'class-grunion-contact-form-endpoint.php';
23
}
24
25
/**
26
 * Sets up various actions, filters, post types, post statuses, shortcodes.
27
 */
28
class Grunion_Contact_Form_Plugin {
29
30
	/**
31
	 * @var string The Widget ID of the widget currently being processed.  Used to build the unique contact-form ID for forms embedded in widgets.
32
	 */
33
	public $current_widget_id;
34
35
	static $using_contact_form_field = false;
0 ignored issues
show
Coding Style introduced by
The visibility should be declared for property $using_contact_form_field.

The PSR-2 coding standard requires that all properties in a class have their visibility explicitly declared. If you declare a property using

class A {
    var $property;
}

the property is implicitly global.

To learn more about the PSR-2, please see the PHP-FIG site on the PSR-2.

Loading history...
36
37
	static function init() {
38
		static $instance = false;
39
40
		if ( ! $instance ) {
41
			$instance = new Grunion_Contact_Form_Plugin;
42
43
			// Schedule our daily cleanup
44
			add_action( 'wp_scheduled_delete', array( $instance, 'daily_akismet_meta_cleanup' ) );
45
		}
46
47
		return $instance;
48
	}
49
50
	/**
51
	 * Runs daily to clean up spam detection metadata after 15 days.  Keeps your DB squeaky clean.
52
	 */
53
	public function daily_akismet_meta_cleanup() {
54
		global $wpdb;
55
56
		$feedback_ids = $wpdb->get_col( "SELECT p.ID FROM {$wpdb->posts} as p INNER JOIN {$wpdb->postmeta} as m on m.post_id = p.ID WHERE p.post_type = 'feedback' AND m.meta_key = '_feedback_akismet_values' AND DATE_SUB(NOW(), INTERVAL 15 DAY) > p.post_date_gmt LIMIT 10000" );
57
58
		if ( empty( $feedback_ids ) ) {
59
			return;
60
		}
61
62
		foreach ( $feedback_ids as $feedback_id ) {
63
			delete_post_meta( $feedback_id, '_feedback_akismet_values' );
64
		}
65
	}
66
67
	/**
68
	 * Strips HTML tags from input.  Output is NOT HTML safe.
69
	 *
70
	 * @param mixed $data_with_tags
71
	 * @return mixed
72
	 */
73
	public static function strip_tags( $data_with_tags ) {
74
		if ( is_array( $data_with_tags ) ) {
75
			foreach ( $data_with_tags as $index => $value ) {
76
				$index = sanitize_text_field( strval( $index ) );
77
				$value = wp_kses( strval( $value ), array() );
78
				$value = str_replace( '&amp;', '&', $value ); // undo damage done by wp_kses_normalize_entities()
79
80
				$data_without_tags[ $index ] = $value;
0 ignored issues
show
Coding Style Comprehensibility introduced by
$data_without_tags was never initialized. Although not strictly required by PHP, it is generally a good practice to add $data_without_tags = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
81
			}
82
		} else {
83
			$data_without_tags = wp_kses( $data_with_tags, array() );
84
			$data_without_tags = str_replace( '&amp;', '&', $data_without_tags ); // undo damage done by wp_kses_normalize_entities()
85
		}
86
87
		return $data_without_tags;
0 ignored issues
show
Bug introduced by
The variable $data_without_tags does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
88
	}
89
90
	function __construct() {
91
		$this->add_shortcode();
92
93
		// While generating the output of a text widget with a contact-form shortcode, we need to know its widget ID.
94
		add_action( 'dynamic_sidebar', array( $this, 'track_current_widget' ) );
95
96
		// Add a "widget" shortcode attribute to all contact-form shortcodes embedded in widgets
97
		add_filter( 'widget_text', array( $this, 'widget_atts' ), 0 );
98
99
		// If Text Widgets don't get shortcode processed, hack ours into place.
100
		if ( ! has_filter( 'widget_text', 'do_shortcode' ) ) {
101
			add_filter( 'widget_text', array( $this, 'widget_shortcode_hack' ), 5 );
102
		}
103
104
		// Akismet to the rescue
105
		if ( defined( 'AKISMET_VERSION' ) || function_exists( 'akismet_http_post' ) ) {
106
			add_filter( 'jetpack_contact_form_is_spam', array( $this, 'is_spam_akismet' ), 10, 2 );
107
			add_action( 'contact_form_akismet', array( $this, 'akismet_submit' ), 10, 2 );
108
		}
109
110
		add_action( 'loop_start', array( 'Grunion_Contact_Form', '_style_on' ) );
111
112
		add_action( 'wp_ajax_grunion-contact-form', array( $this, 'ajax_request' ) );
113
		add_action( 'wp_ajax_nopriv_grunion-contact-form', array( $this, 'ajax_request' ) );
114
115
		// Export to CSV feature
116
		if ( is_admin() ) {
117
			add_action( 'admin_init',            array( $this, 'download_feedback_as_csv' ) );
118
			add_action( 'admin_footer-edit.php', array( $this, 'export_form' ) );
119
			add_action( 'admin_menu',            array( $this, 'admin_menu' ) );
120
			add_action( 'current_screen',        array( $this, 'unread_count' ) );
121
		}
122
123
		// custom post type we'll use to keep copies of the feedback items
124
		register_post_type( 'feedback', array(
125
			'labels'            => array(
126
				'name'               => __( 'Feedback', 'jetpack' ),
127
				'singular_name'      => __( 'Feedback', 'jetpack' ),
128
				'search_items'       => __( 'Search Feedback', 'jetpack' ),
129
				'not_found'          => __( 'No feedback found', 'jetpack' ),
130
				'not_found_in_trash' => __( 'No feedback found', 'jetpack' ),
131
			),
132
			'menu_icon'         	=> 'dashicons-feedback',
133
			'show_ui'           	=> TRUE,
134
			'show_in_admin_bar' 	=> FALSE,
135
			'public'            	=> FALSE,
136
			'rewrite'           	=> FALSE,
137
			'query_var'         	=> FALSE,
138
			'capability_type'   	=> 'page',
139
			'show_in_rest'      	=> true,
140
			'rest_controller_class' => 'Grunion_Contact_Form_Endpoint',
141
			'capabilities'			=> array(
142
				'create_posts'        => false,
143
				'publish_posts'       => 'publish_pages',
144
				'edit_posts'          => 'edit_pages',
145
				'edit_others_posts'   => 'edit_others_pages',
146
				'delete_posts'        => 'delete_pages',
147
				'delete_others_posts' => 'delete_others_pages',
148
				'read_private_posts'  => 'read_private_pages',
149
				'edit_post'           => 'edit_page',
150
				'delete_post'         => 'delete_page',
151
				'read_post'           => 'read_page',
152
			),
153
			'map_meta_cap'			=> true,
154
		) );
155
156
		// Add to REST API post type whitelist
157
		add_filter( 'rest_api_allowed_post_types', array( $this, 'allow_feedback_rest_api_type' ) );
158
159
		// Add "spam" as a post status
160
		register_post_status( 'spam', array(
161
			'label'                  => 'Spam',
162
			'public'                 => false,
163
			'exclude_from_search'    => true,
164
			'show_in_admin_all_list' => false,
165
			'label_count'            => _n_noop( 'Spam <span class="count">(%s)</span>', 'Spam <span class="count">(%s)</span>', 'jetpack' ),
166
			'protected'              => true,
167
			'_builtin'               => false,
168
		) );
169
170
		// POST handler
171
		if (
172
			isset( $_SERVER['REQUEST_METHOD'] ) && 'POST' == strtoupper( $_SERVER['REQUEST_METHOD'] )
173
		&&
174
			isset( $_POST['action'] ) && 'grunion-contact-form' == $_POST['action']
175
		&&
176
			isset( $_POST['contact-form-id'] )
177
		) {
178
			add_action( 'template_redirect', array( $this, 'process_form_submission' ) );
179
		}
180
181
		/*
182
		 Can be dequeued by placing the following in wp-content/themes/yourtheme/functions.php
183
		 *
184
		 * 	function remove_grunion_style() {
185
		 *		wp_deregister_style('grunion.css');
186
		 *	}
187
		 *	add_action('wp_print_styles', 'remove_grunion_style');
188
		 */
189
		wp_register_style( 'grunion.css', GRUNION_PLUGIN_URL . 'css/grunion.css', array(), JETPACK__VERSION );
190
		wp_style_add_data( 'grunion.css', 'rtl', 'replace' );
191
	}
192
193
	/**
194
	 * Add the 'Export' menu item as a submenu of Feedback.
195
	 */
196
	public function admin_menu() {
197
		add_submenu_page(
198
			'edit.php?post_type=feedback',
199
			__( 'Export feedback as CSV', 'jetpack' ),
200
			__( 'Export CSV', 'jetpack' ),
201
			'export',
202
			'feedback-export',
203
			array( $this, 'export_form' )
204
		);
205
	}
206
207
	/**
208
	 * Add to REST API post type whitelist
209
	 */
210
	function allow_feedback_rest_api_type( $post_types ) {
211
		$post_types[] = 'feedback';
212
		return $post_types;
213
	}
214
215
	/**
216
	 * Display the count of new feedback entries received. It's reset when user visits the Feedback screen.
217
	 *
218
	 * @since 4.1.0
219
	 *
220
	 * @param object $screen Information about the current screen.
221
	 */
222
	function unread_count( $screen ) {
223
		if ( isset( $screen->post_type ) && 'feedback' == $screen->post_type ) {
224
			update_option( 'feedback_unread_count', 0 );
225
		} else {
226
			global $menu;
227
			if ( isset( $menu ) && is_array( $menu ) && ! empty( $menu ) ) {
228
				foreach ( $menu as $index => $menu_item ) {
229
					if ( 'edit.php?post_type=feedback' == $menu_item[2] ) {
230
						$unread = get_option( 'feedback_unread_count', 0 );
231
						if ( $unread > 0 ) {
232
							$unread_count = current_user_can( 'publish_pages' ) ? " <span class='feedback-unread count-{$unread} awaiting-mod'><span class='feedback-unread-count'>" . number_format_i18n( $unread ) . '</span></span>' : '';
233
							$menu[ $index ][0] .= $unread_count;
234
						}
235
						break;
236
					}
237
				}
238
			}
239
		}
240
	}
241
242
	/**
243
	 * Handles all contact-form POST submissions
244
	 *
245
	 * Conditionally attached to `template_redirect`
246
	 */
247
	function process_form_submission() {
248
		// Add a filter to replace tokens in the subject field with sanitized field values
249
		add_filter( 'contact_form_subject', array( $this, 'replace_tokens_with_input' ), 10, 2 );
250
251
		$id = stripslashes( $_POST['contact-form-id'] );
252
253
		if ( is_user_logged_in() ) {
254
			check_admin_referer( "contact-form_{$id}" );
255
		}
256
257
		$is_widget = 0 === strpos( $id, 'widget-' );
258
259
		$form = false;
0 ignored issues
show
Unused Code introduced by
$form is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
260
261
		if ( $is_widget ) {
262
			// It's a form embedded in a text widget
263
			$this->current_widget_id = substr( $id, 7 ); // remove "widget-"
264
			$widget_type = implode( '-', array_slice( explode( '-', $this->current_widget_id ), 0, -1 ) ); // Remove trailing -#
265
266
			// Is the widget active?
267
			$sidebar = is_active_widget( false, $this->current_widget_id, $widget_type );
268
269
			// This is lame - no core API for getting a widget by ID
270
			$widget = isset( $GLOBALS['wp_registered_widgets'][ $this->current_widget_id ] ) ? $GLOBALS['wp_registered_widgets'][ $this->current_widget_id ] : false;
271
272
			if ( $sidebar && $widget && isset( $widget['callback'] ) ) {
273
				// prevent PHP notices by populating widget args
274
				$widget_args = array(
275
					'before_widget' => '',
276
					'after_widget' => '',
277
					'before_title' => '',
278
					'after_title' => '',
279
				);
280
				// This is lamer - no API for outputting a given widget by ID
281
				ob_start();
282
				// Process the widget to populate Grunion_Contact_Form::$last
283
				call_user_func( $widget['callback'], $widget_args, $widget['params'][0] );
284
				ob_end_clean();
285
			}
286
		} else {
287
			// It's a form embedded in a post
288
			$post = get_post( $id );
289
290
			// Process the content to populate Grunion_Contact_Form::$last
291
			/** This filter is already documented in core. wp-includes/post-template.php */
292
			apply_filters( 'the_content', $post->post_content );
293
		}
294
295
		$form = Grunion_Contact_Form::$last;
0 ignored issues
show
Bug introduced by
The property last cannot be accessed from this context as it is declared private in class Grunion_Contact_Form.

This check looks for access to properties that are not accessible from the current context.

If you need to make a property accessible to another context you can either raise its visibility level or provide an accessible getter in the defining class.

Loading history...
296
297
		// No form may mean user is using do_shortcode, grab the form using the stored post meta
298
		if ( ! $form ) {
299
300
			// Get shortcode from post meta
301
			$shortcode = get_post_meta( $_POST['contact-form-id'], '_g_feedback_shortcode', true );
302
303
			// Format it
304
			if ( $shortcode != '' ) {
305
306
				// Get attributes from post meta.
307
				$parameters = '';
308
				$attributes = get_post_meta( $_POST['contact-form-id'], '_g_feedback_shortcode_atts', true );
309
				if ( ! empty( $attributes ) && is_array( $attributes ) ) {
310
					foreach( array_filter( $attributes ) as $param => $value  ) {
311
						$parameters .= " $param=\"$value\"";
312
					}
313
				}
314
315
				$shortcode = '[contact-form' . $parameters . ']' . $shortcode . '[/contact-form]';
316
				do_shortcode( $shortcode );
317
318
				// Recreate form
319
				$form = Grunion_Contact_Form::$last;
0 ignored issues
show
Bug introduced by
The property last cannot be accessed from this context as it is declared private in class Grunion_Contact_Form.

This check looks for access to properties that are not accessible from the current context.

If you need to make a property accessible to another context you can either raise its visibility level or provide an accessible getter in the defining class.

Loading history...
320
			}
321
322
			if ( ! $form ) {
323
				return false;
324
			}
325
		}
326
327
		if ( is_wp_error( $form->errors ) && $form->errors->get_error_codes() ) {
328
			return $form->errors;
329
		}
330
331
		// Process the form
332
		return $form->process_submission();
333
	}
334
335
	function ajax_request() {
336
		$submission_result = self::process_form_submission();
337
338
		if ( ! $submission_result ) {
339
			header( 'HTTP/1.1 500 Server Error', 500, true );
340
			echo '<div class="form-error"><ul class="form-errors"><li class="form-error-message">';
341
			esc_html_e( 'An error occurred. Please try again later.', 'jetpack' );
342
			echo '</li></ul></div>';
343
		} elseif ( is_wp_error( $submission_result ) ) {
344
			header( 'HTTP/1.1 400 Bad Request', 403, true );
345
			echo '<div class="form-error"><ul class="form-errors"><li class="form-error-message">';
346
			echo esc_html( $submission_result->get_error_message() );
347
			echo '</li></ul></div>';
348
		} else {
349
			echo '<h3>' . esc_html__( 'Message Sent', 'jetpack' ) . '</h3>' . $submission_result;
350
		}
351
352
		die;
0 ignored issues
show
Coding Style Compatibility introduced by
The method ajax_request() contains an exit expression.

An exit expression should only be used in rare cases. For example, if you write a short command line script.

In most cases however, using an exit expression makes the code untestable and often causes incompatibilities with other libraries. Thus, unless you are absolutely sure it is required here, we recommend to refactor your code to avoid its usage.

Loading history...
353
	}
354
355
	/**
356
	 * Ensure the post author is always zero for contact-form feedbacks
357
	 * Attached to `wp_insert_post_data`
358
	 *
359
	 * @see Grunion_Contact_Form::process_submission()
360
	 *
361
	 * @param array $data the data to insert
362
	 * @param array $postarr the data sent to wp_insert_post()
363
	 * @return array The filtered $data to insert
364
	 */
365
	function insert_feedback_filter( $data, $postarr ) {
366
		if ( $data['post_type'] == 'feedback' && $postarr['post_type'] == 'feedback' ) {
367
			$data['post_author'] = 0;
368
		}
369
370
		return $data;
371
	}
372
	/*
373
	 * Adds our contact-form shortcode
374
	 * The "child" contact-field shortcode is enabled as needed by the contact-form shortcode handler
375
	 */
376
	function add_shortcode() {
377
		add_shortcode( 'contact-form',         array( 'Grunion_Contact_Form', 'parse' ) );
378
		add_shortcode( 'contact-field',        array( 'Grunion_Contact_Form', 'parse_contact_field' ) );
379
	}
380
381
	static function tokenize_label( $label ) {
382
		return '{' . trim( preg_replace( '#^\d+_#', '', $label ) ) . '}';
383
	}
384
385
	static function sanitize_value( $value ) {
386
		return preg_replace( '=((<CR>|<LF>|0x0A/%0A|0x0D/%0D|\\n|\\r)\S).*=i', null, $value );
387
	}
388
389
	/**
390
	 * Replaces tokens like {city} or {City} (case insensitive) with the value
391
	 * of an input field of that name
392
	 *
393
	 * @param string $subject
394
	 * @param array  $field_values Array with field label => field value associations
395
	 *
396
	 * @return string The filtered $subject with the tokens replaced
397
	 */
398
	function replace_tokens_with_input( $subject, $field_values ) {
399
		// Wrap labels into tokens (inside {})
400
		$wrapped_labels = array_map( array( 'Grunion_Contact_Form_Plugin', 'tokenize_label' ), array_keys( $field_values ) );
401
		// Sanitize all values
402
		$sanitized_values = array_map( array( 'Grunion_Contact_Form_Plugin', 'sanitize_value' ), array_values( $field_values ) );
403
404
		foreach ( $sanitized_values as $k => $sanitized_value ) {
405
			if ( is_array( $sanitized_value ) ) {
406
				$sanitized_values[ $k ] = implode( ', ', $sanitized_value );
407
			}
408
		}
409
410
		// Search for all valid tokens (based on existing fields) and replace with the field's value
411
		$subject = str_ireplace( $wrapped_labels, $sanitized_values, $subject );
412
		return $subject;
413
	}
414
415
	/**
416
	 * Tracks the widget currently being processed.
417
	 * Attached to `dynamic_sidebar`
418
	 *
419
	 * @see $current_widget_id
420
	 *
421
	 * @param array $widget The widget data
422
	 */
423
	function track_current_widget( $widget ) {
424
		$this->current_widget_id = $widget['id'];
425
	}
426
427
	/**
428
	 * Adds a "widget" attribute to every contact-form embedded in a text widget.
429
	 * Used to tell the difference between post-embedded contact-forms and widget-embedded contact-forms
430
	 * Attached to `widget_text`
431
	 *
432
	 * @param string $text The widget text
433
	 * @return string The filtered widget text
434
	 */
435
	function widget_atts( $text ) {
436
		Grunion_Contact_Form::style( true );
437
438
		return preg_replace( '/\[contact-form([^a-zA-Z_-])/', '[contact-form widget="' . $this->current_widget_id . '"\\1', $text );
439
	}
440
441
	/**
442
	 * For sites where text widgets are not processed for shortcodes, we add this hack to process just our shortcode
443
	 * Attached to `widget_text`
444
	 *
445
	 * @param string $text The widget text
446
	 * @return string The contact-form filtered widget text
447
	 */
448
	function widget_shortcode_hack( $text ) {
449
		if ( ! preg_match( '/\[contact-form([^a-zA-Z_-])/', $text ) ) {
450
			return $text;
451
		}
452
453
		$old = $GLOBALS['shortcode_tags'];
454
		remove_all_shortcodes();
455
		Grunion_Contact_Form_Plugin::$using_contact_form_field = true;
456
		$this->add_shortcode();
457
458
		$text = do_shortcode( $text );
459
460
		Grunion_Contact_Form_Plugin::$using_contact_form_field = false;
461
		$GLOBALS['shortcode_tags'] = $old;
462
463
		return $text;
464
	}
465
466
	/**
467
	 * Populate an array with all values necessary to submit a NEW contact-form feedback to Akismet.
468
	 * Note that this includes the current user_ip etc, so this should only be called when accepting a new item via $_POST
469
	 *
470
	 * @param array $form Contact form feedback array
471
	 * @return array feedback array with additional data ready for submission to Akismet
472
	 */
473
	function prepare_for_akismet( $form ) {
474
		$form['comment_type'] = 'contact_form';
475
		$form['user_ip']      = $_SERVER['REMOTE_ADDR'];
476
		$form['user_agent']   = $_SERVER['HTTP_USER_AGENT'];
477
		$form['referrer']     = $_SERVER['HTTP_REFERER'];
478
		$form['blog']         = get_option( 'home' );
479
480
		foreach ( $_SERVER as $key => $value ) {
481
			if ( ! is_string( $value ) ) {
482
				continue;
483
			}
484
			if ( in_array( $key, array( 'HTTP_COOKIE', 'HTTP_COOKIE2', 'HTTP_USER_AGENT', 'HTTP_REFERER' ) ) ) {
485
				// We don't care about cookies, and the UA and Referrer were caught above.
486
				continue;
487
			} elseif ( in_array( $key, array( 'REMOTE_ADDR', 'REQUEST_URI', 'DOCUMENT_URI' ) ) ) {
488
				// All three of these are relevant indicators and should be passed along.
489
				$form[ $key ] = $value;
490
			} elseif ( wp_startswith( $key, 'HTTP_' ) ) {
491
				// Any other HTTP header indicators.
492
				// `wp_startswith()` is a wpcom helper function and is included in Jetpack via `functions.compat.php`
493
				$form[ $key ] = $value;
494
			}
495
		}
496
497
		return $form;
498
	}
499
500
	/**
501
	 * Submit contact-form data to Akismet to check for spam.
502
	 * If you're accepting a new item via $_POST, run it Grunion_Contact_Form_Plugin::prepare_for_akismet() first
503
	 * Attached to `jetpack_contact_form_is_spam`
504
	 *
505
	 * @param bool  $is_spam
506
	 * @param array $form
507
	 * @return bool|WP_Error TRUE => spam, FALSE => not spam, WP_Error => stop processing entirely
508
	 */
509
	function is_spam_akismet( $is_spam, $form = array() ) {
510
		global $akismet_api_host, $akismet_api_port;
511
512
		// The signature of this function changed from accepting just $form.
513
		// If something only sends an array, assume it's still using the old
514
		// signature and work around it.
515
		if ( empty( $form ) && is_array( $is_spam ) ) {
516
			$form = $is_spam;
517
			$is_spam = false;
518
		}
519
520
		// If a previous filter has alrady marked this as spam, trust that and move on.
521
		if ( $is_spam ) {
522
			return $is_spam;
523
		}
524
525
		if ( ! function_exists( 'akismet_http_post' ) && ! defined( 'AKISMET_VERSION' ) ) {
526
			return false;
527
		}
528
529
		$query_string = http_build_query( $form );
530
531
		if ( method_exists( 'Akismet', 'http_post' ) ) {
532
			$response = Akismet::http_post( $query_string, 'comment-check' );
533
		} else {
534
			$response = akismet_http_post( $query_string, $akismet_api_host, '/1.1/comment-check', $akismet_api_port );
535
		}
536
537
		$result = false;
538
539
		if ( isset( $response[0]['x-akismet-pro-tip'] ) && 'discard' === trim( $response[0]['x-akismet-pro-tip'] ) && get_option( 'akismet_strictness' ) === '1' ) {
540
			$result = new WP_Error( 'feedback-discarded', __( 'Feedback discarded.', 'jetpack' ) );
541
		} elseif ( isset( $response[1] ) && 'true' == trim( $response[1] ) ) { // 'true' is spam
542
			$result = true;
543
		}
544
545
		/**
546
		 * Filter the results returned by Akismet for each submitted contact form.
547
		 *
548
		 * @module contact-form
549
		 *
550
		 * @since 1.3.1
551
		 *
552
		 * @param WP_Error|bool $result Is the submitted feedback spam.
553
		 * @param array|bool $form Submitted feedback.
554
		 */
555
		return apply_filters( 'contact_form_is_spam_akismet', $result, $form );
556
	}
557
558
	/**
559
	 * Submit a feedback as either spam or ham
560
	 *
561
	 * @param string $as Either 'spam' or 'ham'.
562
	 * @param array  $form the contact-form data
563
	 */
564
	function akismet_submit( $as, $form ) {
565
		global $akismet_api_host, $akismet_api_port;
566
567
		if ( ! in_array( $as, array( 'ham', 'spam' ) ) ) {
568
			return false;
569
		}
570
571
		$query_string = '';
572
		if ( is_array( $form ) ) {
573
			$query_string = http_build_query( $form );
574
		}
575
		if ( method_exists( 'Akismet', 'http_post' ) ) {
576
		    $response = Akismet::http_post( $query_string, "submit-{$as}" );
577
		} else {
578
		    $response = akismet_http_post( $query_string, $akismet_api_host, "/1.1/submit-{$as}", $akismet_api_port );
579
		}
580
581
		return trim( $response[1] );
582
	}
583
584
	/**
585
	 * Prints the menu
586
	 */
587
	function export_form() {
588
		$current_screen = get_current_screen();
589
		if ( ! in_array( $current_screen->id, array( 'edit-feedback', 'feedback_page_feedback-export' ) ) ) {
590
			return;
591
		}
592
593
		if ( ! current_user_can( 'export' ) ) {
594
			return;
595
		}
596
597
		// if there aren't any feedbacks, bail out
598
		if ( ! (int) wp_count_posts( 'feedback' )->publish ) {
599
			return;
600
		}
601
		?>
602
603
		<div id="feedback-export" style="display:none">
604
			<h2><?php _e( 'Export feedback as CSV', 'jetpack' ) ?></h2>
605
			<div class="clear"></div>
606
			<form action="<?php echo admin_url( 'admin-post.php' ); ?>" method="post" class="form">
607
				<?php wp_nonce_field( 'feedback_export','feedback_export_nonce' ); ?>
608
609
				<input name="action" value="feedback_export" type="hidden">
610
				<label for="post"><?php _e( 'Select feedback to download', 'jetpack' ) ?></label>
611
				<select name="post">
612
					<option value="all"><?php esc_html_e( 'All posts', 'jetpack' ) ?></option>
613
					<?php echo $this->get_feedbacks_as_options() ?>
614
				</select>
615
616
				<br><br>
617
				<input type="submit" name="submit" id="submit" class="button button-primary" value="<?php esc_html_e( 'Download', 'jetpack' ); ?>">
618
			</form>
619
		</div>
620
621
		<?php
622
		// There aren't any usable actions in core to output the "export feedback" form in the correct place,
623
		// so this inline JS moves it from the top of the page to the bottom.
624
		?>
625
		<script type='text/javascript'>
626
		var menu = document.getElementById( 'feedback-export' ),
627
		wrapper = document.getElementsByClassName( 'wrap' )[0];
628
		<?php if ( 'edit-feedback' === $current_screen->id ) : ?>
629
		wrapper.appendChild(menu);
630
		<?php endif; ?>
631
		menu.style.display = 'block';
632
		</script>
633
		<?php
634
	}
635
636
	/**
637
	 * Fetch post content for a post and extract just the comment.
638
	 *
639
	 * @param int $post_id The post id to fetch the content for.
640
	 *
641
	 * @return string Trimmed post comment.
642
	 *
643
	 * @codeCoverageIgnore
644
	 */
645
	public function get_post_content_for_csv_export( $post_id ) {
646
		$post_content = get_post_field( 'post_content', $post_id );
647
		$content      = explode( '<!--more-->', $post_content );
648
649
		return trim( $content[0] );
650
	}
651
652
	/**
653
	 * Get `_feedback_extra_fields` field from post meta data.
654
	 *
655
	 * @param int $post_id Id of the post to fetch meta data for.
656
	 *
657
	 * @return mixed
658
	 *
659
	 * @codeCoverageIgnore - No need to be covered.
660
	 */
661
	public function get_post_meta_for_csv_export( $post_id ) {
662
		return get_post_meta( $post_id, '_feedback_extra_fields', true );
663
	}
664
665
	/**
666
	 * Get parsed feedback post fields.
667
	 *
668
	 * @param int $post_id Id of the post to fetch parsed contents for.
669
	 *
670
	 * @return array
671
	 *
672
	 * @codeCoverageIgnore - No need to be covered.
673
	 */
674
	public function get_parsed_field_contents_of_post( $post_id ) {
675
		return self::parse_fields_from_content( $post_id );
676
	}
677
678
	/**
679
	 * Properly maps fields that are missing from the post meta data
680
	 * to names, that are similar to those of the post meta.
681
	 *
682
	 * @param array $parsed_post_content Parsed post content
683
	 *
684
	 * @see parse_fields_from_content for how the input data is generated.
685
	 *
686
	 * @return array Mapped fields.
687
	 */
688
	public function map_parsed_field_contents_of_post_to_field_names( $parsed_post_content ) {
689
690
		$mapped_fields = array();
691
692
		$field_mapping = array(
693
			'_feedback_subject'      => __( 'Contact Form', 'jetpack' ),
694
			'_feedback_author'       => '1_Name',
695
			'_feedback_author_email' => '2_Email',
696
			'_feedback_author_url'   => '3_Website',
697
			'_feedback_main_comment' => '4_Comment',
698
		);
699
700
		foreach ( $field_mapping as $parsed_field_name => $field_name ) {
701
			if (
702
				isset( $parsed_post_content[ $parsed_field_name ] )
703
				&& ! empty( $parsed_post_content[ $parsed_field_name ] )
704
			) {
705
				$mapped_fields[ $field_name ] = $parsed_post_content[ $parsed_field_name ];
706
			}
707
		}
708
709
		return $mapped_fields;
710
	}
711
712
713
	/**
714
	 * Prepares feedback post data for CSV export.
715
	 *
716
	 * @param array $post_ids Post IDs to fetch the data for. These need to be Feedback posts.
717
	 *
718
	 * @return array
719
	 */
720
	public function get_export_data_for_posts( $post_ids ) {
721
722
		$posts_data  = array();
723
		$field_names = array();
724
		$result      = array();
725
726
		/**
727
		 * Fetch posts and get the possible field names for later use
728
		 */
729
		foreach ( $post_ids as $post_id ) {
730
731
			/**
732
			 * Fetch post main data, because we need the subject and author data for the feedback form.
733
			 */
734
			$post_real_data = $this->get_parsed_field_contents_of_post( $post_id );
735
736
			/**
737
			 * If `$post_real_data` is not an array or there is no `_feedback_subject` set,
738
			 * then something must be wrong with the feedback post. Skip it.
739
			 */
740
			if ( ! is_array( $post_real_data ) || ! isset( $post_real_data['_feedback_subject'] ) ) {
741
				continue;
742
			}
743
744
			/**
745
			 * Fetch main post comment. This is from the default textarea fields.
746
			 * If it is non-empty, then we add it to data, otherwise skip it.
747
			 */
748
			$post_comment_content = $this->get_post_content_for_csv_export( $post_id );
749
			if ( ! empty( $post_comment_content ) ) {
750
				$post_real_data['_feedback_main_comment'] = $post_comment_content;
751
			}
752
753
			/**
754
			 * Map parsed fields to proper field names
755
			 */
756
			$mapped_fields = $this->map_parsed_field_contents_of_post_to_field_names( $post_real_data );
757
758
			/**
759
			 * Fetch post meta data.
760
			 */
761
			$post_meta_data = $this->get_post_meta_for_csv_export( $post_id );
762
763
			/**
764
			 * If `$post_meta_data` is not an array or if it is empty, then there is no
765
			 * extra feedback to work with. Create an empty array.
766
			 */
767
			if ( ! is_array( $post_meta_data ) || empty( $post_meta_data ) ) {
768
				$post_meta_data = array();
769
			}
770
771
			/**
772
			 * Prepend the feedback subject to the list of fields.
773
			 */
774
			$post_meta_data = array_merge(
775
				$mapped_fields,
776
				$post_meta_data
777
			);
778
779
			/**
780
			 * Save post metadata for later usage.
781
			 */
782
			$posts_data[ $post_id ] = $post_meta_data;
783
784
			/**
785
			 * Save field names, so we can use them as header fields later in the CSV.
786
			 */
787
			$field_names = array_merge( $field_names, array_keys( $post_meta_data ) );
788
		}
789
790
		/**
791
		 * Make sure the field names are unique, because we don't want duplicate data.
792
		 */
793
		$field_names = array_unique( $field_names );
794
795
		/**
796
		 * Sort the field names by the field id number
797
		 */
798
		sort( $field_names, SORT_NUMERIC );
799
800
		/**
801
		 * Loop through every post, which is essentially CSV row.
802
		 */
803
		foreach ( $posts_data as $post_id => $single_post_data ) {
804
805
			/**
806
			 * Go through all the possible fields and check if the field is available
807
			 * in the current post.
808
			 *
809
			 * If it is - add the data as a value.
810
			 * If it is not - add an empty string, which is just a placeholder in the CSV.
811
			 */
812
			foreach ( $field_names as $single_field_name ) {
813
				if (
814
					isset( $single_post_data[ $single_field_name ] )
815
					&& ! empty( $single_post_data[ $single_field_name ] )
816
				) {
817
					$result[ $single_field_name ][] = trim( $single_post_data[ $single_field_name ] );
818
				} else {
819
					$result[ $single_field_name ][] = '';
820
				}
821
			}
822
		}
823
824
		return $result;
825
	}
826
827
	/**
828
	 * download as a csv a contact form or all of them in a csv file
829
	 */
830
	function download_feedback_as_csv() {
831
		if ( empty( $_POST['feedback_export_nonce'] ) ) {
832
			return;
833
		}
834
835
		check_admin_referer( 'feedback_export', 'feedback_export_nonce' );
836
837
		if ( ! current_user_can( 'export' ) ) {
838
			return;
839
		}
840
841
		$args = array(
842
			'posts_per_page'   => -1,
843
			'post_type'        => 'feedback',
844
			'post_status'      => 'publish',
845
			'order'            => 'ASC',
846
			'fields'           => 'ids',
847
			'suppress_filters' => false,
848
		);
849
850
		$filename = date( 'Y-m-d' ) . '-feedback-export.csv';
851
852
		// Check if we want to download all the feedbacks or just a certain contact form
853
		if ( ! empty( $_POST['post'] ) && $_POST['post'] !== 'all' ) {
854
			$args['post_parent'] = (int) $_POST['post'];
855
			$filename            = date( 'Y-m-d' ) . '-' . str_replace( '&nbsp;', '-', get_the_title( (int) $_POST['post'] ) ) . '.csv';
856
		}
857
858
		$feedbacks = get_posts( $args );
859
860
		if ( empty( $feedbacks ) ) {
861
			return;
862
		}
863
864
		$filename  = sanitize_file_name( $filename );
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned correctly; expected 1 space but found 2 spaces

This check looks for improperly formatted assignments.

Every assignment must have exactly one space before and one space after the equals operator.

To illustrate:

$a = "a";
$ab = "ab";
$abc = "abc";

will have no issues, while

$a   = "a";
$ab  = "ab";
$abc = "abc";

will report issues in lines 1 and 2.

Loading history...
865
866
		/**
867
		 * Prepare data for export.
868
		 */
869
		$data = $this->get_export_data_for_posts( $feedbacks );
870
871
		/**
872
		 * If `$data` is empty, there's nothing we can do below.
873
		 */
874
		if ( ! is_array( $data ) || empty( $data ) ) {
875
			return;
876
		}
877
878
		/**
879
		 * Extract field names from `$data` for later use.
880
		 */
881
		$fields = array_keys( $data );
882
883
		/**
884
		 * Count how many rows will be exported.
885
		 */
886
		$row_count = count( reset( $data ) );
887
888
		// Forces the download of the CSV instead of echoing
889
		header( 'Content-Disposition: attachment; filename=' . $filename );
890
		header( 'Pragma: no-cache' );
891
		header( 'Expires: 0' );
892
		header( 'Content-Type: text/csv; charset=utf-8' );
893
894
		$output = fopen( 'php://output', 'w' );
895
896
		/**
897
		 * Print CSV headers
898
		 */
899
		fputcsv( $output, $fields );
900
901
		/**
902
		 * Print rows to the output.
903
		 */
904
		for ( $i = 0; $i < $row_count; $i ++ ) {
905
906
			$current_row = array();
907
908
			/**
909
			 * Put all the fields in `$current_row` array.
910
			 */
911
			foreach ( $fields as $single_field_name ) {
912
				$current_row[] = $this->esc_csv( $data[ $single_field_name ][ $i ] );
913
			}
914
915
			/**
916
			 * Output the complete CSV row
917
			 */
918
			fputcsv( $output, $current_row );
919
		}
920
921
		fclose( $output );
922
	}
923
924
	/**
925
	 * Escape a string to be used in a CSV context
926
	 *
927
	 * Malicious input can inject formulas into CSV files, opening up the possibility for phishing attacks and
928
	 * disclosure of sensitive information.
929
	 *
930
	 * Additionally, Excel exposes the ability to launch arbitrary commands through the DDE protocol.
931
	 *
932
	 * @see http://www.contextis.com/resources/blog/comma-separated-vulnerabilities/
933
	 *
934
	 * @param string $field
935
	 *
936
	 * @return string
937
	 */
938
	public function esc_csv( $field ) {
939
		$active_content_triggers = array( '=', '+', '-', '@' );
940
941
		if ( in_array( mb_substr( $field, 0, 1 ), $active_content_triggers, true ) ) {
942
			$field = "'" . $field;
943
		}
944
945
		return $field;
946
	}
947
948
	/**
949
	 * Returns a string of HTML <option> items from an array of posts
950
	 *
951
	 * @return string a string of HTML <option> items
952
	 */
953
	protected function get_feedbacks_as_options() {
954
		$options = '';
955
956
		// Get the feedbacks' parents' post IDs
957
		$feedbacks = get_posts( array(
958
			'fields'           => 'id=>parent',
959
			'posts_per_page'   => 100000,
960
			'post_type'        => 'feedback',
961
			'post_status'      => 'publish',
962
			'suppress_filters' => false,
963
		) );
964
		$parents = array_unique( array_values( $feedbacks ) );
965
966
		$posts = get_posts( array(
967
			'orderby'          => 'ID',
968
			'posts_per_page'   => 1000,
969
			'post_type'        => 'any',
970
			'post__in'         => array_values( $parents ),
971
			'suppress_filters' => false,
972
		) );
973
974
		// creates the string of <option> elements
975
		foreach ( $posts as $post ) {
976
			$options .= sprintf( '<option value="%s">%s</option>', esc_attr( $post->ID ), esc_html( $post->post_title ) );
977
		}
978
979
		return $options;
980
	}
981
982
	/**
983
	 * Get the names of all the form's fields
984
	 *
985
	 * @param  array|int $posts the post we want the fields of
986
	 *
987
	 * @return array     the array of fields
988
	 *
989
	 * @deprecated As this is no longer necessary as of the CSV export rewrite. - 2015-12-29
990
	 */
991
	protected function get_field_names( $posts ) {
992
		$posts = (array) $posts;
993
		$all_fields = array();
994
995
		foreach ( $posts as $post ) {
996
			$fields = self::parse_fields_from_content( $post );
997
998
			if ( isset( $fields['_feedback_all_fields'] ) ) {
999
				$extra_fields = array_keys( $fields['_feedback_all_fields'] );
1000
				$all_fields = array_merge( $all_fields, $extra_fields );
1001
			}
1002
		}
1003
1004
		$all_fields = array_unique( $all_fields );
1005
		return $all_fields;
1006
	}
1007
1008
	public static function parse_fields_from_content( $post_id ) {
1009
		static $post_fields;
1010
1011
		if ( ! is_array( $post_fields ) ) {
1012
			$post_fields = array();
1013
		}
1014
1015
		if ( isset( $post_fields[ $post_id ] ) ) {
1016
			return $post_fields[ $post_id ];
1017
		}
1018
1019
		$all_values   = array();
1020
		$post_content = get_post_field( 'post_content', $post_id );
1021
		$content      = explode( '<!--more-->', $post_content );
1022
		$lines        = array();
1023
1024
		if ( count( $content ) > 1 ) {
1025
			$content  = str_ireplace( array( '<br />', ')</p>' ), '', $content[1] );
1026
			$one_line = preg_replace( '/\s+/', ' ', $content );
1027
			$one_line = preg_replace( '/.*Array \( (.*)\)/', '$1', $one_line );
1028
1029
			preg_match_all( '/\[([^\]]+)\] =\&gt\; ([^\[]+)/', $one_line, $matches );
1030
1031
			if ( count( $matches ) > 1 ) {
1032
				$all_values = array_combine( array_map( 'trim', $matches[1] ), array_map( 'trim', $matches[2] ) );
1033
			}
1034
1035
			$lines = array_filter( explode( "\n", $content ) );
1036
		}
1037
1038
		$var_map = array(
1039
			'AUTHOR'       => '_feedback_author',
1040
			'AUTHOR EMAIL' => '_feedback_author_email',
1041
			'AUTHOR URL'   => '_feedback_author_url',
1042
			'SUBJECT'      => '_feedback_subject',
1043
			'IP'           => '_feedback_ip',
1044
		);
1045
1046
		$fields = array();
1047
1048
		foreach ( $lines as $line ) {
1049
			$vars = explode( ': ', $line, 2 );
1050
			if ( ! empty( $vars ) ) {
1051
				if ( isset( $var_map[ $vars[0] ] ) ) {
1052
					$fields[ $var_map[ $vars[0] ] ] = self::strip_tags( trim( $vars[1] ) );
1053
				}
1054
			}
1055
		}
1056
1057
		$fields['_feedback_all_fields'] = $all_values;
1058
1059
		$post_fields[ $post_id ] = $fields;
1060
1061
		return $fields;
1062
	}
1063
1064
	/**
1065
	 * Creates a valid csv row from a post id
1066
	 *
1067
	 * @param  int   $post_id The id of the post
1068
	 * @param  array $fields  An array containing the names of all the fields of the csv
1069
	 * @return String The csv row
1070
	 *
1071
	 * @deprecated This is no longer needed, as of the CSV export rewrite.
1072
	 */
1073
	protected static function make_csv_row_from_feedback( $post_id, $fields ) {
1074
		$content_fields = self::parse_fields_from_content( $post_id );
1075
		$all_fields     = array();
1076
1077
		if ( isset( $content_fields['_feedback_all_fields'] ) ) {
1078
			$all_fields = $content_fields['_feedback_all_fields'];
1079
		}
1080
1081
		// Overwrite the parsed content with the content we stored in post_meta in a better format.
1082
		$extra_fields   = get_post_meta( $post_id, '_feedback_extra_fields', true );
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned correctly; expected 1 space but found 3 spaces

This check looks for improperly formatted assignments.

Every assignment must have exactly one space before and one space after the equals operator.

To illustrate:

$a = "a";
$ab = "ab";
$abc = "abc";

will have no issues, while

$a   = "a";
$ab  = "ab";
$abc = "abc";

will report issues in lines 1 and 2.

Loading history...
1083
		foreach ( $extra_fields as $extra_field => $extra_value ) {
1084
			$all_fields[ $extra_field ] = $extra_value;
1085
		}
1086
1087
		// The first element in all of the exports will be the subject
1088
		$row_items[] = $content_fields['_feedback_subject'];
0 ignored issues
show
Coding Style Comprehensibility introduced by
$row_items was never initialized. Although not strictly required by PHP, it is generally a good practice to add $row_items = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
1089
1090
		// Loop the fields array in order to fill the $row_items array correctly
1091
		foreach ( $fields as $field ) {
1092
			if ( $field === __( 'Contact Form', 'jetpack' ) ) { // the first field will ever be the contact form, so we can continue
1093
				continue;
1094
			} elseif ( array_key_exists( $field, $all_fields ) ) {
1095
				$row_items[] = $all_fields[ $field ];
1096
			} else { $row_items[] = '';
1097
			}
1098
		}
1099
1100
		return $row_items;
1101
	}
1102
1103
	public static function get_ip_address() {
1104
		return isset( $_SERVER['REMOTE_ADDR'] ) ? $_SERVER['REMOTE_ADDR'] : null;
1105
	}
1106
}
1107
1108
/**
1109
 * Generic shortcode class.
1110
 * Does nothing other than store structured data and output the shortcode as a string
1111
 *
1112
 * Not very general - specific to Grunion.
1113
 */
1114
class Crunion_Contact_Form_Shortcode {
0 ignored issues
show
Coding Style Compatibility introduced by
PSR1 recommends that each class should be in its own file to aid autoloaders.

Having each class in a dedicated file usually plays nice with PSR autoloaders and is therefore a well established practice. If you use other autoloaders, you might not want to follow this rule.

Loading history...
1115
	/**
1116
	 * @var string the name of the shortcode: [$shortcode_name /]
1117
	 */
1118
	public $shortcode_name;
1119
1120
	/**
1121
	 * @var array key => value pairs for the shortcode's attributes: [$shortcode_name key="value" ... /]
1122
	 */
1123
	public $attributes;
1124
1125
	/**
1126
	 * @var array key => value pair for attribute defaults
1127
	 */
1128
	public $defaults = array();
1129
1130
	/**
1131
	 * @var null|string Null for selfclosing shortcodes.  Hhe inner content of otherwise: [$shortcode_name]$content[/$shortcode_name]
1132
	 */
1133
	public $content;
1134
1135
	/**
1136
	 * @var array Associative array of inner "child" shortcodes equivalent to the $content: [$shortcode_name][child 1/][child 2/][/$shortcode_name]
1137
	 */
1138
	public $fields;
1139
1140
	/**
1141
	 * @var null|string The HTML of the parsed inner "child" shortcodes".  Null for selfclosing shortcodes.
1142
	 */
1143
	public $body;
1144
1145
	/**
1146
	 * @param array       $attributes An associative array of shortcode attributes.  @see shortcode_atts()
1147
	 * @param null|string $content Null for selfclosing shortcodes.  The inner content otherwise.
1148
	 */
1149
	function __construct( $attributes, $content = null ) {
1150
		$this->attributes = $this->unesc_attr( $attributes );
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->unesc_attr($attributes) of type * is incompatible with the declared type array of property $attributes.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
1151
		if ( is_array( $content ) ) {
1152
			$string_content = '';
1153
			foreach ( $content as $field ) {
1154
				$string_content .= (string) $field;
1155
			}
1156
1157
			$this->content = $string_content;
1158
		} else {
1159
			$this->content = $content;
1160
		}
1161
1162
		$this->parse_content( $this->content );
1163
	}
1164
1165
	/**
1166
	 * Processes the shortcode's inner content for "child" shortcodes
1167
	 *
1168
	 * @param string $content The shortcode's inner content: [shortcode]$content[/shortcode]
1169
	 */
1170
	function parse_content( $content ) {
1171
		if ( is_null( $content ) ) {
1172
			$this->body = null;
1173
		}
1174
1175
		$this->body = do_shortcode( $content );
1176
	}
1177
1178
	/**
1179
	 * Returns the value of the requested attribute.
1180
	 *
1181
	 * @param string $key The attribute to retrieve
1182
	 * @return mixed
1183
	 */
1184
	function get_attribute( $key ) {
1185
		return isset( $this->attributes[ $key ] ) ? $this->attributes[ $key ] : null;
1186
	}
1187
1188
	function esc_attr( $value ) {
1189
		if ( is_array( $value ) ) {
1190
			return array_map( array( $this, 'esc_attr' ), $value );
1191
		}
1192
1193
		$value = Grunion_Contact_Form_Plugin::strip_tags( $value );
1194
		$value = _wp_specialchars( $value, ENT_QUOTES, false, true );
1195
1196
		// Shortcode attributes can't contain "]"
1197
		$value = str_replace( ']', '', $value );
1198
		$value = str_replace( ',', '&#x002c;', $value ); // store commas encoded
1199
		$value = strtr( $value, array( '%' => '%25', '&' => '%26' ) );
1200
1201
		// shortcode_parse_atts() does stripcslashes()
1202
		$value = addslashes( $value );
1203
		return $value;
1204
	}
1205
1206
	function unesc_attr( $value ) {
1207
		if ( is_array( $value ) ) {
1208
			return array_map( array( $this, 'unesc_attr' ), $value );
1209
		}
1210
1211
		// For back-compat with old Grunion encoding
1212
		// Also, unencode commas
1213
		$value = strtr( $value, array( '%26' => '&', '%25' => '%' ) );
1214
		$value = preg_replace( array( '/&#x0*22;/i', '/&#x0*27;/i', '/&#x0*26;/i', '/&#x0*2c;/i' ), array( '"', "'", '&', ',' ), $value );
1215
		$value = htmlspecialchars_decode( $value, ENT_QUOTES );
1216
		$value = Grunion_Contact_Form_Plugin::strip_tags( $value );
1217
1218
		return $value;
1219
	}
1220
1221
	/**
1222
	 * Generates the shortcode
1223
	 */
1224
	function __toString() {
1225
		$r = "[{$this->shortcode_name} ";
1226
1227
		foreach ( $this->attributes as $key => $value ) {
1228
			if ( ! $value ) {
1229
				continue;
1230
			}
1231
1232
			if ( isset( $this->defaults[ $key ] ) && $this->defaults[ $key ] == $value ) {
1233
				continue;
1234
			}
1235
1236
			if ( 'id' == $key ) {
1237
				continue;
1238
			}
1239
1240
			$value = $this->esc_attr( $value );
1241
1242
			if ( is_array( $value ) ) {
1243
				$value = join( ',', $value );
1244
			}
1245
1246
			if ( false === strpos( $value, "'" ) ) {
1247
				$value = "'$value'";
1248
			} elseif ( false === strpos( $value, '"' ) ) {
1249
				$value = '"' . $value . '"';
1250
			} else {
1251
				// Shortcodes can't contain both '"' and "'".  Strip one.
1252
				$value = str_replace( "'", '', $value );
1253
				$value = "'$value'";
1254
			}
1255
1256
			$r .= "{$key}={$value} ";
1257
		}
1258
1259
		$r = rtrim( $r );
1260
1261
		if ( $this->fields ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->fields of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
1262
			$r .= ']';
1263
1264
			foreach ( $this->fields as $field ) {
1265
				$r .= (string) $field;
1266
			}
1267
1268
			$r .= "[/{$this->shortcode_name}]";
1269
		} else {
1270
			$r .= '/]';
1271
		}
1272
1273
		return $r;
1274
	}
1275
}
1276
1277
/**
1278
 * Class for the contact-form shortcode.
1279
 * Parses shortcode to output the contact form as HTML
1280
 * Sends email and stores the contact form response (a.k.a. "feedback")
1281
 */
1282
class Grunion_Contact_Form extends Crunion_Contact_Form_Shortcode {
0 ignored issues
show
Coding Style Compatibility introduced by
PSR1 recommends that each class should be in its own file to aid autoloaders.

Having each class in a dedicated file usually plays nice with PSR autoloaders and is therefore a well established practice. If you use other autoloaders, you might not want to follow this rule.

Loading history...
1283
	public $shortcode_name = 'contact-form';
1284
1285
	/**
1286
	 * @var WP_Error stores form submission errors
1287
	 */
1288
	public $errors;
1289
1290
	/**
1291
	 * @var Grunion_Contact_Form The most recent (inclusive) contact-form shortcode processed
1292
	 */
1293
	static $last;
0 ignored issues
show
Coding Style introduced by
The visibility should be declared for property $last.

The PSR-2 coding standard requires that all properties in a class have their visibility explicitly declared. If you declare a property using

class A {
    var $property;
}

the property is implicitly global.

To learn more about the PSR-2, please see the PHP-FIG site on the PSR-2.

Loading history...
1294
1295
	/**
1296
	 * @var Whatever form we are currently looking at. If processed, will become $last
1297
	 */
1298
	static $current_form;
0 ignored issues
show
Coding Style introduced by
The visibility should be declared for property $current_form.

The PSR-2 coding standard requires that all properties in a class have their visibility explicitly declared. If you declare a property using

class A {
    var $property;
}

the property is implicitly global.

To learn more about the PSR-2, please see the PHP-FIG site on the PSR-2.

Loading history...
1299
1300
	/**
1301
	 * @var bool Whether to print the grunion.css style when processing the contact-form shortcode
1302
	 */
1303
	static $style = false;
0 ignored issues
show
Coding Style introduced by
The visibility should be declared for property $style.

The PSR-2 coding standard requires that all properties in a class have their visibility explicitly declared. If you declare a property using

class A {
    var $property;
}

the property is implicitly global.

To learn more about the PSR-2, please see the PHP-FIG site on the PSR-2.

Loading history...
1304
1305
	function __construct( $attributes, $content = null ) {
1306
		global $post;
1307
1308
		// Set up the default subject and recipient for this form
1309
		$default_to = '';
1310
		$default_subject = '[' . get_option( 'blogname' ) . ']';
1311
1312
		if ( ! isset( $attributes ) || ! is_array( $attributes ) ) {
1313
			$attributes = array();
1314
		}
1315
1316
		if ( ! empty( $attributes['widget'] ) && $attributes['widget'] ) {
1317
			$default_to .= get_option( 'admin_email' );
1318
			$attributes['id'] = 'widget-' . $attributes['widget'];
1319
			$default_subject = sprintf( _x( '%1$s Sidebar', '%1$s = blog name', 'jetpack' ), $default_subject );
1320
		} elseif ( $post ) {
1321
			$attributes['id'] = $post->ID;
1322
			$default_subject = sprintf( _x( '%1$s %2$s', '%1$s = blog name, %2$s = post title', 'jetpack' ), $default_subject, Grunion_Contact_Form_Plugin::strip_tags( $post->post_title ) );
1323
			$post_author = get_userdata( $post->post_author );
1324
			$default_to .= $post_author->user_email;
1325
		}
1326
1327
		// Keep reference to $this for parsing form fields
1328
		self::$current_form = $this;
0 ignored issues
show
Documentation Bug introduced by
It seems like $this of type this<Grunion_Contact_Form> is incompatible with the declared type object<Whatever> of property $current_form.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
1329
1330
		$this->defaults = array(
1331
			'to'                 => $default_to,
1332
			'subject'            => $default_subject,
1333
			'show_subject'       => 'no', // only used in back-compat mode
1334
			'widget'             => 0,    // Not exposed to the user. Works with Grunion_Contact_Form_Plugin::widget_atts()
1335
			'id'                 => null, // Not exposed to the user. Set above.
1336
			'submit_button_text' => __( 'Submit &#187;', 'jetpack' ),
1337
		);
1338
1339
		$attributes = shortcode_atts( $this->defaults, $attributes, 'contact-form' );
1340
1341
		// We only enable the contact-field shortcode temporarily while processing the contact-form shortcode
1342
		Grunion_Contact_Form_Plugin::$using_contact_form_field = true;
0 ignored issues
show
Bug introduced by
The property using_contact_form_field cannot be accessed from this context as it is declared private in class Grunion_Contact_Form_Plugin.

This check looks for access to properties that are not accessible from the current context.

If you need to make a property accessible to another context you can either raise its visibility level or provide an accessible getter in the defining class.

Loading history...
1343
1344
		parent::__construct( $attributes, $content );
1345
1346
		// There were no fields in the contact form. The form was probably just [contact-form /]. Build a default form.
1347
		if ( empty( $this->fields ) ) {
1348
			// same as the original Grunion v1 form
1349
			$default_form = '
1350
				[contact-field label="' . __( 'Name', 'jetpack' ) . '" type="name"  required="true" /]
1351
				[contact-field label="' . __( 'Email', 'jetpack' ) . '" type="email" required="true" /]
1352
				[contact-field label="' . __( 'Website', 'jetpack' ) . '" type="url" /]';
1353
1354
			if ( 'yes' == strtolower( $this->get_attribute( 'show_subject' ) ) ) {
1355
				$default_form .= '
1356
					[contact-field label="' . __( 'Subject', 'jetpack' ) . '" type="subject" /]';
1357
			}
1358
1359
			$default_form .= '
1360
				[contact-field label="' . __( 'Message', 'jetpack' ) . '" type="textarea" /]';
1361
1362
			$this->parse_content( $default_form );
1363
1364
			// Store the shortcode
1365
			$this->store_shortcode( $default_form, $attributes );
1366
		} else {
1367
			// Store the shortcode
1368
			$this->store_shortcode( $content, $attributes );
1369
		}
1370
1371
		// $this->body and $this->fields have been setup.  We no longer need the contact-field shortcode.
1372
		Grunion_Contact_Form_Plugin::$using_contact_form_field = false;
0 ignored issues
show
Bug introduced by
The property using_contact_form_field cannot be accessed from this context as it is declared private in class Grunion_Contact_Form_Plugin.

This check looks for access to properties that are not accessible from the current context.

If you need to make a property accessible to another context you can either raise its visibility level or provide an accessible getter in the defining class.

Loading history...
1373
	}
1374
1375
	/**
1376
	 * Store shortcode content for recall later
1377
	 *	- used to receate shortcode when user uses do_shortcode
1378
	 *
1379
	 * @param string $content
0 ignored issues
show
Documentation introduced by
Should the type for parameter $content not be string|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
1380
	 */
1381
	static function store_shortcode( $content = null, $attributes = null ) {
1382
1383
		if ( $content != null and isset( $attributes['id'] ) ) {
0 ignored issues
show
Bug introduced by
It seems like you are loosely comparing $content of type string|null against null; this is ambiguous if the string can be empty. Consider using a strict comparison !== instead.
Loading history...
Comprehensibility Best Practice introduced by
Using logical operators such as and instead of && is generally not recommended.

PHP has two types of connecting operators (logical operators, and boolean operators):

  Logical Operators Boolean Operator
AND - meaning and &&
OR - meaning or ||

The difference between these is the order in which they are executed. In most cases, you would want to use a boolean operator like &&, or ||.

Let’s take a look at a few examples:

// Logical operators have lower precedence:
$f = false or true;

// is executed like this:
($f = false) or true;


// Boolean operators have higher precedence:
$f = false || true;

// is executed like this:
$f = (false || true);

Logical Operators are used for Control-Flow

One case where you explicitly want to use logical operators is for control-flow such as this:

$x === 5
    or die('$x must be 5.');

// Instead of
if ($x !== 5) {
    die('$x must be 5.');
}

Since die introduces problems of its own, f.e. it makes our code hardly testable, and prevents any kind of more sophisticated error handling; you probably do not want to use this in real-world code. Unfortunately, logical operators cannot be combined with throw at this point:

// The following is currently a parse error.
$x === 5
    or throw new RuntimeException('$x must be 5.');

These limitations lead to logical operators rarely being of use in current PHP code.

Loading history...
1384
1385
			$shortcode_meta = get_post_meta( $attributes['id'], '_g_feedback_shortcode', true );
1386
1387
			if ( $shortcode_meta != '' or $shortcode_meta != $content ) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
Using logical operators such as or instead of || is generally not recommended.

PHP has two types of connecting operators (logical operators, and boolean operators):

  Logical Operators Boolean Operator
AND - meaning and &&
OR - meaning or ||

The difference between these is the order in which they are executed. In most cases, you would want to use a boolean operator like &&, or ||.

Let’s take a look at a few examples:

// Logical operators have lower precedence:
$f = false or true;

// is executed like this:
($f = false) or true;


// Boolean operators have higher precedence:
$f = false || true;

// is executed like this:
$f = (false || true);

Logical Operators are used for Control-Flow

One case where you explicitly want to use logical operators is for control-flow such as this:

$x === 5
    or die('$x must be 5.');

// Instead of
if ($x !== 5) {
    die('$x must be 5.');
}

Since die introduces problems of its own, f.e. it makes our code hardly testable, and prevents any kind of more sophisticated error handling; you probably do not want to use this in real-world code. Unfortunately, logical operators cannot be combined with throw at this point:

// The following is currently a parse error.
$x === 5
    or throw new RuntimeException('$x must be 5.');

These limitations lead to logical operators rarely being of use in current PHP code.

Loading history...
1388
				update_post_meta( $attributes['id'], '_g_feedback_shortcode', $content );
1389
1390
				// Save attributes to post_meta for later use. They're not available later in do_shortcode situations.
1391
				update_post_meta( $attributes['id'], '_g_feedback_shortcode_atts', $attributes );
1392
			}
1393
		}
1394
	}
1395
1396
	/**
1397
	 * Toggle for printing the grunion.css stylesheet
1398
	 *
1399
	 * @param bool $style
1400
	 */
1401
	static function style( $style ) {
1402
		$previous_style = self::$style;
1403
		self::$style = (bool) $style;
1404
		return $previous_style;
1405
	}
1406
1407
	/**
1408
	 * Turn on printing of grunion.css stylesheet
1409
	 *
1410
	 * @see ::style()
1411
	 * @internal
1412
	 * @param bool $style
0 ignored issues
show
Bug introduced by
There is no parameter named $style. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
1413
	 */
1414
	static function _style_on() {
1415
		return self::style( true );
1416
	}
1417
1418
	/**
1419
	 * The contact-form shortcode processor
1420
	 *
1421
	 * @param array       $attributes Key => Value pairs as parsed by shortcode_parse_atts()
1422
	 * @param string|null $content The shortcode's inner content: [contact-form]$content[/contact-form]
1423
	 * @return string HTML for the concat form.
1424
	 */
1425
	static function parse( $attributes, $content ) {
1426
		require_once JETPACK__PLUGIN_DIR . '/sync/class.jetpack-sync-settings.php';
1427
		if ( Jetpack_Sync_Settings::is_syncing() ) {
1428
			return '';
1429
		}
1430
		// Create a new Grunion_Contact_Form object (this class)
1431
		$form = new Grunion_Contact_Form( $attributes, $content );
1432
1433
		$id = $form->get_attribute( 'id' );
1434
1435
		if ( ! $id ) { // something terrible has happened
1436
			return '[contact-form]';
1437
		}
1438
1439
		if ( is_feed() ) {
1440
			return '[contact-form]';
1441
		}
1442
1443
		// Only allow one contact form per post/widget
1444
		if ( self::$last && $id == self::$last->get_attribute( 'id' ) ) {
1445
			// We're processing the same post
1446
			if ( self::$last->attributes != $form->attributes || self::$last->content != $form->content ) {
1447
				// And we're processing a different shortcode;
1448
				return '';
1449
			} // else, we're processing the same shortcode - probably a separate run of do_shortcode() - let it through
1450
1451
		} else {
1452
			self::$last = $form;
1453
		}
1454
1455
		// Enqueue the grunion.css stylesheet if self::$style allows it
1456
		if ( self::$style && ( empty( $_REQUEST['action'] ) || $_REQUEST['action'] != 'grunion_shortcode_to_json' ) ) {
1457
			// Enqueue the style here instead of printing it, because if some other plugin has run the_post()+rewind_posts(),
1458
			// (like VideoPress does), the style tag gets "printed" the first time and discarded, leaving the contact form unstyled.
1459
			// when WordPress does the real loop.
1460
			wp_enqueue_style( 'grunion.css' );
1461
		}
1462
1463
		$r = '';
1464
		$r .= "<div id='contact-form-$id'>\n";
1465
1466
		if ( is_wp_error( $form->errors ) && $form->errors->get_error_codes() ) {
1467
			// There are errors.  Display them
1468
			$r .= "<div class='form-error'>\n<h3>" . __( 'Error!', 'jetpack' ) . "</h3>\n<ul class='form-errors'>\n";
1469
			foreach ( $form->errors->get_error_messages() as $message ) {
1470
				$r .= "\t<li class='form-error-message'>" . esc_html( $message ) . "</li>\n";
1471
			}
1472
			$r .= "</ul>\n</div>\n\n";
1473
		}
1474
1475
		if ( isset( $_GET['contact-form-id'] ) && $_GET['contact-form-id'] == self::$last->get_attribute( 'id' ) && isset( $_GET['contact-form-sent'] ) ) {
1476
			// The contact form was submitted.  Show the success message/results
1477
			$feedback_id = (int) $_GET['contact-form-sent'];
1478
1479
			$back_url = remove_query_arg( array( 'contact-form-id', 'contact-form-sent', '_wpnonce' ) );
1480
1481
			$r_success_message =
1482
				'<h3>' . __( 'Message Sent', 'jetpack' ) .
1483
				' (<a href="' . esc_url( $back_url ) . '">' . esc_html__( 'go back', 'jetpack' ) . '</a>)' .
1484
				"</h3>\n\n";
1485
1486
			// Don't show the feedback details unless the nonce matches
1487
			if ( $feedback_id && wp_verify_nonce( stripslashes( $_GET['_wpnonce'] ), "contact-form-sent-{$feedback_id}" ) ) {
1488
				$r_success_message .= self::success_message( $feedback_id, $form );
1489
			}
1490
1491
			/**
1492
			 * Filter the message returned after a successfull contact form submission.
1493
			 *
1494
			 * @module contact-form
1495
			 *
1496
			 * @since 1.3.1
1497
			 *
1498
			 * @param string $r_success_message Success message.
1499
			 */
1500
			$r .= apply_filters( 'grunion_contact_form_success_message', $r_success_message );
1501
		} else {
1502
			// Nothing special - show the normal contact form
1503
			if ( $form->get_attribute( 'widget' ) ) {
1504
				// Submit form to the current URL
1505
				$url = remove_query_arg( array( 'contact-form-id', 'contact-form-sent', 'action', '_wpnonce' ) );
1506
			} else {
1507
				// Submit form to the post permalink
1508
				$url = get_permalink();
1509
			}
1510
1511
			// For SSL/TLS page. See RFC 3986 Section 4.2
1512
			$url = set_url_scheme( $url );
1513
1514
			// May eventually want to send this to admin-post.php...
1515
			/**
1516
			 * Filter the contact form action URL.
1517
			 *
1518
			 * @module contact-form
1519
			 *
1520
			 * @since 1.3.1
1521
			 *
1522
			 * @param string $contact_form_id Contact form post URL.
1523
			 * @param $post $GLOBALS['post'] Post global variable.
0 ignored issues
show
Documentation introduced by
The doc-type $post could not be parsed: Unknown type name "$post" at position 0. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
1524
			 * @param int $id Contact Form ID.
1525
			 */
1526
			$url = apply_filters( 'grunion_contact_form_form_action', "{$url}#contact-form-{$id}", $GLOBALS['post'], $id );
1527
1528
			$r .= "<form action='" . esc_url( $url ) . "' method='post' class='contact-form commentsblock'>\n";
1529
			$r .= $form->body;
1530
			$r .= "\t<p class='contact-submit'>\n";
1531
			$r .= "\t\t<input type='submit' value='" . esc_attr( $form->get_attribute( 'submit_button_text' ) ) . "' class='pushbutton-wide'/>\n";
1532
			if ( is_user_logged_in() ) {
1533
				$r .= "\t\t" . wp_nonce_field( 'contact-form_' . $id, '_wpnonce', true, false ) . "\n"; // nonce and referer
1534
			}
1535
			$r .= "\t\t<input type='hidden' name='contact-form-id' value='$id' />\n";
1536
			$r .= "\t\t<input type='hidden' name='action' value='grunion-contact-form' />\n";
1537
			$r .= "\t</p>\n";
1538
			$r .= "</form>\n";
1539
		}
1540
1541
		$r .= '</div>';
1542
1543
		return $r;
1544
	}
1545
1546
	/**
1547
	 * Returns a success message to be returned if the form is sent via AJAX.
1548
	 *
1549
	 * @param int                         $feedback_id
1550
	 * @param object Grunion_Contact_Form $form
1551
	 *
1552
	 * @return string $message
1553
	 */
1554
	static function success_message( $feedback_id, $form ) {
1555
		return wp_kses(
1556
			'<blockquote class="contact-form-submission">'
1557
			. '<p>' . join( self::get_compiled_form( $feedback_id, $form ), '</p><p>' ) . '</p>'
1558
			. '</blockquote>',
1559
			array( 'br' => array(), 'blockquote' => array( 'class' => array() ), 'p' => array() )
1560
		);
1561
	}
1562
1563
	/**
1564
	 * Returns a compiled form with labels and values in a form of  an array
1565
	 * of lines.
1566
	 *
1567
	 * @param int                         $feedback_id
1568
	 * @param object Grunion_Contact_Form $form
1569
	 *
1570
	 * @return array $lines
1571
	 */
1572
	static function get_compiled_form( $feedback_id, $form ) {
1573
		$feedback       = get_post( $feedback_id );
1574
		$field_ids      = $form->get_field_ids();
1575
		$content_fields = Grunion_Contact_Form_Plugin::parse_fields_from_content( $feedback_id );
1576
1577
		// Maps field_ids to post_meta keys
1578
		$field_value_map = array(
1579
			'name'     => 'author',
1580
			'email'    => 'author_email',
1581
			'url'      => 'author_url',
1582
			'subject'  => 'subject',
1583
			'textarea' => false, // not a post_meta key.  This is stored in post_content
1584
		);
1585
1586
		$compiled_form = array();
1587
1588
		// "Standard" field whitelist
1589
		foreach ( $field_value_map as $type => $meta_key ) {
1590
			if ( isset( $field_ids[ $type ] ) ) {
1591
				$field = $form->fields[ $field_ids[ $type ] ];
1592
1593
				if ( $meta_key ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $meta_key of type string|false is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
1594
					if ( isset( $content_fields[ "_feedback_{$meta_key}" ] ) ) {
1595
						$value = $content_fields[ "_feedback_{$meta_key}" ];
1596
					}
1597
				} else {
1598
					// The feedback content is stored as the first "half" of post_content
1599
					$value = $feedback->post_content;
1600
					list( $value ) = explode( '<!--more-->', $value );
1601
					$value = trim( $value );
1602
				}
1603
1604
				$field_index = array_search( $field_ids[ $type ], $field_ids['all'] );
1605
				$compiled_form[ $field_index ] = sprintf(
1606
					'<b>%1$s:</b> %2$s<br /><br />',
1607
					wp_kses( $field->get_attribute( 'label' ), array() ),
1608
					nl2br( wp_kses( $value, array() ) )
0 ignored issues
show
Bug introduced by
The variable $value does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
1609
				);
1610
			}
1611
		}
1612
1613
		// "Non-standard" fields
1614
		if ( $field_ids['extra'] ) {
1615
			// array indexed by field label (not field id)
1616
			$extra_fields = get_post_meta( $feedback_id, '_feedback_extra_fields', true );
1617
1618
			/**
1619
			 * Only get data for the compiled form if `$extra_fields` is a valid and non-empty array.
1620
			 */
1621
			if ( is_array( $extra_fields ) && ! empty( $extra_fields ) ) {
1622
1623
				$extra_field_keys = array_keys( $extra_fields );
1624
1625
				$i = 0;
1626
				foreach ( $field_ids['extra'] as $field_id ) {
1627
					$field       = $form->fields[ $field_id ];
1628
					$field_index = array_search( $field_id, $field_ids['all'] );
1629
1630
					$label = $field->get_attribute( 'label' );
1631
1632
					$compiled_form[ $field_index ] = sprintf(
1633
						'<b>%1$s:</b> %2$s<br /><br />',
1634
						wp_kses( $label, array() ),
1635
						nl2br( wp_kses( $extra_fields[ $extra_field_keys[ $i ] ], array() ) )
1636
					);
1637
1638
					$i++;
1639
				}
1640
			}
1641
		}
1642
1643
		// Sorting lines by the field index
1644
		ksort( $compiled_form );
1645
1646
		return $compiled_form;
1647
	}
1648
1649
	/**
1650
	 * The contact-field shortcode processor
1651
	 * We use an object method here instead of a static Grunion_Contact_Form_Field class method to parse contact-field shortcodes so that we can tie them to the contact-form object.
1652
	 *
1653
	 * @param array       $attributes Key => Value pairs as parsed by shortcode_parse_atts()
1654
	 * @param string|null $content The shortcode's inner content: [contact-field]$content[/contact-field]
1655
	 * @return HTML for the contact form field
1656
	 */
1657
	static function parse_contact_field( $attributes, $content ) {
1658
		// Don't try to parse contact form fields if not inside a contact form
1659
		if ( ! Grunion_Contact_Form_Plugin::$using_contact_form_field ) {
0 ignored issues
show
Bug introduced by
The property using_contact_form_field cannot be accessed from this context as it is declared private in class Grunion_Contact_Form_Plugin.

This check looks for access to properties that are not accessible from the current context.

If you need to make a property accessible to another context you can either raise its visibility level or provide an accessible getter in the defining class.

Loading history...
1660
			$att_strs = array();
1661
			foreach ( $attributes as $att => $val ) {
1662
				if ( is_numeric( $att ) ) { // Is a valueless attribute
1663
					$att_strs[] = esc_html( $val );
1664
				} elseif ( isset( $val ) ) { // A regular attr - value pair
1665
					$att_strs[] = esc_html( $att ) . '=\'' . esc_html( $val ) . '\'';
1666
				}
1667
			}
1668
1669
			$html = '[contact-field ' . implode( ' ', $att_strs );
1670
1671
			if ( isset( $content ) && ! empty( $content ) ) { // If there is content, let's add a closing tag
1672
				$html .= ']' . esc_html( $content ) . '[/contact-field]';
1673
			} else { // Otherwise let's add a closing slash in the first tag
1674
				$html .= '/]';
1675
			}
1676
1677
			return $html;
1678
		}
1679
1680
		$form = Grunion_Contact_Form::$current_form;
1681
1682
		$field = new Grunion_Contact_Form_Field( $attributes, $content, $form );
1683
1684
		$field_id = $field->get_attribute( 'id' );
1685
		if ( $field_id ) {
1686
			$form->fields[ $field_id ] = $field;
1687
		} else {
1688
			$form->fields[] = $field;
1689
		}
1690
1691
		if (
1692
			isset( $_POST['action'] ) && 'grunion-contact-form' === $_POST['action']
1693
		&&
1694
			isset( $_POST['contact-form-id'] ) && $form->get_attribute( 'id' ) == $_POST['contact-form-id']
1695
		) {
1696
			// If we're processing a POST submission for this contact form, validate the field value so we can show errors as necessary.
1697
			$field->validate();
1698
		}
1699
1700
		// Output HTML
1701
		return $field->render();
1702
	}
1703
1704
	/**
1705
	 * Loops through $this->fields to generate a (structured) list of field IDs.
1706
	 *
1707
	 * Important: Currently the whitelisted fields are defined as follows:
1708
	 *  `name`, `email`, `url`, `subject`, `textarea`
1709
	 *
1710
	 * If you need to add new fields to the Contact Form, please don't add them
1711
	 * to the whitelisted fields and leave them as extra fields.
1712
	 *
1713
	 * The reasoning behind this is that both the admin Feedback view and the CSV
1714
	 * export will not include any fields that are added to the list of
1715
	 * whitelisted fields without taking proper care to add them to all the
1716
	 * other places where they accessed/used/saved.
1717
	 *
1718
	 * The safest way to add new fields is to add them to the dropdown and the
1719
	 * HTML list ( @see Grunion_Contact_Form_Field::render ) and don't add them
1720
	 * to the list of whitelisted fields. This way they will become a part of the
1721
	 * `extra fields` which are saved in the post meta and will be properly
1722
	 * handled by the admin Feedback view and the CSV Export without any extra
1723
	 * work.
1724
	 *
1725
	 * If there is need to add a field to the whitelisted fields, then please
1726
	 * take proper care to add logic to handle the field in the following places:
1727
	 *
1728
	 *  - Below in the switch statement - so the field is recognized as whitelisted.
1729
	 *
1730
	 *  - Grunion_Contact_Form::process_submission - validation and logic.
1731
	 *
1732
	 *  - Grunion_Contact_Form::process_submission - add the field as an additional
1733
	 *      field in the `post_content` when saving the feedback content.
1734
	 *
1735
	 *  - Grunion_Contact_Form_Plugin::parse_fields_from_content - add mapping
1736
	 *      for the field, defined in the above method.
1737
	 *
1738
	 *  - Grunion_Contact_Form_Plugin::map_parsed_field_contents_of_post_to_field_names -
1739
	 *      add mapping of the field for the CSV Export. Otherwise it will be missing
1740
	 *      from the exported data.
1741
	 *
1742
	 *  - admin.php / grunion_manage_post_columns - add the field to the render logic.
1743
	 *      Otherwise it will be missing from the admin Feedback view.
1744
	 *
1745
	 * @return array
1746
	 */
1747
	function get_field_ids() {
1748
		$field_ids = array(
1749
			'all'   => array(), // array of all field_ids
1750
			'extra' => array(), // array of all non-whitelisted field IDs
1751
1752
			// Whitelisted "standard" field IDs:
1753
			// 'email'    => field_id,
1754
			// 'name'     => field_id,
1755
			// 'url'      => field_id,
1756
			// 'subject'  => field_id,
1757
			// 'textarea' => field_id,
1758
		);
1759
1760
		foreach ( $this->fields as $id => $field ) {
1761
			$field_ids['all'][] = $id;
1762
1763
			$type = $field->get_attribute( 'type' );
1764
			if ( isset( $field_ids[ $type ] ) ) {
1765
				// This type of field is already present in our whitelist of "standard" fields for this form
1766
				// Put it in extra
1767
				$field_ids['extra'][] = $id;
1768
				continue;
1769
			}
1770
1771
			/**
1772
			 * See method description before modifying the switch cases.
1773
			 */
1774
			switch ( $type ) {
1775
				case 'email' :
1776
				case 'name' :
1777
				case 'url' :
1778
				case 'subject' :
1779
				case 'textarea' :
1780
					$field_ids[ $type ] = $id;
1781
					break;
1782
				default :
0 ignored issues
show
Coding Style introduced by
There must be no space before the colon in a DEFAULT statement

As per the PSR-2 coding standard, there must not be a space in front of the colon in the default statement.

switch ($expr) {
    default : //wrong
        doSomething();
        break;
}

switch ($expr) {
    default: //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
1783
					// Put everything else in extra
1784
					$field_ids['extra'][] = $id;
1785
			}
1786
		}
1787
1788
		return $field_ids;
1789
	}
1790
1791
	/**
1792
	 * Process the contact form's POST submission
1793
	 * Stores feedback.  Sends email.
1794
	 */
1795
	function process_submission() {
1796
		global $post;
1797
1798
		$plugin = Grunion_Contact_Form_Plugin::init();
1799
1800
		$id     = $this->get_attribute( 'id' );
1801
		$to     = $this->get_attribute( 'to' );
1802
		$widget = $this->get_attribute( 'widget' );
1803
1804
		$contact_form_subject = $this->get_attribute( 'subject' );
1805
1806
		$to = str_replace( ' ', '', $to );
1807
		$emails = explode( ',', $to );
1808
1809
		$valid_emails = array();
1810
1811
		foreach ( (array) $emails as $email ) {
1812
			if ( ! is_email( $email ) ) {
1813
				continue;
1814
			}
1815
1816
			if ( function_exists( 'is_email_address_unsafe' ) && is_email_address_unsafe( $email ) ) {
1817
				continue;
1818
			}
1819
1820
			$valid_emails[] = $email;
1821
		}
1822
1823
		// No one to send it to, which means none of the "to" attributes are valid emails.
1824
		// Use default email instead.
1825
		if ( ! $valid_emails ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $valid_emails of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
1826
			$valid_emails = $this->defaults['to'];
1827
		}
1828
1829
		$to = $valid_emails;
1830
1831
		// Last ditch effort to set a recipient if somehow none have been set.
1832
		if ( empty( $to ) ) {
1833
			$to = get_option( 'admin_email' );
1834
		}
1835
1836
		// Make sure we're processing the form we think we're processing... probably a redundant check.
1837
		if ( $widget ) {
1838
			if ( 'widget-' . $widget != $_POST['contact-form-id'] ) {
1839
				return false;
1840
			}
1841
		} else {
1842
			if ( $post->ID != $_POST['contact-form-id'] ) {
1843
				return false;
1844
			}
1845
		}
1846
1847
		$field_ids = $this->get_field_ids();
1848
1849
		// Initialize all these "standard" fields to null
1850
		$comment_author_email = $comment_author_email_label = // v
1851
		$comment_author       = $comment_author_label       = // v
1852
		$comment_author_url   = $comment_author_url_label   = // v
1853
		$comment_content      = $comment_content_label      = null;
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned correctly; expected 1 space but found 6 spaces

This check looks for improperly formatted assignments.

Every assignment must have exactly one space before and one space after the equals operator.

To illustrate:

$a = "a";
$ab = "ab";
$abc = "abc";

will have no issues, while

$a   = "a";
$ab  = "ab";
$abc = "abc";

will report issues in lines 1 and 2.

Loading history...
1854
1855
		// For each of the "standard" fields, grab their field label and value.
1856 View Code Duplication
		if ( isset( $field_ids['name'] ) ) {
1857
			$field = $this->fields[ $field_ids['name'] ];
1858
			$comment_author = Grunion_Contact_Form_Plugin::strip_tags(
1859
				stripslashes(
1860
					/** This filter is already documented in core/wp-includes/comment-functions.php */
1861
					apply_filters( 'pre_comment_author_name', addslashes( $field->value ) )
1862
				)
1863
			);
1864
			$comment_author_label = Grunion_Contact_Form_Plugin::strip_tags( $field->get_attribute( 'label' ) );
1865
		}
1866
1867 View Code Duplication
		if ( isset( $field_ids['email'] ) ) {
1868
			$field = $this->fields[ $field_ids['email'] ];
1869
			$comment_author_email = Grunion_Contact_Form_Plugin::strip_tags(
1870
				stripslashes(
1871
					/** This filter is already documented in core/wp-includes/comment-functions.php */
1872
					apply_filters( 'pre_comment_author_email', addslashes( $field->value ) )
1873
				)
1874
			);
1875
			$comment_author_email_label = Grunion_Contact_Form_Plugin::strip_tags( $field->get_attribute( 'label' ) );
1876
		}
1877
1878
		if ( isset( $field_ids['url'] ) ) {
1879
			$field = $this->fields[ $field_ids['url'] ];
1880
			$comment_author_url = Grunion_Contact_Form_Plugin::strip_tags(
1881
				stripslashes(
1882
					/** This filter is already documented in core/wp-includes/comment-functions.php */
1883
					apply_filters( 'pre_comment_author_url', addslashes( $field->value ) )
1884
				)
1885
			);
1886
			if ( 'http://' == $comment_author_url ) {
1887
				$comment_author_url = '';
1888
			}
1889
			$comment_author_url_label = Grunion_Contact_Form_Plugin::strip_tags( $field->get_attribute( 'label' ) );
1890
		}
1891
1892
		if ( isset( $field_ids['textarea'] ) ) {
1893
			$field = $this->fields[ $field_ids['textarea'] ];
1894
			$comment_content = trim( Grunion_Contact_Form_Plugin::strip_tags( $field->value ) );
1895
			$comment_content_label = Grunion_Contact_Form_Plugin::strip_tags( $field->get_attribute( 'label' ) );
1896
		}
1897
1898
		if ( isset( $field_ids['subject'] ) ) {
1899
			$field = $this->fields[ $field_ids['subject'] ];
1900
			if ( $field->value ) {
1901
				$contact_form_subject = Grunion_Contact_Form_Plugin::strip_tags( $field->value );
1902
			}
1903
		}
1904
1905
		$all_values = $extra_values = array();
1906
		$i = 1; // Prefix counter for stored metadata
1907
1908
		// For all fields, grab label and value
1909
		foreach ( $field_ids['all'] as $field_id ) {
1910
			$field = $this->fields[ $field_id ];
1911
			$label = $i . '_' . $field->get_attribute( 'label' );
1912
			$value = $field->value;
1913
1914
			$all_values[ $label ] = $value;
1915
			$i++; // Increment prefix counter for the next field
1916
		}
1917
1918
		// For the "non-standard" fields, grab label and value
1919
		// Extra fields have their prefix starting from count( $all_values ) + 1
1920
		foreach ( $field_ids['extra'] as $field_id ) {
1921
			$field = $this->fields[ $field_id ];
1922
			$label = $i . '_' . $field->get_attribute( 'label' );
1923
			$value = $field->value;
1924
1925
			if ( is_array( $value ) ) {
1926
				$value = implode( ', ', $value );
1927
			}
1928
1929
			$extra_values[ $label ] = $value;
1930
			$i++; // Increment prefix counter for the next extra field
1931
		}
1932
1933
		$contact_form_subject = trim( $contact_form_subject );
1934
1935
		$comment_author_IP = Grunion_Contact_Form_Plugin::get_ip_address();
1936
1937
		$vars = array( 'comment_author', 'comment_author_email', 'comment_author_url', 'contact_form_subject', 'comment_author_IP' );
1938
		foreach ( $vars as $var ) {
1939
			$$var = str_replace( array( "\n", "\r" ), '', $$var );
1940
		}
1941
1942
		// Ensure that Akismet gets all of the relevant information from the contact form,
1943
		// not just the textarea field and predetermined subject.
1944
		$akismet_vars = compact( $vars );
1945
		$akismet_vars['comment_content'] = $comment_content;
1946
1947
		foreach ( array_merge( $field_ids['all'], $field_ids['extra'] ) as $field_id ) {
1948
			$field = $this->fields[ $field_id ];
1949
1950
			// Skip any fields that are just a choice from a pre-defined list. They wouldn't have any value
1951
			// from a spam-filtering point of view.
1952
			if ( in_array( $field->get_attribute( 'type' ), array( 'select', 'checkbox', 'checkbox-multiple', 'radio' ) ) ) {
1953
				continue;
1954
			}
1955
1956
			// Normalize the label into a slug.
1957
			$field_slug = trim( // Strip all leading/trailing dashes.
1958
				preg_replace(   // Normalize everything to a-z0-9_-
1959
					'/[^a-z0-9_]+/',
1960
					'-',
1961
					strtolower( $field->get_attribute( 'label' ) ) // Lowercase
1962
				),
1963
				'-'
1964
			);
1965
1966
			$field_value = ( is_array( $field->value ) ) ? trim( implode( ', ', $field->value ) ) : trim( $field->value );
1967
1968
			// Skip any values that are already in the array we're sending.
1969
			if ( $field_value && in_array( $field_value, $akismet_vars ) ) {
1970
				continue;
1971
			}
1972
1973
			$akismet_vars[ 'contact_form_field_' . $field_slug ] = $field_value;
1974
		}
1975
1976
		$spam = '';
1977
		$akismet_values = $plugin->prepare_for_akismet( $akismet_vars );
1978
1979
		// Is it spam?
1980
		/** This filter is already documented in modules/contact-form/admin.php */
1981
		$is_spam = apply_filters( 'jetpack_contact_form_is_spam', false, $akismet_values );
1982
		if ( is_wp_error( $is_spam ) ) { // WP_Error to abort
1983
			return $is_spam; // abort
1984
		} elseif ( $is_spam === true ) {  // TRUE to flag a spam
1985
			$spam = '***SPAM*** ';
1986
		}
1987
1988
		if ( ! $comment_author ) {
1989
			$comment_author = $comment_author_email;
1990
		}
1991
1992
		/**
1993
		 * Filter the email where a submitted feedback is sent.
1994
		 *
1995
		 * @module contact-form
1996
		 *
1997
		 * @since 1.3.1
1998
		 *
1999
		 * @param string|array $to Array of valid email addresses, or single email address.
2000
		 */
2001
		$to = (array) apply_filters( 'contact_form_to', $to );
2002
		$reply_to_addr = $to[0]; // get just the address part before the name part is added
2003
2004
		foreach ( $to as $to_key => $to_value ) {
2005
			$to[ $to_key ] = Grunion_Contact_Form_Plugin::strip_tags( $to_value );
2006
			$to[ $to_key ] = self::add_name_to_address( $to_value );
2007
		}
2008
2009
		$blog_url = parse_url( site_url() );
2010
		$from_email_addr = 'wordpress@' . $blog_url['host'];
2011
2012
		if ( ! empty( $comment_author_email ) ) {
2013
			$reply_to_addr = $comment_author_email;
2014
		}
2015
2016
		$headers = 'From: "' . $comment_author . '" <' . $from_email_addr . ">\r\n" .
2017
					'Reply-To: "' . $comment_author . '" <' . $reply_to_addr . ">\r\n";
2018
2019
		// Build feedback reference
2020
		$feedback_time  = current_time( 'mysql' );
2021
		$feedback_title = "{$comment_author} - {$feedback_time}";
2022
		$feedback_id    = md5( $feedback_title );
2023
2024
		$all_values = array_merge( $all_values, array(
2025
			'entry_title'     => the_title_attribute( 'echo=0' ),
2026
			'entry_permalink' => esc_url( get_permalink( get_the_ID() ) ),
2027
			'feedback_id'     => $feedback_id,
2028
		) );
2029
2030
		/** This filter is already documented in modules/contact-form/admin.php */
2031
		$subject = apply_filters( 'contact_form_subject', $contact_form_subject, $all_values );
2032
		$url     = $widget ? home_url( '/' ) : get_permalink( $post->ID );
2033
2034
		$date_time_format = _x( '%1$s \a\t %2$s', '{$date_format} \a\t {$time_format}', 'jetpack' );
2035
		$date_time_format = sprintf( $date_time_format, get_option( 'date_format' ), get_option( 'time_format' ) );
2036
		$time = date_i18n( $date_time_format, current_time( 'timestamp' ) );
2037
2038
		// keep a copy of the feedback as a custom post type
2039
		$feedback_status = $is_spam === true ? 'spam' : 'publish';
2040
2041
		foreach ( (array) $akismet_values as $av_key => $av_value ) {
2042
			$akismet_values[ $av_key ] = Grunion_Contact_Form_Plugin::strip_tags( $av_value );
2043
		}
2044
2045
		foreach ( (array) $all_values as $all_key => $all_value ) {
2046
			$all_values[ $all_key ] = Grunion_Contact_Form_Plugin::strip_tags( $all_value );
2047
		}
2048
2049
		foreach ( (array) $extra_values as $ev_key => $ev_value ) {
2050
			$extra_values[ $ev_key ] = Grunion_Contact_Form_Plugin::strip_tags( $ev_value );
2051
		}
2052
2053
		/*
2054
		 We need to make sure that the post author is always zero for contact
2055
		 * form submissions.  This prevents export/import from trying to create
2056
		 * new users based on form submissions from people who were logged in
2057
		 * at the time.
2058
		 *
2059
		 * Unfortunately wp_insert_post() tries very hard to make sure the post
2060
		 * author gets the currently logged in user id.  That is how we ended up
2061
		 * with this work around. */
2062
		add_filter( 'wp_insert_post_data', array( $plugin, 'insert_feedback_filter' ), 10, 2 );
2063
2064
		$post_id = wp_insert_post( array(
2065
			'post_date'    => addslashes( $feedback_time ),
2066
			'post_type'    => 'feedback',
2067
			'post_status'  => addslashes( $feedback_status ),
2068
			'post_parent'  => (int) $post->ID,
2069
			'post_title'   => addslashes( wp_kses( $feedback_title, array() ) ),
2070
			'post_content' => addslashes( wp_kses( $comment_content . "\n<!--more-->\n" . "AUTHOR: {$comment_author}\nAUTHOR EMAIL: {$comment_author_email}\nAUTHOR URL: {$comment_author_url}\nSUBJECT: {$subject}\nIP: {$comment_author_IP}\n" . @print_r( $all_values, true ), array() ) ), // so that search will pick up this data
2071
			'post_name'    => $feedback_id,
2072
		) );
2073
2074
		// once insert has finished we don't need this filter any more
2075
		remove_filter( 'wp_insert_post_data', array( $plugin, 'insert_feedback_filter' ), 10 );
2076
2077
		update_post_meta( $post_id, '_feedback_extra_fields', $this->addslashes_deep( $extra_values ) );
2078
2079
		if ( 'publish' == $feedback_status ) {
2080
			// Increase count of unread feedback.
2081
			$unread = get_option( 'feedback_unread_count', 0 ) + 1;
2082
			update_option( 'feedback_unread_count', $unread );
2083
		}
2084
2085
		if ( defined( 'AKISMET_VERSION' ) ) {
2086
			update_post_meta( $post_id, '_feedback_akismet_values', $this->addslashes_deep( $akismet_values ) );
2087
		}
2088
2089
		$message = self::get_compiled_form( $post_id, $this );
2090
2091
		array_push(
2092
			$message,
2093
			"<br />",
2094
			'<hr />',
2095
			__( 'Time:', 'jetpack' ) . ' ' . $time . '<br />',
2096
			__( 'IP Address:', 'jetpack' ) . ' ' . $comment_author_IP . '<br />',
2097
			__( 'Contact Form URL:', 'jetpack' ) . ' ' . $url . '<br />'
2098
		);
2099
2100
		if ( is_user_logged_in() ) {
2101
			array_push(
2102
				$message,
2103
				sprintf(
2104
					'<p>' . __( 'Sent by a verified %s user.', 'jetpack' ) . '</p>',
2105
					isset( $GLOBALS['current_site']->site_name ) && $GLOBALS['current_site']->site_name ?
2106
						$GLOBALS['current_site']->site_name : '"' . get_option( 'blogname' ) . '"'
2107
				)
2108
			);
2109
		} else {
2110
			array_push( $message, '<p>' . __( 'Sent by an unverified visitor to your site.', 'jetpack' ) . '</p>' );
2111
		}
2112
2113
		$message = join( $message, '' );
2114
2115
		/**
2116
		 * Filters the message sent via email after a successfull form submission.
2117
		 *
2118
		 * @module contact-form
2119
		 *
2120
		 * @since 1.3.1
2121
		 *
2122
		 * @param string $message Feedback email message.
2123
		 */
2124
		$message = apply_filters( 'contact_form_message', $message );
2125
2126
		// This is called after `contact_form_message`, in order to preserve back-compat
2127
		$message = self::wrap_message_in_html_tags( $message );
2128
2129
		update_post_meta( $post_id, '_feedback_email', $this->addslashes_deep( compact( 'to', 'message' ) ) );
2130
2131
		/**
2132
		 * Fires right before the contact form message is sent via email to
2133
		 * the recipient specified in the contact form.
2134
		 *
2135
		 * @module contact-form
2136
		 *
2137
		 * @since 1.3.1
2138
		 *
2139
		 * @param integer $post_id Post contact form lives on
2140
		 * @param array $all_values Contact form fields
2141
		 * @param array $extra_values Contact form fields not included in $all_values
2142
		 */
2143
		do_action( 'grunion_pre_message_sent', $post_id, $all_values, $extra_values );
2144
2145
		// schedule deletes of old spam feedbacks
2146
		if ( ! wp_next_scheduled( 'grunion_scheduled_delete' ) ) {
2147
			wp_schedule_event( time() + 250, 'daily', 'grunion_scheduled_delete' );
2148
		}
2149
2150
		if (
2151
			$is_spam !== true &&
2152
			/**
2153
			 * Filter to choose whether an email should be sent after each successfull contact form submission.
2154
			 *
2155
			 * @module contact-form
2156
			 *
2157
			 * @since 2.6.0
2158
			 *
2159
			 * @param bool true Should an email be sent after a form submission. Default to true.
2160
			 * @param int $post_id Post ID.
2161
			 */
2162
			true === apply_filters( 'grunion_should_send_email', true, $post_id )
2163
		) {
2164
			self::wp_mail( $to, "{$spam}{$subject}", $message, $headers );
2165
		} elseif (
2166
			true === $is_spam &&
2167
			/**
2168
			 * Choose whether an email should be sent for each spam contact form submission.
2169
			 *
2170
			 * @module contact-form
2171
			 *
2172
			 * @since 1.3.1
2173
			 *
2174
			 * @param bool false Should an email be sent after a spam form submission. Default to false.
2175
			 */
2176
			apply_filters( 'grunion_still_email_spam', false ) == true
2177
		) { // don't send spam by default.  Filterable.
2178
			self::wp_mail( $to, "{$spam}{$subject}", $message, $headers );
2179
		}
2180
2181
		if ( defined( 'DOING_AJAX' ) && DOING_AJAX ) {
2182
			return self::success_message( $post_id, $this );
2183
		}
2184
2185
		$redirect = wp_get_referer();
2186
		if ( ! $redirect ) { // wp_get_referer() returns false if the referer is the same as the current page
2187
			$redirect = $_SERVER['REQUEST_URI'];
2188
		}
2189
2190
		$redirect = add_query_arg( urlencode_deep( array(
2191
			'contact-form-id'   => $id,
2192
			'contact-form-sent' => $post_id,
2193
			'_wpnonce'          => wp_create_nonce( "contact-form-sent-{$post_id}" ), // wp_nonce_url HTMLencodes :(
2194
		) ), $redirect );
2195
2196
		/**
2197
		 * Filter the URL where the reader is redirected after submitting a form.
2198
		 *
2199
		 * @module contact-form
2200
		 *
2201
		 * @since 1.9.0
2202
		 *
2203
		 * @param string $redirect Post submission URL.
2204
		 * @param int $id Contact Form ID.
2205
		 * @param int $post_id Post ID.
2206
		 */
2207
		$redirect = apply_filters( 'grunion_contact_form_redirect_url', $redirect, $id, $post_id );
2208
2209
		wp_safe_redirect( $redirect );
2210
		exit;
0 ignored issues
show
Coding Style Compatibility introduced by
The method process_submission() contains an exit expression.

An exit expression should only be used in rare cases. For example, if you write a short command line script.

In most cases however, using an exit expression makes the code untestable and often causes incompatibilities with other libraries. Thus, unless you are absolutely sure it is required here, we recommend to refactor your code to avoid its usage.

Loading history...
2211
	}
2212
2213
	/**
2214
	 * Wrapper for wp_mail() that enables HTML messages with text alternatives
2215
	 *
2216
	 * @param string|array $to          Array or comma-separated list of email addresses to send message.
2217
	 * @param string       $subject     Email subject.
2218
	 * @param string       $message     Message contents.
2219
	 * @param string|array $headers     Optional. Additional headers.
2220
	 * @param string|array $attachments Optional. Files to attach.
2221
	 *
2222
	 * @return bool Whether the email contents were sent successfully.
2223
	 */
2224
	public static function wp_mail( $to, $subject, $message, $headers = '', $attachments = array() ) {
2225
		add_filter( 'wp_mail_content_type', __CLASS__ . '::get_mail_content_type' );
2226
		add_action( 'phpmailer_init',       __CLASS__ . '::add_plain_text_alternative' );
2227
2228
		$result = wp_mail( $to, $subject, $message, $headers, $attachments );
2229
2230
		remove_filter( 'wp_mail_content_type', __CLASS__ . '::get_mail_content_type' );
2231
		remove_action( 'phpmailer_init',       __CLASS__ . '::add_plain_text_alternative' );
2232
2233
		return $result;
2234
	}
2235
2236
	/**
2237
	 * Add a display name part to an email address
2238
	 *
2239
	 * SpamAssassin doesn't like addresses in HTML messages that are missing display names (e.g., `[email protected]`
2240
	 * instead of `"Foo Bar" <[email protected]>`.
2241
	 *
2242
	 * @param string $address
2243
	 *
2244
	 * @return string
2245
	 */
2246
	function add_name_to_address( $address ) {
2247
		// If it's just the address, without a display name
2248
		if ( is_email( $address ) ) {
2249
			$address = sprintf( '"%s" <%s>', $address, $address );
2250
		}
2251
2252
		return $address;
2253
	}
2254
2255
	/**
2256
	 * Get the content type that should be assigned to outbound emails
2257
	 *
2258
	 * @return string
2259
	 */
2260
	static function get_mail_content_type() {
2261
		return 'text/html';
2262
	}
2263
2264
	/**
2265
	 * Wrap a message body with the appropriate in HTML tags
2266
	 *
2267
	 * This helps to ensure correct parsing by clients, and also helps avoid triggering spam filtering rules
2268
	 *
2269
	 * @param string $body
2270
	 *
2271
	 * @return string
2272
	 */
2273
	static function wrap_message_in_html_tags( $body ) {
2274
		// Don't do anything if the message was already wrapped in HTML tags
2275
		// That could have be done by a plugin via filters
2276
		if ( false !== strpos( $body, '<html' ) ) {
2277
			return $body;
2278
		}
2279
2280
		$html_message = sprintf(
2281
			// The tabs are just here so that the raw code is correctly formatted for developers
2282
			// They're removed so that they don't affect the final message sent to users
2283
			str_replace( "\t", '',
2284
				"<!doctype html>
2285
				<html xmlns=\"http://www.w3.org/1999/xhtml\">
2286
				<body>
2287
2288
				%s
2289
2290
				</body>
2291
				</html>"
2292
			),
2293
			$body
2294
		);
2295
2296
		return $html_message;
2297
	}
2298
2299
	/**
2300
	 * Add a plain-text alternative part to an outbound email
2301
	 *
2302
	 * This makes the message more accessible to mail clients that aren't HTML-aware, and decreases the likelihood
2303
	 * that the message will be flagged as spam.
2304
	 *
2305
	 * @param PHPMailer $phpmailer
2306
	 */
2307
	static function add_plain_text_alternative( $phpmailer ) {
2308
		// Add an extra break so that the extra space above the <p> is preserved after the <p> is stripped out
2309
		$alt_body = str_replace( '<p>', '<p><br />', $phpmailer->Body );
2310
2311
		// Convert <br> to \n breaks, to preserve the space between lines that we want to keep
2312
		$alt_body = str_replace( array( '<br>', '<br />' ), "\n", $alt_body );
2313
2314
		// Convert <hr> to an plain-text equivalent, to preserve the integrity of the message
2315
		$alt_body = str_replace( array( "<hr>", "<hr />" ), "----\n", $alt_body );
2316
2317
		// Trim the plain text message to remove the \n breaks that were after <doctype>, <html>, and <body>
2318
		$phpmailer->AltBody = trim( strip_tags( $alt_body ) );
2319
	}
2320
2321
	function addslashes_deep( $value ) {
2322
		if ( is_array( $value ) ) {
2323
			return array_map( array( $this, 'addslashes_deep' ), $value );
2324
		} elseif ( is_object( $value ) ) {
2325
			$vars = get_object_vars( $value );
2326
			foreach ( $vars as $key => $data ) {
2327
				$value->{$key} = $this->addslashes_deep( $data );
2328
			}
2329
			return $value;
2330
		}
2331
2332
		return addslashes( $value );
2333
	}
2334
}
2335
2336
/**
2337
 * Class for the contact-field shortcode.
2338
 * Parses shortcode to output the contact form field as HTML.
2339
 * Validates input.
2340
 */
2341
class Grunion_Contact_Form_Field extends Crunion_Contact_Form_Shortcode {
0 ignored issues
show
Coding Style Compatibility introduced by
PSR1 recommends that each class should be in its own file to aid autoloaders.

Having each class in a dedicated file usually plays nice with PSR autoloaders and is therefore a well established practice. If you use other autoloaders, you might not want to follow this rule.

Loading history...
2342
	public $shortcode_name = 'contact-field';
2343
2344
	/**
2345
	 * @var Grunion_Contact_Form parent form
2346
	 */
2347
	public $form;
2348
2349
	/**
2350
	 * @var string default or POSTed value
2351
	 */
2352
	public $value;
2353
2354
	/**
2355
	 * @var bool Is the input invalid?
2356
	 */
2357
	public $error = false;
2358
2359
	/**
2360
	 * @param array                $attributes An associative array of shortcode attributes.  @see shortcode_atts()
2361
	 * @param null|string          $content Null for selfclosing shortcodes.  The inner content otherwise.
2362
	 * @param Grunion_Contact_Form $form The parent form
0 ignored issues
show
Documentation introduced by
Should the type for parameter $form not be Grunion_Contact_Form|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
2363
	 */
2364
	function __construct( $attributes, $content = null, $form = null ) {
2365
		$attributes = shortcode_atts( array(
2366
					'label'       => null,
2367
					'type'        => 'text',
2368
					'required'    => false,
2369
					'options'     => array(),
2370
					'id'          => null,
2371
					'default'     => null,
2372
					'values'      => null,
2373
					'placeholder' => null,
2374
					'class'       => null,
2375
		), $attributes, 'contact-field' );
2376
2377
		// special default for subject field
2378
		if ( 'subject' == $attributes['type'] && is_null( $attributes['default'] ) && ! is_null( $form ) ) {
2379
			$attributes['default'] = $form->get_attribute( 'subject' );
2380
		}
2381
2382
		// allow required=1 or required=true
2383
		if ( '1' == $attributes['required'] || 'true' == strtolower( $attributes['required'] ) ) {
2384
			$attributes['required'] = true;
2385
		} else { $attributes['required'] = false;
2386
		}
2387
2388
		// parse out comma-separated options list (for selects, radios, and checkbox-multiples)
2389
		if ( ! empty( $attributes['options'] ) && is_string( $attributes['options'] ) ) {
2390
			$attributes['options'] = array_map( 'trim', explode( ',', $attributes['options'] ) );
2391
2392 View Code Duplication
			if ( ! empty( $attributes['values'] ) && is_string( $attributes['values'] ) ) {
2393
				$attributes['values'] = array_map( 'trim', explode( ',', $attributes['values'] ) );
2394
			}
2395
		}
2396
2397
		if ( $form ) {
2398
			// make a unique field ID based on the label, with an incrementing number if needed to avoid clashes
2399
			$form_id = $form->get_attribute( 'id' );
2400
			$id = isset( $attributes['id'] ) ? $attributes['id'] : false;
2401
2402
			$unescaped_label = $this->unesc_attr( $attributes['label'] );
2403
			$unescaped_label = str_replace( '%', '-', $unescaped_label ); // jQuery doesn't like % in IDs?
2404
			$unescaped_label = preg_replace( '/[^a-zA-Z0-9.-_:]/', '', $unescaped_label );
2405
2406
			if ( empty( $id ) ) {
2407
				$id = sanitize_title_with_dashes( 'g' . $form_id . '-' . $unescaped_label );
2408
				$i = 0;
2409
				$max_tries = 99;
2410
				while ( isset( $form->fields[ $id ] ) ) {
2411
					$i++;
2412
					$id = sanitize_title_with_dashes( 'g' . $form_id . '-' . $unescaped_label . '-' . $i );
2413
2414
					if ( $i > $max_tries ) {
2415
						break;
2416
					}
2417
				}
2418
			}
2419
2420
			$attributes['id'] = $id;
2421
		}
2422
2423
		parent::__construct( $attributes, $content );
2424
2425
		// Store parent form
2426
		$this->form = $form;
2427
	}
2428
2429
	/**
2430
	 * This field's input is invalid.  Flag as invalid and add an error to the parent form
2431
	 *
2432
	 * @param string $message The error message to display on the form.
2433
	 */
2434
	function add_error( $message ) {
2435
		$this->is_error = true;
0 ignored issues
show
Bug introduced by
The property is_error does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
2436
2437
		if ( ! is_wp_error( $this->form->errors ) ) {
2438
			$this->form->errors = new WP_Error;
2439
		}
2440
2441
		$this->form->errors->add( $this->get_attribute( 'id' ), $message );
2442
	}
2443
2444
	/**
2445
	 * Is the field input invalid?
2446
	 *
2447
	 * @see $error
2448
	 *
2449
	 * @return bool
2450
	 */
2451
	function is_error() {
2452
		return $this->error;
2453
	}
2454
2455
	/**
2456
	 * Validates the form input
2457
	 */
2458
	function validate() {
2459
		// If it's not required, there's nothing to validate
2460
		if ( ! $this->get_attribute( 'required' ) ) {
2461
			return;
2462
		}
2463
2464
		$field_id    = $this->get_attribute( 'id' );
2465
		$field_type  = $this->get_attribute( 'type' );
2466
		$field_label = $this->get_attribute( 'label' );
2467
2468
		if ( isset( $_POST[ $field_id ] ) ) {
2469
			if ( is_array( $_POST[ $field_id ] ) ) {
2470
				$field_value = array_map( 'stripslashes', $_POST[ $field_id ] );
2471
			} else {
2472
				$field_value = stripslashes( $_POST[ $field_id ] );
2473
			}
2474
		} else {
2475
			$field_value = '';
2476
		}
2477
2478
		switch ( $field_type ) {
2479
			case 'email' :
2480
				// Make sure the email address is valid
2481
				if ( ! is_email( $field_value ) ) {
2482
					/* translators: %s is the name of a form field */
2483
					$this->add_error( sprintf( __( '%s requires a valid email address', 'jetpack' ), $field_label ) );
2484
				}
2485
			break;
2486
			case 'checkbox-multiple' :
2487
				// Check that there is at least one option selected
2488
				if ( empty( $field_value ) ) {
2489
					/* translators: %s is the name of a form field */
2490
					$this->add_error( sprintf( __( '%s requires at least one selection', 'jetpack' ), $field_label ) );
2491
				}
2492
			break;
2493
			default :
0 ignored issues
show
Coding Style introduced by
There must be no space before the colon in a DEFAULT statement

As per the PSR-2 coding standard, there must not be a space in front of the colon in the default statement.

switch ($expr) {
    default : //wrong
        doSomething();
        break;
}

switch ($expr) {
    default: //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
2494
				// Just check for presence of any text
2495
				if ( ! strlen( trim( $field_value ) ) ) {
2496
					/* translators: %s is the name of a form field */
2497
					$this->add_error( sprintf( __( '%s is required', 'jetpack' ), $field_label ) );
2498
				}
2499
		}
2500
	}
2501
2502
2503
	/**
2504
	 * Check the default value for options field
2505
	 *
2506
	 * @param string value
2507
	 * @param int index
2508
	 * @param string default value
2509
	 *
2510
	 * @return string
2511
	 */
2512
	public function get_option_value( $value, $index, $options ) {
2513
		if ( empty( $value[ $index ] ) ) {
2514
			return $options;
2515
		}
2516
		return $value[ $index ];
2517
	}
2518
2519
	/**
2520
	 * Outputs the HTML for this form field
2521
	 *
2522
	 * @return string HTML
2523
	 */
2524
	function render() {
2525
		global $current_user, $user_identity;
2526
2527
		$r = '';
2528
2529
		$field_id          = $this->get_attribute( 'id' );
2530
		$field_type        = $this->get_attribute( 'type' );
2531
		$field_label       = $this->get_attribute( 'label' );
2532
		$field_required    = $this->get_attribute( 'required' );
2533
		$placeholder       = $this->get_attribute( 'placeholder' );
2534
		$class             = $this->get_attribute( 'class' );
2535
		$field_placeholder = ( ! empty( $placeholder ) ) ? "placeholder='" . esc_attr( $placeholder ) . "'" : '';
2536
		$field_class       = "class='" . trim( esc_attr( $field_type ) . ' ' . esc_attr( $class ) ) . "' ";
2537
2538
		if ( isset( $_POST[ $field_id ] ) ) {
2539
			if ( is_array( $_POST[ $field_id ] ) ) {
2540
				$this->value = array_map( 'stripslashes', $_POST[ $field_id ] );
0 ignored issues
show
Documentation Bug introduced by
It seems like array_map('stripslashes', $_POST[$field_id]) of type array is incompatible with the declared type string of property $value.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
2541
			} else {
2542
				$this->value = stripslashes( (string) $_POST[ $field_id ] );
2543
			}
2544
		} elseif ( isset( $_GET[ $field_id ] ) ) {
2545
			$this->value = stripslashes( (string) $_GET[ $field_id ] );
2546
		} elseif (
2547
			is_user_logged_in() &&
2548
			( ( defined( 'IS_WPCOM' ) && IS_WPCOM ) ||
2549
			/**
2550
			 * Allow third-party tools to prefill the contact form with the user's details when they're logged in.
2551
			 *
2552
			 * @module contact-form
2553
			 *
2554
			 * @since 3.2.0
2555
			 *
2556
			 * @param bool false Should the Contact Form be prefilled with your details when you're logged in. Default to false.
2557
			 */
2558
			true === apply_filters( 'jetpack_auto_fill_logged_in_user', false )
2559
			)
2560
		) {
2561
			// Special defaults for logged-in users
2562
			switch ( $this->get_attribute( 'type' ) ) {
2563
				case 'email' :
2564
					$this->value = $current_user->data->user_email;
2565
				break;
2566
				case 'name' :
2567
					$this->value = $user_identity;
2568
				break;
2569
				case 'url' :
2570
					$this->value = $current_user->data->user_url;
2571
				break;
2572
				default :
0 ignored issues
show
Coding Style introduced by
There must be no space before the colon in a DEFAULT statement

As per the PSR-2 coding standard, there must not be a space in front of the colon in the default statement.

switch ($expr) {
    default : //wrong
        doSomething();
        break;
}

switch ($expr) {
    default: //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
2573
					$this->value = $this->get_attribute( 'default' );
2574
			}
2575
		} else {
2576
			$this->value = $this->get_attribute( 'default' );
2577
		}
2578
2579
		$field_value = Grunion_Contact_Form_Plugin::strip_tags( $this->value );
2580
		$field_label = Grunion_Contact_Form_Plugin::strip_tags( $field_label );
2581
2582
		/**
2583
		 * Filter the Contact Form required field text
2584
		 *
2585
		 * @module contact-form
2586
		 *
2587
		 * @since 3.8.0
2588
		 *
2589
		 * @param string $var Required field text. Default is "(required)".
2590
		 */
2591
		$required_field_text = esc_html( apply_filters( 'jetpack_required_field_text', __( '(required)', 'jetpack' ) ) );
2592
2593
		switch ( $field_type ) {
2594 View Code Duplication
			case 'email' :
2595
				$r .= "\n<div>\n";
2596
				$r .= "\t\t<label for='" . esc_attr( $field_id ) . "' class='grunion-field-label email" . ( $this->is_error() ? ' form-error' : '' ) . "'>" . esc_html( $field_label ) . ( $field_required ? '<span>' . $required_field_text . '</span>' : '' ) . "</label>\n";
2597
				$r .= "\t\t<input type='email' name='" . esc_attr( $field_id ) . "' id='" . esc_attr( $field_id ) . "' value='" . esc_attr( $field_value ) . "' " . $field_class . $field_placeholder . ' ' . ( $field_required ? "required aria-required='true'" : '' ) . "/>\n";
2598
				$r .= "\t</div>\n";
2599
			break;
2600
			case 'telephone' :
2601
				$r .= "\n<div>\n";
2602
				$r .= "\t\t<label for='" . esc_attr( $field_id ) . "' class='grunion-field-label telephone" . ( $this->is_error() ? ' form-error' : '' ) . "'>" . esc_html( $field_label ) . ( $field_required ? '<span>' . $required_field_text . '</span>' : '' ) . "</label>\n";
2603
				$r .= "\t\t<input type='tel' name='" . esc_attr( $field_id ) . "' id='" . esc_attr( $field_id ) . "' value='" . esc_attr( $field_value ) . "' " . $field_class . $field_placeholder . "/>\n";
2604
			break;
2605 View Code Duplication
			case 'textarea' :
2606
				$r .= "\n<div>\n";
2607
				$r .= "\t\t<label for='contact-form-comment-" . esc_attr( $field_id ) . "' class='grunion-field-label textarea" . ( $this->is_error() ? ' form-error' : '' ) . "'>" . esc_html( $field_label ) . ( $field_required ? '<span>' . $required_field_text . '</span>' : '' ) . "</label>\n";
2608
				$r .= "\t\t<textarea name='" . esc_attr( $field_id ) . "' id='contact-form-comment-" . esc_attr( $field_id ) . "' rows='20' " . $field_class . $field_placeholder . ' ' . ( $field_required ? "required aria-required='true'" : '' ) . '>' . esc_textarea( $field_value ) . "</textarea>\n";
2609
				$r .= "\t</div>\n";
2610
			break;
2611
			case 'radio' :
2612
				$r .= "\t<div><label class='grunion-field-label" . ( $this->is_error() ? ' form-error' : '' ) . "'>" . esc_html( $field_label ) . ( $field_required ? '<span>' . $required_field_text . '</span>' : '' ) . "</label>\n";
2613
				foreach ( $this->get_attribute( 'options' ) as $optionIndex => $option ) {
2614
					$option = Grunion_Contact_Form_Plugin::strip_tags( $option );
2615
					$r .= "\t\t<label class='grunion-radio-label radio" . ( $this->is_error() ? ' form-error' : '' ) . "'>";
2616
					$r .= "<input type='radio' name='" . esc_attr( $field_id ) . "' value='" . esc_attr( $this->get_option_value( $this->get_attribute( 'values' ), $optionIndex, $option ) ) . "' " . $field_class . checked( $option, $field_value, false ) . ' ' . ( $field_required ? "required aria-required='true'" : '' ) . '/> ';
2617
					$r .= esc_html( $option ) . "</label>\n";
2618
					$r .= "\t\t<div class='clear-form'></div>\n";
2619
				}
2620
				$r .= "\t\t</div>\n";
2621
			break;
2622
			case 'checkbox' :
2623
				$r .= "\t<div>\n";
2624
				$r .= "\t\t<label class='grunion-field-label checkbox" . ( $this->is_error() ? ' form-error' : '' ) . "'>\n";
2625
				$r .= "\t\t<input type='checkbox' name='" . esc_attr( $field_id ) . "' value='" . esc_attr__( 'Yes', 'jetpack' ) . "' " . $field_class . checked( (bool) $field_value, true, false ) . ' ' . ( $field_required ? "required aria-required='true'" : '' ) . "/> \n";
2626
				$r .= "\t\t" . esc_html( $field_label ) . ( $field_required ? '<span>' . $required_field_text . '</span>' : '' ) . "</label>\n";
2627
				$r .= "\t\t<div class='clear-form'></div>\n";
2628
				$r .= "\t</div>\n";
2629
			break;
2630
			case 'checkbox-multiple' :
2631
				$r .= "\t<div><label class='grunion-field-label" . ( $this->is_error() ? ' form-error' : '' ) . "'>" . esc_html( $field_label ) . ( $field_required ? '<span>' . $required_field_text . '</span>' : '' ) . "</label>\n";
2632
				foreach ( $this->get_attribute( 'options' ) as $optionIndex => $option ) {
2633
					$option = Grunion_Contact_Form_Plugin::strip_tags( $option );
2634
					$r .= "\t\t<label class='grunion-checkbox-multiple-label checkbox-multiple" . ( $this->is_error() ? ' form-error' : '' ) . "'>";
2635
					$r .= "<input type='checkbox' name='" . esc_attr( $field_id ) . "[]' value='" . esc_attr( $this->get_option_value( $this->get_attribute( 'values' ), $optionIndex, $option ) ) . "' " . $field_class . checked( in_array( $option, (array) $field_value ), true, false ) . ' /> ';
2636
					$r .= esc_html( $option ) . "</label>\n";
2637
					$r .= "\t\t<div class='clear-form'></div>\n";
2638
				}
2639
				$r .= "\t\t</div>\n";
2640
			break;
2641
			case 'select' :
2642
				$r .= "\n<div>\n";
2643
				$r .= "\t\t<label for='" . esc_attr( $field_id ) . "' class='grunion-field-label select" . ( $this->is_error() ? ' form-error' : '' ) . "'>" . esc_html( $field_label ) . ( $field_required ? '<span>' . $required_field_text . '</span>' : '' ) . "</label>\n";
2644
				$r .= "\t<select name='" . esc_attr( $field_id ) . "' id='" . esc_attr( $field_id ) . "' " . $field_class . ( $field_required ? "required aria-required='true'" : '' ) . ">\n";
2645
				foreach ( $this->get_attribute( 'options' ) as $optionIndex => $option ) {
2646
					$option = Grunion_Contact_Form_Plugin::strip_tags( $option );
2647
					$r .= "\t\t<option" . selected( $option, $field_value, false ) . " value='" . esc_attr( $this->get_option_value( $this->get_attribute( 'values' ), $optionIndex, $option ) ) . "'>" . esc_html( $option ) . "</option>\n";
2648
				}
2649
				$r .= "\t</select>\n";
2650
				$r .= "\t</div>\n";
2651
			break;
2652
			case 'date' :
2653
				$r .= "\n<div>\n";
2654
				$r .= "\t\t<label for='" . esc_attr( $field_id ) . "' class='grunion-field-label " . esc_attr( $field_type ) . ( $this->is_error() ? ' form-error' : '' ) . "'>" . esc_html( $field_label ) . ( $field_required ? '<span>' . $required_field_text . '</span>' : '' ) . "</label>\n";
2655
				$r .= "\t\t<input type='date' name='" . esc_attr( $field_id ) . "' id='" . esc_attr( $field_id ) . "' value='" . esc_attr( $field_value ) . "' " . $field_class . ( $field_required ? "required aria-required='true'" : '' ) . "/>\n";
2656
				$r .= "\t</div>\n";
2657
2658
				wp_enqueue_script( 'grunion-frontend', plugins_url( 'js/grunion-frontend.js', __FILE__ ), array( 'jquery', 'jquery-ui-datepicker' ) );
2659
			break;
2660 View Code Duplication
			default : // text field
0 ignored issues
show
Coding Style introduced by
There must be no space before the colon in a DEFAULT statement

As per the PSR-2 coding standard, there must not be a space in front of the colon in the default statement.

switch ($expr) {
    default : //wrong
        doSomething();
        break;
}

switch ($expr) {
    default: //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
2661
				// note that any unknown types will produce a text input, so we can use arbitrary type names to handle
2662
				// input fields like name, email, url that require special validation or handling at POST
2663
				$r .= "\n<div>\n";
2664
				$r .= "\t\t<label for='" . esc_attr( $field_id ) . "' class='grunion-field-label " . esc_attr( $field_type ) . ( $this->is_error() ? ' form-error' : '' ) . "'>" . esc_html( $field_label ) . ( $field_required ? '<span>' . $required_field_text . '</span>' : '' ) . "</label>\n";
2665
				$r .= "\t\t<input type='text' name='" . esc_attr( $field_id ) . "' id='" . esc_attr( $field_id ) . "' value='" . esc_attr( $field_value ) . "' " . $field_class . $field_placeholder . ' ' . ( $field_required ? "required aria-required='true'" : '' ) . "/>\n";
2666
				$r .= "\t</div>\n";
2667
		}
2668
2669
		/**
2670
		 * Filter the HTML of the Contact Form.
2671
		 *
2672
		 * @module contact-form
2673
		 *
2674
		 * @since 2.6.0
2675
		 *
2676
		 * @param string $r Contact Form HTML output.
2677
		 * @param string $field_label Field label.
2678
		 * @param int|null $id Post ID.
2679
		 */
2680
		return apply_filters( 'grunion_contact_form_field_html', $r, $field_label, ( in_the_loop() ? get_the_ID() : null ) );
2681
	}
2682
}
2683
2684
add_action( 'init', array( 'Grunion_Contact_Form_Plugin', 'init' ) );
2685
2686
add_action( 'grunion_scheduled_delete', 'grunion_delete_old_spam' );
2687
2688
/**
2689
 * Deletes old spam feedbacks to keep the posts table size under control
2690
 */
2691
function grunion_delete_old_spam() {
2692
	global $wpdb;
2693
2694
	$grunion_delete_limit = 100;
2695
2696
	$now_gmt = current_time( 'mysql', 1 );
2697
	$sql = $wpdb->prepare( "
2698
		SELECT `ID`
2699
		FROM $wpdb->posts
2700
		WHERE DATE_SUB( %s, INTERVAL 15 DAY ) > `post_date_gmt`
2701
			AND `post_type` = 'feedback'
2702
			AND `post_status` = 'spam'
2703
		LIMIT %d
2704
	", $now_gmt, $grunion_delete_limit );
2705
	$post_ids = $wpdb->get_col( $sql );
2706
2707
	foreach ( (array) $post_ids as $post_id ) {
2708
		// force a full delete, skip the trash
2709
		wp_delete_post( $post_id, true );
2710
	}
2711
2712
	// Arbitrary check points for running OPTIMIZE
2713
	// nothing special about 5000 or 11
2714
	// just trying to periodically recover deleted rows
2715
	$random_num = mt_rand( 1, 5000 );
2716
	if (
2717
		/**
2718
		 * Filter how often the module run OPTIMIZE TABLE on the core WP tables.
2719
		 *
2720
		 * @module contact-form
2721
		 *
2722
		 * @since 1.3.1
2723
		 *
2724
		 * @param int $random_num Random number.
2725
		 */
2726
		apply_filters( 'grunion_optimize_table', ( $random_num == 11 ) )
2727
	) {
2728
		$wpdb->query( "OPTIMIZE TABLE $wpdb->posts" );
2729
	}
2730
2731
	// if we hit the max then schedule another run
2732
	if ( count( $post_ids ) >= $grunion_delete_limit ) {
2733
		wp_schedule_single_event( time() + 700, 'grunion_scheduled_delete' );
2734
	}
2735
}
2736