Completed
Push — branch-4.1 ( 1541d4...3fb9ba )
by Jeremy
09:38
created

Grunion_Contact_Form_Plugin::unread_count()   B

Complexity

Conditions 7
Paths 6

Size

Total Lines 17
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
cc 7
eloc 12
c 1
b 0
f 1
nc 6
nop 1
dl 0
loc 17
rs 8.2222
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
 * Sets up various actions, filters, post types, post statuses, shortcodes.
21
 */
22
class Grunion_Contact_Form_Plugin {
23
24
	/**
25
	 * @var string The Widget ID of the widget currently being processed.  Used to build the unique contact-form ID for forms embedded in widgets.
26
	 */
27
	public $current_widget_id;
28
29
	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...
30
31
	static function init() {
32
		static $instance = false;
33
34
		if ( !$instance ) {
35
			$instance = new Grunion_Contact_Form_Plugin;
36
37
			// Schedule our daily cleanup
38
			add_action( 'wp_scheduled_delete', array( $instance, 'daily_akismet_meta_cleanup' ) );
39
		}
40
41
		return $instance;
42
	}
43
44
	/**
45
	 * Runs daily to clean up spam detection metadata after 15 days.  Keeps your DB squeaky clean.
46
	 */
47
	public function daily_akismet_meta_cleanup() {
48
		global $wpdb;
49
50
		$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" );
51
52
		if ( empty( $feedback_ids ) ) {
53
			return;
54
		}
55
56
		foreach ( $feedback_ids as $feedback_id ) {
57
			delete_post_meta( $feedback_id, '_feedback_akismet_values' );
58
		}
59
	}
60
61
		/**
62
	 * Strips HTML tags from input.  Output is NOT HTML safe.
63
	 *
64
	 * @param mixed $data_with_tags
65
	 * @return mixed
66
	 */
67
	public static function strip_tags( $data_with_tags ) {
68
		if ( is_array( $data_with_tags ) ) {
69
			foreach ( $data_with_tags as $index => $value ) {
70
				$index = sanitize_text_field( strval( $index ) );
71
				$value = wp_kses( strval( $value ), array() );
72
				$value = str_replace( '&amp;', '&', $value ); // undo damage done by wp_kses_normalize_entities()
73
74
				$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...
75
			}
76
		} else {
77
			$data_without_tags = wp_kses( $data_with_tags, array() );
78
			$data_without_tags = str_replace( '&amp;', '&', $data_without_tags ); // undo damage done by wp_kses_normalize_entities()
79
		}
80
81
		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...
82
	}
83
84
	function __construct() {
85
		$this->add_shortcode();
86
87
		// While generating the output of a text widget with a contact-form shortcode, we need to know its widget ID.
88
		add_action( 'dynamic_sidebar', array( $this, 'track_current_widget' ) );
89
90
		// Add a "widget" shortcode attribute to all contact-form shortcodes embedded in widgets
91
		add_filter( 'widget_text', array( $this, 'widget_atts' ), 0 );
92
93
		// If Text Widgets don't get shortcode processed, hack ours into place.
94
		if ( !has_filter( 'widget_text', 'do_shortcode' ) )
95
			add_filter( 'widget_text', array( $this, 'widget_shortcode_hack' ), 5 );
96
97
		// Akismet to the rescue
98
		if ( defined( 'AKISMET_VERSION' ) || function_exists( 'akismet_http_post' ) ) {
99
			add_filter( 'jetpack_contact_form_is_spam', array( $this, 'is_spam_akismet' ), 10, 2 );
100
			add_action( 'contact_form_akismet', array( $this, 'akismet_submit' ), 10, 2 );
101
		}
102
103
		add_action( 'loop_start', array( 'Grunion_Contact_Form', '_style_on' ) );
104
105
		add_action( 'wp_ajax_grunion-contact-form', array( $this, 'ajax_request' ) );
106
		add_action( 'wp_ajax_nopriv_grunion-contact-form', array( $this, 'ajax_request' ) );
107
108
		// Export to CSV feature
109
		if ( is_admin() ) {
110
			add_action( 'admin_init',            array( $this, 'download_feedback_as_csv' ) );
111
			add_action( 'admin_footer-edit.php', array( $this, 'export_form' ) );
112
			add_action( 'current_screen', array( $this, 'unread_count' ) );
113
		}
114
115
		// custom post type we'll use to keep copies of the feedback items
116
		register_post_type( 'feedback', array(
117
			'labels'            => array(
118
				'name'               => __( 'Feedback', 'jetpack' ),
119
				'singular_name'      => __( 'Feedback', 'jetpack' ),
120
				'search_items'       => __( 'Search Feedback', 'jetpack' ),
121
				'not_found'          => __( 'No feedback found', 'jetpack' ),
122
				'not_found_in_trash' => __( 'No feedback found', 'jetpack' )
123
			),
124
			'menu_icon'         => 'dashicons-feedback',
125
			'show_ui'           => TRUE,
126
			'show_in_admin_bar' => FALSE,
127
			'public'            => FALSE,
128
			'rewrite'           => FALSE,
129
			'query_var'         => FALSE,
130
			'capability_type'   => 'page',
131
			'show_in_rest'      => true,
132
			'capabilities'		=> array(
133
				'create_posts'        => false,
134
				'publish_posts'       => 'publish_pages',
135
				'edit_posts'          => 'edit_pages',
136
				'edit_others_posts'   => 'edit_others_pages',
137
				'delete_posts'        => 'delete_pages',
138
				'delete_others_posts' => 'delete_others_pages',
139
				'read_private_posts'  => 'read_private_pages',
140
				'edit_post'           => 'edit_page',
141
				'delete_post'         => 'delete_page',
142
				'read_post'           => 'read_page',
143
			),
144
			'map_meta_cap'		=> true,
145
		) );
146
147
		// Add to REST API post type whitelist
148
		add_filter( 'rest_api_allowed_post_types', array( $this, 'allow_feedback_rest_api_type' ) );
149
150
		// Add "spam" as a post status
151
		register_post_status( 'spam', array(
152
			'label'                  => 'Spam',
153
			'public'                 => FALSE,
154
			'exclude_from_search'    => TRUE,
155
			'show_in_admin_all_list' => FALSE,
156
			'label_count'            => _n_noop( 'Spam <span class="count">(%s)</span>', 'Spam <span class="count">(%s)</span>', 'jetpack' ),
157
			'protected'              => TRUE,
158
			'_builtin'               => FALSE
159
		) );
160
161
		// POST handler
162
		if (
163
			isset( $_SERVER['REQUEST_METHOD'] ) && 'POST' == strtoupper( $_SERVER['REQUEST_METHOD'] )
164
		&&
165
			isset( $_POST['action'] ) && 'grunion-contact-form' == $_POST['action']
166
		&&
167
			isset( $_POST['contact-form-id'] )
168
		) {
169
			add_action( 'template_redirect', array( $this, 'process_form_submission' ) );
170
		}
171
172
		/* Can be dequeued by placing the following in wp-content/themes/yourtheme/functions.php
173
		 *
174
		 * 	function remove_grunion_style() {
175
		 *		wp_deregister_style('grunion.css');
176
		 *	}
177
		 *	add_action('wp_print_styles', 'remove_grunion_style');
178
		 */
179
		if( is_rtl() ){
180
			wp_register_style( 'grunion.css', GRUNION_PLUGIN_URL . 'css/rtl/grunion-rtl.css', array(), JETPACK__VERSION );
181
		} else {
182
			wp_register_style( 'grunion.css', GRUNION_PLUGIN_URL . 'css/grunion.css', array(), JETPACK__VERSION );
183
		}
184
	}
185
186
	/**
187
	 * Add to REST API post type whitelist
188
	 */
189
	function allow_feedback_rest_api_type( $post_types ) {
190
		$post_types[] = 'feedback';
191
		return $post_types;
192
	}
193
194
	/**
195
	 * Display the count of new feedback entries received. It's reset when user visits the Feedback screen.
196
	 *
197
	 * @since 4.1.0
198
	 */
199
	function unread_count( $screen ) {
200
		if ( isset( $screen->post_type ) && 'feedback' == $screen->post_type ) {
201
			update_option( 'feedback_unread_count', 0 );
202
		} else {
203
			global $menu;
204
			foreach ( $menu as $index => $menu_item ) {
205
				if ( 'edit.php?post_type=feedback' == $menu_item[2] ) {
206
					$unread = get_option( 'feedback_unread_count', 0 );
207
					if ( $unread > 0 ) {
208
						$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>" : '';
209
						$menu[ $index ][0] .= $unread_count;
210
					}
211
					break;
212
				}
213
			}
214
		}
215
	}
216
217
	/**
218
	 * Handles all contact-form POST submissions
219
	 *
220
	 * Conditionally attached to `template_redirect`
221
	 */
222
	function process_form_submission() {
223
		// Add a filter to replace tokens in the subject field with sanitized field values
224
		add_filter( 'contact_form_subject', array( $this, 'replace_tokens_with_input' ), 10, 2 );
225
226
		$id = stripslashes( $_POST['contact-form-id'] );
227
228
		if ( is_user_logged_in() ) {
229
			check_admin_referer( "contact-form_{$id}" );
230
		}
231
232
		$is_widget = 0 === strpos( $id, 'widget-' );
233
234
		$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...
235
236
		if ( $is_widget ) {
237
			// It's a form embedded in a text widget
238
239
			$this->current_widget_id = substr( $id, 7 ); // remove "widget-"
240
			$widget_type = implode( '-', array_slice( explode( '-', $this->current_widget_id ), 0, -1 ) ); // Remove trailing -#
241
242
			// Is the widget active?
243
			$sidebar = is_active_widget( false, $this->current_widget_id, $widget_type );
244
245
			// This is lame - no core API for getting a widget by ID
246
			$widget = isset( $GLOBALS['wp_registered_widgets'][$this->current_widget_id] ) ? $GLOBALS['wp_registered_widgets'][$this->current_widget_id] : false;
247
248
			if ( $sidebar && $widget && isset( $widget['callback'] ) ) {
249
				// This is lamer - no API for outputting a given widget by ID
250
				ob_start();
251
				// Process the widget to populate Grunion_Contact_Form::$last
252
				call_user_func( $widget['callback'], array(), $widget['params'][0] );
253
				ob_end_clean();
254
			}
255
		} else {
256
			// It's a form embedded in a post
257
258
			$post = get_post( $id );
259
260
			// Process the content to populate Grunion_Contact_Form::$last
261
			/** This filter is already documented in core. wp-includes/post-template.php */
262
			apply_filters( 'the_content', $post->post_content );
263
		}
264
265
		$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...
266
267
		// No form may mean user is using do_shortcode, grab the form using the stored post meta
268
		if ( ! $form ) {
269
270
			// Get shortcode from post meta
271
			$shortcode = get_post_meta( $_POST['contact-form-id'], '_g_feedback_shortcode', true );
272
273
			// Format it
274
			if ( $shortcode != '' ) {
275
				$shortcode = '[contact-form]' . $shortcode . '[/contact-form]';
276
				do_shortcode( $shortcode );
277
278
				// Recreate form
279
				$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...
280
			}
281
282
			if ( ! $form ) {
283
				return false;
284
			}
285
		}
286
287
		if ( is_wp_error( $form->errors ) && $form->errors->get_error_codes() )
288
			return $form->errors;
289
290
		// Process the form
291
		return $form->process_submission();
292
	}
293
294
	function ajax_request() {
295
		$submission_result = self::process_form_submission();
296
297
		if ( ! $submission_result ) {
298
			header( "HTTP/1.1 500 Server Error", 500, true );
299
			echo '<div class="form-error"><ul class="form-errors"><li class="form-error-message">';
300
			esc_html_e( 'An error occurred. Please try again later.', 'jetpack' );
301
			echo '</li></ul></div>';
302
		} elseif ( is_wp_error( $submission_result ) ) {
303
			header( "HTTP/1.1 400 Bad Request", 403, true );
304
			echo '<div class="form-error"><ul class="form-errors"><li class="form-error-message">';
305
			echo esc_html( $submission_result->get_error_message() );
306
			echo '</li></ul></div>';
307
		} else {
308
			echo '<h3>' . esc_html__( 'Message Sent', 'jetpack' ) . '</h3>' . $submission_result;
309
		}
310
311
		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...
312
	}
313
314
	/**
315
	 * Ensure the post author is always zero for contact-form feedbacks
316
	 * Attached to `wp_insert_post_data`
317
	 *
318
	 * @see Grunion_Contact_Form::process_submission()
319
	 *
320
	 * @param array $data the data to insert
321
	 * @param array $postarr the data sent to wp_insert_post()
322
	 * @return array The filtered $data to insert
323
	 */
324
	function insert_feedback_filter( $data, $postarr ) {
325
		if ( $data['post_type'] == 'feedback' && $postarr['post_type'] == 'feedback' ) {
326
			$data['post_author'] = 0;
327
		}
328
329
		return $data;
330
	}
331
	/*
332
	 * Adds our contact-form shortcode
333
	 * The "child" contact-field shortcode is enabled as needed by the contact-form shortcode handler
334
	 */
335
	function add_shortcode() {
336
		add_shortcode( 'contact-form',         array( 'Grunion_Contact_Form', 'parse' ) );
337
		add_shortcode( 'contact-field',        array( 'Grunion_Contact_Form', 'parse_contact_field' ) );
338
	}
339
340
	static function tokenize_label( $label ) {
341
		return '{' . trim( preg_replace( '#^\d+_#', '', $label ) ) . '}';
342
	}
343
344
	static function sanitize_value( $value ) {
345
		return preg_replace( '=((<CR>|<LF>|0x0A/%0A|0x0D/%0D|\\n|\\r)\S).*=i', null, $value );
346
	}
347
348
	/**
349
	 * Replaces tokens like {city} or {City} (case insensitive) with the value
350
	 * of an input field of that name
351
	 *
352
	 * @param string $subject
353
	 * @param array $field_values Array with field label => field value associations
354
	 *
355
	 * @return string The filtered $subject with the tokens replaced
356
	 */
357
	function replace_tokens_with_input( $subject, $field_values ) {
358
		// Wrap labels into tokens (inside {})
359
		$wrapped_labels = array_map( array( 'Grunion_Contact_Form_Plugin', 'tokenize_label' ), array_keys( $field_values ) );
360
		// Sanitize all values
361
		$sanitized_values = array_map( array( 'Grunion_Contact_Form_Plugin', 'sanitize_value' ), array_values( $field_values ) );
362
363
		foreach ( $sanitized_values as $k => $sanitized_value ) {
364
			if ( is_array( $sanitized_value ) ) {
365
				$sanitized_values[ $k ] = implode( ', ', $sanitized_value );
366
			}
367
		}
368
369
		// Search for all valid tokens (based on existing fields) and replace with the field's value
370
		$subject = str_ireplace( $wrapped_labels, $sanitized_values, $subject );
371
		return $subject;
372
	}
373
374
	/**
375
	 * Tracks the widget currently being processed.
376
	 * Attached to `dynamic_sidebar`
377
	 *
378
	 * @see $current_widget_id
379
	 *
380
	 * @param array $widget The widget data
381
	 */
382
	function track_current_widget( $widget ) {
383
		$this->current_widget_id = $widget['id'];
384
	}
385
386
	/**
387
	 * Adds a "widget" attribute to every contact-form embedded in a text widget.
388
	 * Used to tell the difference between post-embedded contact-forms and widget-embedded contact-forms
389
	 * Attached to `widget_text`
390
	 *
391
	 * @param string $text The widget text
392
	 * @return string The filtered widget text
393
	 */
394
	function widget_atts( $text ) {
395
		Grunion_Contact_Form::style( true );
396
397
		return preg_replace( '/\[contact-form([^a-zA-Z_-])/', '[contact-form widget="' . $this->current_widget_id . '"\\1', $text );
398
	}
399
400
	/**
401
	 * For sites where text widgets are not processed for shortcodes, we add this hack to process just our shortcode
402
	 * Attached to `widget_text`
403
	 *
404
	 * @param string $text The widget text
405
	 * @return string The contact-form filtered widget text
406
	 */
407
	function widget_shortcode_hack( $text ) {
408
		if ( !preg_match( '/\[contact-form([^a-zA-Z_-])/', $text ) ) {
409
			return $text;
410
		}
411
412
		$old = $GLOBALS['shortcode_tags'];
413
		remove_all_shortcodes();
414
		Grunion_Contact_Form_Plugin::$using_contact_form_field = true;
415
		$this->add_shortcode();
416
417
		$text = do_shortcode( $text );
418
419
		Grunion_Contact_Form_Plugin::$using_contact_form_field = false;
420
		$GLOBALS['shortcode_tags'] = $old;
421
422
		return $text;
423
	}
424
425
	/**
426
	 * Populate an array with all values necessary to submit a NEW contact-form feedback to Akismet.
427
	 * Note that this includes the current user_ip etc, so this should only be called when accepting a new item via $_POST
428
	 *
429
	 * @param array $form Contact form feedback array
430
	 * @return array feedback array with additional data ready for submission to Akismet
431
	 */
432
	function prepare_for_akismet( $form ) {
433
		$form['comment_type'] = 'contact_form';
434
		$form['user_ip']      = $_SERVER['REMOTE_ADDR'];
435
		$form['user_agent']   = $_SERVER['HTTP_USER_AGENT'];
436
		$form['referrer']     = $_SERVER['HTTP_REFERER'];
437
		$form['blog']         = get_option( 'home' );
438
439
		foreach ( $_SERVER as $key => $value ) {
440
			if ( ! is_string( $value ) ) {
441
				continue;
442
			}
443
			if ( in_array( $key, array( 'HTTP_COOKIE', 'HTTP_COOKIE2', 'HTTP_USER_AGENT', 'HTTP_REFERER' ) ) ) {
444
				// We don't care about cookies, and the UA and Referrer were caught above.
445
				continue;
446
			} elseif ( in_array( $key, array( 'REMOTE_ADDR', 'REQUEST_URI', 'DOCUMENT_URI' ) ) ) {
447
				// All three of these are relevant indicators and should be passed along.
448
				$form[ $key ] = $value;
449
			} elseif ( wp_startswith( $key, 'HTTP_' ) ) {
450
				// Any other HTTP header indicators.
451
				// `wp_startswith()` is a wpcom helper function and is included in Jetpack via `functions.compat.php`
452
				$form[ $key ] = $value;
453
			}
454
		}
455
456
		return $form;
457
	}
458
459
	/**
460
	 * Submit contact-form data to Akismet to check for spam.
461
	 * If you're accepting a new item via $_POST, run it Grunion_Contact_Form_Plugin::prepare_for_akismet() first
462
	 * Attached to `jetpack_contact_form_is_spam`
463
	 *
464
	 * @param bool $is_spam
465
	 * @param array $form
466
	 * @return bool|WP_Error TRUE => spam, FALSE => not spam, WP_Error => stop processing entirely
467
	 */
468
	function is_spam_akismet( $is_spam, $form = array() ) {
469
		global $akismet_api_host, $akismet_api_port;
470
471
		// The signature of this function changed from accepting just $form.
472
		// If something only sends an array, assume it's still using the old
473
		// signature and work around it.
474
		if ( empty( $form ) && is_array( $is_spam ) ) {
475
			$form = $is_spam;
476
			$is_spam = false;
477
		}
478
479
		// If a previous filter has alrady marked this as spam, trust that and move on.
480
		if ( $is_spam ) {
481
			return $is_spam;
482
		}
483
484
		if ( !function_exists( 'akismet_http_post' ) && !defined( 'AKISMET_VERSION' ) )
485
			return false;
486
487
		$query_string = http_build_query( $form );
488
489
		if ( method_exists( 'Akismet', 'http_post' ) ) {
490
			$response = Akismet::http_post( $query_string, 'comment-check' );
491
		} else {
492
			$response = akismet_http_post( $query_string, $akismet_api_host, '/1.1/comment-check', $akismet_api_port );
493
		}
494
495
		$result = false;
496
497
		if ( isset( $response[0]['x-akismet-pro-tip'] ) && 'discard' === trim( $response[0]['x-akismet-pro-tip'] ) && get_option( 'akismet_strictness' ) === '1' )
498
			$result = new WP_Error( 'feedback-discarded', __('Feedback discarded.', 'jetpack' ) );
499
		elseif ( isset( $response[1] ) && 'true' == trim( $response[1] ) ) // 'true' is spam
500
			$result = true;
501
502
		/**
503
		 * Filter the results returned by Akismet for each submitted contact form.
504
		 *
505
		 * @module contact-form
506
		 *
507
		 * @since 1.3.1
508
		 *
509
		 * @param WP_Error|bool $result Is the submitted feedback spam.
510
		 * @param array|bool $form Submitted feedback.
511
		 */
512
		return apply_filters( 'contact_form_is_spam_akismet', $result, $form );
513
	}
514
515
	/**
516
	 * Submit a feedback as either spam or ham
517
	 *
518
	 * @param string $as Either 'spam' or 'ham'.
519
	 * @param array $form the contact-form data
520
	 */
521
	function akismet_submit( $as, $form ) {
522
		global $akismet_api_host, $akismet_api_port;
523
524
		if ( !in_array( $as, array( 'ham', 'spam' ) ) )
525
			return false;
526
527
		$query_string = '';
528
		if ( is_array( $form ) )
529
			$query_string = http_build_query( $form );
530
		if ( method_exists( 'Akismet', 'http_post' ) ) {
531
		    $response = Akismet::http_post( $query_string, "submit-{$as}" );
532
		} else {
533
		    $response = akismet_http_post( $query_string, $akismet_api_host, "/1.1/submit-{$as}", $akismet_api_port );
534
		}
535
536
		return trim( $response[1] );
537
	}
538
539
	/**
540
	 * Prints the menu
541
	 */
542
	function export_form() {
543
		if ( get_current_screen()->id != 'edit-feedback' )
544
			return;
545
546
		if ( ! current_user_can( 'export' ) ) {
547
			return;
548
		}
549
550
		// if there aren't any feedbacks, bail out
551
		if ( ! (int) wp_count_posts( 'feedback' )->publish )
552
			return;
553
		?>
554
555
		<div id="feedback-export" style="display:none">
556
			<h2><?php _e( 'Export feedback as CSV', 'jetpack' ) ?></h2>
557
			<div class="clear"></div>
558
			<form action="<?php echo admin_url( 'admin-post.php' ); ?>" method="post" class="form">
559
				<?php wp_nonce_field( 'feedback_export','feedback_export_nonce' ); ?>
560
561
				<input name="action" value="feedback_export" type="hidden">
562
				<label for="post"><?php _e( 'Select feedback to download', 'jetpack' ) ?></label>
563
				<select name="post">
564
					<option value="all"><?php esc_html_e( 'All posts', 'jetpack' ) ?></option>
565
					<?php echo $this->get_feedbacks_as_options() ?>
566
				</select>
567
568
				<br><br>
569
				<input type="submit" name="submit" id="submit" class="button button-primary" value="<?php esc_html_e( 'Download', 'jetpack' ); ?>">
570
			</form>
571
		</div>
572
573
		<?php
574
		// There aren't any usable actions in core to output the "export feedback" form in the correct place,
575
		// so this inline JS moves it from the top of the page to the bottom.
576
		?>
577
		<script type='text/javascript'>
578
		var menu = document.getElementById( 'feedback-export' ),
579
		wrapper = document.getElementsByClassName( 'wrap' )[0];
580
		wrapper.appendChild(menu);
581
		menu.style.display = 'block';
582
		</script>
583
		<?php
584
	}
585
586
	/**
587
	 * Fetch post content for a post and extract just the comment.
588
	 *
589
	 * @param int $post_id The post id to fetch the content for.
590
	 *
591
	 * @return string Trimmed post comment.
592
	 *
593
	 * @codeCoverageIgnore
594
	 */
595
	public function get_post_content_for_csv_export( $post_id ) {
596
		$post_content = get_post_field( 'post_content', $post_id );
597
		$content      = explode( '<!--more-->', $post_content );
598
599
		return trim( $content[0] );
600
	}
601
602
	/**
603
	 * Get `_feedback_extra_fields` field from post meta data.
604
	 *
605
	 * @param int $post_id Id of the post to fetch meta data for.
606
	 *
607
	 * @return mixed
608
	 *
609
	 * @codeCoverageIgnore - No need to be covered.
610
	 */
611
	public function get_post_meta_for_csv_export( $post_id ) {
612
		return get_post_meta( $post_id, '_feedback_extra_fields', true );
613
	}
614
615
	/**
616
	 * Get parsed feedback post fields.
617
	 *
618
	 * @param int $post_id Id of the post to fetch parsed contents for.
619
	 *
620
	 * @return array
621
	 *
622
	 * @codeCoverageIgnore - No need to be covered.
623
	 */
624
	public function get_parsed_field_contents_of_post( $post_id ) {
625
		return self::parse_fields_from_content( $post_id );
626
	}
627
628
	/**
629
	 * Properly maps fields that are missing from the post meta data
630
	 * to names, that are similar to those of the post meta.
631
	 *
632
	 * @param array $parsed_post_content Parsed post content
633
	 *
634
	 * @see parse_fields_from_content for how the input data is generated.
635
	 *
636
	 * @return array Mapped fields.
637
	 */
638
	public function map_parsed_field_contents_of_post_to_field_names( $parsed_post_content ) {
639
640
		$mapped_fields = array();
641
642
		$field_mapping = array(
643
			'_feedback_subject'      => __( 'Contact Form', 'jetpack' ),
644
			'_feedback_author'       => '1_Name',
645
			'_feedback_author_email' => '2_Email',
646
			'_feedback_author_url'   => '3_Website',
647
			'_feedback_main_comment' => '4_Comment',
648
		);
649
650
		foreach ( $field_mapping as $parsed_field_name => $field_name ) {
651
			if (
652
				isset( $parsed_post_content[ $parsed_field_name ] )
653
				&& ! empty( $parsed_post_content[ $parsed_field_name ] )
654
			) {
655
				$mapped_fields[ $field_name ] = $parsed_post_content[ $parsed_field_name ];
656
			}
657
		}
658
659
		return $mapped_fields;
660
	}
661
662
663
	/**
664
	 * Prepares feedback post data for CSV export.
665
	 *
666
	 * @param array $post_ids Post IDs to fetch the data for. These need to be Feedback posts.
667
	 *
668
	 * @return array
669
	 */
670
	public function get_export_data_for_posts( $post_ids ) {
671
672
		$posts_data  = array();
673
		$field_names = array();
674
		$result      = array();
675
676
		/**
677
		 * Fetch posts and get the possible field names for later use
678
		 */
679
		foreach ( $post_ids as $post_id ) {
680
681
			/**
682
			 * Fetch post main data, because we need the subject and author data for the feedback form.
683
			 */
684
			$post_real_data = $this->get_parsed_field_contents_of_post( $post_id );
685
686
			/**
687
			 * If `$post_real_data` is not an array or there is no `_feedback_subject` set,
688
			 * then something must be wrong with the feedback post. Skip it.
689
			 */
690
			if ( ! is_array( $post_real_data ) || ! isset( $post_real_data['_feedback_subject'] ) ) {
691
				continue;
692
			}
693
694
			/**
695
			 * Fetch main post comment. This is from the default textarea fields.
696
			 * If it is non-empty, then we add it to data, otherwise skip it.
697
			 */
698
			$post_comment_content = $this->get_post_content_for_csv_export( $post_id );
699
			if ( ! empty( $post_comment_content ) ) {
700
				$post_real_data['_feedback_main_comment'] = $post_comment_content;
701
			}
702
703
			/**
704
			 * Map parsed fields to proper field names
705
			 */
706
			$mapped_fields = $this->map_parsed_field_contents_of_post_to_field_names( $post_real_data );
707
708
			/**
709
			 * Fetch post meta data.
710
			 */
711
			$post_meta_data = $this->get_post_meta_for_csv_export( $post_id );
712
713
			/**
714
			 * If `$post_meta_data` is not an array or if it is empty, then there is no
715
			 * extra feedback to work with. Create an empty array.
716
			 */
717
			if ( ! is_array( $post_meta_data ) || empty( $post_meta_data ) ) {
718
				$post_meta_data = array();
719
			}
720
721
			/**
722
			 * Prepend the feedback subject to the list of fields.
723
			 */
724
			$post_meta_data = array_merge(
725
				$mapped_fields,
726
				$post_meta_data
727
			);
728
729
730
			/**
731
			 * Save post metadata for later usage.
732
			 */
733
			$posts_data[ $post_id ] = $post_meta_data;
734
735
			/**
736
			 * Save field names, so we can use them as header fields later in the CSV.
737
			 */
738
			$field_names = array_merge( $field_names, array_keys( $post_meta_data ) );
739
		}
740
741
		/**
742
		 * Make sure the field names are unique, because we don't want duplicate data.
743
		 */
744
		$field_names = array_unique( $field_names );
745
746
747
		/**
748
		 * Sort the field names by the field id number
749
		 */
750
		sort( $field_names, SORT_NUMERIC );
751
752
		/**
753
		 * Loop through every post, which is essentially CSV row.
754
		 */
755
		foreach ( $posts_data as $post_id => $single_post_data ) {
756
757
			/**
758
			 * Go through all the possible fields and check if the field is available
759
			 * in the current post.
760
			 *
761
			 * If it is - add the data as a value.
762
			 * If it is not - add an empty string, which is just a placeholder in the CSV.
763
			 */
764
			foreach ( $field_names as $single_field_name ) {
765
				if (
766
					isset( $single_post_data[ $single_field_name ] )
767
					&& ! empty( $single_post_data[ $single_field_name ] )
768
				) {
769
					$result[ $single_field_name ][] = trim( $single_post_data[ $single_field_name ] );
770
				}
771
				else {
772
					$result[ $single_field_name ][] = '';
773
				}
774
			}
775
		}
776
777
		return $result;
778
	}
779
780
	/**
781
	 * download as a csv a contact form or all of them in a csv file
782
	 */
783
	function download_feedback_as_csv() {
784
		if ( empty( $_POST['feedback_export_nonce'] ) )
785
			return;
786
787
		check_admin_referer( 'feedback_export', 'feedback_export_nonce' );
788
789
		if ( ! current_user_can( 'export' ) ) {
790
			return;
791
		}
792
793
		$args = array(
794
			'posts_per_page'   => -1,
795
			'post_type'        => 'feedback',
796
			'post_status'      => 'publish',
797
			'order'            => 'ASC',
798
			'fields'           => 'ids',
799
			'suppress_filters' => false,
800
		);
801
802
		$filename = date( "Y-m-d" ) . '-feedback-export.csv';
803
804
		// Check if we want to download all the feedbacks or just a certain contact form
805
		if ( ! empty( $_POST['post'] ) && $_POST['post'] !== 'all' ) {
806
			$args['post_parent'] = (int) $_POST['post'];
807
			$filename            = date( "Y-m-d" ) . '-' . str_replace( '&nbsp;', '-', get_the_title( (int) $_POST['post'] ) ) . '.csv';
808
		}
809
810
		$feedbacks = get_posts( $args );
811
812
		if ( empty( $feedbacks ) ) {
813
			return;
814
		}
815
816
		$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...
817
818
		/**
819
		 * Prepare data for export.
820
		 */
821
		$data = $this->get_export_data_for_posts( $feedbacks );
822
823
		/**
824
		 * If `$data` is empty, there's nothing we can do below.
825
		 */
826
		if ( ! is_array( $data ) || empty( $data ) ) {
827
			return;
828
		}
829
830
		/**
831
		 * Extract field names from `$data` for later use.
832
		 */
833
		$fields = array_keys( $data );
834
835
		/**
836
		 * Count how many rows will be exported.
837
		 */
838
		$row_count = count( reset( $data ) );
839
840
841
		// Forces the download of the CSV instead of echoing
842
		header( 'Content-Disposition: attachment; filename=' . $filename );
843
		header( 'Pragma: no-cache' );
844
		header( 'Expires: 0' );
845
		header( 'Content-Type: text/csv; charset=utf-8' );
846
847
		$output = fopen( 'php://output', 'w' );
848
849
		/**
850
		 * Print CSV headers
851
		 */
852
		fputcsv( $output, $fields );
853
854
855
		/**
856
		 * Print rows to the output.
857
		 */
858
		for ( $i = 0; $i < $row_count; $i ++ ) {
859
860
			$current_row = array();
861
862
			/**
863
			 * Put all the fields in `$current_row` array.
864
			 */
865
			foreach ( $fields as $single_field_name ) {
866
				$current_row[] = $data[ $single_field_name ][ $i ];
867
			}
868
869
			/**
870
			 * Output the complete CSV row
871
			 */
872
			fputcsv( $output, $current_row );
873
		}
874
875
		fclose( $output );
876
	}
877
878
	/**
879
	 * Returns a string of HTML <option> items from an array of posts
880
	 *
881
	 * @return string a string of HTML <option> items
882
	 */
883
	protected function get_feedbacks_as_options() {
884
		$options = '';
885
886
		// Get the feedbacks' parents' post IDs
887
		$feedbacks = get_posts( array(
888
			'fields'           => 'id=>parent',
889
			'posts_per_page'   => 100000,
890
			'post_type'        => 'feedback',
891
			'post_status'      => 'publish',
892
			'suppress_filters' => false,
893
		) );
894
		$parents = array_unique( array_values( $feedbacks ) );
895
896
		$posts = get_posts( array(
897
			'orderby'          => 'ID',
898
			'posts_per_page'   => 1000,
899
			'post_type'        => 'any',
900
			'post__in'         => array_values( $parents ),
901
			'suppress_filters' => false,
902
		) );
903
904
		// creates the string of <option> elements
905
		foreach ( $posts as $post ) {
906
			$options .= sprintf( '<option value="%s">%s</option>', esc_attr( $post->ID ), esc_html( $post->post_title ) );
907
		}
908
909
		return $options;
910
	}
911
912
	/**
913
	 * Get the names of all the form's fields
914
	 *
915
	 * @param  array|int $posts the post we want the fields of
916
	 *
917
	 * @return array     the array of fields
918
	 *
919
	 * @deprecated As this is no longer necessary as of the CSV export rewrite. - 2015-12-29
920
	 */
921
	protected function get_field_names( $posts ) {
922
		$posts = (array) $posts;
923
		$all_fields = array();
924
925
		foreach ( $posts as $post ){
926
			$fields = self::parse_fields_from_content( $post );
927
928
			if ( isset( $fields['_feedback_all_fields'] ) ) {
929
				$extra_fields = array_keys( $fields['_feedback_all_fields'] );
930
				$all_fields = array_merge( $all_fields, $extra_fields );
931
			}
932
		}
933
934
		$all_fields = array_unique( $all_fields );
935
		return $all_fields;
936
	}
937
938
	public static function parse_fields_from_content( $post_id ) {
939
		static $post_fields;
940
941
		if ( !is_array( $post_fields ) )
942
			$post_fields = array();
943
944
		if ( isset( $post_fields[$post_id] ) )
945
			return $post_fields[$post_id];
946
947
		$all_values   = array();
948
		$post_content = get_post_field( 'post_content', $post_id );
949
		$content      = explode( '<!--more-->', $post_content );
950
		$lines        = array();
951
952
		if ( count( $content ) > 1 ) {
953
			$content  = str_ireplace( array( '<br />', ')</p>' ), '', $content[1] );
954
			$one_line = preg_replace( '/\s+/', ' ', $content );
955
			$one_line = preg_replace( '/.*Array \( (.*)\)/', '$1', $one_line );
956
957
			preg_match_all( '/\[([^\]]+)\] =\&gt\; ([^\[]+)/', $one_line, $matches );
958
959
			if ( count( $matches ) > 1 )
960
				$all_values = array_combine( array_map('trim', $matches[1]), array_map('trim', $matches[2]) );
961
962
			$lines = array_filter( explode( "\n", $content ) );
963
		}
964
965
		$var_map = array(
966
			'AUTHOR'       => '_feedback_author',
967
			'AUTHOR EMAIL' => '_feedback_author_email',
968
			'AUTHOR URL'   => '_feedback_author_url',
969
			'SUBJECT'      => '_feedback_subject',
970
			'IP'           => '_feedback_ip'
971
		);
972
973
		$fields = array();
974
975
		foreach( $lines as $line ) {
976
			$vars = explode( ': ', $line, 2 );
977
			if ( !empty( $vars ) ) {
978
				if ( isset( $var_map[$vars[0]] ) ) {
979
					$fields[$var_map[$vars[0]]] = self::strip_tags( trim( $vars[1] ) );
980
				}
981
			}
982
		}
983
984
		$fields['_feedback_all_fields'] = $all_values;
985
986
		$post_fields[$post_id] = $fields;
987
988
		return $fields;
989
	}
990
991
	/**
992
	 * Creates a valid csv row from a post id
993
	 *
994
	 * @param  int    $post_id The id of the post
995
	 * @param  array  $fields  An array containing the names of all the fields of the csv
996
	 * @return String The csv row
997
	 *
998
	 * @deprecated This is no longer needed, as of the CSV export rewrite.
999
	 */
1000
	protected static function make_csv_row_from_feedback( $post_id, $fields ) {
1001
		$content_fields = self::parse_fields_from_content( $post_id );
1002
		$all_fields     = array();
1003
1004
		if ( isset( $content_fields['_feedback_all_fields'] ) )
1005
			$all_fields = $content_fields['_feedback_all_fields'];
1006
1007
		// Overwrite the parsed content with the content we stored in post_meta in a better format.
1008
		$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...
1009
		foreach ( $extra_fields as $extra_field => $extra_value ) {
1010
			$all_fields[$extra_field] = $extra_value;
1011
		}
1012
1013
		// The first element in all of the exports will be the subject
1014
		$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...
1015
1016
		// Loop the fields array in order to fill the $row_items array correctly
1017
		foreach ( $fields as $field ) {
1018
			if ( $field === __( 'Contact Form', 'jetpack' ) ) // the first field will ever be the contact form, so we can continue
1019
				continue;
1020
			elseif ( array_key_exists( $field, $all_fields ) )
1021
				$row_items[] = $all_fields[$field];
1022
			else
1023
				$row_items[] = '';
1024
		}
1025
1026
		return $row_items;
1027
	}
1028
1029
	public static function get_ip_address() {
1030
		return isset( $_SERVER['REMOTE_ADDR'] ) ? $_SERVER['REMOTE_ADDR'] : null;
1031
	}
1032
}
1033
1034
/**
1035
 * Generic shortcode class.
1036
 * Does nothing other than store structured data and output the shortcode as a string
1037
 *
1038
 * Not very general - specific to Grunion.
1039
 */
1040
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...
1041
	/**
1042
	 * @var string the name of the shortcode: [$shortcode_name /]
1043
 	 */
1044
	public $shortcode_name;
1045
1046
	/**
1047
	 * @var array key => value pairs for the shortcode's attributes: [$shortcode_name key="value" ... /]
1048
	 */
1049
	public $attributes;
1050
1051
	/**
1052
	 * @var array key => value pair for attribute defaults
1053
	 */
1054
	public $defaults = array();
1055
1056
	/**
1057
	 * @var null|string Null for selfclosing shortcodes.  Hhe inner content of otherwise: [$shortcode_name]$content[/$shortcode_name]
1058
	 */
1059
	public $content;
1060
1061
	/**
1062
	 * @var array Associative array of inner "child" shortcodes equivalent to the $content: [$shortcode_name][child 1/][child 2/][/$shortcode_name]
1063
	 */
1064
	public $fields;
1065
1066
	/**
1067
	 * @var null|string The HTML of the parsed inner "child" shortcodes".  Null for selfclosing shortcodes.
1068
	 */
1069
	public $body;
1070
1071
	/**
1072
	 * @param array $attributes An associative array of shortcode attributes.  @see shortcode_atts()
1073
	 * @param null|string $content Null for selfclosing shortcodes.  The inner content otherwise.
1074
	 */
1075
	function __construct( $attributes, $content = null ) {
1076
		$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...
1077
		if ( is_array( $content ) ) {
1078
			$string_content = '';
1079
			foreach ( $content as $field ) {
1080
				$string_content .= (string) $field;
1081
			}
1082
1083
			$this->content = $string_content;
1084
		} else {
1085
			$this->content = $content;
1086
		}
1087
1088
		$this->parse_content( $this->content );
1089
	}
1090
1091
	/**
1092
	 * Processes the shortcode's inner content for "child" shortcodes
1093
	 *
1094
	 * @param string $content The shortcode's inner content: [shortcode]$content[/shortcode]
1095
	 */
1096
	function parse_content( $content ) {
1097
		if ( is_null( $content ) ) {
1098
			$this->body = null;
1099
		}
1100
1101
		$this->body = do_shortcode( $content );
1102
	}
1103
1104
	/**
1105
	 * Returns the value of the requested attribute.
1106
	 *
1107
	 * @param string $key The attribute to retrieve
1108
	 * @return mixed
1109
	 */
1110
	function get_attribute( $key ) {
1111
		return isset( $this->attributes[$key] ) ? $this->attributes[$key] : null;
1112
	}
1113
1114
	function esc_attr( $value ) {
1115
		if ( is_array( $value ) ) {
1116
			return array_map( array( $this, 'esc_attr' ), $value );
1117
		}
1118
1119
		$value = Grunion_Contact_Form_Plugin::strip_tags( $value );
1120
		$value = _wp_specialchars( $value, ENT_QUOTES, false, true );
1121
1122
		// Shortcode attributes can't contain "]"
1123
		$value = str_replace( ']', '', $value );
1124
		$value = str_replace( ',', '&#x002c;', $value ); // store commas encoded
1125
		$value = strtr( $value, array( '%' => '%25', '&' => '%26' ) );
1126
1127
		// shortcode_parse_atts() does stripcslashes()
1128
		$value = addslashes( $value );
1129
		return $value;
1130
	}
1131
1132
	function unesc_attr( $value ) {
1133
		if ( is_array( $value ) ) {
1134
			return array_map( array( $this, 'unesc_attr' ), $value );
1135
		}
1136
1137
		// For back-compat with old Grunion encoding
1138
		// Also, unencode commas
1139
		$value = strtr( $value, array( '%26' => '&', '%25' => '%' ) );
1140
		$value = preg_replace( array( '/&#x0*22;/i', '/&#x0*27;/i', '/&#x0*26;/i', '/&#x0*2c;/i' ), array( '"', "'", '&', ',' ), $value );
1141
		$value = htmlspecialchars_decode( $value, ENT_QUOTES );
1142
		$value = Grunion_Contact_Form_Plugin::strip_tags( $value );
1143
1144
		return $value;
1145
	}
1146
1147
	/**
1148
	 * Generates the shortcode
1149
	 */
1150
	function __toString() {
1151
		$r = "[{$this->shortcode_name} ";
1152
1153
		foreach ( $this->attributes as $key => $value ) {
1154
			if ( !$value ) {
1155
				continue;
1156
			}
1157
1158
			if ( isset( $this->defaults[$key] ) && $this->defaults[$key] == $value ) {
1159
				continue;
1160
			}
1161
1162
			if ( 'id' == $key ) {
1163
				continue;
1164
			}
1165
1166
			$value = $this->esc_attr( $value );
1167
1168
			if ( is_array( $value ) ) {
1169
				$value = join( ',', $value );
1170
			}
1171
1172
			if ( false === strpos( $value, "'" ) ) {
1173
				$value = "'$value'";
1174
			} elseif ( false === strpos( $value, '"' ) ) {
1175
				$value = '"' . $value . '"';
1176
			} else {
1177
				// Shortcodes can't contain both '"' and "'".  Strip one.
1178
				$value = str_replace( "'", '', $value );
1179
				$value = "'$value'";
1180
			}
1181
1182
			$r .= "{$key}={$value} ";
1183
		}
1184
1185
		$r = rtrim( $r );
1186
1187
		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...
1188
			$r .= ']';
1189
1190
			foreach ( $this->fields as $field ) {
1191
				$r .= (string) $field;
1192
			}
1193
1194
			$r .= "[/{$this->shortcode_name}]";
1195
		} else {
1196
			$r .= '/]';
1197
		}
1198
1199
		return $r;
1200
	}
1201
}
1202
1203
/**
1204
 * Class for the contact-form shortcode.
1205
 * Parses shortcode to output the contact form as HTML
1206
 * Sends email and stores the contact form response (a.k.a. "feedback")
1207
 */
1208
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...
1209
	public $shortcode_name = 'contact-form';
1210
1211
	/**
1212
	 * @var WP_Error stores form submission errors
1213
	 */
1214
	public $errors;
1215
1216
	/**
1217
	 * @var Grunion_Contact_Form The most recent (inclusive) contact-form shortcode processed
1218
	 */
1219
	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...
1220
1221
	/**
1222
	 * @var Whatever form we are currently looking at. If processed, will become $last
1223
	 */
1224
	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...
1225
1226
	/**
1227
	 * @var bool Whether to print the grunion.css style when processing the contact-form shortcode
1228
	 */
1229
	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...
1230
1231
	function __construct( $attributes, $content = null ) {
1232
		global $post;
1233
1234
		// Set up the default subject and recipient for this form
1235
		$default_to = '';
1236
		$default_subject = "[" . get_option( 'blogname' ) . "]";
1237
1238
		if ( !empty( $attributes['widget'] ) && $attributes['widget'] ) {
1239
			$default_to .= get_option( 'admin_email' );
1240
			$attributes['id'] = 'widget-' . $attributes['widget'];
1241
			$default_subject = sprintf( _x( '%1$s Sidebar', '%1$s = blog name', 'jetpack' ), $default_subject );
1242
		} else if ( $post ) {
1243
			$attributes['id'] = $post->ID;
1244
			$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 ) );
1245
			$post_author = get_userdata( $post->post_author );
1246
			$default_to .= $post_author->user_email;
1247
		}
1248
1249
		// Keep reference to $this for parsing form fields
1250
		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...
1251
1252
		$this->defaults = array(
1253
			'to'                 => $default_to,
1254
			'subject'            => $default_subject,
1255
			'show_subject'       => 'no', // only used in back-compat mode
1256
			'widget'             => 0,    // Not exposed to the user. Works with Grunion_Contact_Form_Plugin::widget_atts()
1257
			'id'                 => null, // Not exposed to the user. Set above.
1258
			'submit_button_text' => __( 'Submit &#187;', 'jetpack' ),
1259
		);
1260
1261
		$attributes = shortcode_atts( $this->defaults, $attributes, 'contact-form' );
1262
1263
		// We only enable the contact-field shortcode temporarily while processing the contact-form shortcode
1264
		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...
1265
1266
		parent::__construct( $attributes, $content );
1267
1268
		// There were no fields in the contact form. The form was probably just [contact-form /]. Build a default form.
1269
		if ( empty( $this->fields ) ) {
1270
			// same as the original Grunion v1 form
1271
			$default_form = '
1272
				[contact-field label="' . __( 'Name', 'jetpack' )    . '" type="name"  required="true" /]
1273
				[contact-field label="' . __( 'Email', 'jetpack' )   . '" type="email" required="true" /]
1274
				[contact-field label="' . __( 'Website', 'jetpack' ) . '" type="url" /]';
1275
1276
			if ( 'yes' == strtolower( $this->get_attribute( 'show_subject' ) ) ) {
1277
				$default_form .= '
1278
					[contact-field label="' . __( 'Subject', 'jetpack' ) . '" type="subject" /]';
1279
			}
1280
1281
			$default_form .= '
1282
				[contact-field label="' . __( 'Message', 'jetpack' ) . '" type="textarea" /]';
1283
1284
			$this->parse_content( $default_form );
1285
1286
			// Store the shortcode
1287
			$this->store_shortcode( $default_form, $attributes );
1288
		} else {
1289
			// Store the shortcode
1290
			$this->store_shortcode( $content, $attributes );
1291
		}
1292
1293
		// $this->body and $this->fields have been setup.  We no longer need the contact-field shortcode.
1294
		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...
1295
	}
1296
1297
	/**
1298
	 * Store shortcode content for recall later
1299
	 *	- used to receate shortcode when user uses do_shortcode
1300
	 *
1301
	 * @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...
1302
	 */
1303
	static function store_shortcode( $content = null, $attributes = null ) {
1304
1305
		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...
1306
1307
			$shortcode_meta = get_post_meta( $attributes['id'], '_g_feedback_shortcode', true );
1308
1309
			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...
1310
				update_post_meta( $attributes['id'], '_g_feedback_shortcode', $content );
1311
			}
1312
1313
		}
1314
	}
1315
1316
	/**
1317
	 * Toggle for printing the grunion.css stylesheet
1318
	 *
1319
	 * @param bool $style
1320
	 */
1321
	static function style( $style ) {
1322
		$previous_style = self::$style;
1323
		self::$style = (bool) $style;
1324
		return $previous_style;
1325
	}
1326
1327
	/**
1328
	 * Turn on printing of grunion.css stylesheet
1329
	 * @see ::style()
1330
	 * @internal
1331
	 * @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...
1332
	 */
1333
	static function _style_on() {
1334
		return self::style( true );
1335
	}
1336
1337
	/**
1338
	 * The contact-form shortcode processor
1339
	 *
1340
	 * @param array $attributes Key => Value pairs as parsed by shortcode_parse_atts()
1341
	 * @param string|null $content The shortcode's inner content: [contact-form]$content[/contact-form]
1342
	 * @return string HTML for the concat form.
1343
	 */
1344
	static function parse( $attributes, $content ) {
1345
		// Create a new Grunion_Contact_Form object (this class)
1346
		$form = new Grunion_Contact_Form( $attributes, $content );
1347
1348
		$id = $form->get_attribute( 'id' );
1349
1350
		if ( !$id ) { // something terrible has happened
1351
			return '[contact-form]';
1352
		}
1353
1354
		if ( is_feed() ) {
1355
			return '[contact-form]';
1356
		}
1357
1358
		// Only allow one contact form per post/widget
1359
		if ( self::$last && $id == self::$last->get_attribute( 'id' ) ) {
1360
			// We're processing the same post
1361
1362
			if ( self::$last->attributes != $form->attributes || self::$last->content != $form->content ) {
1363
				// And we're processing a different shortcode;
1364
				return '';
1365
			} // else, we're processing the same shortcode - probably a separate run of do_shortcode() - let it through
1366
1367
		} else {
1368
			self::$last = $form;
1369
		}
1370
1371
		// Enqueue the grunion.css stylesheet if self::$style allows it
1372
		if ( self::$style && ( empty( $_REQUEST['action'] ) || $_REQUEST['action'] != 'grunion_shortcode_to_json' ) ) {
1373
			// Enqueue the style here instead of printing it, because if some other plugin has run the_post()+rewind_posts(),
1374
			// (like VideoPress does), the style tag gets "printed" the first time and discarded, leaving the contact form unstyled.
1375
			// when WordPress does the real loop.
1376
			wp_enqueue_style( 'grunion.css' );
1377
		}
1378
1379
		$r = '';
1380
		$r .= "<div id='contact-form-$id'>\n";
1381
1382
		if ( is_wp_error( $form->errors ) && $form->errors->get_error_codes() ) {
1383
			// There are errors.  Display them
1384
			$r .= "<div class='form-error'>\n<h3>" . __( 'Error!', 'jetpack' ) . "</h3>\n<ul class='form-errors'>\n";
1385
			foreach ( $form->errors->get_error_messages() as $message )
1386
				$r .= "\t<li class='form-error-message'>" . esc_html( $message ) . "</li>\n";
1387
			$r .= "</ul>\n</div>\n\n";
1388
		}
1389
1390
		if ( isset( $_GET['contact-form-id'] ) && $_GET['contact-form-id'] == self::$last->get_attribute( 'id' ) && isset( $_GET['contact-form-sent'] ) ) {
1391
			// The contact form was submitted.  Show the success message/results
1392
1393
			$feedback_id = (int) $_GET['contact-form-sent'];
1394
1395
			$back_url = remove_query_arg( array( 'contact-form-id', 'contact-form-sent', '_wpnonce' ) );
1396
1397
			$r_success_message =
1398
				"<h3>" . __( 'Message Sent', 'jetpack' ) .
1399
				' (<a href="' . esc_url( $back_url ) . '">' . esc_html__( 'go back', 'jetpack' ) . '</a>)' .
1400
				"</h3>\n\n";
1401
1402
			// Don't show the feedback details unless the nonce matches
1403
			if ( $feedback_id && wp_verify_nonce( stripslashes( $_GET['_wpnonce'] ), "contact-form-sent-{$feedback_id}" ) ) {
1404
				$r_success_message .= self::success_message( $feedback_id, $form );
1405
			}
1406
1407
			/**
1408
			 * Filter the message returned after a successfull contact form submission.
1409
			 *
1410
			 * @module contact-form
1411
			 *
1412
			 * @since 1.3.1
1413
			 *
1414
			 * @param string $r_success_message Success message.
1415
			 */
1416
			$r .= apply_filters( 'grunion_contact_form_success_message', $r_success_message );
1417
		} else {
1418
			// Nothing special - show the normal contact form
1419
1420
			if ( $form->get_attribute( 'widget' ) ) {
1421
				// Submit form to the current URL
1422
				$url = remove_query_arg( array( 'contact-form-id', 'contact-form-sent', 'action', '_wpnonce' ) );
1423
			} else {
1424
				// Submit form to the post permalink
1425
				$url = get_permalink();
1426
			}
1427
1428
			// For SSL/TLS page. See RFC 3986 Section 4.2
1429
			$url = set_url_scheme( $url );
1430
1431
			// May eventually want to send this to admin-post.php...
1432
			/**
1433
			 * Filter the contact form action URL.
1434
			 *
1435
			 * @module contact-form
1436
			 *
1437
			 * @since 1.3.1
1438
			 *
1439
			 * @param string $contact_form_id Contact form post URL.
1440
			 * @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...
1441
			 * @param int $id Contact Form ID.
1442
			 */
1443
			$url = apply_filters( 'grunion_contact_form_form_action', "{$url}#contact-form-{$id}", $GLOBALS['post'], $id );
1444
1445
			$r .= "<form action='" . esc_url( $url ) . "' method='post' class='contact-form commentsblock'>\n";
1446
			$r .= $form->body;
1447
			$r .= "\t<p class='contact-submit'>\n";
1448
			$r .= "\t\t<input type='submit' value='" . esc_attr( $form->get_attribute( 'submit_button_text' ) ) . "' class='pushbutton-wide'/>\n";
1449
			if ( is_user_logged_in() ) {
1450
				$r .= "\t\t" . wp_nonce_field( 'contact-form_' . $id, '_wpnonce', true, false ) . "\n"; // nonce and referer
1451
			}
1452
			$r .= "\t\t<input type='hidden' name='contact-form-id' value='$id' />\n";
1453
			$r .= "\t\t<input type='hidden' name='action' value='grunion-contact-form' />\n";
1454
			$r .= "\t</p>\n";
1455
			$r .= "</form>\n";
1456
		}
1457
1458
		$r .= "</div>";
1459
1460
		return $r;
1461
	}
1462
1463
	/**
1464
	 * Returns a success message to be returned if the form is sent via AJAX.
1465
	 *
1466
	 * @param int $feedback_id
1467
	 * @param object Grunion_Contact_Form $form
1468
	 *
1469
	 * @return string $message
1470
	 */
1471
	static function success_message( $feedback_id, $form ) {
1472
		return wp_kses(
1473
			'<blockquote class="contact-form-submission">'
1474
			. '<p>' . join( self::get_compiled_form( $feedback_id, $form ), '</p><p>' ) . '</p>'
1475
			. '</blockquote>',
1476
			array( 'br' => array(), 'blockquote' => array( 'class' => array() ), 'p' => array() )
1477
		);
1478
	}
1479
1480
	/**
1481
	 * Returns a compiled form with labels and values in a form of  an array
1482
	 * of lines.
1483
	 * @param int $feedback_id
1484
	 * @param object Grunion_Contact_Form $form
1485
	 *
1486
	 * @return array $lines
1487
	 */
1488
	static function get_compiled_form( $feedback_id, $form ) {
1489
		$feedback       = get_post( $feedback_id );
1490
		$field_ids      = $form->get_field_ids();
1491
		$content_fields = Grunion_Contact_Form_Plugin::parse_fields_from_content( $feedback_id );
1492
1493
		// Maps field_ids to post_meta keys
1494
		$field_value_map = array(
1495
			'name'     => 'author',
1496
			'email'    => 'author_email',
1497
			'url'      => 'author_url',
1498
			'subject'  => 'subject',
1499
			'textarea' => false, // not a post_meta key.  This is stored in post_content
1500
		);
1501
1502
		$compiled_form = array();
1503
1504
		// "Standard" field whitelist
1505
		foreach ( $field_value_map as $type => $meta_key ) {
1506
			if ( isset( $field_ids[$type] ) ) {
1507
				$field = $form->fields[$field_ids[$type]];
1508
1509
				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...
1510
					if ( isset( $content_fields["_feedback_{$meta_key}"] ) )
1511
						$value = $content_fields["_feedback_{$meta_key}"];
1512
				} else {
1513
					// The feedback content is stored as the first "half" of post_content
1514
					$value = $feedback->post_content;
1515
					list( $value ) = explode( '<!--more-->', $value );
1516
					$value = trim( $value );
1517
				}
1518
1519
				$field_index = array_search( $field_ids[ $type ], $field_ids['all'] );
1520
				$compiled_form[ $field_index ] = sprintf(
1521
					'<b>%1$s:</b> %2$s<br /><br />',
1522
					wp_kses( $field->get_attribute( 'label' ), array() ),
1523
					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...
1524
				);
1525
			}
1526
		}
1527
1528
		// "Non-standard" fields
1529
		if ( $field_ids['extra'] ) {
1530
			// array indexed by field label (not field id)
1531
			$extra_fields = get_post_meta( $feedback_id, '_feedback_extra_fields', true );
1532
1533
			/**
1534
			 * Only get data for the compiled form if `$extra_fields` is a valid and non-empty array.
1535
			 */
1536
			if ( is_array( $extra_fields ) && ! empty( $extra_fields ) ) {
1537
1538
				$extra_field_keys = array_keys( $extra_fields );
1539
1540
				$i = 0;
1541
				foreach ( $field_ids['extra'] as $field_id ) {
1542
					$field       = $form->fields[ $field_id ];
1543
					$field_index = array_search( $field_id, $field_ids['all'] );
1544
1545
					$label = $field->get_attribute( 'label' );
1546
1547
					$compiled_form[ $field_index ] = sprintf(
1548
						'<b>%1$s:</b> %2$s<br /><br />',
1549
						wp_kses( $label, array() ),
1550
						nl2br( wp_kses( $extra_fields[ $extra_field_keys[ $i ] ], array() ) )
1551
					);
1552
1553
					$i++;
1554
				}
1555
			}
1556
		}
1557
1558
		// Sorting lines by the field index
1559
		ksort( $compiled_form );
1560
1561
		return $compiled_form;
1562
	}
1563
1564
	/**
1565
	 * The contact-field shortcode processor
1566
	 * 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.
1567
	 *
1568
	 * @param array $attributes Key => Value pairs as parsed by shortcode_parse_atts()
1569
	 * @param string|null $content The shortcode's inner content: [contact-field]$content[/contact-field]
1570
	 * @return HTML for the contact form field
1571
	 */
1572
	static function parse_contact_field( $attributes, $content ) {
1573
		// Don't try to parse contact form fields if not inside a contact form
1574
		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...
1575
			$att_strs = array();
1576
			foreach ( $attributes as $att => $val ) {
1577
				if ( is_numeric( $att ) ) { // Is a valueless attribute
1578
					$att_strs[] = esc_html( $val );
1579
				} else if ( isset( $val ) ) { // A regular attr - value pair
1580
					$att_strs[] = esc_html( $att ) . '=\'' . esc_html( $val ) . '\'';
1581
				}
1582
			}
1583
1584
			$html = '[contact-field ' . implode( ' ', $att_strs );
1585
1586
			if ( isset( $content ) && ! empty( $content ) ) { // If there is content, let's add a closing tag
1587
				$html .=  ']' . esc_html( $content ) . '[/contact-field]';
1588
			} else { // Otherwise let's add a closing slash in the first tag
1589
				$html .= '/]';
1590
			}
1591
1592
			return $html;
1593
		}
1594
1595
		$form = Grunion_Contact_Form::$current_form;
1596
1597
		$field = new Grunion_Contact_Form_Field( $attributes, $content, $form );
1598
1599
		$field_id = $field->get_attribute( 'id' );
1600
		if ( $field_id ) {
1601
			$form->fields[$field_id] = $field;
1602
		} else {
1603
			$form->fields[] = $field;
1604
		}
1605
1606
		if (
1607
			isset( $_POST['action'] ) && 'grunion-contact-form' === $_POST['action']
1608
		&&
1609
			isset( $_POST['contact-form-id'] ) && $form->get_attribute( 'id' ) == $_POST['contact-form-id']
1610
		) {
1611
			// If we're processing a POST submission for this contact form, validate the field value so we can show errors as necessary.
1612
			$field->validate();
1613
		}
1614
1615
		// Output HTML
1616
		return $field->render();
1617
	}
1618
1619
	/**
1620
	 * Loops through $this->fields to generate a (structured) list of field IDs.
1621
	 *
1622
	 * Important: Currently the whitelisted fields are defined as follows:
1623
	 *  `name`, `email`, `url`, `subject`, `textarea`
1624
	 *
1625
	 * If you need to add new fields to the Contact Form, please don't add them
1626
	 * to the whitelisted fields and leave them as extra fields.
1627
	 *
1628
	 * The reasoning behind this is that both the admin Feedback view and the CSV
1629
	 * export will not include any fields that are added to the list of
1630
	 * whitelisted fields without taking proper care to add them to all the
1631
	 * other places where they accessed/used/saved.
1632
	 *
1633
	 * The safest way to add new fields is to add them to the dropdown and the
1634
	 * HTML list ( @see Grunion_Contact_Form_Field::render ) and don't add them
1635
	 * to the list of whitelisted fields. This way they will become a part of the
1636
	 * `extra fields` which are saved in the post meta and will be properly
1637
	 * handled by the admin Feedback view and the CSV Export without any extra
1638
	 * work.
1639
	 *
1640
	 * If there is need to add a field to the whitelisted fields, then please
1641
	 * take proper care to add logic to handle the field in the following places:
1642
	 *
1643
	 *  - Below in the switch statement - so the field is recognized as whitelisted.
1644
	 *
1645
	 *  - Grunion_Contact_Form::process_submission - validation and logic.
1646
	 *
1647
	 *  - Grunion_Contact_Form::process_submission - add the field as an additional
1648
	 *      field in the `post_content` when saving the feedback content.
1649
	 *
1650
	 *  - Grunion_Contact_Form_Plugin::parse_fields_from_content - add mapping
1651
	 *      for the field, defined in the above method.
1652
	 *
1653
	 *  - Grunion_Contact_Form_Plugin::map_parsed_field_contents_of_post_to_field_names -
1654
	 *      add mapping of the field for the CSV Export. Otherwise it will be missing
1655
	 *      from the exported data.
1656
	 *
1657
	 *  - admin.php / grunion_manage_post_columns - add the field to the render logic.
1658
	 *      Otherwise it will be missing from the admin Feedback view.
1659
	 *
1660
	 * @return array
1661
	 */
1662
	function get_field_ids() {
1663
		$field_ids = array(
1664
			'all'   => array(), // array of all field_ids
1665
			'extra' => array(), // array of all non-whitelisted field IDs
1666
1667
			// Whitelisted "standard" field IDs:
1668
			// 'email'    => field_id,
1669
			// 'name'     => field_id,
1670
			// 'url'      => field_id,
1671
			// 'subject'  => field_id,
1672
			// 'textarea' => field_id,
1673
		);
1674
1675
		foreach ( $this->fields as $id => $field ) {
1676
			$field_ids[ 'all' ][] = $id;
1677
1678
			$type = $field->get_attribute( 'type' );
1679
			if ( isset( $field_ids[ $type ] ) ) {
1680
				// This type of field is already present in our whitelist of "standard" fields for this form
1681
				// Put it in extra
1682
				$field_ids[ 'extra' ][] = $id;
1683
				continue;
1684
			}
1685
1686
			/**
1687
			 * See method description before modifying the switch cases.
1688
			 */
1689
			switch ( $type ) {
1690
				case 'email' :
1691
				case 'name' :
1692
				case 'url' :
1693
				case 'subject' :
1694
				case 'textarea' :
1695
					$field_ids[ $type ] = $id;
1696
					break;
1697
			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...
1698
				// Put everything else in extra
1699
				$field_ids[ 'extra' ][] = $id;
1700
			}
1701
		}
1702
1703
		return $field_ids;
1704
	}
1705
1706
	/**
1707
	 * Process the contact form's POST submission
1708
	 * Stores feedback.  Sends email.
1709
	 */
1710
	function process_submission() {
1711
		global $post;
1712
1713
		$plugin = Grunion_Contact_Form_Plugin::init();
1714
1715
		$id     = $this->get_attribute( 'id' );
1716
		$to     = $this->get_attribute( 'to' );
1717
		$widget = $this->get_attribute( 'widget' );
1718
1719
		$contact_form_subject = $this->get_attribute( 'subject' );
1720
1721
		$to = str_replace( ' ', '', $to );
1722
		$emails = explode( ',', $to );
1723
1724
		$valid_emails = array();
1725
1726
		foreach ( (array) $emails as $email ) {
1727
			if ( !is_email( $email ) ) {
1728
				continue;
1729
			}
1730
1731
			if ( function_exists( 'is_email_address_unsafe' ) && is_email_address_unsafe( $email ) ) {
1732
				continue;
1733
			}
1734
1735
			$valid_emails[] = $email;
1736
		}
1737
1738
		// No one to send it to, which means none of the "to" attributes are valid emails.
1739
		// Use default email instead.
1740
		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...
1741
			$valid_emails = $this->defaults['to'];
1742
		}
1743
1744
		$to = $valid_emails;
1745
1746
		// Last ditch effort to set a recipient if somehow none have been set.
1747
		if ( empty( $to ) ) {
1748
			$to = get_option( 'admin_email' );
1749
		}
1750
1751
		// Make sure we're processing the form we think we're processing... probably a redundant check.
1752
		if ( $widget ) {
1753
			if ( 'widget-' . $widget != $_POST['contact-form-id'] ) {
1754
				return false;
1755
			}
1756
		} else {
1757
			if ( $post->ID != $_POST['contact-form-id'] ) {
1758
				return false;
1759
			}
1760
		}
1761
1762
		$field_ids = $this->get_field_ids();
1763
1764
		// Initialize all these "standard" fields to null
1765
		$comment_author_email = $comment_author_email_label = // v
1766
		$comment_author       = $comment_author_label       = // v
1767
		$comment_author_url   = $comment_author_url_label   = // v
1768
		$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...
1769
1770
		// For each of the "standard" fields, grab their field label and value.
1771
1772 View Code Duplication
		if ( isset( $field_ids['name'] ) ) {
1773
			$field = $this->fields[$field_ids['name']];
1774
			$comment_author = Grunion_Contact_Form_Plugin::strip_tags(
1775
				stripslashes(
1776
					/** This filter is already documented in core/wp-includes/comment-functions.php */
1777
					apply_filters( 'pre_comment_author_name', addslashes( $field->value ) )
1778
				)
1779
			);
1780
			$comment_author_label = Grunion_Contact_Form_Plugin::strip_tags( $field->get_attribute( 'label' ) );
1781
		}
1782
1783 View Code Duplication
		if ( isset( $field_ids['email'] ) ) {
1784
			$field = $this->fields[$field_ids['email']];
1785
			$comment_author_email = Grunion_Contact_Form_Plugin::strip_tags(
1786
				stripslashes(
1787
					/** This filter is already documented in core/wp-includes/comment-functions.php */
1788
					apply_filters( 'pre_comment_author_email', addslashes( $field->value ) )
1789
				)
1790
			);
1791
			$comment_author_email_label = Grunion_Contact_Form_Plugin::strip_tags( $field->get_attribute( 'label' ) );
1792
		}
1793
1794
		if ( isset( $field_ids['url'] ) ) {
1795
			$field = $this->fields[$field_ids['url']];
1796
			$comment_author_url = Grunion_Contact_Form_Plugin::strip_tags(
1797
				stripslashes(
1798
					/** This filter is already documented in core/wp-includes/comment-functions.php */
1799
					apply_filters( 'pre_comment_author_url', addslashes( $field->value ) )
1800
				)
1801
			);
1802
			if ( 'http://' == $comment_author_url ) {
1803
				$comment_author_url = '';
1804
			}
1805
			$comment_author_url_label = Grunion_Contact_Form_Plugin::strip_tags( $field->get_attribute( 'label' ) );
1806
		}
1807
1808
		if ( isset( $field_ids['textarea'] ) ) {
1809
			$field = $this->fields[$field_ids['textarea']];
1810
			$comment_content = trim( Grunion_Contact_Form_Plugin::strip_tags( $field->value ) );
1811
			$comment_content_label = Grunion_Contact_Form_Plugin::strip_tags( $field->get_attribute( 'label' ) );
1812
		}
1813
1814
		if ( isset( $field_ids['subject'] ) ) {
1815
			$field = $this->fields[$field_ids['subject']];
1816
			if ( $field->value ) {
1817
				$contact_form_subject = Grunion_Contact_Form_Plugin::strip_tags( $field->value );
1818
			}
1819
		}
1820
1821
		$all_values = $extra_values = array();
1822
		$i = 1; // Prefix counter for stored metadata
1823
1824
		// For all fields, grab label and value
1825
		foreach ( $field_ids['all'] as $field_id ) {
1826
			$field = $this->fields[$field_id];
1827
			$label = $i . '_' . $field->get_attribute( 'label' );
1828
			$value = $field->value;
1829
1830
			$all_values[$label] = $value;
1831
			$i++; // Increment prefix counter for the next field
1832
		}
1833
1834
		// For the "non-standard" fields, grab label and value
1835
		// Extra fields have their prefix starting from count( $all_values ) + 1
1836
		foreach ( $field_ids['extra'] as $field_id ) {
1837
			$field = $this->fields[$field_id];
1838
			$label = $i . '_' . $field->get_attribute( 'label' );
1839
			$value = $field->value;
1840
1841
			if ( is_array( $value ) ) {
1842
				$value = implode( ', ', $value );
1843
			}
1844
1845
			$extra_values[$label] = $value;
1846
			$i++; // Increment prefix counter for the next extra field
1847
		}
1848
1849
		$contact_form_subject = trim( $contact_form_subject );
1850
1851
		$comment_author_IP = Grunion_Contact_Form_Plugin::get_ip_address();
1852
1853
		$vars = array( 'comment_author', 'comment_author_email', 'comment_author_url', 'contact_form_subject', 'comment_author_IP' );
1854
		foreach ( $vars as $var )
1855
			$$var = str_replace( array( "\n", "\r" ), '', $$var );
1856
1857
		// Ensure that Akismet gets all of the relevant information from the contact form,
1858
		// not just the textarea field and predetermined subject.
1859
		$akismet_vars = compact( $vars );
1860
		$akismet_vars['comment_content'] = $comment_content;
1861
1862
		foreach ( array_merge( $field_ids['all'], $field_ids['extra'] ) as $field_id ) {
1863
			$field = $this->fields[$field_id];
1864
1865
			// Skip any fields that are just a choice from a pre-defined list. They wouldn't have any value
1866
			// from a spam-filtering point of view.
1867
			if ( in_array( $field->get_attribute( 'type' ), array( 'select', 'checkbox', 'checkbox-multiple', 'radio' ) ) ) {
1868
				continue;
1869
			}
1870
1871
			// Normalize the label into a slug.
1872
			$field_slug = trim( // Strip all leading/trailing dashes.
1873
				preg_replace(   // Normalize everything to a-z0-9_-
1874
					'/[^a-z0-9_]+/',
1875
					'-',
1876
					strtolower( $field->get_attribute( 'label' ) ) // Lowercase
1877
				),
1878
				'-'
1879
			);
1880
1881
			$field_value = ( is_array( $field->value ) ) ? trim( implode( ', ', $field->value ) ) : trim( $field->value );
1882
1883
			// Skip any values that are already in the array we're sending.
1884
			if ( $field_value && in_array( $field_value, $akismet_vars ) ) {
1885
				continue;
1886
			}
1887
1888
			$akismet_vars[ 'contact_form_field_' . $field_slug ] = $field_value;
1889
		}
1890
1891
		$spam = '';
1892
		$akismet_values = $plugin->prepare_for_akismet( $akismet_vars );
1893
1894
		// Is it spam?
1895
		/** This filter is already documented in modules/contact-form/admin.php */
1896
		$is_spam = apply_filters( 'jetpack_contact_form_is_spam', false, $akismet_values );
1897
		if ( is_wp_error( $is_spam ) ) // WP_Error to abort
1898
			return $is_spam; // abort
1899
		elseif ( $is_spam === TRUE )  // TRUE to flag a spam
1900
			$spam = '***SPAM*** ';
1901
1902
		if ( !$comment_author )
1903
			$comment_author = $comment_author_email;
1904
1905
		/**
1906
		 * Filter the email where a submitted feedback is sent.
1907
		 *
1908
		 * @module contact-form
1909
		 *
1910
		 * @since 1.3.1
1911
		 *
1912
		 * @param string|array $to Array of valid email addresses, or single email address.
1913
		 */
1914
		$to = (array) apply_filters( 'contact_form_to', $to );
1915
		foreach ( $to as $to_key => $to_value ) {
1916
			$to[$to_key] = Grunion_Contact_Form_Plugin::strip_tags( $to_value );
1917
		}
1918
1919
		$blog_url = parse_url( site_url() );
1920
		$from_email_addr = 'wordpress@' . $blog_url['host'];
1921
1922
		$reply_to_addr = $to[0];
1923
		if ( ! empty( $comment_author_email ) ) {
1924
			$reply_to_addr = $comment_author_email;
1925
		}
1926
1927
		$headers =  'From: "' . $comment_author  .'" <' . $from_email_addr  . ">\r\n" .
1928
					'Reply-To: "' . $comment_author . '" <' . $reply_to_addr  . ">\r\n" .
1929
					"Content-Type: text/html; charset=\"" . get_option('blog_charset') . "\"";
1930
1931
		// Build feedback reference
1932
		$feedback_time  = current_time( 'mysql' );
1933
		$feedback_title = "{$comment_author} - {$feedback_time}";
1934
		$feedback_id    = md5( $feedback_title );
1935
1936
		$all_values = array_merge( $all_values, array(
1937
			'entry_title'     => the_title_attribute( 'echo=0' ),
1938
			'entry_permalink' => esc_url( get_permalink( get_the_ID() ) ),
1939
			'feedback_id'     => $feedback_id,
1940
		) );
1941
1942
		/** This filter is already documented in modules/contact-form/admin.php */
1943
		$subject = apply_filters( 'contact_form_subject', $contact_form_subject, $all_values );
1944
		$url     = $widget ? home_url( '/' ) : get_permalink( $post->ID );
1945
1946
		$date_time_format = _x( '%1$s \a\t %2$s', '{$date_format} \a\t {$time_format}', 'jetpack' );
1947
		$date_time_format = sprintf( $date_time_format, get_option( 'date_format' ), get_option( 'time_format' ) );
1948
		$time = date_i18n( $date_time_format, current_time( 'timestamp' ) );
1949
1950
		// keep a copy of the feedback as a custom post type
1951
		$feedback_status = $is_spam === TRUE ? 'spam' : 'publish';
1952
1953
		foreach ( (array) $akismet_values as $av_key => $av_value ) {
1954
			$akismet_values[$av_key] = Grunion_Contact_Form_Plugin::strip_tags( $av_value );
1955
		}
1956
1957
		foreach ( (array) $all_values as $all_key => $all_value ) {
1958
			$all_values[$all_key] = Grunion_Contact_Form_Plugin::strip_tags( $all_value );
1959
		}
1960
1961
		foreach ( (array) $extra_values as $ev_key => $ev_value ) {
1962
			$extra_values[$ev_key] = Grunion_Contact_Form_Plugin::strip_tags( $ev_value );
1963
		}
1964
1965
		/* We need to make sure that the post author is always zero for contact
1966
		 * form submissions.  This prevents export/import from trying to create
1967
		 * new users based on form submissions from people who were logged in
1968
		 * at the time.
1969
		 *
1970
		 * Unfortunately wp_insert_post() tries very hard to make sure the post
1971
		 * author gets the currently logged in user id.  That is how we ended up
1972
		 * with this work around. */
1973
		add_filter( 'wp_insert_post_data', array( $plugin, 'insert_feedback_filter' ), 10, 2 );
1974
1975
		$post_id = wp_insert_post( array(
1976
			'post_date'    => addslashes( $feedback_time ),
1977
			'post_type'    => 'feedback',
1978
			'post_status'  => addslashes( $feedback_status ),
1979
			'post_parent'  => (int) $post->ID,
1980
			'post_title'   => addslashes( wp_kses( $feedback_title, array() ) ),
1981
			'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
1982
			'post_name'    => $feedback_id,
1983
		) );
1984
1985
		// once insert has finished we don't need this filter any more
1986
		remove_filter( 'wp_insert_post_data', array( $plugin, 'insert_feedback_filter' ), 10, 2 );
1987
1988
		update_post_meta( $post_id, '_feedback_extra_fields', $this->addslashes_deep( $extra_values ) );
1989
1990
		// Increase count of unread feedback.
1991
		$unread = get_option( 'feedback_unread_count', 0 ) + 1;
1992
		update_option( 'feedback_unread_count', $unread );
1993
1994
		if ( defined( 'AKISMET_VERSION' ) ) {
1995
			update_post_meta( $post_id, '_feedback_akismet_values', $this->addslashes_deep( $akismet_values ) );
1996
		}
1997
1998
		$message = self::get_compiled_form( $post_id, $this );
1999
2000
		array_push(
2001
			$message,
2002
			"", // Empty line left intentionally
2003
			__( 'Time:', 'jetpack' ) . ' ' . $time . '<br />',
2004
			__( 'IP Address:', 'jetpack' ) . ' ' . $comment_author_IP . '<br />',
2005
			__( 'Contact Form URL:', 'jetpack' ) . " " . $url . '<br />'
2006
		);
2007
2008
		if ( is_user_logged_in() ) {
2009
			array_push(
2010
				$message,
2011
				"",
2012
				sprintf(
2013
					__( 'Sent by a verified %s user.', 'jetpack' ),
2014
					isset( $GLOBALS['current_site']->site_name ) && $GLOBALS['current_site']->site_name ?
2015
						$GLOBALS['current_site']->site_name : '"' . get_option( 'blogname' ) . '"'
2016
				)
2017
			);
2018
		} else {
2019
			array_push( $message, __( 'Sent by an unverified visitor to your site.', 'jetpack' ) );
2020
		}
2021
2022
		$message = join( $message, "\n" );
2023
		/**
2024
		 * Filters the message sent via email after a successfull form submission.
2025
		 *
2026
		 * @module contact-form
2027
		 *
2028
		 * @since 1.3.1
2029
		 *
2030
		 * @param string $message Feedback email message.
2031
		 */
2032
		$message = apply_filters( 'contact_form_message', $message );
2033
2034
		update_post_meta( $post_id, '_feedback_email', $this->addslashes_deep( compact( 'to', 'message' ) ) );
2035
2036
		/**
2037
		 * Fires right before the contact form message is sent via email to
2038
		 * the recipient specified in the contact form.
2039
		 *
2040
		 * @module contact-form
2041
		 *
2042
		 * @since 1.3.1
2043
		 *
2044
		 * @param integer $post_id Post contact form lives on
2045
		 * @param array $all_values Contact form fields
2046
		 * @param array $extra_values Contact form fields not included in $all_values
2047
		 */
2048
		do_action( 'grunion_pre_message_sent', $post_id, $all_values, $extra_values );
2049
2050
		// schedule deletes of old spam feedbacks
2051
		if ( ! wp_next_scheduled( 'grunion_scheduled_delete' ) ) {
2052
			wp_schedule_event( time() + 250, 'daily', 'grunion_scheduled_delete' );
2053
		}
2054
2055
		if (
2056
			$is_spam !== TRUE &&
2057
			/**
2058
			 * Filter to choose whether an email should be sent after each successfull contact form submission.
2059
			 *
2060
			 * @module contact-form
2061
			 *
2062
			 * @since 2.6.0
2063
			 *
2064
			 * @param bool true Should an email be sent after a form submission. Default to true.
2065
			 * @param int $post_id Post ID.
2066
			 */
2067
			true === apply_filters( 'grunion_should_send_email', true, $post_id )
2068
		) {
2069
			wp_mail( $to, "{$spam}{$subject}", $message, $headers );
2070
		} elseif (
2071
			true === $is_spam &&
2072
			/**
2073
			 * Choose whether an email should be sent for each spam contact form submission.
2074
			 *
2075
			 * @module contact-form
2076
			 *
2077
			 * @since 1.3.1
2078
			 *
2079
			 * @param bool false Should an email be sent after a spam form submission. Default to false.
2080
			 */
2081
			apply_filters( 'grunion_still_email_spam', FALSE ) == TRUE
2082
		) { // don't send spam by default.  Filterable.
2083
			wp_mail( $to, "{$spam}{$subject}", $message, $headers );
2084
		}
2085
2086
		if ( defined( 'DOING_AJAX' ) && DOING_AJAX ) {
2087
			return self::success_message( $post_id, $this );
2088
		}
2089
2090
		$redirect = wp_get_referer();
2091
		if ( !$redirect ) { // wp_get_referer() returns false if the referer is the same as the current page
2092
			$redirect = $_SERVER['REQUEST_URI'];
2093
		}
2094
2095
		$redirect = add_query_arg( urlencode_deep( array(
2096
			'contact-form-id'   => $id,
2097
			'contact-form-sent' => $post_id,
2098
			'_wpnonce'          => wp_create_nonce( "contact-form-sent-{$post_id}" ), // wp_nonce_url HTMLencodes :(
2099
		) ), $redirect );
2100
2101
		/**
2102
		 * Filter the URL where the reader is redirected after submitting a form.
2103
		 *
2104
		 * @module contact-form
2105
		 *
2106
		 * @since 1.9.0
2107
		 *
2108
		 * @param string $redirect Post submission URL.
2109
		 * @param int $id Contact Form ID.
2110
		 * @param int $post_id Post ID.
2111
		 */
2112
		$redirect = apply_filters( 'grunion_contact_form_redirect_url', $redirect, $id, $post_id );
2113
2114
		wp_safe_redirect( $redirect );
2115
		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...
2116
	}
2117
2118
	function addslashes_deep( $value ) {
2119
		if ( is_array( $value ) ) {
2120
			return array_map( array( $this, 'addslashes_deep' ), $value );
2121
		} elseif ( is_object( $value ) ) {
2122
			$vars = get_object_vars( $value );
2123
			foreach ( $vars as $key => $data ) {
2124
				$value->{$key} = $this->addslashes_deep( $data );
2125
			}
2126
			return $value;
2127
		}
2128
2129
		return addslashes( $value );
2130
	}
2131
}
2132
2133
/**
2134
 * Class for the contact-field shortcode.
2135
 * Parses shortcode to output the contact form field as HTML.
2136
 * Validates input.
2137
 */
2138
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...
2139
	public $shortcode_name = 'contact-field';
2140
2141
	/**
2142
	 * @var Grunion_Contact_Form parent form
2143
	 */
2144
	public $form;
2145
2146
	/**
2147
	 * @var string default or POSTed value
2148
	 */
2149
	public $value;
2150
2151
	/**
2152
	 * @var bool Is the input invalid?
2153
	 */
2154
	public $error = false;
2155
2156
	/**
2157
	 * @param array $attributes An associative array of shortcode attributes.  @see shortcode_atts()
2158
	 * @param null|string $content Null for selfclosing shortcodes.  The inner content otherwise.
2159
	 * @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...
2160
	 */
2161
	function __construct( $attributes, $content = null, $form = null ) {
2162
		$attributes = shortcode_atts( array(
2163
			'label'       => null,
2164
			'type'        => 'text',
2165
			'required'    => false,
2166
			'options'     => array(),
2167
			'id'          => null,
2168
			'default'     => null,
2169
			'placeholder' => null,
2170
			'class'       => null,
2171
		), $attributes, 'contact-field' );
2172
2173
		// special default for subject field
2174
		if ( 'subject' == $attributes['type'] && is_null( $attributes['default'] ) && !is_null( $form ) ) {
2175
			$attributes['default'] = $form->get_attribute( 'subject' );
2176
		}
2177
2178
		// allow required=1 or required=true
2179
		if ( '1' == $attributes['required'] || 'true' == strtolower( $attributes['required'] ) )
2180
			$attributes['required'] = true;
2181
		else
2182
			$attributes['required'] = false;
2183
2184
		// parse out comma-separated options list (for selects, radios, and checkbox-multiples)
2185
		if ( !empty( $attributes['options'] ) && is_string( $attributes['options'] ) ) {
2186
			$attributes['options'] = array_map( 'trim', explode( ',', $attributes['options'] ) );
2187
		}
2188
2189
		if ( $form ) {
2190
			// make a unique field ID based on the label, with an incrementing number if needed to avoid clashes
2191
			$form_id = $form->get_attribute( 'id' );
2192
			$id = isset( $attributes['id'] ) ? $attributes['id'] : false;
2193
2194
			$unescaped_label = $this->unesc_attr( $attributes['label'] );
2195
			$unescaped_label = str_replace( '%', '-', $unescaped_label ); // jQuery doesn't like % in IDs?
2196
			$unescaped_label = preg_replace( '/[^a-zA-Z0-9.-_:]/', '', $unescaped_label );
2197
2198
			if ( empty( $id ) ) {
2199
				$id = sanitize_title_with_dashes( 'g' . $form_id . '-' . $unescaped_label );
2200
				$i = 0;
2201
				$max_tries = 99;
2202
				while ( isset( $form->fields[$id] ) ) {
2203
					$i++;
2204
					$id = sanitize_title_with_dashes( 'g' . $form_id . '-' . $unescaped_label . '-' . $i );
2205
2206
					if ( $i > $max_tries ) {
2207
						break;
2208
					}
2209
				}
2210
			}
2211
2212
			$attributes['id'] = $id;
2213
		}
2214
2215
		parent::__construct( $attributes, $content );
2216
2217
		// Store parent form
2218
		$this->form = $form;
2219
	}
2220
2221
	/**
2222
	 * This field's input is invalid.  Flag as invalid and add an error to the parent form
2223
	 *
2224
	 * @param string $message The error message to display on the form.
2225
	 */
2226
	function add_error( $message ) {
2227
		$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...
2228
2229
		if ( !is_wp_error( $this->form->errors ) ) {
2230
			$this->form->errors = new WP_Error;
2231
		}
2232
2233
		$this->form->errors->add( $this->get_attribute( 'id' ), $message );
2234
	}
2235
2236
	/**
2237
	 * Is the field input invalid?
2238
	 *
2239
	 * @see $error
2240
	 *
2241
	 * @return bool
2242
	 */
2243
	function is_error() {
2244
		return $this->error;
2245
	}
2246
2247
	/**
2248
	 * Validates the form input
2249
	 */
2250
	function validate() {
2251
		// If it's not required, there's nothing to validate
2252
		if ( !$this->get_attribute( 'required' ) ) {
2253
			return;
2254
		}
2255
2256
		$field_id    = $this->get_attribute( 'id' );
2257
		$field_type  = $this->get_attribute( 'type' );
2258
		$field_label = $this->get_attribute( 'label' );
2259
2260
		if ( isset( $_POST[ $field_id ] ) ) {
2261
			if ( is_array( $_POST[ $field_id ] ) ) {
2262
				$field_value = array_map( 'stripslashes', $_POST[ $field_id ] );
2263
			} else {
2264
				$field_value = stripslashes( $_POST[ $field_id ] );
2265
			}
2266
		} else {
2267
			$field_value = '';
2268
		}
2269
2270
		switch ( $field_type ) {
2271
		case 'email' :
2272
			// Make sure the email address is valid
2273
			if ( !is_email( $field_value ) ) {
2274
				$this->add_error( sprintf( __( '%s requires a valid email address', 'jetpack' ), $field_label ) );
2275
			}
2276
			break;
2277
		case 'checkbox-multiple' :
2278
			// Check that there is at least one option selected
2279
			if ( empty( $field_value ) ) {
2280
				$this->add_error( sprintf( __( '%s requires at least one selection', 'jetpack' ), $field_label ) );
2281
			}
2282
			break;
2283
		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...
2284
			// Just check for presence of any text
2285
			if ( !strlen( trim( $field_value ) ) ) {
2286
				$this->add_error( sprintf( __( '%s is required', 'jetpack' ), $field_label ) );
2287
			}
2288
		}
2289
	}
2290
2291
	/**
2292
	 * Outputs the HTML for this form field
2293
	 *
2294
	 * @return string HTML
2295
	 */
2296
	function render() {
2297
		global $current_user, $user_identity;
2298
2299
		$r = '';
2300
2301
		$field_id          = $this->get_attribute( 'id' );
2302
		$field_type        = $this->get_attribute( 'type' );
2303
		$field_label       = $this->get_attribute( 'label' );
2304
		$field_required    = $this->get_attribute( 'required' );
2305
		$placeholder       = $this->get_attribute( 'placeholder' );
2306
		$class             = $this->get_attribute( 'class' );
2307
		$field_placeholder = ( ! empty( $placeholder ) ) ? "placeholder='" . esc_attr( $placeholder ) . "'" : '';
2308
		$field_class       = "class='" . trim( esc_attr( $field_type ) . " " . esc_attr( $class ) ) . "' ";
2309
2310
		if ( isset( $_POST[ $field_id ] ) ) {
2311
			if ( is_array( $_POST[ $field_id ] ) ) {
2312
				$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...
2313
			} else {
2314
				$this->value = stripslashes( (string) $_POST[ $field_id ] );
2315
			}
2316
		} elseif ( isset( $_GET[ $field_id ] ) ) {
2317
			$this->value = stripslashes( (string) $_GET[ $field_id ] );
2318
		} elseif (
2319
			is_user_logged_in() &&
2320
			( ( defined( 'IS_WPCOM' ) && IS_WPCOM ) ||
2321
			/**
2322
			 * Allow third-party tools to prefill the contact form with the user's details when they're logged in.
2323
			 *
2324
			 * @module contact-form
2325
			 *
2326
			 * @since 3.2.0
2327
			 *
2328
			 * @param bool false Should the Contact Form be prefilled with your details when you're logged in. Default to false.
2329
			 */
2330
			true === apply_filters( 'jetpack_auto_fill_logged_in_user', false )
2331
			)
2332
		) {
2333
			// Special defaults for logged-in users
2334
			switch ( $this->get_attribute( 'type' ) ) {
2335
			case 'email' :
2336
				$this->value = $current_user->data->user_email;
2337
				break;
2338
			case 'name' :
2339
				$this->value = $user_identity;
2340
				break;
2341
			case 'url' :
2342
				$this->value = $current_user->data->user_url;
2343
				break;
2344
			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...
2345
				$this->value = $this->get_attribute( 'default' );
2346
			}
2347
		} else {
2348
			$this->value = $this->get_attribute( 'default' );
2349
		}
2350
2351
		$field_value = Grunion_Contact_Form_Plugin::strip_tags( $this->value );
2352
		$field_label = Grunion_Contact_Form_Plugin::strip_tags( $field_label );
2353
2354
		/**
2355
		 * Filter the Contact Form required field text
2356
		 *
2357
		 * @module contact-form
2358
		 *
2359
		 * @since 3.8.0
2360
		 *
2361
		 * @param string $var Required field text. Default is "(required)".
2362
		 */
2363
		$required_field_text = esc_html( apply_filters( 'jetpack_required_field_text', __( "(required)", 'jetpack' ) ) );
2364
2365
		switch ( $field_type ) {
2366 View Code Duplication
		case 'email' :
2367
			$r .= "\n<div>\n";
2368
			$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";
2369
			$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";
2370
			$r .= "\t</div>\n";
2371
			break;
2372
		case 'telephone' :
2373
			$r .= "\n<div>\n";
2374
			$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";
2375
			$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";
2376
			break;
2377 View Code Duplication
		case 'textarea' :
2378
			$r .= "\n<div>\n";
2379
			$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";
2380
			$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";
2381
			$r .= "\t</div>\n";
2382
			break;
2383 View Code Duplication
		case 'radio' :
2384
			$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";
2385
			foreach ( $this->get_attribute( 'options' ) as $option ) {
2386
				$option = Grunion_Contact_Form_Plugin::strip_tags( $option );
2387
				$r .= "\t\t<label class='grunion-radio-label radio" . ( $this->is_error() ? ' form-error' : '' ) . "'>";
2388
				$r .= "<input type='radio' name='" . esc_attr( $field_id ) . "' value='" . esc_attr( $option ) . "' " . $field_class . checked( $option, $field_value, false ) . " " . ( $field_required ? "required aria-required='true'" : "" ) . "/> ";
2389
				$r .= esc_html( $option ) . "</label>\n";
2390
				$r .= "\t\t<div class='clear-form'></div>\n";
2391
			}
2392
			$r .= "\t\t</div>\n";
2393
			break;
2394
		case 'checkbox' :
2395
			$r .= "\t<div>\n";
2396
			$r .= "\t\t<label class='grunion-field-label checkbox" . ( $this->is_error() ? ' form-error' : '' ) . "'>\n";
2397
			$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";
2398
			$r .= "\t\t" . esc_html( $field_label ) . ( $field_required ? '<span>'. $required_field_text . '</span>' : '' ) . "</label>\n";
2399
			$r .= "\t\t<div class='clear-form'></div>\n";
2400
			$r .= "\t</div>\n";
2401
			break;
2402
		case 'checkbox-multiple' :
2403
			$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";
2404
			foreach ( $this->get_attribute( 'options' ) as $option ) {
2405
				$option = Grunion_Contact_Form_Plugin::strip_tags( $option );
2406
				$r .= "\t\t<label class='grunion-checkbox-multiple-label checkbox-multiple" . ( $this->is_error() ? ' form-error' : '' ) . "'>";
2407
				$r .= "<input type='checkbox' name='" . esc_attr( $field_id ) . "[]' value='" . esc_attr( $option ) . "' " . $field_class . checked( in_array( $option, (array) $field_value ), true, false ) . " /> ";
2408
				$r .= esc_html( $option ) . "</label>\n";
2409
				$r .= "\t\t<div class='clear-form'></div>\n";
2410
			}
2411
			$r .= "\t\t</div>\n";
2412
			break;
2413 View Code Duplication
		case 'select' :
2414
			$r .= "\n<div>\n";
2415
			$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";
2416
			$r .= "\t<select name='" . esc_attr( $field_id ) . "' id='" . esc_attr( $field_id ) . "' " . $field_class . ( $field_required ? "required aria-required='true'" : "" ) . ">\n";
2417
			foreach ( $this->get_attribute( 'options' ) as $option ) {
2418
				$option = Grunion_Contact_Form_Plugin::strip_tags( $option );
2419
				$r .= "\t\t<option" . selected( $option, $field_value, false ) . ">" . esc_html( $option ) . "</option>\n";
2420
			}
2421
			$r .= "\t</select>\n";
2422
			$r .= "\t</div>\n";
2423
			break;
2424
		case 'date' :
2425
			$r .= "\n<div>\n";
2426
			$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";
2427
			$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";
2428
			$r .= "\t</div>\n";
2429
2430
			wp_enqueue_script( 'grunion-frontend', plugins_url( 'js/grunion-frontend.js', __FILE__ ), array( 'jquery', 'jquery-ui-datepicker' ) );
2431
			break;
2432 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...
2433
			// note that any unknown types will produce a text input, so we can use arbitrary type names to handle
2434
			// input fields like name, email, url that require special validation or handling at POST
2435
			$r .= "\n<div>\n";
2436
			$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";
2437
			$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";
2438
			$r .= "\t</div>\n";
2439
		}
2440
2441
		/**
2442
		 * Filter the HTML of the Contact Form.
2443
		 *
2444
		 * @module contact-form
2445
		 *
2446
		 * @since 2.6.0
2447
		 *
2448
		 * @param string $r Contact Form HTML output.
2449
		 * @param string $field_label Field label.
2450
		 * @param int|null $id Post ID.
2451
		 */
2452
		return apply_filters( 'grunion_contact_form_field_html', $r, $field_label, ( in_the_loop() ? get_the_ID() : null ) );
2453
	}
2454
}
2455
2456
add_action( 'init', array( 'Grunion_Contact_Form_Plugin', 'init' ) );
2457
2458
add_action( 'grunion_scheduled_delete', 'grunion_delete_old_spam' );
2459
2460
/**
2461
 * Deletes old spam feedbacks to keep the posts table size under control
2462
 */
2463
function grunion_delete_old_spam() {
2464
	global $wpdb;
2465
2466
	$grunion_delete_limit = 100;
2467
2468
	$now_gmt = current_time( 'mysql', 1 );
2469
	$sql = $wpdb->prepare( "
2470
		SELECT `ID`
2471
		FROM $wpdb->posts
2472
		WHERE DATE_SUB( %s, INTERVAL 15 DAY ) > `post_date_gmt`
2473
			AND `post_type` = 'feedback'
2474
			AND `post_status` = 'spam'
2475
		LIMIT %d
2476
	", $now_gmt, $grunion_delete_limit );
2477
	$post_ids = $wpdb->get_col( $sql );
2478
2479
	foreach ( (array) $post_ids as $post_id ) {
2480
		# force a full delete, skip the trash
2481
		wp_delete_post( $post_id, TRUE );
2482
	}
2483
2484
	# Arbitrary check points for running OPTIMIZE
2485
	# nothing special about 5000 or 11
2486
	# just trying to periodically recover deleted rows
2487
	$random_num = mt_rand( 1, 5000 );
2488
	if (
2489
		/**
2490
		 * Filter how often the module run OPTIMIZE TABLE on the core WP tables.
2491
		 *
2492
		 * @module contact-form
2493
		 *
2494
		 * @since 1.3.1
2495
		 *
2496
		 * @param int $random_num Random number.
2497
		 */
2498
		apply_filters( 'grunion_optimize_table', ( $random_num == 11 ) )
2499
	) {
2500
		$wpdb->query( "OPTIMIZE TABLE $wpdb->posts" );
2501
	}
2502
2503
	# if we hit the max then schedule another run
2504
	if ( count( $post_ids ) >= $grunion_delete_limit ) {
2505
		wp_schedule_single_event( time() + 700, 'grunion_scheduled_delete' );
2506
	}
2507
}
2508