Completed
Push — fix/contact_form_checkbox_mult... ( df595e )
by
unknown
10:30
created

Grunion_Contact_Form_Field::validate()   D

Complexity

Conditions 9
Paths 19

Size

Total Lines 40
Code Lines 25

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 40
rs 4.9091
cc 9
eloc 25
nc 19
nop 0
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
38
		return $instance;
39
	}
40
41
	/**
42
	 * Strips HTML tags from input.  Output is NOT HTML safe.
43
	 *
44
	 * @param mixed $data_with_tags
45
	 * @return mixed
46
	 */
47
	public static function strip_tags( $data_with_tags ) {
48
		if ( is_array( $data_with_tags ) ) {
49
			foreach ( $data_with_tags as $index => $value ) {
50
				$index = sanitize_text_field( strval( $index ) );
51
				$value = wp_kses( strval( $value ), array() );
52
				$value = str_replace( '&amp;', '&', $value ); // undo damage done by wp_kses_normalize_entities()
53
54
				$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...
55
			}
56
		} else {
57
			$data_without_tags = wp_kses( $data_with_tags, array() );
58
			$data_without_tags = str_replace( '&amp;', '&', $data_without_tags ); // undo damage done by wp_kses_normalize_entities()
59
		}
60
61
		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...
62
	}
63
64
	function __construct() {
65
		$this->add_shortcode();
66
67
		// While generating the output of a text widget with a contact-form shortcode, we need to know its widget ID.
68
		add_action( 'dynamic_sidebar', array( $this, 'track_current_widget' ) );
69
70
		// Add a "widget" shortcode attribute to all contact-form shortcodes embedded in widgets
71
		add_filter( 'widget_text', array( $this, 'widget_atts' ), 0 );
72
73
		// If Text Widgets don't get shortcode processed, hack ours into place.
74
		if ( !has_filter( 'widget_text', 'do_shortcode' ) )
75
			add_filter( 'widget_text', array( $this, 'widget_shortcode_hack' ), 5 );
76
77
		// Akismet to the rescue
78
		if ( defined( 'AKISMET_VERSION' ) || function_exists( 'akismet_http_post' ) ) {
79
			add_filter( 'jetpack_contact_form_is_spam', array( $this, 'is_spam_akismet' ), 10, 2 );
80
			add_action( 'contact_form_akismet', array( $this, 'akismet_submit' ), 10, 2 );
81
		}
82
83
		add_action( 'loop_start', array( 'Grunion_Contact_Form', '_style_on' ) );
84
85
		add_action( 'wp_ajax_grunion-contact-form', array( $this, 'ajax_request' ) );
86
		add_action( 'wp_ajax_nopriv_grunion-contact-form', array( $this, 'ajax_request' ) );
87
88
		// Export to CSV feature
89
		if ( is_admin() ) {
90
			add_action( 'admin_init',            array( $this, 'download_feedback_as_csv' ) );
91
			add_action( 'admin_footer-edit.php', array( $this, 'export_form' ) );
92
		}
93
94
		// custom post type we'll use to keep copies of the feedback items
95
		register_post_type( 'feedback', array(
96
			'labels'            => array(
97
				'name'               => __( 'Feedback', 'jetpack' ),
98
				'singular_name'      => __( 'Feedback', 'jetpack' ),
99
				'search_items'       => __( 'Search Feedback', 'jetpack' ),
100
				'not_found'          => __( 'No feedback found', 'jetpack' ),
101
				'not_found_in_trash' => __( 'No feedback found', 'jetpack' )
102
			),
103
			'menu_icon'         => GRUNION_PLUGIN_URL . '/images/grunion-menu.png',
104
			'show_ui'           => TRUE,
105
			'show_in_admin_bar' => FALSE,
106
			'public'            => FALSE,
107
			'rewrite'           => FALSE,
108
			'query_var'         => FALSE,
109
			'capability_type'   => 'page'
110
		) );
111
112
		// Add to REST API post type whitelist
113
		add_filter( 'rest_api_allowed_post_types', array( $this, 'allow_feedback_rest_api_type' ) );
114
115
		// Add "spam" as a post status
116
		register_post_status( 'spam', array(
117
			'label'                  => 'Spam',
118
			'public'                 => FALSE,
119
			'exclude_from_search'    => TRUE,
120
			'show_in_admin_all_list' => FALSE,
121
			'label_count'            => _n_noop( 'Spam <span class="count">(%s)</span>', 'Spam <span class="count">(%s)</span>', 'jetpack' ),
122
			'protected'              => TRUE,
123
			'_builtin'               => FALSE
124
		) );
125
126
		// POST handler
127
		if (
128
			isset( $_SERVER['REQUEST_METHOD'] ) && 'POST' == strtoupper( $_SERVER['REQUEST_METHOD'] )
129
		&&
130
			isset( $_POST['action'] ) && 'grunion-contact-form' == $_POST['action']
131
		&&
132
			isset( $_POST['contact-form-id'] )
133
		) {
134
			add_action( 'template_redirect', array( $this, 'process_form_submission' ) );
135
		}
136
137
		/* Can be dequeued by placing the following in wp-content/themes/yourtheme/functions.php
138
		 *
139
		 * 	function remove_grunion_style() {
140
		 *		wp_deregister_style('grunion.css');
141
		 *	}
142
		 *	add_action('wp_print_styles', 'remove_grunion_style');
143
		 */
144
		if( is_rtl() ){
145
			wp_register_style( 'grunion.css', GRUNION_PLUGIN_URL . 'css/rtl/grunion-rtl.css', array(), JETPACK__VERSION );
146
		} else {
147
			wp_register_style( 'grunion.css', GRUNION_PLUGIN_URL . 'css/grunion.css', array(), JETPACK__VERSION );
148
		}
149
	}
150
151
	/**
152
	 * Add to REST API post type whitelist
153
	 */
154
	function allow_feedback_rest_api_type( $post_types ) {
155
		$post_types[] = 'feedback';
156
		return $post_types;
157
	}
158
159
	/**
160
	 * Handles all contact-form POST submissions
161
	 *
162
	 * Conditionally attached to `template_redirect`
163
	 */
164
	function process_form_submission() {
165
		// Add a filter to replace tokens in the subject field with sanitized field values
166
		add_filter( 'contact_form_subject', array( $this, 'replace_tokens_with_input' ), 10, 2 );
167
168
		$id = stripslashes( $_POST['contact-form-id'] );
169
170
		if ( is_user_logged_in() ) {
171
			check_admin_referer( "contact-form_{$id}" );
172
		}
173
174
		$is_widget = 0 === strpos( $id, 'widget-' );
175
176
		$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...
177
178
		if ( $is_widget ) {
179
			// It's a form embedded in a text widget
180
181
			$this->current_widget_id = substr( $id, 7 ); // remove "widget-"
182
			$widget_type = implode( '-', array_slice( explode( '-', $this->current_widget_id ), 0, -1 ) ); // Remove trailing -#
183
184
			// Is the widget active?
185
			$sidebar = is_active_widget( false, $this->current_widget_id, $widget_type );
186
187
			// This is lame - no core API for getting a widget by ID
188
			$widget = isset( $GLOBALS['wp_registered_widgets'][$this->current_widget_id] ) ? $GLOBALS['wp_registered_widgets'][$this->current_widget_id] : false;
189
190
			if ( $sidebar && $widget && isset( $widget['callback'] ) ) {
191
				// This is lamer - no API for outputting a given widget by ID
192
				ob_start();
193
				// Process the widget to populate Grunion_Contact_Form::$last
194
				call_user_func( $widget['callback'], array(), $widget['params'][0] );
195
				ob_end_clean();
196
			}
197
		} else {
198
			// It's a form embedded in a post
199
200
			$post = get_post( $id );
201
202
			// Process the content to populate Grunion_Contact_Form::$last
203
			/** This filter is already documented in core. wp-includes/post-template.php */
204
			apply_filters( 'the_content', $post->post_content );
205
		}
206
207
		$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...
208
209
		// No form may mean user is using do_shortcode, grab the form using the stored post meta
210
		if ( ! $form ) {
211
212
			// Get shortcode from post meta
213
			$shortcode = get_post_meta( $_POST['contact-form-id'], '_g_feedback_shortcode', true );
214
215
			// Format it
216
			if ( $shortcode != '' ) {
217
				$shortcode = '[contact-form]' . $shortcode . '[/contact-form]';
218
				do_shortcode( $shortcode );
219
220
				// Recreate form
221
				$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...
222
			}
223
224
			if ( ! $form ) {
225
				return false;
226
			}
227
		}
228
229
		if ( is_wp_error( $form->errors ) && $form->errors->get_error_codes() )
230
			return $form->errors;
231
232
		// Process the form
233
		return $form->process_submission();
234
	}
235
236
	function ajax_request() {
237
		$submission_result = self::process_form_submission();
238
239
		if ( ! $submission_result ) {
240
			header( "HTTP/1.1 500 Server Error", 500, true );
241
			echo '<div class="form-error"><ul class="form-errors"><li class="form-error-message">';
242
			esc_html_e( 'An error occurred. Please try again later.', 'jetpack' );
243
			echo '</li></ul></div>';
244
		} elseif ( is_wp_error( $submission_result ) ) {
245
			header( "HTTP/1.1 400 Bad Request", 403, true );
246
			echo '<div class="form-error"><ul class="form-errors"><li class="form-error-message">';
247
			echo esc_html( $submission_result->get_error_message() );
248
			echo '</li></ul></div>';
249
		} else {
250
			echo '<h3>' . esc_html__( 'Message Sent', 'jetpack' ) . '</h3>' . $submission_result;
251
		}
252
253
		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...
254
	}
255
256
	/**
257
	 * Ensure the post author is always zero for contact-form feedbacks
258
	 * Attached to `wp_insert_post_data`
259
	 *
260
	 * @see Grunion_Contact_Form::process_submission()
261
	 *
262
	 * @param array $data the data to insert
263
	 * @param array $postarr the data sent to wp_insert_post()
264
	 * @return array The filtered $data to insert
265
	 */
266
	function insert_feedback_filter( $data, $postarr ) {
267
		if ( $data['post_type'] == 'feedback' && $postarr['post_type'] == 'feedback' ) {
268
			$data['post_author'] = 0;
269
		}
270
271
		return $data;
272
	}
273
	/*
274
	 * Adds our contact-form shortcode
275
	 * The "child" contact-field shortcode is enabled as needed by the contact-form shortcode handler
276
	 */
277
	function add_shortcode() {
278
		add_shortcode( 'contact-form',         array( 'Grunion_Contact_Form', 'parse' ) );
279
		add_shortcode( 'contact-field',        array( 'Grunion_Contact_Form', 'parse_contact_field' ) );
280
	}
281
282
	static function tokenize_label( $label ) {
283
		return '{' . trim( preg_replace( '#^\d+_#', '', $label ) ) . '}';
284
	}
285
286
	static function sanitize_value( $value ) {
287
		return preg_replace( '=((<CR>|<LF>|0x0A/%0A|0x0D/%0D|\\n|\\r)\S).*=i', null, $value );
288
	}
289
290
	/**
291
	 * Replaces tokens like {city} or {City} (case insensitive) with the value
292
	 * of an input field of that name
293
	 *
294
	 * @param string $subject
295
	 * @param array $field_values Array with field label => field value associations
296
	 *
297
	 * @return string The filtered $subject with the tokens replaced
298
	 */
299
	function replace_tokens_with_input( $subject, $field_values ) {
300
		// Wrap labels into tokens (inside {})
301
		$wrapped_labels = array_map( array( 'Grunion_Contact_Form_Plugin', 'tokenize_label' ), array_keys( $field_values ) );
302
		// Sanitize all values
303
		$sanitized_values = array_map( array( 'Grunion_Contact_Form_Plugin', 'sanitize_value' ), array_values( $field_values ) );
304
305
		foreach ( $sanitized_values as $k => $sanitized_value ) {
306
			if ( is_array( $sanitized_value ) ) {
307
				$sanitized_values[ $k ] = implode( ', ', $sanitized_value );
308
			}
309
		}
310
311
		// Search for all valid tokens (based on existing fields) and replace with the field's value
312
		$subject = str_ireplace( $wrapped_labels, $sanitized_values, $subject );
313
		return $subject;
314
	}
315
316
	/**
317
	 * Tracks the widget currently being processed.
318
	 * Attached to `dynamic_sidebar`
319
	 *
320
	 * @see $current_widget_id
321
	 *
322
	 * @param array $widget The widget data
323
	 */
324
	function track_current_widget( $widget ) {
325
		$this->current_widget_id = $widget['id'];
326
	}
327
328
	/**
329
	 * Adds a "widget" attribute to every contact-form embedded in a text widget.
330
	 * Used to tell the difference between post-embedded contact-forms and widget-embedded contact-forms
331
	 * Attached to `widget_text`
332
	 *
333
	 * @param string $text The widget text
334
	 * @return string The filtered widget text
335
	 */
336
	function widget_atts( $text ) {
337
		Grunion_Contact_Form::style( true );
338
339
		return preg_replace( '/\[contact-form([^a-zA-Z_-])/', '[contact-form widget="' . $this->current_widget_id . '"\\1', $text );
340
	}
341
342
	/**
343
	 * For sites where text widgets are not processed for shortcodes, we add this hack to process just our shortcode
344
	 * Attached to `widget_text`
345
	 *
346
	 * @param string $text The widget text
347
	 * @return string The contact-form filtered widget text
348
	 */
349
	function widget_shortcode_hack( $text ) {
350
		if ( !preg_match( '/\[contact-form([^a-zA-Z_-])/', $text ) ) {
351
			return $text;
352
		}
353
354
		$old = $GLOBALS['shortcode_tags'];
355
		remove_all_shortcodes();
356
		Grunion_Contact_Form_Plugin::$using_contact_form_field = true;
357
		$this->add_shortcode();
358
359
		$text = do_shortcode( $text );
360
361
		Grunion_Contact_Form_Plugin::$using_contact_form_field = false;
362
		$GLOBALS['shortcode_tags'] = $old;
363
364
		return $text;
365
	}
366
367
	/**
368
	 * Populate an array with all values necessary to submit a NEW contact-form feedback to Akismet.
369
	 * Note that this includes the current user_ip etc, so this should only be called when accepting a new item via $_POST
370
	 *
371
	 * @param array $form Contact form feedback array
372
	 * @return array feedback array with additional data ready for submission to Akismet
373
	 */
374
	function prepare_for_akismet( $form ) {
375
		$form['comment_type'] = 'contact_form';
376
		$form['user_ip']      = preg_replace( '/[^0-9., ]/', '', $_SERVER['REMOTE_ADDR'] );
377
		$form['user_agent']   = $_SERVER['HTTP_USER_AGENT'];
378
		$form['referrer']     = $_SERVER['HTTP_REFERER'];
379
		$form['blog']         = get_option( 'home' );
380
381
		$ignore = array( 'HTTP_COOKIE' );
382
383
		foreach ( $_SERVER as $k => $value )
384
			if ( !in_array( $k, $ignore ) && is_string( $value ) )
385
				$form["$k"] = $value;
386
387
		return $form;
388
	}
389
390
	/**
391
	 * Submit contact-form data to Akismet to check for spam.
392
	 * If you're accepting a new item via $_POST, run it Grunion_Contact_Form_Plugin::prepare_for_akismet() first
393
	 * Attached to `jetpack_contact_form_is_spam`
394
	 *
395
	 * @param bool $is_spam
396
	 * @param array $form
397
	 * @return bool|WP_Error TRUE => spam, FALSE => not spam, WP_Error => stop processing entirely
398
	 */
399
	function is_spam_akismet( $is_spam, $form = array() ) {
400
		global $akismet_api_host, $akismet_api_port;
401
402
		// The signature of this function changed from accepting just $form.
403
		// If something only sends an array, assume it's still using the old
404
		// signature and work around it.
405
		if ( empty( $form ) && is_array( $is_spam ) ) {
406
			$form = $is_spam;
407
			$is_spam = false;
408
		}
409
410
		// If a previous filter has alrady marked this as spam, trust that and move on.
411
		if ( $is_spam ) {
412
			return $is_spam;
413
		}
414
415
		if ( !function_exists( 'akismet_http_post' ) && !defined( 'AKISMET_VERSION' ) )
416
			return false;
417
418
		$query_string = http_build_query( $form );
419
420
		if ( method_exists( 'Akismet', 'http_post' ) ) {
421
			$response = Akismet::http_post( $query_string, 'comment-check' );
422
		} else {
423
			$response = akismet_http_post( $query_string, $akismet_api_host, '/1.1/comment-check', $akismet_api_port );
424
		}
425
426
		$result = false;
427
428
		if ( isset( $response[0]['x-akismet-pro-tip'] ) && 'discard' === trim( $response[0]['x-akismet-pro-tip'] ) && get_option( 'akismet_strictness' ) === '1' )
429
			$result = new WP_Error( 'feedback-discarded', __('Feedback discarded.', 'jetpack' ) );
430
		elseif ( isset( $response[1] ) && 'true' == trim( $response[1] ) ) // 'true' is spam
431
			$result = true;
432
433
		/**
434
		 * Filter the results returned by Akismet for each submitted contact form.
435
		 *
436
		 * @module contact-form
437
		 *
438
		 * @since 1.3.1
439
		 *
440
		 * @param WP_Error|bool $result Is the submitted feedback spam.
441
		 * @param array|bool $form Submitted feedback.
442
		 */
443
		return apply_filters( 'contact_form_is_spam_akismet', $result, $form );
444
	}
445
446
	/**
447
	 * Submit a feedback as either spam or ham
448
	 *
449
	 * @param string $as Either 'spam' or 'ham'.
450
	 * @param array $form the contact-form data
451
	 */
452
	function akismet_submit( $as, $form ) {
453
		global $akismet_api_host, $akismet_api_port;
454
455
		if ( !in_array( $as, array( 'ham', 'spam' ) ) )
456
			return false;
457
458
		$query_string = '';
459
		if ( is_array( $form ) )
460
			$query_string = http_build_query( $form );
461
		if ( method_exists( 'Akismet', 'http_post' ) ) {
462
		    $response = Akismet::http_post( $query_string, "submit-{$as}" );
463
		} else {
464
		    $response = akismet_http_post( $query_string, $akismet_api_host, "/1.1/submit-{$as}", $akismet_api_port );
465
		}
466
467
		return trim( $response[1] );
468
	}
469
470
	/**
471
	 * Prints the menu
472
	 */
473
	function export_form() {
474
		if ( get_current_screen()->id != 'edit-feedback' )
475
			return;
476
477
		if ( ! current_user_can( 'export' ) ) {
478
			return;
479
		}
480
481
		// if there aren't any feedbacks, bail out
482
		if ( ! (int) wp_count_posts( 'feedback' )->publish )
483
			return;
484
		?>
485
486
		<div id="feedback-export" style="display:none">
487
			<h2><?php _e( 'Export feedback as CSV', 'jetpack' ) ?></h2>
488
			<div class="clear"></div>
489
			<form action="<?php echo admin_url( 'admin-post.php' ); ?>" method="post" class="form">
490
				<?php wp_nonce_field( 'feedback_export','feedback_export_nonce' ); ?>
491
492
				<input name="action" value="feedback_export" type="hidden">
493
				<label for="post"><?php _e( 'Select feedback to download', 'jetpack' ) ?></label>
494
				<select name="post">
495
					<option value="all"><?php esc_html_e( 'All posts', 'jetpack' ) ?></option>
496
					<?php echo $this->get_feedbacks_as_options() ?>
497
				</select>
498
499
				<br><br>
500
				<input type="submit" name="submit" id="submit" class="button button-primary" value="<?php esc_html_e( 'Download', 'jetpack' ); ?>">
501
			</form>
502
		</div>
503
504
		<?php
505
		// There aren't any usable actions in core to output the "export feedback" form in the correct place,
506
		// so this inline JS moves it from the top of the page to the bottom.
507
		?>
508
		<script type='text/javascript'>
509
		var menu = document.getElementById( 'feedback-export' ),
510
		wrapper = document.getElementsByClassName( 'wrap' )[0];
511
		wrapper.appendChild(menu);
512
		menu.style.display = 'block';
513
		</script>
514
		<?php
515
	}
516
517
	/**
518
	 * download as a csv a contact form or all of them in a csv file
519
	 */
520
	function download_feedback_as_csv() {
521
		if ( empty( $_POST['feedback_export_nonce'] ) )
522
			return;
523
524
		check_admin_referer( 'feedback_export', 'feedback_export_nonce' );
525
526
		if ( ! current_user_can( 'export' ) ) {
527
			return;
528
		}
529
530
		$args = array(
531
			'posts_per_page'   => -1,
532
			'post_type'        => 'feedback',
533
			'post_status'      => 'publish',
534
			'order'            => 'ASC',
535
			'fields'           => 'ids',
536
			'suppress_filters' => false,
537
		);
538
539
		$filename = date( "Y-m-d" ) . '-feedback-export.csv';
540
541
		// Check if we want to download all the feedbacks or just a certain contact form
542
		if ( ! empty( $_POST['post'] ) && $_POST['post'] !== 'all' ) {
543
			$args['post_parent'] = (int) $_POST['post'];
544
			$filename            = date( "Y-m-d" ) . '-' . str_replace( '&nbsp;', '-', get_the_title( (int) $_POST['post'] ) ) . '.csv';
545
		}
546
547
		$feedbacks = get_posts( $args );
548
		$filename  = sanitize_file_name( $filename );
549
		$fields    = $this->get_field_names( $feedbacks );
550
551
		array_unshift( $fields, __( 'Contact Form', 'jetpack' ) );
552
553
		if ( empty( $feedbacks ) )
554
			return;
555
556
		// Forces the download of the CSV instead of echoing
557
		header( 'Content-Disposition: attachment; filename=' . $filename );
558
		header( 'Pragma: no-cache' );
559
		header( 'Expires: 0' );
560
		header( 'Content-Type: text/csv; charset=utf-8' );
561
562
		$output = fopen( 'php://output', 'w' );
563
564
		// Prints the header
565
		fputcsv( $output, $fields );
566
567
		// Create the csv string from the array of post ids
568
		foreach ( $feedbacks as $feedback ) {
569
			fputcsv( $output, self::make_csv_row_from_feedback( $feedback, $fields ) );
570
		}
571
572
		fclose( $output );
573
	}
574
575
	/**
576
	 * Returns a string of HTML <option> items from an array of posts
577
	 *
578
	 * @return string a string of HTML <option> items
579
	 */
580
	protected function get_feedbacks_as_options() {
581
		$options = '';
582
583
		// Get the feedbacks' parents' post IDs
584
		$feedbacks = get_posts( array(
585
			'fields'           => 'id=>parent',
586
			'posts_per_page'   => 100000,
587
			'post_type'        => 'feedback',
588
			'post_status'      => 'publish',
589
			'suppress_filters' => false,
590
		) );
591
		$parents = array_unique( array_values( $feedbacks ) );
592
593
		$posts = get_posts( array(
594
			'orderby'          => 'ID',
595
			'posts_per_page'   => 1000,
596
			'post_type'        => 'any',
597
			'post__in'         => array_values( $parents ),
598
			'suppress_filters' => false,
599
		) );
600
601
		// creates the string of <option> elements
602
		foreach ( $posts as $post ) {
603
			$options .= sprintf( '<option value="%s">%s</option>', esc_attr( $post->ID ), esc_html( $post->post_title ) );
604
		}
605
606
		return $options;
607
	}
608
609
	/**
610
	 * Get the names of all the form's fields
611
	 *
612
	 * @param  array|int $posts the post we want the fields of
613
	 * @return array     the array of fields
614
	 */
615
	protected function get_field_names( $posts ) {
616
		$posts = (array) $posts;
617
		$all_fields = array();
618
619
		foreach ( $posts as $post ){
620
			$fields = self::parse_fields_from_content( $post );
621
622
			if ( isset( $fields['_feedback_all_fields'] ) ) {
623
				$extra_fields = array_keys( $fields['_feedback_all_fields'] );
624
				$all_fields = array_merge( $all_fields, $extra_fields );
625
			}
626
		}
627
628
		$all_fields = array_unique( $all_fields );
629
		return $all_fields;
630
	}
631
632
	public static function parse_fields_from_content( $post_id ) {
633
		static $post_fields;
634
635
		if ( !is_array( $post_fields ) )
636
			$post_fields = array();
637
638
		if ( isset( $post_fields[$post_id] ) )
639
			return $post_fields[$post_id];
640
641
		$all_values   = array();
642
		$post_content = get_post_field( 'post_content', $post_id );
643
		$content      = explode( '<!--more-->', $post_content );
644
		$lines        = array();
645
646
		if ( count( $content ) > 1 ) {
647
			$content  = str_ireplace( array( '<br />', ')</p>' ), '', $content[1] );
648
			$one_line = preg_replace( '/\s+/', ' ', $content );
649
			$one_line = preg_replace( '/.*Array \( (.*)\)/', '$1', $one_line );
650
651
			preg_match_all( '/\[([^\]]+)\] =\&gt\; ([^\[]+)/', $one_line, $matches );
652
653
			if ( count( $matches ) > 1 )
654
				$all_values = array_combine( array_map('trim', $matches[1]), array_map('trim', $matches[2]) );
655
656
			$lines = array_filter( explode( "\n", $content ) );
657
		}
658
659
		$var_map = array(
660
			'AUTHOR'       => '_feedback_author',
661
			'AUTHOR EMAIL' => '_feedback_author_email',
662
			'AUTHOR URL'   => '_feedback_author_url',
663
			'SUBJECT'      => '_feedback_subject',
664
			'IP'           => '_feedback_ip'
665
		);
666
667
		$fields = array();
668
669
		foreach( $lines as $line ) {
670
			$vars = explode( ': ', $line, 2 );
671
			if ( !empty( $vars ) ) {
672
				if ( isset( $var_map[$vars[0]] ) ) {
673
					$fields[$var_map[$vars[0]]] = self::strip_tags( trim( $vars[1] ) );
674
				}
675
			}
676
		}
677
678
		$fields['_feedback_all_fields'] = $all_values;
679
680
		$post_fields[$post_id] = $fields;
681
682
		return $fields;
683
	}
684
685
	/**
686
	 * Creates a valid csv row from a post id
687
	 *
688
	 * @param  int    $post_id The id of the post
689
	 * @param  array  $fields  An array containing the names of all the fields of the csv
690
	 * @return String The csv row
691
	 */
692
	protected static function make_csv_row_from_feedback( $post_id, $fields ) {
693
		$content_fields = self::parse_fields_from_content( $post_id );
694
		$all_fields     = array();
695
696
		if ( isset( $content_fields['_feedback_all_fields'] ) )
697
			$all_fields = $content_fields['_feedback_all_fields'];
698
699
		// Overwrite the parsed content with the content we stored in post_meta in a better format.
700
		$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...
701
		foreach ( $extra_fields as $extra_field => $extra_value ) {
702
			$all_fields[$extra_field] = $extra_value;
703
		}
704
705
		// The first element in all of the exports will be the subject
706
		$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...
707
708
		// Loop the fields array in order to fill the $row_items array correctly
709
		foreach ( $fields as $field ) {
710
			if ( $field === __( 'Contact Form', 'jetpack' ) ) // the first field will ever be the contact form, so we can continue
711
				continue;
712
			elseif ( array_key_exists( $field, $all_fields ) )
713
				$row_items[] = $all_fields[$field];
714
			else
715
				$row_items[] = '';
716
		}
717
718
		return $row_items;
719
	}
720
721
	public static function get_ip_address() {
722
		return isset( $_SERVER['REMOTE_ADDR'] ) ? $_SERVER['REMOTE_ADDR'] : null;
723
	}
724
}
725
726
/**
727
 * Generic shortcode class.
728
 * Does nothing other than store structured data and output the shortcode as a string
729
 *
730
 * Not very general - specific to Grunion.
731
 */
732
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...
733
	/**
734
	 * @var string the name of the shortcode: [$shortcode_name /]
735
 	 */
736
	public $shortcode_name;
737
738
	/**
739
	 * @var array key => value pairs for the shortcode's attributes: [$shortcode_name key="value" ... /]
740
	 */
741
	public $attributes;
742
743
	/**
744
	 * @var array key => value pair for attribute defaults
745
	 */
746
	public $defaults = array();
747
748
	/**
749
	 * @var null|string Null for selfclosing shortcodes.  Hhe inner content of otherwise: [$shortcode_name]$content[/$shortcode_name]
750
	 */
751
	public $content;
752
753
	/**
754
	 * @var array Associative array of inner "child" shortcodes equivalent to the $content: [$shortcode_name][child 1/][child 2/][/$shortcode_name]
755
	 */
756
	public $fields;
757
758
	/**
759
	 * @var null|string The HTML of the parsed inner "child" shortcodes".  Null for selfclosing shortcodes.
760
	 */
761
	public $body;
762
763
	/**
764
	 * @param array $attributes An associative array of shortcode attributes.  @see shortcode_atts()
765
	 * @param null|string $content Null for selfclosing shortcodes.  The inner content otherwise.
766
	 */
767
	function __construct( $attributes, $content = null ) {
768
		$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...
769
		if ( is_array( $content ) ) {
770
			$string_content = '';
771
			foreach ( $content as $field ) {
772
				$string_content .= (string) $field;
773
			}
774
775
			$this->content = $string_content;
776
		} else {
777
			$this->content = $content;
778
		}
779
780
		$this->parse_content( $this->content );
781
	}
782
783
	/**
784
	 * Processes the shortcode's inner content for "child" shortcodes
785
	 *
786
	 * @param string $content The shortcode's inner content: [shortcode]$content[/shortcode]
787
	 */
788
	function parse_content( $content ) {
789
		if ( is_null( $content ) ) {
790
			$this->body = null;
791
		}
792
793
		$this->body = do_shortcode( $content );
794
	}
795
796
	/**
797
	 * Returns the value of the requested attribute.
798
	 *
799
	 * @param string $key The attribute to retrieve
800
	 * @return mixed
801
	 */
802
	function get_attribute( $key ) {
803
		return isset( $this->attributes[$key] ) ? $this->attributes[$key] : null;
804
	}
805
806
	function esc_attr( $value ) {
807
		if ( is_array( $value ) ) {
808
			return array_map( array( $this, 'esc_attr' ), $value );
809
		}
810
811
		$value = Grunion_Contact_Form_Plugin::strip_tags( $value );
812
		$value = _wp_specialchars( $value, ENT_QUOTES, false, true );
813
814
		// Shortcode attributes can't contain "]"
815
		$value = str_replace( ']', '', $value );
816
		$value = str_replace( ',', '&#x002c;', $value ); // store commas encoded
817
		$value = strtr( $value, array( '%' => '%25', '&' => '%26' ) );
818
819
		// shortcode_parse_atts() does stripcslashes()
0 ignored issues
show
Unused Code Comprehensibility introduced by
40% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
820
		$value = addslashes( $value );
821
		return $value;
822
	}
823
824
	function unesc_attr( $value ) {
825
		if ( is_array( $value ) ) {
826
			return array_map( array( $this, 'unesc_attr' ), $value );
827
		}
828
829
		// For back-compat with old Grunion encoding
830
		// Also, unencode commas
831
		$value = strtr( $value, array( '%26' => '&', '%25' => '%' ) );
832
		$value = preg_replace( array( '/&#x0*22;/i', '/&#x0*27;/i', '/&#x0*26;/i', '/&#x0*2c;/i' ), array( '"', "'", '&', ',' ), $value );
833
		$value = htmlspecialchars_decode( $value, ENT_QUOTES );
834
		$value = Grunion_Contact_Form_Plugin::strip_tags( $value );
835
836
		return $value;
837
	}
838
839
	/**
840
	 * Generates the shortcode
841
	 */
842
	function __toString() {
843
		$r = "[{$this->shortcode_name} ";
844
845
		foreach ( $this->attributes as $key => $value ) {
846
			if ( !$value ) {
847
				continue;
848
			}
849
850
			if ( isset( $this->defaults[$key] ) && $this->defaults[$key] == $value ) {
851
				continue;
852
			}
853
854
			if ( 'id' == $key ) {
855
				continue;
856
			}
857
858
			$value = $this->esc_attr( $value );
859
860
			if ( is_array( $value ) ) {
861
				$value = join( ',', $value );
862
			}
863
864
			if ( false === strpos( $value, "'" ) ) {
865
				$value = "'$value'";
866
			} elseif ( false === strpos( $value, '"' ) ) {
867
				$value = '"' . $value . '"';
868
			} else {
869
				// Shortcodes can't contain both '"' and "'".  Strip one.
870
				$value = str_replace( "'", '', $value );
871
				$value = "'$value'";
872
			}
873
874
			$r .= "{$key}={$value} ";
875
		}
876
877
		$r = rtrim( $r );
878
879
		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...
880
			$r .= ']';
881
882
			foreach ( $this->fields as $field ) {
883
				$r .= (string) $field;
884
			}
885
886
			$r .= "[/{$this->shortcode_name}]";
887
		} else {
888
			$r .= '/]';
889
		}
890
891
		return $r;
892
	}
893
}
894
895
/**
896
 * Class for the contact-form shortcode.
897
 * Parses shortcode to output the contact form as HTML
898
 * Sends email and stores the contact form response (a.k.a. "feedback")
899
 */
900
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...
901
	public $shortcode_name = 'contact-form';
902
903
	/**
904
	 * @var WP_Error stores form submission errors
905
	 */
906
	public $errors;
907
908
	/**
909
	 * @var Grunion_Contact_Form The most recent (inclusive) contact-form shortcode processed
910
	 */
911
	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...
912
913
	/**
914
	 * @var Whatever form we are currently looking at. If processed, will become $last
915
	 */
916
	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...
917
918
	/**
919
	 * @var bool Whether to print the grunion.css style when processing the contact-form shortcode
920
	 */
921
	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...
922
923
	function __construct( $attributes, $content = null ) {
924
		global $post;
925
926
		// Set up the default subject and recipient for this form
927
		$default_to = '';
928
		$default_subject = "[" . get_option( 'blogname' ) . "]";
929
930
		if ( !empty( $attributes['widget'] ) && $attributes['widget'] ) {
931
			$default_to .= get_option( 'admin_email' );
932
			$attributes['id'] = 'widget-' . $attributes['widget'];
933
			$default_subject = sprintf( _x( '%1$s Sidebar', '%1$s = blog name', 'jetpack' ), $default_subject );
934
		} else if ( $post ) {
935
			$attributes['id'] = $post->ID;
936
			$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 ) );
937
			$post_author = get_userdata( $post->post_author );
938
			$default_to .= $post_author->user_email;
939
		}
940
941
		// Keep reference to $this for parsing form fields
942
		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...
943
944
		$this->defaults = array(
945
			'to'                 => $default_to,
946
			'subject'            => $default_subject,
947
			'show_subject'       => 'no', // only used in back-compat mode
948
			'widget'             => 0,    // Not exposed to the user. Works with Grunion_Contact_Form_Plugin::widget_atts()
949
			'id'                 => null, // Not exposed to the user. Set above.
950
			'submit_button_text' => __( 'Submit &#187;', 'jetpack' ),
951
		);
952
953
		$attributes = shortcode_atts( $this->defaults, $attributes, 'contact-form' );
954
955
		// We only enable the contact-field shortcode temporarily while processing the contact-form shortcode
956
		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...
957
958
		parent::__construct( $attributes, $content );
959
960
		// There were no fields in the contact form. The form was probably just [contact-form /]. Build a default form.
961
		if ( empty( $this->fields ) ) {
962
			// same as the original Grunion v1 form
963
			$default_form = '
964
				[contact-field label="' . __( 'Name', 'jetpack' )    . '" type="name"  required="true" /]
965
				[contact-field label="' . __( 'Email', 'jetpack' )   . '" type="email" required="true" /]
966
				[contact-field label="' . __( 'Website', 'jetpack' ) . '" type="url" /]';
967
968
			if ( 'yes' == strtolower( $this->get_attribute( 'show_subject' ) ) ) {
969
				$default_form .= '
970
					[contact-field label="' . __( 'Subject', 'jetpack' ) . '" type="subject" /]';
971
			}
972
973
			$default_form .= '
974
				[contact-field label="' . __( 'Message', 'jetpack' ) . '" type="textarea" /]';
975
976
			$this->parse_content( $default_form );
977
978
			// Store the shortcode
979
			$this->store_shortcode( $default_form, $attributes );
980
		} else {
981
			// Store the shortcode
982
			$this->store_shortcode( $content, $attributes );
983
		}
984
985
		// $this->body and $this->fields have been setup.  We no longer need the contact-field shortcode.
986
		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...
987
	}
988
989
	/**
990
	 * Store shortcode content for recall later
991
	 *	- used to receate shortcode when user uses do_shortcode
992
	 *
993
	 * @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...
994
	 */
995
	static function store_shortcode( $content = null, $attributes = null ) {
996
997
		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...
998
999
			$shortcode_meta = get_post_meta( $attributes['id'], '_g_feedback_shortcode', true );
1000
1001
			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...
1002
				update_post_meta( $attributes['id'], '_g_feedback_shortcode', $content );
1003
			}
1004
1005
		}
1006
	}
1007
1008
	/**
1009
	 * Toggle for printing the grunion.css stylesheet
1010
	 *
1011
	 * @param bool $style
1012
	 */
1013
	static function style( $style ) {
1014
		$previous_style = self::$style;
1015
		self::$style = (bool) $style;
1016
		return $previous_style;
1017
	}
1018
1019
	/**
1020
	 * Turn on printing of grunion.css stylesheet
1021
	 * @see ::style()
1022
	 * @internal
1023
	 * @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...
1024
	 */
1025
	static function _style_on() {
1026
		return self::style( true );
1027
	}
1028
1029
	/**
1030
	 * The contact-form shortcode processor
1031
	 *
1032
	 * @param array $attributes Key => Value pairs as parsed by shortcode_parse_atts()
1033
	 * @param string|null $content The shortcode's inner content: [contact-form]$content[/contact-form]
1034
	 * @return string HTML for the concat form.
1035
	 */
1036
	static function parse( $attributes, $content ) {
1037
		// Create a new Grunion_Contact_Form object (this class)
1038
		$form = new Grunion_Contact_Form( $attributes, $content );
1039
1040
		$id = $form->get_attribute( 'id' );
1041
1042
		if ( !$id ) { // something terrible has happened
1043
			return '[contact-form]';
1044
		}
1045
1046
		if ( is_feed() ) {
1047
			return '[contact-form]';
1048
		}
1049
1050
		// Only allow one contact form per post/widget
1051
		if ( self::$last && $id == self::$last->get_attribute( 'id' ) ) {
1052
			// We're processing the same post
1053
1054
			if ( self::$last->attributes != $form->attributes || self::$last->content != $form->content ) {
1055
				// And we're processing a different shortcode;
1056
				return '';
1057
			} // else, we're processing the same shortcode - probably a separate run of do_shortcode() - let it through
1058
1059
		} else {
1060
			self::$last = $form;
1061
		}
1062
1063
		// Enqueue the grunion.css stylesheet if self::$style allows it
1064
		if ( self::$style && ( empty( $_REQUEST['action'] ) || $_REQUEST['action'] != 'grunion_shortcode_to_json' ) ) {
1065
			// Enqueue the style here instead of printing it, because if some other plugin has run the_post()+rewind_posts(),
1066
			// (like VideoPress does), the style tag gets "printed" the first time and discarded, leaving the contact form unstyled.
1067
			// when WordPress does the real loop.
1068
			wp_enqueue_style( 'grunion.css' );
1069
		}
1070
1071
		$r = '';
1072
		$r .= "<div id='contact-form-$id'>\n";
1073
1074
		if ( is_wp_error( $form->errors ) && $form->errors->get_error_codes() ) {
1075
			// There are errors.  Display them
1076
			$r .= "<div class='form-error'>\n<h3>" . __( 'Error!', 'jetpack' ) . "</h3>\n<ul class='form-errors'>\n";
1077
			foreach ( $form->errors->get_error_messages() as $message )
1078
				$r .= "\t<li class='form-error-message'>" . esc_html( $message ) . "</li>\n";
1079
			$r .= "</ul>\n</div>\n\n";
1080
		}
1081
1082
		if ( isset( $_GET['contact-form-id'] ) && $_GET['contact-form-id'] == self::$last->get_attribute( 'id' ) && isset( $_GET['contact-form-sent'] ) ) {
1083
			// The contact form was submitted.  Show the success message/results
1084
1085
			$feedback_id = (int) $_GET['contact-form-sent'];
1086
1087
			$back_url = remove_query_arg( array( 'contact-form-id', 'contact-form-sent', '_wpnonce' ) );
1088
1089
			$r_success_message =
1090
				"<h3>" . __( 'Message Sent', 'jetpack' ) .
1091
				' (<a href="' . esc_url( $back_url ) . '">' . esc_html__( 'go back', 'jetpack' ) . '</a>)' .
1092
				"</h3>\n\n";
1093
1094
			// Don't show the feedback details unless the nonce matches
1095
			if ( $feedback_id && wp_verify_nonce( stripslashes( $_GET['_wpnonce'] ), "contact-form-sent-{$feedback_id}" ) ) {
1096
				$r_success_message .= self::success_message( $feedback_id, $form );
1097
			}
1098
1099
			/**
1100
			 * Filter the message returned after a successfull contact form submission.
1101
			 *
1102
			 * @module contact-form
1103
			 *
1104
			 * @since 1.3.1
1105
			 *
1106
			 * @param string $r_success_message Success message.
1107
			 */
1108
			$r .= apply_filters( 'grunion_contact_form_success_message', $r_success_message );
1109
		} else {
1110
			// Nothing special - show the normal contact form
1111
1112
			if ( $form->get_attribute( 'widget' ) ) {
1113
				// Submit form to the current URL
1114
				$url = remove_query_arg( array( 'contact-form-id', 'contact-form-sent', 'action', '_wpnonce' ) );
1115
			} else {
1116
				// Submit form to the post permalink
1117
				$url = get_permalink();
1118
			}
1119
1120
			// For SSL/TLS page. See RFC 3986 Section 4.2
1121
			$url = set_url_scheme( $url );
1122
1123
			// May eventually want to send this to admin-post.php...
1124
			/**
1125
			 * Filter the contact form action URL.
1126
			 *
1127
			 * @module contact-form
1128
			 *
1129
			 * @since 1.3.1
1130
			 *
1131
			 * @param string $contact_form_id Contact form post URL.
1132
			 * @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...
1133
			 * @param int $id Contact Form ID.
1134
			 */
1135
			$url = apply_filters( 'grunion_contact_form_form_action', "{$url}#contact-form-{$id}", $GLOBALS['post'], $id );
1136
1137
			$r .= "<form action='" . esc_url( $url ) . "' method='post' class='contact-form commentsblock'>\n";
1138
			$r .= $form->body;
1139
			$r .= "\t<p class='contact-submit'>\n";
1140
			$r .= "\t\t<input type='submit' value='" . esc_attr( $form->get_attribute( 'submit_button_text' ) ) . "' class='pushbutton-wide'/>\n";
1141
			if ( is_user_logged_in() ) {
1142
				$r .= "\t\t" . wp_nonce_field( 'contact-form_' . $id, '_wpnonce', true, false ) . "\n"; // nonce and referer
1143
			}
1144
			$r .= "\t\t<input type='hidden' name='contact-form-id' value='$id' />\n";
1145
			$r .= "\t\t<input type='hidden' name='action' value='grunion-contact-form' />\n";
1146
			$r .= "\t</p>\n";
1147
			$r .= "</form>\n";
1148
		}
1149
1150
		$r .= "</div>";
1151
1152
		return $r;
1153
	}
1154
1155
	/**
1156
	 * Returns a success message to be returned if the form is sent via AJAX.
1157
	 *
1158
	 * @param int $feedback_id
1159
	 * @param object Grunion_Contact_Form $form
1160
	 *
1161
	 * @return string $message
1162
	 */
1163
	static function success_message( $feedback_id, $form ) {
1164
		return wp_kses(
1165
			'<blockquote class="contact-form-submission">'
1166
			. '<p>' . join( self::get_compiled_form( $feedback_id, $form ), '</p><p>' ) . '</p>'
1167
			. '</blockquote>',
1168
			array( 'br' => array(), 'blockquote' => array( 'class' => array() ), 'p' => array() )
1169
		);
1170
	}
1171
1172
	/**
1173
	 * Returns a compiled form with labels and values in a form of  an array
1174
	 * of lines.
1175
	 * @param int $feedback_id
1176
	 * @param object Grunion_Contact_Form $form
1177
	 *
1178
	 * @return array $lines
1179
	 */
1180
	static function get_compiled_form( $feedback_id, $form ) {
1181
		$feedback       = get_post( $feedback_id );
1182
		$field_ids      = $form->get_field_ids();
1183
		$content_fields = Grunion_Contact_Form_Plugin::parse_fields_from_content( $feedback_id );
1184
1185
		// Maps field_ids to post_meta keys
1186
		$field_value_map = array(
1187
			'name'     => 'author',
1188
			'email'    => 'author_email',
1189
			'url'      => 'author_url',
1190
			'subject'  => 'subject',
1191
			'textarea' => false, // not a post_meta key.  This is stored in post_content
1192
		);
1193
1194
		$compiled_form = array();
1195
1196
		// "Standard" field whitelist
1197
		foreach ( $field_value_map as $type => $meta_key ) {
1198
			if ( isset( $field_ids[$type] ) ) {
1199
				$field = $form->fields[$field_ids[$type]];
1200
1201
				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...
1202
					if ( isset( $content_fields["_feedback_{$meta_key}"] ) )
1203
						$value = $content_fields["_feedback_{$meta_key}"];
1204
				} else {
1205
					// The feedback content is stored as the first "half" of post_content
1206
					$value = $feedback->post_content;
1207
					list( $value ) = explode( '<!--more-->', $value );
1208
					$value = trim( $value );
1209
				}
1210
1211
				$field_index = array_search( $field_ids[ $type ], $field_ids['all'] );
1212
				$compiled_form[ $field_index ] = sprintf(
1213
					'<b>%1$s:</b> %2$s<br /><br />',
1214
					wp_kses( $field->get_attribute( 'label' ), array() ),
1215
					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...
1216
				);
1217
			}
1218
		}
1219
1220
		// "Non-standard" fields
1221
		if ( $field_ids['extra'] ) {
1222
			// array indexed by field label (not field id)
1223
			$extra_fields = get_post_meta( $feedback_id, '_feedback_extra_fields', true );
1224
			$extra_field_keys = array_keys( $extra_fields );
1225
1226
			$i = 0;
1227
			foreach ( $field_ids['extra'] as $field_id ) {
1228
				$field = $form->fields[$field_id];
1229
				$field_index = array_search( $field_id, $field_ids['all'] );
1230
1231
				$label = $field->get_attribute( 'label' );
1232
1233
				$compiled_form[ $field_index ] = sprintf(
1234
					'<b>%1$s:</b> %2$s<br /><br />',
1235
					wp_kses( $label, array() ),
1236
					nl2br( wp_kses( $extra_fields[$extra_field_keys[$i]], array() ) )
1237
				);
1238
1239
				$i++;
1240
			}
1241
		}
1242
1243
		// Sorting lines by the field index
1244
		ksort( $compiled_form );
1245
1246
		return $compiled_form;
1247
	}
1248
1249
	/**
1250
	 * The contact-field shortcode processor
1251
	 * 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.
1252
	 *
1253
	 * @param array $attributes Key => Value pairs as parsed by shortcode_parse_atts()
1254
	 * @param string|null $content The shortcode's inner content: [contact-field]$content[/contact-field]
1255
	 * @return HTML for the contact form field
1256
	 */
1257
	static function parse_contact_field( $attributes, $content ) {
1258
		// Don't try to parse contact form fields if not inside a contact form
1259
		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...
1260
			$att_strs = array();
1261
			foreach ( $attributes as $att => $val ) {
1262
				if ( is_numeric( $att ) ) { // Is a valueless attribute
1263
					$att_strs[] = esc_html( $val );
1264
				} else if ( isset( $val ) ) { // A regular attr - value pair
1265
					$att_strs[] = esc_html( $att ) . '=\'' . esc_html( $val ) . '\'';
1266
				}
1267
			}
1268
1269
			$html = '[contact-field ' . implode( ' ', $att_strs );
1270
1271
			if ( isset( $content ) && ! empty( $content ) ) { // If there is content, let's add a closing tag
1272
				$html .=  ']' . esc_html( $content ) . '[/contact-field]';
1273
			} else { // Otherwise let's add a closing slash in the first tag
1274
				$html .= '/]';
1275
			}
1276
1277
			return $html;
1278
		}
1279
1280
		$form = Grunion_Contact_Form::$current_form;
1281
1282
		$field = new Grunion_Contact_Form_Field( $attributes, $content, $form );
1283
1284
		$field_id = $field->get_attribute( 'id' );
1285
		if ( $field_id ) {
1286
			$form->fields[$field_id] = $field;
1287
		} else {
1288
			$form->fields[] = $field;
1289
		}
1290
1291
		if (
1292
			isset( $_POST['action'] ) && 'grunion-contact-form' === $_POST['action']
1293
		&&
1294
			isset( $_POST['contact-form-id'] ) && $form->get_attribute( 'id' ) == $_POST['contact-form-id']
1295
		) {
1296
			// If we're processing a POST submission for this contact form, validate the field value so we can show errors as necessary.
1297
			$field->validate();
1298
		}
1299
1300
		// Output HTML
1301
		return $field->render();
1302
	}
1303
1304
	/**
1305
	 * Loops through $this->fields to generate a (structured) list of field IDs
1306
	 * @return array
1307
	 */
1308
	function get_field_ids() {
1309
		$field_ids = array(
1310
			'all'   => array(), // array of all field_ids
1311
			'extra' => array(), // array of all non-whitelisted field IDs
1312
1313
			// Whitelisted "standard" field IDs:
1314
			// 'email'    => field_id,
0 ignored issues
show
Unused Code Comprehensibility introduced by
43% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
1315
			// 'name'     => field_id,
0 ignored issues
show
Unused Code Comprehensibility introduced by
43% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
1316
			// 'url'      => field_id,
0 ignored issues
show
Unused Code Comprehensibility introduced by
43% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
1317
			// 'subject'  => field_id,
0 ignored issues
show
Unused Code Comprehensibility introduced by
43% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
1318
			// 'textarea' => field_id,
0 ignored issues
show
Unused Code Comprehensibility introduced by
43% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
1319
		);
1320
1321
		foreach ( $this->fields as $id => $field ) {
1322
			$field_ids['all'][] = $id;
1323
1324
			$type = $field->get_attribute( 'type' );
1325
			if ( isset( $field_ids[$type] ) ) {
1326
				// This type of field is already present in our whitelist of "standard" fields for this form
1327
				// Put it in extra
1328
				$field_ids['extra'][] = $id;
1329
				continue;
1330
			}
1331
1332
			switch ( $type ) {
1333
			case 'email' :
1334
			case 'telephone' :
1335
			case 'name' :
1336
			case 'url' :
1337
			case 'subject' :
1338
			case 'textarea' :
1339
				$field_ids[$type] = $id;
1340
				break;
1341
			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...
1342
				// Put everything else in extra
1343
				$field_ids['extra'][] = $id;
1344
			}
1345
		}
1346
1347
		return $field_ids;
1348
	}
1349
1350
	/**
1351
	 * Process the contact form's POST submission
1352
	 * Stores feedback.  Sends email.
1353
	 */
1354
	function process_submission() {
1355
		global $post;
1356
1357
		$plugin = Grunion_Contact_Form_Plugin::init();
1358
1359
		$id     = $this->get_attribute( 'id' );
1360
		$to     = $this->get_attribute( 'to' );
1361
		$widget = $this->get_attribute( 'widget' );
1362
1363
		$contact_form_subject = $this->get_attribute( 'subject' );
1364
1365
		$to = str_replace( ' ', '', $to );
1366
		$emails = explode( ',', $to );
1367
1368
		$valid_emails = array();
1369
1370
		foreach ( (array) $emails as $email ) {
1371
			if ( !is_email( $email ) ) {
1372
				continue;
1373
			}
1374
1375
			if ( function_exists( 'is_email_address_unsafe' ) && is_email_address_unsafe( $email ) ) {
1376
				continue;
1377
			}
1378
1379
			$valid_emails[] = $email;
1380
		}
1381
1382
		// No one to send it to, which means none of the "to" attributes are valid emails.
1383
		// Use default email instead.
1384
		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...
1385
			$valid_emails = $this->defaults['to'];
1386
		}
1387
1388
		$to = $valid_emails;
1389
1390
		// Last ditch effort to set a recipient if somehow none have been set.
1391
		if ( empty( $to ) ) {
1392
			$to = get_option( 'admin_email' );
1393
		}
1394
1395
		// Make sure we're processing the form we think we're processing... probably a redundant check.
1396
		if ( $widget ) {
1397
			if ( 'widget-' . $widget != $_POST['contact-form-id'] ) {
1398
				return false;
1399
			}
1400
		} else {
1401
			if ( $post->ID != $_POST['contact-form-id'] ) {
1402
				return false;
1403
			}
1404
		}
1405
1406
		$field_ids = $this->get_field_ids();
1407
1408
		// Initialize all these "standard" fields to null
1409
		$comment_author_email = $comment_author_email_label = // v
1410
		$comment_author       = $comment_author_label       = // v
1411
		$comment_author_url   = $comment_author_url_label   = // v
1412
		$comment_content      = $comment_content_label      = null;
1413
1414
		// For each of the "standard" fields, grab their field label and value.
1415
1416 View Code Duplication
		if ( isset( $field_ids['name'] ) ) {
1417
			$field = $this->fields[$field_ids['name']];
1418
			$comment_author = Grunion_Contact_Form_Plugin::strip_tags(
1419
				stripslashes(
1420
					/** This filter is already documented in core/wp-includes/comment-functions.php */
1421
					apply_filters( 'pre_comment_author_name', addslashes( $field->value ) )
1422
				)
1423
			);
1424
			$comment_author_label = Grunion_Contact_Form_Plugin::strip_tags( $field->get_attribute( 'label' ) );
1425
		}
1426
1427 View Code Duplication
		if ( isset( $field_ids['email'] ) ) {
1428
			$field = $this->fields[$field_ids['email']];
1429
			$comment_author_email = Grunion_Contact_Form_Plugin::strip_tags(
1430
				stripslashes(
1431
					/** This filter is already documented in core/wp-includes/comment-functions.php */
1432
					apply_filters( 'pre_comment_author_email', addslashes( $field->value ) )
1433
				)
1434
			);
1435
			$comment_author_email_label = Grunion_Contact_Form_Plugin::strip_tags( $field->get_attribute( 'label' ) );
1436
		}
1437
1438
		if ( isset( $field_ids['url'] ) ) {
1439
			$field = $this->fields[$field_ids['url']];
1440
			$comment_author_url = Grunion_Contact_Form_Plugin::strip_tags(
1441
				stripslashes(
1442
					/** This filter is already documented in core/wp-includes/comment-functions.php */
1443
					apply_filters( 'pre_comment_author_url', addslashes( $field->value ) )
1444
				)
1445
			);
1446
			if ( 'http://' == $comment_author_url ) {
1447
				$comment_author_url = '';
1448
			}
1449
			$comment_author_url_label = Grunion_Contact_Form_Plugin::strip_tags( $field->get_attribute( 'label' ) );
1450
		}
1451
1452
		if ( isset( $field_ids['textarea'] ) ) {
1453
			$field = $this->fields[$field_ids['textarea']];
1454
			$comment_content = trim( Grunion_Contact_Form_Plugin::strip_tags( $field->value ) );
1455
			$comment_content_label = Grunion_Contact_Form_Plugin::strip_tags( $field->get_attribute( 'label' ) );
1456
		}
1457
1458
		if ( isset( $field_ids['subject'] ) ) {
1459
			$field = $this->fields[$field_ids['subject']];
1460
			if ( $field->value ) {
1461
				$contact_form_subject = Grunion_Contact_Form_Plugin::strip_tags( $field->value );
1462
			}
1463
		}
1464
1465
		$all_values = $extra_values = array();
1466
		$i = 1; // Prefix counter for stored metadata
1467
1468
		// For all fields, grab label and value
1469
		foreach ( $field_ids['all'] as $field_id ) {
1470
			$field = $this->fields[$field_id];
1471
			$label = $i . '_' . $field->get_attribute( 'label' );
1472
			$value = $field->value;
1473
1474
			$all_values[$label] = $value;
1475
			$i++; // Increment prefix counter for the next field
1476
		}
1477
1478
		// For the "non-standard" fields, grab label and value
1479
		// Extra fields have their prefix starting from count( $all_values ) + 1
1480
		foreach ( $field_ids['extra'] as $field_id ) {
1481
			$field = $this->fields[$field_id];
1482
			$label = $i . '_' . $field->get_attribute( 'label' );
1483
			$value = $field->value;
1484
1485
			if ( is_array( $value ) ) {
1486
				$value = implode( ', ', $value );
1487
			}
1488
1489
			$extra_values[$label] = $value;
1490
			$i++; // Increment prefix counter for the next extra field
1491
		}
1492
1493
		$contact_form_subject = trim( $contact_form_subject );
1494
1495
		$comment_author_IP = Grunion_Contact_Form_Plugin::get_ip_address();
1496
1497
		$vars = array( 'comment_author', 'comment_author_email', 'comment_author_url', 'contact_form_subject', 'comment_author_IP' );
1498
		foreach ( $vars as $var )
1499
			$$var = str_replace( array( "\n", "\r" ), '', $$var );
1500
1501
		// Ensure that Akismet gets all of the relevant information from the contact form,
1502
		// not just the textarea field and predetermined subject.
1503
		$akismet_vars = compact( $vars );
1504
		$akismet_vars['comment_content'] = $comment_content;
1505
1506
		foreach ( array_merge( $field_ids['all'], $field_ids['extra'] ) as $field_id ) {
1507
			$field = $this->fields[$field_id];
1508
1509
			// Normalize the label into a slug.
1510
			$field_slug = trim( // Strip all leading/trailing dashes.
1511
				preg_replace(   // Normalize everything to a-z0-9_-
1512
					'/[^a-z0-9_]+/',
1513
					'-',
1514
					strtolower( $field->get_attribute( 'label' ) ) // Lowercase
1515
				),
1516
				'-'
1517
			);
1518
1519
			$field_value = ( is_array( $field->value ) ) ? trim( implode( ', ', $field->value ) ) : trim( $field->value );
1520
1521
			// Skip any values that are already in the array we're sending.
1522
			if ( $field_value && in_array( $field_value, $akismet_vars ) ) {
1523
				continue;
1524
			}
1525
1526
			$akismet_vars[ 'contact_form_field_' . $field_slug ] = $field_value;
1527
		}
1528
1529
		$spam = '';
1530
		$akismet_values = $plugin->prepare_for_akismet( $akismet_vars );
1531
1532
		// Is it spam?
1533
		/** This filter is already documented in modules/contact-form/admin.php */
1534
		$is_spam = apply_filters( 'jetpack_contact_form_is_spam', false, $akismet_values );
1535
		if ( is_wp_error( $is_spam ) ) // WP_Error to abort
1536
			return $is_spam; // abort
1537
		elseif ( $is_spam === TRUE )  // TRUE to flag a spam
1538
			$spam = '***SPAM*** ';
1539
1540
		if ( !$comment_author )
1541
			$comment_author = $comment_author_email;
1542
1543
		/**
1544
		 * Filter the email where a submitted feedback is sent.
1545
		 *
1546
		 * @module contact-form
1547
		 *
1548
		 * @since 1.3.1
1549
		 *
1550
		 * @param string|array $to Array of valid email addresses, or single email address.
1551
		 */
1552
		$to = (array) apply_filters( 'contact_form_to', $to );
1553
		foreach ( $to as $to_key => $to_value ) {
1554
			$to[$to_key] = Grunion_Contact_Form_Plugin::strip_tags( $to_value );
1555
		}
1556
1557
		$blog_url = parse_url( site_url() );
1558
		$from_email_addr = 'wordpress@' . $blog_url['host'];
1559
1560
		$reply_to_addr = $to[0];
1561
		if ( ! empty( $comment_author_email ) ) {
1562
			$reply_to_addr = $comment_author_email;
1563
		}
1564
1565
		$headers =  'From: "' . $comment_author  .'" <' . $from_email_addr  . ">\r\n" .
1566
					'Reply-To: "' . $comment_author . '" <' . $reply_to_addr  . ">\r\n" .
1567
					"Content-Type: text/html; charset=\"" . get_option('blog_charset') . "\"";
1568
1569
		/** This filter is already documented in modules/contact-form/admin.php */
1570
		$subject = apply_filters( 'contact_form_subject', $contact_form_subject, $all_values );
1571
		$url     = $widget ? home_url( '/' ) : get_permalink( $post->ID );
1572
1573
		$date_time_format = _x( '%1$s \a\t %2$s', '{$date_format} \a\t {$time_format}', 'jetpack' );
1574
		$date_time_format = sprintf( $date_time_format, get_option( 'date_format' ), get_option( 'time_format' ) );
1575
		$time = date_i18n( $date_time_format, current_time( 'timestamp' ) );
1576
1577
		// keep a copy of the feedback as a custom post type
1578
		$feedback_time   = current_time( 'mysql' );
1579
		$feedback_title  = "{$comment_author} - {$feedback_time}";
1580
		$feedback_status = $is_spam === TRUE ? 'spam' : 'publish';
1581
1582
		foreach ( (array) $akismet_values as $av_key => $av_value ) {
1583
			$akismet_values[$av_key] = Grunion_Contact_Form_Plugin::strip_tags( $av_value );
1584
		}
1585
1586
		foreach ( (array) $all_values as $all_key => $all_value ) {
1587
			$all_values[$all_key] = Grunion_Contact_Form_Plugin::strip_tags( $all_value );
1588
		}
1589
1590
		foreach ( (array) $extra_values as $ev_key => $ev_value ) {
1591
			$extra_values[$ev_key] = Grunion_Contact_Form_Plugin::strip_tags( $ev_value );
1592
		}
1593
1594
		/* We need to make sure that the post author is always zero for contact
1595
		 * form submissions.  This prevents export/import from trying to create
1596
		 * new users based on form submissions from people who were logged in
1597
		 * at the time.
1598
		 *
1599
		 * Unfortunately wp_insert_post() tries very hard to make sure the post
1600
		 * author gets the currently logged in user id.  That is how we ended up
1601
		 * with this work around. */
1602
		add_filter( 'wp_insert_post_data', array( $plugin, 'insert_feedback_filter' ), 10, 2 );
1603
1604
		$post_id = wp_insert_post( array(
1605
			'post_date'    => addslashes( $feedback_time ),
1606
			'post_type'    => 'feedback',
1607
			'post_status'  => addslashes( $feedback_status ),
1608
			'post_parent'  => (int) $post->ID,
1609
			'post_title'   => addslashes( wp_kses( $feedback_title, array() ) ),
1610
			'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
1611
			'post_name'    => md5( $feedback_title ),
1612
		) );
1613
1614
		// once insert has finished we don't need this filter any more
1615
		remove_filter( 'wp_insert_post_data', array( $plugin, 'insert_feedback_filter' ), 10, 2 );
1616
1617
		update_post_meta( $post_id, '_feedback_extra_fields', $this->addslashes_deep( $extra_values ) );
1618
		update_post_meta( $post_id, '_feedback_akismet_values', $this->addslashes_deep( $akismet_values ) );
1619
1620
		$message = self::get_compiled_form( $post_id, $this );
1621
1622
		array_push(
1623
			$message,
1624
			"", // Empty line left intentionally
1625
			__( 'Time:', 'jetpack' ) . ' ' . $time . '<br />',
1626
			__( 'IP Address:', 'jetpack' ) . ' ' . $comment_author_IP . '<br />',
1627
			__( 'Contact Form URL:', 'jetpack' ) . " " . $url . '<br />'
1628
		);
1629
1630
		if ( is_user_logged_in() ) {
1631
			array_push(
1632
				$message,
1633
				"",
1634
				sprintf(
1635
					__( 'Sent by a verified %s user.', 'jetpack' ),
1636
					isset( $GLOBALS['current_site']->site_name ) && $GLOBALS['current_site']->site_name ?
1637
						$GLOBALS['current_site']->site_name : '"' . get_option( 'blogname' ) . '"'
1638
				)
1639
			);
1640
		} else {
1641
			array_push( $message, __( 'Sent by an unverified visitor to your site.', 'jetpack' ) );
1642
		}
1643
1644
		$message = join( $message, "" );
1645
		/**
1646
		 * Filters the message sent via email after a successfull form submission.
1647
		 *
1648
		 * @module contact-form
1649
		 *
1650
		 * @since 1.3.1
1651
		 *
1652
		 * @param string $message Feedback email message.
1653
		 */
1654
		$message = apply_filters( 'contact_form_message', $message );
1655
1656
		update_post_meta( $post_id, '_feedback_email', $this->addslashes_deep( compact( 'to', 'message' ) ) );
1657
1658
		/**
1659
		 * Fires right before the contact form message is sent via email to
1660
		 * the recipient specified in the contact form.
1661
		 *
1662
		 * @module contact-form
1663
		 *
1664
		 * @since 1.3.1
1665
		 *
1666
		 * @param integer $post_id Post contact form lives on
1667
		 * @param array $all_values Contact form fields
1668
		 * @param array $extra_values Contact form fields not included in $all_values
1669
		 */
1670
		do_action( 'grunion_pre_message_sent', $post_id, $all_values, $extra_values );
1671
1672
		// schedule deletes of old spam feedbacks
1673
		if ( ! wp_next_scheduled( 'grunion_scheduled_delete' ) ) {
1674
			wp_schedule_event( time() + 250, 'daily', 'grunion_scheduled_delete' );
1675
		}
1676
1677
		if (
1678
			$is_spam !== TRUE &&
1679
			/**
1680
			 * Filter to choose whether an email should be sent after each successfull contact form submission.
1681
			 *
1682
			 * @module contact-form
1683
			 *
1684
			 * @since 2.6.0
1685
			 *
1686
			 * @param bool true Should an email be sent after a form submission. Default to true.
1687
			 * @param int $post_id Post ID.
1688
			 */
1689
			true === apply_filters( 'grunion_should_send_email', true, $post_id )
1690
		) {
1691
			wp_mail( $to, "{$spam}{$subject}", $message, $headers );
1692
		} elseif (
1693
			true === $is_spam &&
1694
			/**
1695
			 * Choose whether an email should be sent for each spam contact form submission.
1696
			 *
1697
			 * @module contact-form
1698
			 *
1699
			 * @since 1.3.1
1700
			 *
1701
			 * @param bool false Should an email be sent after a spam form submission. Default to false.
1702
			 */
1703
			apply_filters( 'grunion_still_email_spam', FALSE ) == TRUE
1704
		) { // don't send spam by default.  Filterable.
1705
			wp_mail( $to, "{$spam}{$subject}", $message, $headers );
1706
		}
1707
1708
		if ( defined( 'DOING_AJAX' ) && DOING_AJAX ) {
1709
			return self::success_message( $post_id, $this );
1710
		}
1711
1712
		$redirect = wp_get_referer();
1713
		if ( !$redirect ) { // wp_get_referer() returns false if the referer is the same as the current page
1714
			$redirect = $_SERVER['REQUEST_URI'];
1715
		}
1716
1717
		$redirect = add_query_arg( urlencode_deep( array(
1718
			'contact-form-id'   => $id,
1719
			'contact-form-sent' => $post_id,
1720
			'_wpnonce'          => wp_create_nonce( "contact-form-sent-{$post_id}" ), // wp_nonce_url HTMLencodes :(
1721
		) ), $redirect );
1722
1723
		/**
1724
		 * Filter the URL where the reader is redirected after submitting a form.
1725
		 *
1726
		 * @module contact-form
1727
		 *
1728
		 * @since 1.9.0
1729
		 *
1730
		 * @param string $redirect Post submission URL.
1731
		 * @param int $id Contact Form ID.
1732
		 * @param int $post_id Post ID.
1733
		 */
1734
		$redirect = apply_filters( 'grunion_contact_form_redirect_url', $redirect, $id, $post_id );
1735
1736
		wp_safe_redirect( $redirect );
1737
		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...
1738
	}
1739
1740
	function addslashes_deep( $value ) {
1741
		if ( is_array( $value ) ) {
1742
			return array_map( array( $this, 'addslashes_deep' ), $value );
1743
		} elseif ( is_object( $value ) ) {
1744
			$vars = get_object_vars( $value );
1745
			foreach ( $vars as $key => $data ) {
1746
				$value->{$key} = $this->addslashes_deep( $data );
1747
			}
1748
			return $value;
1749
		}
1750
1751
		return addslashes( $value );
1752
	}
1753
}
1754
1755
/**
1756
 * Class for the contact-field shortcode.
1757
 * Parses shortcode to output the contact form field as HTML.
1758
 * Validates input.
1759
 */
1760
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...
1761
	public $shortcode_name = 'contact-field';
1762
1763
	/**
1764
	 * @var Grunion_Contact_Form parent form
1765
	 */
1766
	public $form;
1767
1768
	/**
1769
	 * @var string default or POSTed value
1770
	 */
1771
	public $value;
1772
1773
	/**
1774
	 * @var bool Is the input invalid?
1775
	 */
1776
	public $error = false;
1777
1778
	/**
1779
	 * @param array $attributes An associative array of shortcode attributes.  @see shortcode_atts()
1780
	 * @param null|string $content Null for selfclosing shortcodes.  The inner content otherwise.
1781
	 * @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...
1782
	 */
1783
	function __construct( $attributes, $content = null, $form = null ) {
1784
		$attributes = shortcode_atts( array(
1785
			'label'       => null,
1786
			'type'        => 'text',
1787
			'required'    => false,
1788
			'options'     => array(),
1789
			'id'          => null,
1790
			'default'     => null,
1791
			'placeholder' => null,
1792
		), $attributes, 'contact-field' );
1793
1794
		// special default for subject field
1795
		if ( 'subject' == $attributes['type'] && is_null( $attributes['default'] ) && !is_null( $form ) ) {
1796
			$attributes['default'] = $form->get_attribute( 'subject' );
1797
		}
1798
1799
		// allow required=1 or required=true
1800
		if ( '1' == $attributes['required'] || 'true' == strtolower( $attributes['required'] ) )
1801
			$attributes['required'] = true;
1802
		else
1803
			$attributes['required'] = false;
1804
1805
		// parse out comma-separated options list (for selects, radios, and checkbox-multiples)
1806
		if ( !empty( $attributes['options'] ) && is_string( $attributes['options'] ) ) {
1807
			$attributes['options'] = array_map( 'trim', explode( ',', $attributes['options'] ) );
1808
		}
1809
1810
		if ( $form ) {
1811
			// make a unique field ID based on the label, with an incrementing number if needed to avoid clashes
1812
			$form_id = $form->get_attribute( 'id' );
1813
			$id = isset( $attributes['id'] ) ? $attributes['id'] : false;
1814
1815
			$unescaped_label = $this->unesc_attr( $attributes['label'] );
1816
			$unescaped_label = str_replace( '%', '-', $unescaped_label ); // jQuery doesn't like % in IDs?
1817
			$unescaped_label = preg_replace( '/[^a-zA-Z0-9.-_:]/', '', $unescaped_label );
1818
1819
			if ( empty( $id ) ) {
1820
				$id = sanitize_title_with_dashes( 'g' . $form_id . '-' . $unescaped_label );
1821
				$i = 0;
1822
				$max_tries = 99;
1823
				while ( isset( $form->fields[$id] ) ) {
1824
					$i++;
1825
					$id = sanitize_title_with_dashes( 'g' . $form_id . '-' . $unescaped_label . '-' . $i );
1826
1827
					if ( $i > $max_tries ) {
1828
						break;
1829
					}
1830
				}
1831
			}
1832
1833
			$attributes['id'] = $id;
1834
		}
1835
1836
		parent::__construct( $attributes, $content );
1837
1838
		// Store parent form
1839
		$this->form = $form;
1840
	}
1841
1842
	/**
1843
	 * This field's input is invalid.  Flag as invalid and add an error to the parent form
1844
	 *
1845
	 * @param string $message The error message to display on the form.
1846
	 */
1847
	function add_error( $message ) {
1848
		$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...
1849
1850
		if ( !is_wp_error( $this->form->errors ) ) {
1851
			$this->form->errors = new WP_Error;
1852
		}
1853
1854
		$this->form->errors->add( $this->get_attribute( 'id' ), $message );
1855
	}
1856
1857
	/**
1858
	 * Is the field input invalid?
1859
	 *
1860
	 * @see $error
1861
	 *
1862
	 * @return bool
1863
	 */
1864
	function is_error() {
1865
		return $this->error;
1866
	}
1867
1868
	/**
1869
	 * Validates the form input
1870
	 */
1871
	function validate() {
1872
		// If it's not required, there's nothing to validate
1873
		if ( !$this->get_attribute( 'required' ) ) {
1874
			return;
1875
		}
1876
1877
		$field_id    = $this->get_attribute( 'id' );
1878
		$field_type  = $this->get_attribute( 'type' );
1879
		$field_label = $this->get_attribute( 'label' );
1880
1881
		if ( isset( $_POST[$field_id] ) ) {
1882
			if ( is_array( $_POST[$field_id] ) ) {
1883
				$field_value = array_map( 'stripslashes', $_POST[$field_id] );
1884
			} else {
1885
				$field_value = stripslashes( $_POST[$field_id] );
1886
			}
1887
		} else {
1888
			$field_value = '';
1889
		}
1890
1891
		switch ( $field_type ) {
1892
		case 'email' :
1893
			// Make sure the email address is valid
1894
			if ( !is_email( $field_value ) ) {
1895
				$this->add_error( sprintf( __( '%s requires a valid email address', 'jetpack' ), $field_label ) );
1896
			}
1897
			break;
1898
		case 'checkbox-multiple' :
1899
			// Check that there is at least one option selected
1900
			if ( empty( $field_value ) ) {
1901
				$this->add_error( sprintf( __( '%s requires at least one selection', 'jetpack' ), $field_label ) );
1902
			}
1903
			break;
1904
		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...
1905
			// Just check for presence of any text
1906
			if ( !strlen( trim( $field_value ) ) ) {
1907
				$this->add_error( sprintf( __( '%s is required', 'jetpack' ), $field_label ) );
1908
			}
1909
		}
1910
	}
1911
1912
	/**
1913
	 * Outputs the HTML for this form field
1914
	 *
1915
	 * @return string HTML
1916
	 */
1917
	function render() {
1918
		global $current_user, $user_identity;
1919
1920
		$r = '';
1921
1922
		$field_id          = $this->get_attribute( 'id' );
1923
		$field_type        = $this->get_attribute( 'type' );
1924
		$field_label       = $this->get_attribute( 'label' );
1925
		$field_required    = $this->get_attribute( 'required' );
1926
		$placeholder       = $this->get_attribute( 'placeholder' );
1927
		$field_placeholder = ( ! empty( $placeholder ) ) ? "placeholder='" . esc_attr( $placeholder ) . "'" : '';
1928
1929
		if ( isset( $_POST[ $field_id ] ) ) {
1930
			if ( is_array( $_POST[ $field_id ] ) ) {
1931
				$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...
1932
			} else {
1933
				$this->value = stripslashes( (string) $_POST[ $field_id ] );
1934
			}
1935
		} elseif ( isset( $_GET[ $field_id ] ) ) {
1936
			$this->value = stripslashes( (string) $_GET[ $field_id ] );
1937
		} elseif (
1938
			is_user_logged_in() &&
1939
			( ( defined( 'IS_WPCOM' ) && IS_WPCOM ) ||
1940
			/**
1941
			 * Allow third-party tools to prefill the contact form with the user's details when they're logged in.
1942
			 *
1943
			 * @module contact-form
1944
			 *
1945
			 * @since 3.2.0
1946
			 *
1947
			 * @param bool false Should the Contact Form be prefilled with your details when you're logged in. Default to false.
1948
			 */
1949
			true === apply_filters( 'jetpack_auto_fill_logged_in_user', false )
1950
			)
1951
		) {
1952
			// Special defaults for logged-in users
1953
			switch ( $this->get_attribute( 'type' ) ) {
1954
			case 'email' :
1955
				$this->value = $current_user->data->user_email;
1956
				break;
1957
			case 'name' :
1958
				$this->value = $user_identity;
1959
				break;
1960
			case 'url' :
1961
				$this->value = $current_user->data->user_url;
1962
				break;
1963
			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...
1964
				$this->value = $this->get_attribute( 'default' );
1965
			}
1966
		} else {
1967
			$this->value = $this->get_attribute( 'default' );
1968
		}
1969
1970
		$field_value = Grunion_Contact_Form_Plugin::strip_tags( $this->value );
1971
		$field_label = Grunion_Contact_Form_Plugin::strip_tags( $field_label );
1972
1973
		switch ( $field_type ) {
1974 View Code Duplication
		case 'email' :
1975
			$r .= "\n<div>\n";
1976
			$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)", 'jetpack' ) . '</span>' : '' ) . "</label>\n";
1977
			$r .= "\t\t<input type='email' name='" . esc_attr( $field_id ) . "' id='" . esc_attr( $field_id ) . "' value='" . esc_attr( $field_value ) . "' class='email' " . $field_placeholder . " " . ( $field_required ? "required aria-required='true'" : "" ) . "/>\n";
1978
			$r .= "\t</div>\n";
1979
			break;
1980
		case 'telephone' :
0 ignored issues
show
Coding Style introduced by
There must be a comment when fall-through is intentional in a non-empty case body
Loading history...
1981
			$r .= "\n<div>\n";
1982
			$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)", 'jetpack' ) . '</span>' : '' ) . "</label>\n";
1983
			$r .= "\t\t<input type='tel' name='" . esc_attr( $field_id ) . "' id='" . esc_attr( $field_id ) . "' value='" . esc_attr( $field_value ) . "' class='telephone' " . $field_placeholder . "/>\n";
1984 View Code Duplication
		case 'textarea' :
1985
			$r .= "\n<div>\n";
1986
			$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)", 'jetpack' ) . '</span>' : '' ) . "</label>\n";
1987
			$r .= "\t\t<textarea name='" . esc_attr( $field_id ) . "' id='contact-form-comment-" . esc_attr( $field_id ) . "' rows='20' " . $field_placeholder . " " . ( $field_required ? "required aria-required='true'" : "" ) . ">" . esc_textarea( $field_value ) . "</textarea>\n";
1988
			$r .= "\t</div>\n";
1989
			break;
1990 View Code Duplication
		case 'radio' :
1991
			$r .= "\t<div><label class='grunion-field-label" . ( $this->is_error() ? ' form-error' : '' ) . "'>" . esc_html( $field_label ) . ( $field_required ? '<span>' . __( "(required)", 'jetpack' ) . '</span>' : '' ) . "</label>\n";
1992
			foreach ( $this->get_attribute( 'options' ) as $option ) {
1993
				$option = Grunion_Contact_Form_Plugin::strip_tags( $option );
1994
				$r .= "\t\t<label class='grunion-radio-label radio" . ( $this->is_error() ? ' form-error' : '' ) . "'>";
1995
				$r .= "<input type='radio' name='" . esc_attr( $field_id ) . "' value='" . esc_attr( $option ) . "' class='radio' " . checked( $option, $field_value, false ) . " " . ( $field_required ? "required aria-required='true'" : "" ) . "/> ";
1996
				$r .= esc_html( $option ) . "</label>\n";
1997
				$r .= "\t\t<div class='clear-form'></div>\n";
1998
			}
1999
			$r .= "\t\t</div>\n";
2000
			break;
2001
		case 'checkbox' :
2002
			$r .= "\t<div>\n";
2003
			$r .= "\t\t<label class='grunion-field-label checkbox" . ( $this->is_error() ? ' form-error' : '' ) . "'>\n";
2004
			$r .= "\t\t<input type='checkbox' name='" . esc_attr( $field_id ) . "' value='" . esc_attr__( 'Yes', 'jetpack' ) . "' class='checkbox' " . checked( (bool) $field_value, true, false ) . " " . ( $field_required ? "required aria-required='true'" : "" ) . "/> \n";
2005
			$r .= "\t\t" . esc_html( $field_label ) . ( $field_required ? '<span>'. __( "(required)", 'jetpack' ) . '</span>' : '' ) . "</label>\n";
2006
			$r .= "\t\t<div class='clear-form'></div>\n";
2007
			$r .= "\t</div>\n";
2008
			break;
2009
		case 'checkbox-multiple' :
2010
			$r .= "\t<div><label class='grunion-field-label" . ( $this->is_error() ? ' form-error' : '' ) . "'>" . esc_html( $field_label ) . ( $field_required ? '<span>' . __( "(required)", 'jetpack' ) . '</span>' : '' ) . "</label>\n";
2011
			foreach ( $this->get_attribute( 'options' ) as $option ) {
2012
				$option = Grunion_Contact_Form_Plugin::strip_tags( $option );
2013
				$r .= "\t\t<label class='grunion-checkbox-multiple-label checkbox-multiple" . ( $this->is_error() ? ' form-error' : '' ) . "'>";
2014
				$r .= "<input type='checkbox' name='" . esc_attr( $field_id ) . "[]' value='" . esc_attr( $option ) . "' class='checkbox-multiple' " . checked( in_array( $option, (array) $field_value ), true, false ) . " /> ";
2015
				$r .= esc_html( $option ) . "</label>\n";
2016
				$r .= "\t\t<div class='clear-form'></div>\n";
2017
			}
2018
			$r .= "\t\t</div>\n";
2019
			break;
2020 View Code Duplication
		case 'select' :
2021
			$r .= "\n<div>\n";
2022
			$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)", 'jetpack' ) . '</span>' : '' ) . "</label>\n";
2023
			$r .= "\t<select name='" . esc_attr( $field_id ) . "' id='" . esc_attr( $field_id ) . "' class='select' " . ( $field_required ? "required aria-required='true'" : "" ) . ">\n";
2024
			foreach ( $this->get_attribute( 'options' ) as $option ) {
2025
				$option = Grunion_Contact_Form_Plugin::strip_tags( $option );
2026
				$r .= "\t\t<option" . selected( $option, $field_value, false ) . ">" . esc_html( $option ) . "</option>\n";
2027
			}
2028
			$r .= "\t</select>\n";
2029
			$r .= "\t</div>\n";
2030
			break;
2031
		case 'date' :
2032
			$r .= "\n<div>\n";
2033
			$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)", 'jetpack' ) . '</span>' : '' ) . "</label>\n";
2034
			$r .= "\t\t<input type='date' name='" . esc_attr( $field_id ) . "' id='" . esc_attr( $field_id ) . "' value='" . esc_attr( $field_value ) . "' class='" . esc_attr( $field_type ) . "' " . ( $field_required ? "required aria-required='true'" : "" ) . "/>\n";
2035
			$r .= "\t</div>\n";
2036
2037
			wp_enqueue_script( 'grunion-frontend', plugins_url( 'js/grunion-frontend.js', __FILE__ ), array( 'jquery', 'jquery-ui-datepicker' ) );
2038
			break;
2039
		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...
2040
			// note that any unknown types will produce a text input, so we can use arbitrary type names to handle
2041
			// input fields like name, email, url that require special validation or handling at POST
2042
			$r .= "\n<div>\n";
2043
			$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)", 'jetpack' ) . '</span>' : '' ) . "</label>\n";
2044
			$r .= "\t\t<input type='text' name='" . esc_attr( $field_id ) . "' id='" . esc_attr( $field_id ) . "' value='" . esc_attr( $field_value ) . "' class='" . esc_attr( $field_type ) . "' " . $field_placeholder . " " . ( $field_required ? "required aria-required='true'" : "" ) . "/>\n";
2045
			$r .= "\t</div>\n";
2046
		}
2047
2048
		/**
2049
		 * Filter the HTML of the Contact Form.
2050
		 *
2051
		 * @module contact-form
2052
		 *
2053
		 * @since 2.6.0
2054
		 *
2055
		 * @param string $r Contact Form HTML output.
2056
		 * @param string $field_label Field label.
2057
		 * @param int|null $id Post ID.
2058
		 */
2059
		return apply_filters( 'grunion_contact_form_field_html', $r, $field_label, ( in_the_loop() ? get_the_ID() : null ) );
2060
	}
2061
}
2062
2063
add_action( 'init', array( 'Grunion_Contact_Form_Plugin', 'init' ) );
2064
2065
add_action( 'grunion_scheduled_delete', 'grunion_delete_old_spam' );
2066
2067
/**
2068
 * Deletes old spam feedbacks to keep the posts table size under control
2069
 */
2070
function grunion_delete_old_spam() {
2071
	global $wpdb;
2072
2073
	$grunion_delete_limit = 100;
2074
2075
	$now_gmt = current_time( 'mysql', 1 );
2076
	$sql = $wpdb->prepare( "
2077
		SELECT `ID`
2078
		FROM $wpdb->posts
2079
		WHERE DATE_SUB( %s, INTERVAL 15 DAY ) > `post_date_gmt`
2080
			AND `post_type` = 'feedback'
2081
			AND `post_status` = 'spam'
2082
		LIMIT %d
2083
	", $now_gmt, $grunion_delete_limit );
2084
	$post_ids = $wpdb->get_col( $sql );
2085
2086
	foreach ( (array) $post_ids as $post_id ) {
2087
		# force a full delete, skip the trash
2088
		wp_delete_post( $post_id, TRUE );
2089
	}
2090
2091
	# Arbitrary check points for running OPTIMIZE
2092
	# nothing special about 5000 or 11
2093
	# just trying to periodically recover deleted rows
2094
	$random_num = mt_rand( 1, 5000 );
2095
	if (
2096
		/**
2097
		 * Filter how often the module run OPTIMIZE TABLE on the core WP tables.
2098
		 *
2099
		 * @module contact-form
2100
		 *
2101
		 * @since 1.3.1
2102
		 *
2103
		 * @param int $random_num Random number.
2104
		 */
2105
		apply_filters( 'grunion_optimize_table', ( $random_num == 11 ) )
2106
	) {
2107
		$wpdb->query( "OPTIMIZE TABLE $wpdb->posts" );
2108
	}
2109
2110
	# if we hit the max then schedule another run
2111
	if ( count( $post_ids ) >= $grunion_delete_limit ) {
2112
		wp_schedule_single_event( time() + 700, 'grunion_scheduled_delete' );
2113
	}
2114
}
2115