Completed
Push — contact-form/move-export ( 248dd8 )
by George
10:45
created

Grunion_Contact_Form_Plugin   D

Complexity

Total Complexity 125

Size/Duplication

Total Lines 1005
Duplicated Lines 0 %

Coupling/Cohesion

Components 3
Dependencies 1

Importance

Changes 1
Bugs 0 Features 1
Metric Value
wmc 125
c 1
b 0
f 1
lcom 3
cbo 1
dl 0
loc 1005
rs 4.4426

31 Methods

Rating   Name   Duplication   Size   Complexity  
A strip_tags() 0 16 3
A init() 0 12 2
A daily_akismet_meta_cleanup() 0 13 3
C __construct() 0 101 11
A admin_menu() 0 10 1
A allow_feedback_rest_api_type() 0 4 1
C process_form_submission() 0 71 12
A ajax_request() 0 19 3
A insert_feedback_filter() 0 7 3
A add_shortcode() 0 4 1
A tokenize_label() 0 3 1
A sanitize_value() 0 3 1
A replace_tokens_with_input() 0 16 3
A track_current_widget() 0 3 1
A widget_atts() 0 5 1
A widget_shortcode_hack() 0 17 2
B prepare_for_akismet() 0 26 6
C is_spam_akismet() 0 46 12
A akismet_submit() 0 17 4
B export_form() 0 46 5
A get_post_content_for_csv_export() 0 6 1
A get_post_meta_for_csv_export() 0 3 1
A get_parsed_field_contents_of_post() 0 3 1
B map_parsed_field_contents_of_post_to_field_names() 0 23 4
C get_export_data_for_posts() 0 109 11
C download_feedback_as_csv() 0 94 10
B get_feedbacks_as_options() 0 28 2
A get_field_names() 0 16 3
C parse_fields_from_content() 0 52 8
B make_csv_row_from_feedback() 0 28 6
A get_ip_address() 0 3 2

How to fix   Complexity   

Complex Class

Complex classes like Grunion_Contact_Form_Plugin often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Grunion_Contact_Form_Plugin, and based on these observations, apply Extract Interface, too.

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