Completed
Push — branch-4.0 ( fc0d6f...bf547c )
by
unknown
08:22
created

Grunion_Contact_Form::parse()   D

Complexity

Conditions 20
Paths 51

Size

Total Lines 118
Code Lines 50

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 20
eloc 50
nc 51
nop 2
dl 0
loc 118
rs 4.7294
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

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