Completed
Push — v2/videopress ( 946a33...00803f )
by George
35:29
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.909
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
			'capabilities'		=> array(
111
				'create_posts'        => false,
112
				'publish_posts'       => 'publish_pages',
113
				'edit_posts'          => 'edit_pages',
114
				'edit_others_posts'   => 'edit_others_pages',
115
				'delete_posts'        => 'delete_pages',
116
				'delete_others_posts' => 'delete_others_pages',
117
				'read_private_posts'  => 'read_private_pages',
118
				'edit_post'           => 'edit_page',
119
				'delete_post'         => 'delete_page',
120
				'read_post'           => 'read_page',
121
			),
122
			'map_meta_cap'		=> true,
123
		) );
124
125
		// Add to REST API post type whitelist
126
		add_filter( 'rest_api_allowed_post_types', array( $this, 'allow_feedback_rest_api_type' ) );
127
128
		// Add "spam" as a post status
129
		register_post_status( 'spam', array(
130
			'label'                  => 'Spam',
131
			'public'                 => FALSE,
132
			'exclude_from_search'    => TRUE,
133
			'show_in_admin_all_list' => FALSE,
134
			'label_count'            => _n_noop( 'Spam <span class="count">(%s)</span>', 'Spam <span class="count">(%s)</span>', 'jetpack' ),
135
			'protected'              => TRUE,
136
			'_builtin'               => FALSE
137
		) );
138
139
		// POST handler
140
		if (
141
			isset( $_SERVER['REQUEST_METHOD'] ) && 'POST' == strtoupper( $_SERVER['REQUEST_METHOD'] )
142
		&&
143
			isset( $_POST['action'] ) && 'grunion-contact-form' == $_POST['action']
144
		&&
145
			isset( $_POST['contact-form-id'] )
146
		) {
147
			add_action( 'template_redirect', array( $this, 'process_form_submission' ) );
148
		}
149
150
		/* Can be dequeued by placing the following in wp-content/themes/yourtheme/functions.php
151
		 *
152
		 * 	function remove_grunion_style() {
153
		 *		wp_deregister_style('grunion.css');
154
		 *	}
155
		 *	add_action('wp_print_styles', 'remove_grunion_style');
156
		 */
157
		if( is_rtl() ){
158
			wp_register_style( 'grunion.css', GRUNION_PLUGIN_URL . 'css/rtl/grunion-rtl.css', array(), JETPACK__VERSION );
159
		} else {
160
			wp_register_style( 'grunion.css', GRUNION_PLUGIN_URL . 'css/grunion.css', array(), JETPACK__VERSION );
161
		}
162
	}
163
164
	/**
165
	 * Add to REST API post type whitelist
166
	 */
167
	function allow_feedback_rest_api_type( $post_types ) {
168
		$post_types[] = 'feedback';
169
		return $post_types;
170
	}
171
172
	/**
173
	 * Handles all contact-form POST submissions
174
	 *
175
	 * Conditionally attached to `template_redirect`
176
	 */
177
	function process_form_submission() {
178
		// Add a filter to replace tokens in the subject field with sanitized field values
179
		add_filter( 'contact_form_subject', array( $this, 'replace_tokens_with_input' ), 10, 2 );
180
181
		$id = stripslashes( $_POST['contact-form-id'] );
182
183
		if ( is_user_logged_in() ) {
184
			check_admin_referer( "contact-form_{$id}" );
185
		}
186
187
		$is_widget = 0 === strpos( $id, 'widget-' );
188
189
		$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...
190
191
		if ( $is_widget ) {
192
			// It's a form embedded in a text widget
193
194
			$this->current_widget_id = substr( $id, 7 ); // remove "widget-"
195
			$widget_type = implode( '-', array_slice( explode( '-', $this->current_widget_id ), 0, -1 ) ); // Remove trailing -#
196
197
			// Is the widget active?
198
			$sidebar = is_active_widget( false, $this->current_widget_id, $widget_type );
199
200
			// This is lame - no core API for getting a widget by ID
201
			$widget = isset( $GLOBALS['wp_registered_widgets'][$this->current_widget_id] ) ? $GLOBALS['wp_registered_widgets'][$this->current_widget_id] : false;
202
203
			if ( $sidebar && $widget && isset( $widget['callback'] ) ) {
204
				// This is lamer - no API for outputting a given widget by ID
205
				ob_start();
206
				// Process the widget to populate Grunion_Contact_Form::$last
207
				call_user_func( $widget['callback'], array(), $widget['params'][0] );
208
				ob_end_clean();
209
			}
210
		} else {
211
			// It's a form embedded in a post
212
213
			$post = get_post( $id );
214
215
			// Process the content to populate Grunion_Contact_Form::$last
216
			/** This filter is already documented in core. wp-includes/post-template.php */
217
			apply_filters( 'the_content', $post->post_content );
218
		}
219
220
		$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...
221
222
		// No form may mean user is using do_shortcode, grab the form using the stored post meta
223
		if ( ! $form ) {
224
225
			// Get shortcode from post meta
226
			$shortcode = get_post_meta( $_POST['contact-form-id'], '_g_feedback_shortcode', true );
227
228
			// Format it
229
			if ( $shortcode != '' ) {
230
				$shortcode = '[contact-form]' . $shortcode . '[/contact-form]';
231
				do_shortcode( $shortcode );
232
233
				// Recreate form
234
				$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...
235
			}
236
237
			if ( ! $form ) {
238
				return false;
239
			}
240
		}
241
242
		if ( is_wp_error( $form->errors ) && $form->errors->get_error_codes() )
243
			return $form->errors;
244
245
		// Process the form
246
		return $form->process_submission();
247
	}
248
249
	function ajax_request() {
250
		$submission_result = self::process_form_submission();
251
252
		if ( ! $submission_result ) {
253
			header( "HTTP/1.1 500 Server Error", 500, true );
254
			echo '<div class="form-error"><ul class="form-errors"><li class="form-error-message">';
255
			esc_html_e( 'An error occurred. Please try again later.', 'jetpack' );
256
			echo '</li></ul></div>';
257
		} elseif ( is_wp_error( $submission_result ) ) {
258
			header( "HTTP/1.1 400 Bad Request", 403, true );
259
			echo '<div class="form-error"><ul class="form-errors"><li class="form-error-message">';
260
			echo esc_html( $submission_result->get_error_message() );
261
			echo '</li></ul></div>';
262
		} else {
263
			echo '<h3>' . esc_html__( 'Message Sent', 'jetpack' ) . '</h3>' . $submission_result;
264
		}
265
266
		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...
267
	}
268
269
	/**
270
	 * Ensure the post author is always zero for contact-form feedbacks
271
	 * Attached to `wp_insert_post_data`
272
	 *
273
	 * @see Grunion_Contact_Form::process_submission()
274
	 *
275
	 * @param array $data the data to insert
276
	 * @param array $postarr the data sent to wp_insert_post()
277
	 * @return array The filtered $data to insert
278
	 */
279
	function insert_feedback_filter( $data, $postarr ) {
280
		if ( $data['post_type'] == 'feedback' && $postarr['post_type'] == 'feedback' ) {
281
			$data['post_author'] = 0;
282
		}
283
284
		return $data;
285
	}
286
	/*
287
	 * Adds our contact-form shortcode
288
	 * The "child" contact-field shortcode is enabled as needed by the contact-form shortcode handler
289
	 */
290
	function add_shortcode() {
291
		add_shortcode( 'contact-form',         array( 'Grunion_Contact_Form', 'parse' ) );
292
		add_shortcode( 'contact-field',        array( 'Grunion_Contact_Form', 'parse_contact_field' ) );
293
	}
294
295
	static function tokenize_label( $label ) {
296
		return '{' . trim( preg_replace( '#^\d+_#', '', $label ) ) . '}';
297
	}
298
299
	static function sanitize_value( $value ) {
300
		return preg_replace( '=((<CR>|<LF>|0x0A/%0A|0x0D/%0D|\\n|\\r)\S).*=i', null, $value );
301
	}
302
303
	/**
304
	 * Replaces tokens like {city} or {City} (case insensitive) with the value
305
	 * of an input field of that name
306
	 *
307
	 * @param string $subject
308
	 * @param array $field_values Array with field label => field value associations
309
	 *
310
	 * @return string The filtered $subject with the tokens replaced
311
	 */
312
	function replace_tokens_with_input( $subject, $field_values ) {
313
		// Wrap labels into tokens (inside {})
314
		$wrapped_labels = array_map( array( 'Grunion_Contact_Form_Plugin', 'tokenize_label' ), array_keys( $field_values ) );
315
		// Sanitize all values
316
		$sanitized_values = array_map( array( 'Grunion_Contact_Form_Plugin', 'sanitize_value' ), array_values( $field_values ) );
317
318
		foreach ( $sanitized_values as $k => $sanitized_value ) {
319
			if ( is_array( $sanitized_value ) ) {
320
				$sanitized_values[ $k ] = implode( ', ', $sanitized_value );
321
			}
322
		}
323
324
		// Search for all valid tokens (based on existing fields) and replace with the field's value
325
		$subject = str_ireplace( $wrapped_labels, $sanitized_values, $subject );
326
		return $subject;
327
	}
328
329
	/**
330
	 * Tracks the widget currently being processed.
331
	 * Attached to `dynamic_sidebar`
332
	 *
333
	 * @see $current_widget_id
334
	 *
335
	 * @param array $widget The widget data
336
	 */
337
	function track_current_widget( $widget ) {
338
		$this->current_widget_id = $widget['id'];
339
	}
340
341
	/**
342
	 * Adds a "widget" attribute to every contact-form embedded in a text widget.
343
	 * Used to tell the difference between post-embedded contact-forms and widget-embedded contact-forms
344
	 * Attached to `widget_text`
345
	 *
346
	 * @param string $text The widget text
347
	 * @return string The filtered widget text
348
	 */
349
	function widget_atts( $text ) {
350
		Grunion_Contact_Form::style( true );
351
352
		return preg_replace( '/\[contact-form([^a-zA-Z_-])/', '[contact-form widget="' . $this->current_widget_id . '"\\1', $text );
353
	}
354
355
	/**
356
	 * For sites where text widgets are not processed for shortcodes, we add this hack to process just our shortcode
357
	 * Attached to `widget_text`
358
	 *
359
	 * @param string $text The widget text
360
	 * @return string The contact-form filtered widget text
361
	 */
362
	function widget_shortcode_hack( $text ) {
363
		if ( !preg_match( '/\[contact-form([^a-zA-Z_-])/', $text ) ) {
364
			return $text;
365
		}
366
367
		$old = $GLOBALS['shortcode_tags'];
368
		remove_all_shortcodes();
369
		Grunion_Contact_Form_Plugin::$using_contact_form_field = true;
370
		$this->add_shortcode();
371
372
		$text = do_shortcode( $text );
373
374
		Grunion_Contact_Form_Plugin::$using_contact_form_field = false;
375
		$GLOBALS['shortcode_tags'] = $old;
376
377
		return $text;
378
	}
379
380
	/**
381
	 * Populate an array with all values necessary to submit a NEW contact-form feedback to Akismet.
382
	 * Note that this includes the current user_ip etc, so this should only be called when accepting a new item via $_POST
383
	 *
384
	 * @param array $form Contact form feedback array
385
	 * @return array feedback array with additional data ready for submission to Akismet
386
	 */
387
	function prepare_for_akismet( $form ) {
388
		$form['comment_type'] = 'contact_form';
389
		$form['user_ip']      = preg_replace( '/[^0-9., ]/', '', $_SERVER['REMOTE_ADDR'] );
390
		$form['user_agent']   = $_SERVER['HTTP_USER_AGENT'];
391
		$form['referrer']     = $_SERVER['HTTP_REFERER'];
392
		$form['blog']         = get_option( 'home' );
393
394
		$ignore = array( 'HTTP_COOKIE' );
395
396
		foreach ( $_SERVER as $k => $value )
397
			if ( !in_array( $k, $ignore ) && is_string( $value ) )
398
				$form["$k"] = $value;
399
400
		return $form;
401
	}
402
403
	/**
404
	 * Submit contact-form data to Akismet to check for spam.
405
	 * If you're accepting a new item via $_POST, run it Grunion_Contact_Form_Plugin::prepare_for_akismet() first
406
	 * Attached to `jetpack_contact_form_is_spam`
407
	 *
408
	 * @param bool $is_spam
409
	 * @param array $form
410
	 * @return bool|WP_Error TRUE => spam, FALSE => not spam, WP_Error => stop processing entirely
411
	 */
412
	function is_spam_akismet( $is_spam, $form = array() ) {
413
		global $akismet_api_host, $akismet_api_port;
414
415
		// The signature of this function changed from accepting just $form.
416
		// If something only sends an array, assume it's still using the old
417
		// signature and work around it.
418
		if ( empty( $form ) && is_array( $is_spam ) ) {
419
			$form = $is_spam;
420
			$is_spam = false;
421
		}
422
423
		// If a previous filter has alrady marked this as spam, trust that and move on.
424
		if ( $is_spam ) {
425
			return $is_spam;
426
		}
427
428
		if ( !function_exists( 'akismet_http_post' ) && !defined( 'AKISMET_VERSION' ) )
429
			return false;
430
431
		$query_string = http_build_query( $form );
432
433
		if ( method_exists( 'Akismet', 'http_post' ) ) {
434
			$response = Akismet::http_post( $query_string, 'comment-check' );
435
		} else {
436
			$response = akismet_http_post( $query_string, $akismet_api_host, '/1.1/comment-check', $akismet_api_port );
437
		}
438
439
		$result = false;
440
441
		if ( isset( $response[0]['x-akismet-pro-tip'] ) && 'discard' === trim( $response[0]['x-akismet-pro-tip'] ) && get_option( 'akismet_strictness' ) === '1' )
442
			$result = new WP_Error( 'feedback-discarded', __('Feedback discarded.', 'jetpack' ) );
443
		elseif ( isset( $response[1] ) && 'true' == trim( $response[1] ) ) // 'true' is spam
444
			$result = true;
445
446
		/**
447
		 * Filter the results returned by Akismet for each submitted contact form.
448
		 *
449
		 * @module contact-form
450
		 *
451
		 * @since 1.3.1
452
		 *
453
		 * @param WP_Error|bool $result Is the submitted feedback spam.
454
		 * @param array|bool $form Submitted feedback.
455
		 */
456
		return apply_filters( 'contact_form_is_spam_akismet', $result, $form );
457
	}
458
459
	/**
460
	 * Submit a feedback as either spam or ham
461
	 *
462
	 * @param string $as Either 'spam' or 'ham'.
463
	 * @param array $form the contact-form data
464
	 */
465
	function akismet_submit( $as, $form ) {
466
		global $akismet_api_host, $akismet_api_port;
467
468
		if ( !in_array( $as, array( 'ham', 'spam' ) ) )
469
			return false;
470
471
		$query_string = '';
472
		if ( is_array( $form ) )
473
			$query_string = http_build_query( $form );
474
		if ( method_exists( 'Akismet', 'http_post' ) ) {
475
		    $response = Akismet::http_post( $query_string, "submit-{$as}" );
476
		} else {
477
		    $response = akismet_http_post( $query_string, $akismet_api_host, "/1.1/submit-{$as}", $akismet_api_port );
478
		}
479
480
		return trim( $response[1] );
481
	}
482
483
	/**
484
	 * Prints the menu
485
	 */
486
	function export_form() {
487
		if ( get_current_screen()->id != 'edit-feedback' )
488
			return;
489
490
		if ( ! current_user_can( 'export' ) ) {
491
			return;
492
		}
493
494
		// if there aren't any feedbacks, bail out
495
		if ( ! (int) wp_count_posts( 'feedback' )->publish )
496
			return;
497
		?>
498
499
		<div id="feedback-export" style="display:none">
500
			<h2><?php _e( 'Export feedback as CSV', 'jetpack' ) ?></h2>
501
			<div class="clear"></div>
502
			<form action="<?php echo admin_url( 'admin-post.php' ); ?>" method="post" class="form">
503
				<?php wp_nonce_field( 'feedback_export','feedback_export_nonce' ); ?>
504
505
				<input name="action" value="feedback_export" type="hidden">
506
				<label for="post"><?php _e( 'Select feedback to download', 'jetpack' ) ?></label>
507
				<select name="post">
508
					<option value="all"><?php esc_html_e( 'All posts', 'jetpack' ) ?></option>
509
					<?php echo $this->get_feedbacks_as_options() ?>
510
				</select>
511
512
				<br><br>
513
				<input type="submit" name="submit" id="submit" class="button button-primary" value="<?php esc_html_e( 'Download', 'jetpack' ); ?>">
514
			</form>
515
		</div>
516
517
		<?php
518
		// There aren't any usable actions in core to output the "export feedback" form in the correct place,
519
		// so this inline JS moves it from the top of the page to the bottom.
520
		?>
521
		<script type='text/javascript'>
522
		var menu = document.getElementById( 'feedback-export' ),
523
		wrapper = document.getElementsByClassName( 'wrap' )[0];
524
		wrapper.appendChild(menu);
525
		menu.style.display = 'block';
526
		</script>
527
		<?php
528
	}
529
530
	/**
531
	 * Fetch post content for a post and extract just the comment.
532
	 *
533
	 * @param int $post_id The post id to fetch the content for.
534
	 *
535
	 * @return string Trimmed post comment.
536
	 *
537
	 * @codeCoverageIgnore
538
	 */
539
	public function get_post_content_for_csv_export( $post_id ) {
540
		$post_content = get_post_field( 'post_content', $post_id );
541
		$content      = explode( '<!--more-->', $post_content );
542
543
		return trim( $content[0] );
544
	}
545
546
	/**
547
	 * Get `_feedback_extra_fields` field from post meta data.
548
	 *
549
	 * @param int $post_id Id of the post to fetch meta data for.
550
	 *
551
	 * @return mixed
552
	 *
553
	 * @codeCoverageIgnore - No need to be covered.
554
	 */
555
	public function get_post_meta_for_csv_export( $post_id ) {
556
		return get_post_meta( $post_id, '_feedback_extra_fields', true );
557
	}
558
559
	/**
560
	 * Get parsed feedback post fields.
561
	 *
562
	 * @param int $post_id Id of the post to fetch parsed contents for.
563
	 *
564
	 * @return array
565
	 *
566
	 * @codeCoverageIgnore - No need to be covered.
567
	 */
568
	public function get_parsed_field_contents_of_post( $post_id ) {
569
		return self::parse_fields_from_content( $post_id );
570
	}
571
572
	/**
573
	 * Properly maps fields that are missing from the post meta data
574
	 * to names, that are similar to those of the post meta.
575
	 *
576
	 * @param array $parsed_post_content Parsed post content
577
	 *
578
	 * @see parse_fields_from_content for how the input data is generated.
579
	 *
580
	 * @return array Mapped fields.
581
	 */
582
	public function map_parsed_field_contents_of_post_to_field_names( $parsed_post_content ) {
583
584
		$mapped_fields = array();
585
586
		$field_mapping = array(
587
			'_feedback_subject'      => __( 'Contact Form', 'jetpack' ),
588
			'_feedback_author'       => '1_Name',
589
			'_feedback_author_email' => '2_Email',
590
			'_feedback_author_url'   => '3_Website',
591
			'_feedback_main_comment' => '4_Comment',
592
		);
593
594
		foreach ( $field_mapping as $parsed_field_name => $field_name ) {
595
			if (
596
				isset( $parsed_post_content[ $parsed_field_name ] )
597
				&& ! empty( $parsed_post_content[ $parsed_field_name ] )
598
			) {
599
				$mapped_fields[ $field_name ] = $parsed_post_content[ $parsed_field_name ];
600
			}
601
		}
602
603
		return $mapped_fields;
604
	}
605
606
607
	/**
608
	 * Prepares feedback post data for CSV export.
609
	 *
610
	 * @param array $post_ids Post IDs to fetch the data for. These need to be Feedback posts.
611
	 *
612
	 * @return array
613
	 */
614
	public function get_export_data_for_posts( $post_ids ) {
615
616
		$posts_data  = array();
617
		$field_names = array();
618
		$result      = array();
619
620
		/**
621
		 * Fetch posts and get the possible field names for later use
622
		 */
623
		foreach ( $post_ids as $post_id ) {
624
625
			/**
626
			 * Fetch post meta data.
627
			 */
628
			$post_meta_data = $this->get_post_meta_for_csv_export( $post_id );
629
630
			/**
631
			 * If `$post_meta_data` is not an array or if it is empty, then there is no
632
			 * feedback to work with. Skip it.
633
			 */
634
			if ( ! is_array( $post_meta_data ) || empty( $post_meta_data ) ) {
635
				continue;
636
			}
637
638
			/**
639
			 * Fetch post main data, because we need the subject and author data for the feedback form.
640
			 */
641
			$post_real_data = $this->get_parsed_field_contents_of_post( $post_id );
642
643
			/**
644
			 * If `$post_real_data` is not an array or there is no `_feedback_subject` set,
645
			 * then something must be wrong with the feedback post. Skip it.
646
			 */
647
			if ( ! is_array( $post_real_data ) || ! isset( $post_real_data['_feedback_subject'] ) ) {
648
				continue;
649
			}
650
651
			/**
652
			 * Fetch main post comment. This is from the default textarea fields.
653
			 * If it is non-empty, then we add it to data, otherwise skip it.
654
			 */
655
			$post_comment_content = $this->get_post_content_for_csv_export( $post_id );
656
			if ( ! empty( $post_comment_content ) ) {
657
				$post_real_data['_feedback_main_comment'] = $post_comment_content;
658
			}
659
660
			/**
661
			 * Map parsed fields to proper field names
662
			 */
663
			$mapped_fields = $this->map_parsed_field_contents_of_post_to_field_names( $post_real_data );
664
665
			/**
666
			 * Prepend the feedback subject to the list of fields.
667
			 */
668
			$post_meta_data = array_merge(
669
				$mapped_fields,
670
				$post_meta_data
671
			);
672
673
674
			/**
675
			 * Save post metadata for later usage.
676
			 */
677
			$posts_data[ $post_id ] = $post_meta_data;
678
679
			/**
680
			 * Save field names, so we can use them as header fields later in the CSV.
681
			 */
682
			$field_names = array_merge( $field_names, array_keys( $post_meta_data ) );
683
		}
684
685
		/**
686
		 * Make sure the field names are unique, because we don't want duplicate data.
687
		 */
688
		$field_names = array_unique( $field_names );
689
690
691
		/**
692
		 * Sort the field names by the field id number
693
		 */
694
		sort( $field_names, SORT_NUMERIC );
695
696
		/**
697
		 * Loop through every post, which is essentially CSV row.
698
		 */
699
		foreach ( $posts_data as $post_id => $single_post_data ) {
700
701
			/**
702
			 * Go through all the possible fields and check if the field is available
703
			 * in the current post.
704
			 *
705
			 * If it is - add the data as a value.
706
			 * If it is not - add an empty string, which is just a placeholder in the CSV.
707
			 */
708
			foreach ( $field_names as $single_field_name ) {
709
				if (
710
					isset( $single_post_data[ $single_field_name ] )
711
					&& ! empty( $single_post_data[ $single_field_name ] )
712
				) {
713
					$result[ $single_field_name ][] = trim( $single_post_data[ $single_field_name ] );
714
				}
715
				else {
716
					$result[ $single_field_name ][] = '';
717
				}
718
			}
719
		}
720
721
		return $result;
722
	}
723
724
	/**
725
	 * download as a csv a contact form or all of them in a csv file
726
	 */
727
	function download_feedback_as_csv() {
728
		if ( empty( $_POST['feedback_export_nonce'] ) )
729
			return;
730
731
		check_admin_referer( 'feedback_export', 'feedback_export_nonce' );
732
733
		if ( ! current_user_can( 'export' ) ) {
734
			return;
735
		}
736
737
		$args = array(
738
			'posts_per_page'   => -1,
739
			'post_type'        => 'feedback',
740
			'post_status'      => 'publish',
741
			'order'            => 'ASC',
742
			'fields'           => 'ids',
743
			'suppress_filters' => false,
744
		);
745
746
		$filename = date( "Y-m-d" ) . '-feedback-export.csv';
747
748
		// Check if we want to download all the feedbacks or just a certain contact form
749
		if ( ! empty( $_POST['post'] ) && $_POST['post'] !== 'all' ) {
750
			$args['post_parent'] = (int) $_POST['post'];
751
			$filename            = date( "Y-m-d" ) . '-' . str_replace( '&nbsp;', '-', get_the_title( (int) $_POST['post'] ) ) . '.csv';
752
		}
753
754
		$feedbacks = get_posts( $args );
755
756
		if ( empty( $feedbacks ) ) {
757
			return;
758
		}
759
760
		$filename  = sanitize_file_name( $filename );
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned correctly; expected 1 space but found 2 spaces

This check looks for improperly formatted assignments.

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

To illustrate:

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

will have no issues, while

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

will report issues in lines 1 and 2.

Loading history...
761
762
		/**
763
		 * Prepare data for export.
764
		 */
765
		$data = $this->get_export_data_for_posts( $feedbacks );
766
767
		/**
768
		 * If `$data` is empty, there's nothing we can do below.
769
		 */
770
		if ( ! is_array( $data ) || empty( $data ) ) {
771
			return;
772
		}
773
774
		/**
775
		 * Extract field names from `$data` for later use.
776
		 */
777
		$fields = array_keys( $data );
778
779
		/**
780
		 * Count how many rows will be exported.
781
		 */
782
		$row_count = count( reset( $data ) );
783
784
785
		// Forces the download of the CSV instead of echoing
786
		header( 'Content-Disposition: attachment; filename=' . $filename );
787
		header( 'Pragma: no-cache' );
788
		header( 'Expires: 0' );
789
		header( 'Content-Type: text/csv; charset=utf-8' );
790
791
		$output = fopen( 'php://output', 'w' );
792
793
		/**
794
		 * Print CSV headers
795
		 */
796
		fputcsv( $output, $fields );
797
798
799
		/**
800
		 * Print rows to the output.
801
		 */
802
		for ( $i = 0; $i < $row_count; $i ++ ) {
803
804
			$current_row = array();
805
806
			/**
807
			 * Put all the fields in `$current_row` array.
808
			 */
809
			foreach ( $fields as $single_field_name ) {
810
				$current_row[] = $data[ $single_field_name ][ $i ];
811
			}
812
813
			/**
814
			 * Output the complete CSV row
815
			 */
816
			fputcsv( $output, $current_row );
817
		}
818
819
		fclose( $output );
820
	}
821
822
	/**
823
	 * Returns a string of HTML <option> items from an array of posts
824
	 *
825
	 * @return string a string of HTML <option> items
826
	 */
827
	protected function get_feedbacks_as_options() {
828
		$options = '';
829
830
		// Get the feedbacks' parents' post IDs
831
		$feedbacks = get_posts( array(
832
			'fields'           => 'id=>parent',
833
			'posts_per_page'   => 100000,
834
			'post_type'        => 'feedback',
835
			'post_status'      => 'publish',
836
			'suppress_filters' => false,
837
		) );
838
		$parents = array_unique( array_values( $feedbacks ) );
839
840
		$posts = get_posts( array(
841
			'orderby'          => 'ID',
842
			'posts_per_page'   => 1000,
843
			'post_type'        => 'any',
844
			'post__in'         => array_values( $parents ),
845
			'suppress_filters' => false,
846
		) );
847
848
		// creates the string of <option> elements
849
		foreach ( $posts as $post ) {
850
			$options .= sprintf( '<option value="%s">%s</option>', esc_attr( $post->ID ), esc_html( $post->post_title ) );
851
		}
852
853
		return $options;
854
	}
855
856
	/**
857
	 * Get the names of all the form's fields
858
	 *
859
	 * @param  array|int $posts the post we want the fields of
860
	 *
861
	 * @return array     the array of fields
862
	 *
863
	 * @deprecated As this is no longer necessary as of the CSV export rewrite. - 2015-12-29
864
	 */
865
	protected function get_field_names( $posts ) {
866
		$posts = (array) $posts;
867
		$all_fields = array();
868
869
		foreach ( $posts as $post ){
870
			$fields = self::parse_fields_from_content( $post );
871
872
			if ( isset( $fields['_feedback_all_fields'] ) ) {
873
				$extra_fields = array_keys( $fields['_feedback_all_fields'] );
874
				$all_fields = array_merge( $all_fields, $extra_fields );
875
			}
876
		}
877
878
		$all_fields = array_unique( $all_fields );
879
		return $all_fields;
880
	}
881
882
	public static function parse_fields_from_content( $post_id ) {
883
		static $post_fields;
884
885
		if ( !is_array( $post_fields ) )
886
			$post_fields = array();
887
888
		if ( isset( $post_fields[$post_id] ) )
889
			return $post_fields[$post_id];
890
891
		$all_values   = array();
892
		$post_content = get_post_field( 'post_content', $post_id );
893
		$content      = explode( '<!--more-->', $post_content );
894
		$lines        = array();
895
896
		if ( count( $content ) > 1 ) {
897
			$content  = str_ireplace( array( '<br />', ')</p>' ), '', $content[1] );
898
			$one_line = preg_replace( '/\s+/', ' ', $content );
899
			$one_line = preg_replace( '/.*Array \( (.*)\)/', '$1', $one_line );
900
901
			preg_match_all( '/\[([^\]]+)\] =\&gt\; ([^\[]+)/', $one_line, $matches );
902
903
			if ( count( $matches ) > 1 )
904
				$all_values = array_combine( array_map('trim', $matches[1]), array_map('trim', $matches[2]) );
905
906
			$lines = array_filter( explode( "\n", $content ) );
907
		}
908
909
		$var_map = array(
910
			'AUTHOR'       => '_feedback_author',
911
			'AUTHOR EMAIL' => '_feedback_author_email',
912
			'AUTHOR URL'   => '_feedback_author_url',
913
			'SUBJECT'      => '_feedback_subject',
914
			'IP'           => '_feedback_ip'
915
		);
916
917
		$fields = array();
918
919
		foreach( $lines as $line ) {
920
			$vars = explode( ': ', $line, 2 );
921
			if ( !empty( $vars ) ) {
922
				if ( isset( $var_map[$vars[0]] ) ) {
923
					$fields[$var_map[$vars[0]]] = self::strip_tags( trim( $vars[1] ) );
924
				}
925
			}
926
		}
927
928
		$fields['_feedback_all_fields'] = $all_values;
929
930
		$post_fields[$post_id] = $fields;
931
932
		return $fields;
933
	}
934
935
	/**
936
	 * Creates a valid csv row from a post id
937
	 *
938
	 * @param  int    $post_id The id of the post
939
	 * @param  array  $fields  An array containing the names of all the fields of the csv
940
	 * @return String The csv row
941
	 *
942
	 * @deprecated This is no longer needed, as of the CSV export rewrite.
943
	 */
944
	protected static function make_csv_row_from_feedback( $post_id, $fields ) {
945
		$content_fields = self::parse_fields_from_content( $post_id );
946
		$all_fields     = array();
947
948
		if ( isset( $content_fields['_feedback_all_fields'] ) )
949
			$all_fields = $content_fields['_feedback_all_fields'];
950
951
		// Overwrite the parsed content with the content we stored in post_meta in a better format.
952
		$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...
953
		foreach ( $extra_fields as $extra_field => $extra_value ) {
954
			$all_fields[$extra_field] = $extra_value;
955
		}
956
957
		// The first element in all of the exports will be the subject
958
		$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...
959
960
		// Loop the fields array in order to fill the $row_items array correctly
961
		foreach ( $fields as $field ) {
962
			if ( $field === __( 'Contact Form', 'jetpack' ) ) // the first field will ever be the contact form, so we can continue
963
				continue;
964
			elseif ( array_key_exists( $field, $all_fields ) )
965
				$row_items[] = $all_fields[$field];
966
			else
967
				$row_items[] = '';
968
		}
969
970
		return $row_items;
971
	}
972
973
	public static function get_ip_address() {
974
		return isset( $_SERVER['REMOTE_ADDR'] ) ? $_SERVER['REMOTE_ADDR'] : null;
975
	}
976
}
977
978
/**
979
 * Generic shortcode class.
980
 * Does nothing other than store structured data and output the shortcode as a string
981
 *
982
 * Not very general - specific to Grunion.
983
 */
984
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...
985
	/**
986
	 * @var string the name of the shortcode: [$shortcode_name /]
987
 	 */
988
	public $shortcode_name;
989
990
	/**
991
	 * @var array key => value pairs for the shortcode's attributes: [$shortcode_name key="value" ... /]
992
	 */
993
	public $attributes;
994
995
	/**
996
	 * @var array key => value pair for attribute defaults
997
	 */
998
	public $defaults = array();
999
1000
	/**
1001
	 * @var null|string Null for selfclosing shortcodes.  Hhe inner content of otherwise: [$shortcode_name]$content[/$shortcode_name]
1002
	 */
1003
	public $content;
1004
1005
	/**
1006
	 * @var array Associative array of inner "child" shortcodes equivalent to the $content: [$shortcode_name][child 1/][child 2/][/$shortcode_name]
1007
	 */
1008
	public $fields;
1009
1010
	/**
1011
	 * @var null|string The HTML of the parsed inner "child" shortcodes".  Null for selfclosing shortcodes.
1012
	 */
1013
	public $body;
1014
1015
	/**
1016
	 * @param array $attributes An associative array of shortcode attributes.  @see shortcode_atts()
1017
	 * @param null|string $content Null for selfclosing shortcodes.  The inner content otherwise.
1018
	 */
1019
	function __construct( $attributes, $content = null ) {
1020
		$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...
1021
		if ( is_array( $content ) ) {
1022
			$string_content = '';
1023
			foreach ( $content as $field ) {
1024
				$string_content .= (string) $field;
1025
			}
1026
1027
			$this->content = $string_content;
1028
		} else {
1029
			$this->content = $content;
1030
		}
1031
1032
		$this->parse_content( $this->content );
1033
	}
1034
1035
	/**
1036
	 * Processes the shortcode's inner content for "child" shortcodes
1037
	 *
1038
	 * @param string $content The shortcode's inner content: [shortcode]$content[/shortcode]
1039
	 */
1040
	function parse_content( $content ) {
1041
		if ( is_null( $content ) ) {
1042
			$this->body = null;
1043
		}
1044
1045
		$this->body = do_shortcode( $content );
1046
	}
1047
1048
	/**
1049
	 * Returns the value of the requested attribute.
1050
	 *
1051
	 * @param string $key The attribute to retrieve
1052
	 * @return mixed
1053
	 */
1054
	function get_attribute( $key ) {
1055
		return isset( $this->attributes[$key] ) ? $this->attributes[$key] : null;
1056
	}
1057
1058
	function esc_attr( $value ) {
1059
		if ( is_array( $value ) ) {
1060
			return array_map( array( $this, 'esc_attr' ), $value );
1061
		}
1062
1063
		$value = Grunion_Contact_Form_Plugin::strip_tags( $value );
1064
		$value = _wp_specialchars( $value, ENT_QUOTES, false, true );
1065
1066
		// Shortcode attributes can't contain "]"
1067
		$value = str_replace( ']', '', $value );
1068
		$value = str_replace( ',', '&#x002c;', $value ); // store commas encoded
1069
		$value = strtr( $value, array( '%' => '%25', '&' => '%26' ) );
1070
1071
		// 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...
1072
		$value = addslashes( $value );
1073
		return $value;
1074
	}
1075
1076
	function unesc_attr( $value ) {
1077
		if ( is_array( $value ) ) {
1078
			return array_map( array( $this, 'unesc_attr' ), $value );
1079
		}
1080
1081
		// For back-compat with old Grunion encoding
1082
		// Also, unencode commas
1083
		$value = strtr( $value, array( '%26' => '&', '%25' => '%' ) );
1084
		$value = preg_replace( array( '/&#x0*22;/i', '/&#x0*27;/i', '/&#x0*26;/i', '/&#x0*2c;/i' ), array( '"', "'", '&', ',' ), $value );
1085
		$value = htmlspecialchars_decode( $value, ENT_QUOTES );
1086
		$value = Grunion_Contact_Form_Plugin::strip_tags( $value );
1087
1088
		return $value;
1089
	}
1090
1091
	/**
1092
	 * Generates the shortcode
1093
	 */
1094
	function __toString() {
1095
		$r = "[{$this->shortcode_name} ";
1096
1097
		foreach ( $this->attributes as $key => $value ) {
1098
			if ( !$value ) {
1099
				continue;
1100
			}
1101
1102
			if ( isset( $this->defaults[$key] ) && $this->defaults[$key] == $value ) {
1103
				continue;
1104
			}
1105
1106
			if ( 'id' == $key ) {
1107
				continue;
1108
			}
1109
1110
			$value = $this->esc_attr( $value );
1111
1112
			if ( is_array( $value ) ) {
1113
				$value = join( ',', $value );
1114
			}
1115
1116
			if ( false === strpos( $value, "'" ) ) {
1117
				$value = "'$value'";
1118
			} elseif ( false === strpos( $value, '"' ) ) {
1119
				$value = '"' . $value . '"';
1120
			} else {
1121
				// Shortcodes can't contain both '"' and "'".  Strip one.
1122
				$value = str_replace( "'", '', $value );
1123
				$value = "'$value'";
1124
			}
1125
1126
			$r .= "{$key}={$value} ";
1127
		}
1128
1129
		$r = rtrim( $r );
1130
1131
		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...
1132
			$r .= ']';
1133
1134
			foreach ( $this->fields as $field ) {
1135
				$r .= (string) $field;
1136
			}
1137
1138
			$r .= "[/{$this->shortcode_name}]";
1139
		} else {
1140
			$r .= '/]';
1141
		}
1142
1143
		return $r;
1144
	}
1145
}
1146
1147
/**
1148
 * Class for the contact-form shortcode.
1149
 * Parses shortcode to output the contact form as HTML
1150
 * Sends email and stores the contact form response (a.k.a. "feedback")
1151
 */
1152
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...
1153
	public $shortcode_name = 'contact-form';
1154
1155
	/**
1156
	 * @var WP_Error stores form submission errors
1157
	 */
1158
	public $errors;
1159
1160
	/**
1161
	 * @var Grunion_Contact_Form The most recent (inclusive) contact-form shortcode processed
1162
	 */
1163
	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...
1164
1165
	/**
1166
	 * @var Whatever form we are currently looking at. If processed, will become $last
1167
	 */
1168
	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...
1169
1170
	/**
1171
	 * @var bool Whether to print the grunion.css style when processing the contact-form shortcode
1172
	 */
1173
	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...
1174
1175
	function __construct( $attributes, $content = null ) {
1176
		global $post;
1177
1178
		// Set up the default subject and recipient for this form
1179
		$default_to = '';
1180
		$default_subject = "[" . get_option( 'blogname' ) . "]";
1181
1182
		if ( !empty( $attributes['widget'] ) && $attributes['widget'] ) {
1183
			$default_to .= get_option( 'admin_email' );
1184
			$attributes['id'] = 'widget-' . $attributes['widget'];
1185
			$default_subject = sprintf( _x( '%1$s Sidebar', '%1$s = blog name', 'jetpack' ), $default_subject );
1186
		} else if ( $post ) {
1187
			$attributes['id'] = $post->ID;
1188
			$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 ) );
1189
			$post_author = get_userdata( $post->post_author );
1190
			$default_to .= $post_author->user_email;
1191
		}
1192
1193
		// Keep reference to $this for parsing form fields
1194
		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...
1195
1196
		$this->defaults = array(
1197
			'to'                 => $default_to,
1198
			'subject'            => $default_subject,
1199
			'show_subject'       => 'no', // only used in back-compat mode
1200
			'widget'             => 0,    // Not exposed to the user. Works with Grunion_Contact_Form_Plugin::widget_atts()
1201
			'id'                 => null, // Not exposed to the user. Set above.
1202
			'submit_button_text' => __( 'Submit &#187;', 'jetpack' ),
1203
		);
1204
1205
		$attributes = shortcode_atts( $this->defaults, $attributes, 'contact-form' );
1206
1207
		// We only enable the contact-field shortcode temporarily while processing the contact-form shortcode
1208
		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...
1209
1210
		parent::__construct( $attributes, $content );
1211
1212
		// There were no fields in the contact form. The form was probably just [contact-form /]. Build a default form.
1213
		if ( empty( $this->fields ) ) {
1214
			// same as the original Grunion v1 form
1215
			$default_form = '
1216
				[contact-field label="' . __( 'Name', 'jetpack' )    . '" type="name"  required="true" /]
1217
				[contact-field label="' . __( 'Email', 'jetpack' )   . '" type="email" required="true" /]
1218
				[contact-field label="' . __( 'Website', 'jetpack' ) . '" type="url" /]';
1219
1220
			if ( 'yes' == strtolower( $this->get_attribute( 'show_subject' ) ) ) {
1221
				$default_form .= '
1222
					[contact-field label="' . __( 'Subject', 'jetpack' ) . '" type="subject" /]';
1223
			}
1224
1225
			$default_form .= '
1226
				[contact-field label="' . __( 'Message', 'jetpack' ) . '" type="textarea" /]';
1227
1228
			$this->parse_content( $default_form );
1229
1230
			// Store the shortcode
1231
			$this->store_shortcode( $default_form, $attributes );
1232
		} else {
1233
			// Store the shortcode
1234
			$this->store_shortcode( $content, $attributes );
1235
		}
1236
1237
		// $this->body and $this->fields have been setup.  We no longer need the contact-field shortcode.
1238
		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...
1239
	}
1240
1241
	/**
1242
	 * Store shortcode content for recall later
1243
	 *	- used to receate shortcode when user uses do_shortcode
1244
	 *
1245
	 * @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...
1246
	 */
1247
	static function store_shortcode( $content = null, $attributes = null ) {
1248
1249
		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...
1250
1251
			$shortcode_meta = get_post_meta( $attributes['id'], '_g_feedback_shortcode', true );
1252
1253
			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...
1254
				update_post_meta( $attributes['id'], '_g_feedback_shortcode', $content );
1255
			}
1256
1257
		}
1258
	}
1259
1260
	/**
1261
	 * Toggle for printing the grunion.css stylesheet
1262
	 *
1263
	 * @param bool $style
1264
	 */
1265
	static function style( $style ) {
1266
		$previous_style = self::$style;
1267
		self::$style = (bool) $style;
1268
		return $previous_style;
1269
	}
1270
1271
	/**
1272
	 * Turn on printing of grunion.css stylesheet
1273
	 * @see ::style()
1274
	 * @internal
1275
	 * @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...
1276
	 */
1277
	static function _style_on() {
1278
		return self::style( true );
1279
	}
1280
1281
	/**
1282
	 * The contact-form shortcode processor
1283
	 *
1284
	 * @param array $attributes Key => Value pairs as parsed by shortcode_parse_atts()
1285
	 * @param string|null $content The shortcode's inner content: [contact-form]$content[/contact-form]
1286
	 * @return string HTML for the concat form.
1287
	 */
1288
	static function parse( $attributes, $content ) {
1289
		// Create a new Grunion_Contact_Form object (this class)
1290
		$form = new Grunion_Contact_Form( $attributes, $content );
1291
1292
		$id = $form->get_attribute( 'id' );
1293
1294
		if ( !$id ) { // something terrible has happened
1295
			return '[contact-form]';
1296
		}
1297
1298
		if ( is_feed() ) {
1299
			return '[contact-form]';
1300
		}
1301
1302
		// Only allow one contact form per post/widget
1303
		if ( self::$last && $id == self::$last->get_attribute( 'id' ) ) {
1304
			// We're processing the same post
1305
1306
			if ( self::$last->attributes != $form->attributes || self::$last->content != $form->content ) {
1307
				// And we're processing a different shortcode;
1308
				return '';
1309
			} // else, we're processing the same shortcode - probably a separate run of do_shortcode() - let it through
1310
1311
		} else {
1312
			self::$last = $form;
1313
		}
1314
1315
		// Enqueue the grunion.css stylesheet if self::$style allows it
1316
		if ( self::$style && ( empty( $_REQUEST['action'] ) || $_REQUEST['action'] != 'grunion_shortcode_to_json' ) ) {
1317
			// Enqueue the style here instead of printing it, because if some other plugin has run the_post()+rewind_posts(),
1318
			// (like VideoPress does), the style tag gets "printed" the first time and discarded, leaving the contact form unstyled.
1319
			// when WordPress does the real loop.
1320
			wp_enqueue_style( 'grunion.css' );
1321
		}
1322
1323
		$r = '';
1324
		$r .= "<div id='contact-form-$id'>\n";
1325
1326
		if ( is_wp_error( $form->errors ) && $form->errors->get_error_codes() ) {
1327
			// There are errors.  Display them
1328
			$r .= "<div class='form-error'>\n<h3>" . __( 'Error!', 'jetpack' ) . "</h3>\n<ul class='form-errors'>\n";
1329
			foreach ( $form->errors->get_error_messages() as $message )
1330
				$r .= "\t<li class='form-error-message'>" . esc_html( $message ) . "</li>\n";
1331
			$r .= "</ul>\n</div>\n\n";
1332
		}
1333
1334
		if ( isset( $_GET['contact-form-id'] ) && $_GET['contact-form-id'] == self::$last->get_attribute( 'id' ) && isset( $_GET['contact-form-sent'] ) ) {
1335
			// The contact form was submitted.  Show the success message/results
1336
1337
			$feedback_id = (int) $_GET['contact-form-sent'];
1338
1339
			$back_url = remove_query_arg( array( 'contact-form-id', 'contact-form-sent', '_wpnonce' ) );
1340
1341
			$r_success_message =
1342
				"<h3>" . __( 'Message Sent', 'jetpack' ) .
1343
				' (<a href="' . esc_url( $back_url ) . '">' . esc_html__( 'go back', 'jetpack' ) . '</a>)' .
1344
				"</h3>\n\n";
1345
1346
			// Don't show the feedback details unless the nonce matches
1347
			if ( $feedback_id && wp_verify_nonce( stripslashes( $_GET['_wpnonce'] ), "contact-form-sent-{$feedback_id}" ) ) {
1348
				$r_success_message .= self::success_message( $feedback_id, $form );
1349
			}
1350
1351
			/**
1352
			 * Filter the message returned after a successfull contact form submission.
1353
			 *
1354
			 * @module contact-form
1355
			 *
1356
			 * @since 1.3.1
1357
			 *
1358
			 * @param string $r_success_message Success message.
1359
			 */
1360
			$r .= apply_filters( 'grunion_contact_form_success_message', $r_success_message );
1361
		} else {
1362
			// Nothing special - show the normal contact form
1363
1364
			if ( $form->get_attribute( 'widget' ) ) {
1365
				// Submit form to the current URL
1366
				$url = remove_query_arg( array( 'contact-form-id', 'contact-form-sent', 'action', '_wpnonce' ) );
1367
			} else {
1368
				// Submit form to the post permalink
1369
				$url = get_permalink();
1370
			}
1371
1372
			// For SSL/TLS page. See RFC 3986 Section 4.2
1373
			$url = set_url_scheme( $url );
1374
1375
			// May eventually want to send this to admin-post.php...
1376
			/**
1377
			 * Filter the contact form action URL.
1378
			 *
1379
			 * @module contact-form
1380
			 *
1381
			 * @since 1.3.1
1382
			 *
1383
			 * @param string $contact_form_id Contact form post URL.
1384
			 * @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...
1385
			 * @param int $id Contact Form ID.
1386
			 */
1387
			$url = apply_filters( 'grunion_contact_form_form_action', "{$url}#contact-form-{$id}", $GLOBALS['post'], $id );
1388
1389
			$r .= "<form action='" . esc_url( $url ) . "' method='post' class='contact-form commentsblock'>\n";
1390
			$r .= $form->body;
1391
			$r .= "\t<p class='contact-submit'>\n";
1392
			$r .= "\t\t<input type='submit' value='" . esc_attr( $form->get_attribute( 'submit_button_text' ) ) . "' class='pushbutton-wide'/>\n";
1393
			if ( is_user_logged_in() ) {
1394
				$r .= "\t\t" . wp_nonce_field( 'contact-form_' . $id, '_wpnonce', true, false ) . "\n"; // nonce and referer
1395
			}
1396
			$r .= "\t\t<input type='hidden' name='contact-form-id' value='$id' />\n";
1397
			$r .= "\t\t<input type='hidden' name='action' value='grunion-contact-form' />\n";
1398
			$r .= "\t</p>\n";
1399
			$r .= "</form>\n";
1400
		}
1401
1402
		$r .= "</div>";
1403
1404
		return $r;
1405
	}
1406
1407
	/**
1408
	 * Returns a success message to be returned if the form is sent via AJAX.
1409
	 *
1410
	 * @param int $feedback_id
1411
	 * @param object Grunion_Contact_Form $form
1412
	 *
1413
	 * @return string $message
1414
	 */
1415
	static function success_message( $feedback_id, $form ) {
1416
		return wp_kses(
1417
			'<blockquote class="contact-form-submission">'
1418
			. '<p>' . join( self::get_compiled_form( $feedback_id, $form ), '</p><p>' ) . '</p>'
1419
			. '</blockquote>',
1420
			array( 'br' => array(), 'blockquote' => array( 'class' => array() ), 'p' => array() )
1421
		);
1422
	}
1423
1424
	/**
1425
	 * Returns a compiled form with labels and values in a form of  an array
1426
	 * of lines.
1427
	 * @param int $feedback_id
1428
	 * @param object Grunion_Contact_Form $form
1429
	 *
1430
	 * @return array $lines
1431
	 */
1432
	static function get_compiled_form( $feedback_id, $form ) {
1433
		$feedback       = get_post( $feedback_id );
1434
		$field_ids      = $form->get_field_ids();
1435
		$content_fields = Grunion_Contact_Form_Plugin::parse_fields_from_content( $feedback_id );
1436
1437
		// Maps field_ids to post_meta keys
1438
		$field_value_map = array(
1439
			'name'     => 'author',
1440
			'email'    => 'author_email',
1441
			'url'      => 'author_url',
1442
			'subject'  => 'subject',
1443
			'textarea' => false, // not a post_meta key.  This is stored in post_content
1444
		);
1445
1446
		$compiled_form = array();
1447
1448
		// "Standard" field whitelist
1449
		foreach ( $field_value_map as $type => $meta_key ) {
1450
			if ( isset( $field_ids[$type] ) ) {
1451
				$field = $form->fields[$field_ids[$type]];
1452
1453
				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...
1454
					if ( isset( $content_fields["_feedback_{$meta_key}"] ) )
1455
						$value = $content_fields["_feedback_{$meta_key}"];
1456
				} else {
1457
					// The feedback content is stored as the first "half" of post_content
1458
					$value = $feedback->post_content;
1459
					list( $value ) = explode( '<!--more-->', $value );
1460
					$value = trim( $value );
1461
				}
1462
1463
				$field_index = array_search( $field_ids[ $type ], $field_ids['all'] );
1464
				$compiled_form[ $field_index ] = sprintf(
1465
					'<b>%1$s:</b> %2$s<br /><br />',
1466
					wp_kses( $field->get_attribute( 'label' ), array() ),
1467
					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...
1468
				);
1469
			}
1470
		}
1471
1472
		// "Non-standard" fields
1473
		if ( $field_ids['extra'] ) {
1474
			// array indexed by field label (not field id)
1475
			$extra_fields = get_post_meta( $feedback_id, '_feedback_extra_fields', true );
1476
			$extra_field_keys = array_keys( $extra_fields );
1477
1478
			$i = 0;
1479
			foreach ( $field_ids['extra'] as $field_id ) {
1480
				$field = $form->fields[$field_id];
1481
				$field_index = array_search( $field_id, $field_ids['all'] );
1482
1483
				$label = $field->get_attribute( 'label' );
1484
1485
				$compiled_form[ $field_index ] = sprintf(
1486
					'<b>%1$s:</b> %2$s<br /><br />',
1487
					wp_kses( $label, array() ),
1488
					nl2br( wp_kses( $extra_fields[$extra_field_keys[$i]], array() ) )
1489
				);
1490
1491
				$i++;
1492
			}
1493
		}
1494
1495
		// Sorting lines by the field index
1496
		ksort( $compiled_form );
1497
1498
		return $compiled_form;
1499
	}
1500
1501
	/**
1502
	 * The contact-field shortcode processor
1503
	 * 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.
1504
	 *
1505
	 * @param array $attributes Key => Value pairs as parsed by shortcode_parse_atts()
1506
	 * @param string|null $content The shortcode's inner content: [contact-field]$content[/contact-field]
1507
	 * @return HTML for the contact form field
1508
	 */
1509
	static function parse_contact_field( $attributes, $content ) {
1510
		// Don't try to parse contact form fields if not inside a contact form
1511
		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...
1512
			$att_strs = array();
1513
			foreach ( $attributes as $att => $val ) {
1514
				if ( is_numeric( $att ) ) { // Is a valueless attribute
1515
					$att_strs[] = esc_html( $val );
1516
				} else if ( isset( $val ) ) { // A regular attr - value pair
1517
					$att_strs[] = esc_html( $att ) . '=\'' . esc_html( $val ) . '\'';
1518
				}
1519
			}
1520
1521
			$html = '[contact-field ' . implode( ' ', $att_strs );
1522
1523
			if ( isset( $content ) && ! empty( $content ) ) { // If there is content, let's add a closing tag
1524
				$html .=  ']' . esc_html( $content ) . '[/contact-field]';
1525
			} else { // Otherwise let's add a closing slash in the first tag
1526
				$html .= '/]';
1527
			}
1528
1529
			return $html;
1530
		}
1531
1532
		$form = Grunion_Contact_Form::$current_form;
1533
1534
		$field = new Grunion_Contact_Form_Field( $attributes, $content, $form );
1535
1536
		$field_id = $field->get_attribute( 'id' );
1537
		if ( $field_id ) {
1538
			$form->fields[$field_id] = $field;
1539
		} else {
1540
			$form->fields[] = $field;
1541
		}
1542
1543
		if (
1544
			isset( $_POST['action'] ) && 'grunion-contact-form' === $_POST['action']
1545
		&&
1546
			isset( $_POST['contact-form-id'] ) && $form->get_attribute( 'id' ) == $_POST['contact-form-id']
1547
		) {
1548
			// If we're processing a POST submission for this contact form, validate the field value so we can show errors as necessary.
1549
			$field->validate();
1550
		}
1551
1552
		// Output HTML
1553
		return $field->render();
1554
	}
1555
1556
	/**
1557
	 * Loops through $this->fields to generate a (structured) list of field IDs
1558
	 * @return array
1559
	 */
1560
	function get_field_ids() {
1561
		$field_ids = array(
1562
			'all'   => array(), // array of all field_ids
1563
			'extra' => array(), // array of all non-whitelisted field IDs
1564
1565
			// Whitelisted "standard" field IDs:
1566
			// '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...
1567
			// '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...
1568
			// '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...
1569
			// '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...
1570
			// '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...
1571
		);
1572
1573
		foreach ( $this->fields as $id => $field ) {
1574
			$field_ids['all'][] = $id;
1575
1576
			$type = $field->get_attribute( 'type' );
1577
			if ( isset( $field_ids[$type] ) ) {
1578
				// This type of field is already present in our whitelist of "standard" fields for this form
1579
				// Put it in extra
1580
				$field_ids['extra'][] = $id;
1581
				continue;
1582
			}
1583
1584
			switch ( $type ) {
1585
			case 'email' :
1586
			case 'telephone' :
1587
			case 'name' :
1588
			case 'url' :
1589
			case 'subject' :
1590
			case 'textarea' :
1591
				$field_ids[$type] = $id;
1592
				break;
1593
			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...
1594
				// Put everything else in extra
1595
				$field_ids['extra'][] = $id;
1596
			}
1597
		}
1598
1599
		return $field_ids;
1600
	}
1601
1602
	/**
1603
	 * Process the contact form's POST submission
1604
	 * Stores feedback.  Sends email.
1605
	 */
1606
	function process_submission() {
1607
		global $post;
1608
1609
		$plugin = Grunion_Contact_Form_Plugin::init();
1610
1611
		$id     = $this->get_attribute( 'id' );
1612
		$to     = $this->get_attribute( 'to' );
1613
		$widget = $this->get_attribute( 'widget' );
1614
1615
		$contact_form_subject = $this->get_attribute( 'subject' );
1616
1617
		$to = str_replace( ' ', '', $to );
1618
		$emails = explode( ',', $to );
1619
1620
		$valid_emails = array();
1621
1622
		foreach ( (array) $emails as $email ) {
1623
			if ( !is_email( $email ) ) {
1624
				continue;
1625
			}
1626
1627
			if ( function_exists( 'is_email_address_unsafe' ) && is_email_address_unsafe( $email ) ) {
1628
				continue;
1629
			}
1630
1631
			$valid_emails[] = $email;
1632
		}
1633
1634
		// No one to send it to, which means none of the "to" attributes are valid emails.
1635
		// Use default email instead.
1636
		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...
1637
			$valid_emails = $this->defaults['to'];
1638
		}
1639
1640
		$to = $valid_emails;
1641
1642
		// Last ditch effort to set a recipient if somehow none have been set.
1643
		if ( empty( $to ) ) {
1644
			$to = get_option( 'admin_email' );
1645
		}
1646
1647
		// Make sure we're processing the form we think we're processing... probably a redundant check.
1648
		if ( $widget ) {
1649
			if ( 'widget-' . $widget != $_POST['contact-form-id'] ) {
1650
				return false;
1651
			}
1652
		} else {
1653
			if ( $post->ID != $_POST['contact-form-id'] ) {
1654
				return false;
1655
			}
1656
		}
1657
1658
		$field_ids = $this->get_field_ids();
1659
1660
		// Initialize all these "standard" fields to null
1661
		$comment_author_email = $comment_author_email_label = // v
1662
		$comment_author       = $comment_author_label       = // v
1663
		$comment_author_url   = $comment_author_url_label   = // v
1664
		$comment_content      = $comment_content_label      = null;
1665
1666
		// For each of the "standard" fields, grab their field label and value.
1667
1668 View Code Duplication
		if ( isset( $field_ids['name'] ) ) {
1669
			$field = $this->fields[$field_ids['name']];
1670
			$comment_author = Grunion_Contact_Form_Plugin::strip_tags(
1671
				stripslashes(
1672
					/** This filter is already documented in core/wp-includes/comment-functions.php */
1673
					apply_filters( 'pre_comment_author_name', addslashes( $field->value ) )
1674
				)
1675
			);
1676
			$comment_author_label = Grunion_Contact_Form_Plugin::strip_tags( $field->get_attribute( 'label' ) );
1677
		}
1678
1679 View Code Duplication
		if ( isset( $field_ids['email'] ) ) {
1680
			$field = $this->fields[$field_ids['email']];
1681
			$comment_author_email = Grunion_Contact_Form_Plugin::strip_tags(
1682
				stripslashes(
1683
					/** This filter is already documented in core/wp-includes/comment-functions.php */
1684
					apply_filters( 'pre_comment_author_email', addslashes( $field->value ) )
1685
				)
1686
			);
1687
			$comment_author_email_label = Grunion_Contact_Form_Plugin::strip_tags( $field->get_attribute( 'label' ) );
1688
		}
1689
1690
		if ( isset( $field_ids['url'] ) ) {
1691
			$field = $this->fields[$field_ids['url']];
1692
			$comment_author_url = Grunion_Contact_Form_Plugin::strip_tags(
1693
				stripslashes(
1694
					/** This filter is already documented in core/wp-includes/comment-functions.php */
1695
					apply_filters( 'pre_comment_author_url', addslashes( $field->value ) )
1696
				)
1697
			);
1698
			if ( 'http://' == $comment_author_url ) {
1699
				$comment_author_url = '';
1700
			}
1701
			$comment_author_url_label = Grunion_Contact_Form_Plugin::strip_tags( $field->get_attribute( 'label' ) );
1702
		}
1703
1704
		if ( isset( $field_ids['textarea'] ) ) {
1705
			$field = $this->fields[$field_ids['textarea']];
1706
			$comment_content = trim( Grunion_Contact_Form_Plugin::strip_tags( $field->value ) );
1707
			$comment_content_label = Grunion_Contact_Form_Plugin::strip_tags( $field->get_attribute( 'label' ) );
1708
		}
1709
1710
		if ( isset( $field_ids['subject'] ) ) {
1711
			$field = $this->fields[$field_ids['subject']];
1712
			if ( $field->value ) {
1713
				$contact_form_subject = Grunion_Contact_Form_Plugin::strip_tags( $field->value );
1714
			}
1715
		}
1716
1717
		$all_values = $extra_values = array();
1718
		$i = 1; // Prefix counter for stored metadata
1719
1720
		// For all fields, grab label and value
1721
		foreach ( $field_ids['all'] as $field_id ) {
1722
			$field = $this->fields[$field_id];
1723
			$label = $i . '_' . $field->get_attribute( 'label' );
1724
			$value = $field->value;
1725
1726
			$all_values[$label] = $value;
1727
			$i++; // Increment prefix counter for the next field
1728
		}
1729
1730
		// For the "non-standard" fields, grab label and value
1731
		// Extra fields have their prefix starting from count( $all_values ) + 1
1732
		foreach ( $field_ids['extra'] as $field_id ) {
1733
			$field = $this->fields[$field_id];
1734
			$label = $i . '_' . $field->get_attribute( 'label' );
1735
			$value = $field->value;
1736
1737
			if ( is_array( $value ) ) {
1738
				$value = implode( ', ', $value );
1739
			}
1740
1741
			$extra_values[$label] = $value;
1742
			$i++; // Increment prefix counter for the next extra field
1743
		}
1744
1745
		$contact_form_subject = trim( $contact_form_subject );
1746
1747
		$comment_author_IP = Grunion_Contact_Form_Plugin::get_ip_address();
1748
1749
		$vars = array( 'comment_author', 'comment_author_email', 'comment_author_url', 'contact_form_subject', 'comment_author_IP' );
1750
		foreach ( $vars as $var )
1751
			$$var = str_replace( array( "\n", "\r" ), '', $$var );
1752
1753
		// Ensure that Akismet gets all of the relevant information from the contact form,
1754
		// not just the textarea field and predetermined subject.
1755
		$akismet_vars = compact( $vars );
1756
		$akismet_vars['comment_content'] = $comment_content;
1757
1758
		foreach ( array_merge( $field_ids['all'], $field_ids['extra'] ) as $field_id ) {
1759
			$field = $this->fields[$field_id];
1760
1761
			// Normalize the label into a slug.
1762
			$field_slug = trim( // Strip all leading/trailing dashes.
1763
				preg_replace(   // Normalize everything to a-z0-9_-
1764
					'/[^a-z0-9_]+/',
1765
					'-',
1766
					strtolower( $field->get_attribute( 'label' ) ) // Lowercase
1767
				),
1768
				'-'
1769
			);
1770
1771
			$field_value = ( is_array( $field->value ) ) ? trim( implode( ', ', $field->value ) ) : trim( $field->value );
1772
1773
			// Skip any values that are already in the array we're sending.
1774
			if ( $field_value && in_array( $field_value, $akismet_vars ) ) {
1775
				continue;
1776
			}
1777
1778
			$akismet_vars[ 'contact_form_field_' . $field_slug ] = $field_value;
1779
		}
1780
1781
		$spam = '';
1782
		$akismet_values = $plugin->prepare_for_akismet( $akismet_vars );
1783
1784
		// Is it spam?
1785
		/** This filter is already documented in modules/contact-form/admin.php */
1786
		$is_spam = apply_filters( 'jetpack_contact_form_is_spam', false, $akismet_values );
1787
		if ( is_wp_error( $is_spam ) ) // WP_Error to abort
1788
			return $is_spam; // abort
1789
		elseif ( $is_spam === TRUE )  // TRUE to flag a spam
1790
			$spam = '***SPAM*** ';
1791
1792
		if ( !$comment_author )
1793
			$comment_author = $comment_author_email;
1794
1795
		/**
1796
		 * Filter the email where a submitted feedback is sent.
1797
		 *
1798
		 * @module contact-form
1799
		 *
1800
		 * @since 1.3.1
1801
		 *
1802
		 * @param string|array $to Array of valid email addresses, or single email address.
1803
		 */
1804
		$to = (array) apply_filters( 'contact_form_to', $to );
1805
		foreach ( $to as $to_key => $to_value ) {
1806
			$to[$to_key] = Grunion_Contact_Form_Plugin::strip_tags( $to_value );
1807
		}
1808
1809
		$blog_url = parse_url( site_url() );
1810
		$from_email_addr = 'wordpress@' . $blog_url['host'];
1811
1812
		$reply_to_addr = $to[0];
1813
		if ( ! empty( $comment_author_email ) ) {
1814
			$reply_to_addr = $comment_author_email;
1815
		}
1816
1817
		$headers =  'From: "' . $comment_author  .'" <' . $from_email_addr  . ">\r\n" .
1818
					'Reply-To: "' . $comment_author . '" <' . $reply_to_addr  . ">\r\n" .
1819
					"Content-Type: text/html; charset=\"" . get_option('blog_charset') . "\"";
1820
1821
		/** This filter is already documented in modules/contact-form/admin.php */
1822
		$subject = apply_filters( 'contact_form_subject', $contact_form_subject, $all_values );
1823
		$url     = $widget ? home_url( '/' ) : get_permalink( $post->ID );
1824
1825
		$date_time_format = _x( '%1$s \a\t %2$s', '{$date_format} \a\t {$time_format}', 'jetpack' );
1826
		$date_time_format = sprintf( $date_time_format, get_option( 'date_format' ), get_option( 'time_format' ) );
1827
		$time = date_i18n( $date_time_format, current_time( 'timestamp' ) );
1828
1829
		// keep a copy of the feedback as a custom post type
1830
		$feedback_time   = current_time( 'mysql' );
1831
		$feedback_title  = "{$comment_author} - {$feedback_time}";
1832
		$feedback_status = $is_spam === TRUE ? 'spam' : 'publish';
1833
1834
		foreach ( (array) $akismet_values as $av_key => $av_value ) {
1835
			$akismet_values[$av_key] = Grunion_Contact_Form_Plugin::strip_tags( $av_value );
1836
		}
1837
1838
		foreach ( (array) $all_values as $all_key => $all_value ) {
1839
			$all_values[$all_key] = Grunion_Contact_Form_Plugin::strip_tags( $all_value );
1840
		}
1841
1842
		foreach ( (array) $extra_values as $ev_key => $ev_value ) {
1843
			$extra_values[$ev_key] = Grunion_Contact_Form_Plugin::strip_tags( $ev_value );
1844
		}
1845
1846
		/* We need to make sure that the post author is always zero for contact
1847
		 * form submissions.  This prevents export/import from trying to create
1848
		 * new users based on form submissions from people who were logged in
1849
		 * at the time.
1850
		 *
1851
		 * Unfortunately wp_insert_post() tries very hard to make sure the post
1852
		 * author gets the currently logged in user id.  That is how we ended up
1853
		 * with this work around. */
1854
		add_filter( 'wp_insert_post_data', array( $plugin, 'insert_feedback_filter' ), 10, 2 );
1855
1856
		$post_id = wp_insert_post( array(
1857
			'post_date'    => addslashes( $feedback_time ),
1858
			'post_type'    => 'feedback',
1859
			'post_status'  => addslashes( $feedback_status ),
1860
			'post_parent'  => (int) $post->ID,
1861
			'post_title'   => addslashes( wp_kses( $feedback_title, array() ) ),
1862
			'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
1863
			'post_name'    => md5( $feedback_title ),
1864
		) );
1865
1866
		// once insert has finished we don't need this filter any more
1867
		remove_filter( 'wp_insert_post_data', array( $plugin, 'insert_feedback_filter' ), 10, 2 );
1868
1869
		update_post_meta( $post_id, '_feedback_extra_fields', $this->addslashes_deep( $extra_values ) );
1870
		update_post_meta( $post_id, '_feedback_akismet_values', $this->addslashes_deep( $akismet_values ) );
1871
1872
		$message = self::get_compiled_form( $post_id, $this );
1873
1874
		array_push(
1875
			$message,
1876
			"", // Empty line left intentionally
1877
			__( 'Time:', 'jetpack' ) . ' ' . $time . '<br />',
1878
			__( 'IP Address:', 'jetpack' ) . ' ' . $comment_author_IP . '<br />',
1879
			__( 'Contact Form URL:', 'jetpack' ) . " " . $url . '<br />'
1880
		);
1881
1882
		if ( is_user_logged_in() ) {
1883
			array_push(
1884
				$message,
1885
				"",
1886
				sprintf(
1887
					__( 'Sent by a verified %s user.', 'jetpack' ),
1888
					isset( $GLOBALS['current_site']->site_name ) && $GLOBALS['current_site']->site_name ?
1889
						$GLOBALS['current_site']->site_name : '"' . get_option( 'blogname' ) . '"'
1890
				)
1891
			);
1892
		} else {
1893
			array_push( $message, __( 'Sent by an unverified visitor to your site.', 'jetpack' ) );
1894
		}
1895
1896
		$message = join( $message, "" );
1897
		/**
1898
		 * Filters the message sent via email after a successfull form submission.
1899
		 *
1900
		 * @module contact-form
1901
		 *
1902
		 * @since 1.3.1
1903
		 *
1904
		 * @param string $message Feedback email message.
1905
		 */
1906
		$message = apply_filters( 'contact_form_message', $message );
1907
1908
		update_post_meta( $post_id, '_feedback_email', $this->addslashes_deep( compact( 'to', 'message' ) ) );
1909
1910
		/**
1911
		 * Fires right before the contact form message is sent via email to
1912
		 * the recipient specified in the contact form.
1913
		 *
1914
		 * @module contact-form
1915
		 *
1916
		 * @since 1.3.1
1917
		 *
1918
		 * @param integer $post_id Post contact form lives on
1919
		 * @param array $all_values Contact form fields
1920
		 * @param array $extra_values Contact form fields not included in $all_values
1921
		 */
1922
		do_action( 'grunion_pre_message_sent', $post_id, $all_values, $extra_values );
1923
1924
		// schedule deletes of old spam feedbacks
1925
		if ( ! wp_next_scheduled( 'grunion_scheduled_delete' ) ) {
1926
			wp_schedule_event( time() + 250, 'daily', 'grunion_scheduled_delete' );
1927
		}
1928
1929
		if (
1930
			$is_spam !== TRUE &&
1931
			/**
1932
			 * Filter to choose whether an email should be sent after each successfull contact form submission.
1933
			 *
1934
			 * @module contact-form
1935
			 *
1936
			 * @since 2.6.0
1937
			 *
1938
			 * @param bool true Should an email be sent after a form submission. Default to true.
1939
			 * @param int $post_id Post ID.
1940
			 */
1941
			true === apply_filters( 'grunion_should_send_email', true, $post_id )
1942
		) {
1943
			wp_mail( $to, "{$spam}{$subject}", $message, $headers );
1944
		} elseif (
1945
			true === $is_spam &&
1946
			/**
1947
			 * Choose whether an email should be sent for each spam contact form submission.
1948
			 *
1949
			 * @module contact-form
1950
			 *
1951
			 * @since 1.3.1
1952
			 *
1953
			 * @param bool false Should an email be sent after a spam form submission. Default to false.
1954
			 */
1955
			apply_filters( 'grunion_still_email_spam', FALSE ) == TRUE
1956
		) { // don't send spam by default.  Filterable.
1957
			wp_mail( $to, "{$spam}{$subject}", $message, $headers );
1958
		}
1959
1960
		if ( defined( 'DOING_AJAX' ) && DOING_AJAX ) {
1961
			return self::success_message( $post_id, $this );
1962
		}
1963
1964
		$redirect = wp_get_referer();
1965
		if ( !$redirect ) { // wp_get_referer() returns false if the referer is the same as the current page
1966
			$redirect = $_SERVER['REQUEST_URI'];
1967
		}
1968
1969
		$redirect = add_query_arg( urlencode_deep( array(
1970
			'contact-form-id'   => $id,
1971
			'contact-form-sent' => $post_id,
1972
			'_wpnonce'          => wp_create_nonce( "contact-form-sent-{$post_id}" ), // wp_nonce_url HTMLencodes :(
1973
		) ), $redirect );
1974
1975
		/**
1976
		 * Filter the URL where the reader is redirected after submitting a form.
1977
		 *
1978
		 * @module contact-form
1979
		 *
1980
		 * @since 1.9.0
1981
		 *
1982
		 * @param string $redirect Post submission URL.
1983
		 * @param int $id Contact Form ID.
1984
		 * @param int $post_id Post ID.
1985
		 */
1986
		$redirect = apply_filters( 'grunion_contact_form_redirect_url', $redirect, $id, $post_id );
1987
1988
		wp_safe_redirect( $redirect );
1989
		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...
1990
	}
1991
1992
	function addslashes_deep( $value ) {
1993
		if ( is_array( $value ) ) {
1994
			return array_map( array( $this, 'addslashes_deep' ), $value );
1995
		} elseif ( is_object( $value ) ) {
1996
			$vars = get_object_vars( $value );
1997
			foreach ( $vars as $key => $data ) {
1998
				$value->{$key} = $this->addslashes_deep( $data );
1999
			}
2000
			return $value;
2001
		}
2002
2003
		return addslashes( $value );
2004
	}
2005
}
2006
2007
/**
2008
 * Class for the contact-field shortcode.
2009
 * Parses shortcode to output the contact form field as HTML.
2010
 * Validates input.
2011
 */
2012
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...
2013
	public $shortcode_name = 'contact-field';
2014
2015
	/**
2016
	 * @var Grunion_Contact_Form parent form
2017
	 */
2018
	public $form;
2019
2020
	/**
2021
	 * @var string default or POSTed value
2022
	 */
2023
	public $value;
2024
2025
	/**
2026
	 * @var bool Is the input invalid?
2027
	 */
2028
	public $error = false;
2029
2030
	/**
2031
	 * @param array $attributes An associative array of shortcode attributes.  @see shortcode_atts()
2032
	 * @param null|string $content Null for selfclosing shortcodes.  The inner content otherwise.
2033
	 * @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...
2034
	 */
2035
	function __construct( $attributes, $content = null, $form = null ) {
2036
		$attributes = shortcode_atts( array(
2037
			'label'       => null,
2038
			'type'        => 'text',
2039
			'required'    => false,
2040
			'options'     => array(),
2041
			'id'          => null,
2042
			'default'     => null,
2043
			'placeholder' => null,
2044
			'class'       => null,
2045
		), $attributes, 'contact-field' );
2046
2047
		// special default for subject field
2048
		if ( 'subject' == $attributes['type'] && is_null( $attributes['default'] ) && !is_null( $form ) ) {
2049
			$attributes['default'] = $form->get_attribute( 'subject' );
2050
		}
2051
2052
		// allow required=1 or required=true
2053
		if ( '1' == $attributes['required'] || 'true' == strtolower( $attributes['required'] ) )
2054
			$attributes['required'] = true;
2055
		else
2056
			$attributes['required'] = false;
2057
2058
		// parse out comma-separated options list (for selects, radios, and checkbox-multiples)
2059
		if ( !empty( $attributes['options'] ) && is_string( $attributes['options'] ) ) {
2060
			$attributes['options'] = array_map( 'trim', explode( ',', $attributes['options'] ) );
2061
		}
2062
2063
		if ( $form ) {
2064
			// make a unique field ID based on the label, with an incrementing number if needed to avoid clashes
2065
			$form_id = $form->get_attribute( 'id' );
2066
			$id = isset( $attributes['id'] ) ? $attributes['id'] : false;
2067
2068
			$unescaped_label = $this->unesc_attr( $attributes['label'] );
2069
			$unescaped_label = str_replace( '%', '-', $unescaped_label ); // jQuery doesn't like % in IDs?
2070
			$unescaped_label = preg_replace( '/[^a-zA-Z0-9.-_:]/', '', $unescaped_label );
2071
2072
			if ( empty( $id ) ) {
2073
				$id = sanitize_title_with_dashes( 'g' . $form_id . '-' . $unescaped_label );
2074
				$i = 0;
2075
				$max_tries = 99;
2076
				while ( isset( $form->fields[$id] ) ) {
2077
					$i++;
2078
					$id = sanitize_title_with_dashes( 'g' . $form_id . '-' . $unescaped_label . '-' . $i );
2079
2080
					if ( $i > $max_tries ) {
2081
						break;
2082
					}
2083
				}
2084
			}
2085
2086
			$attributes['id'] = $id;
2087
		}
2088
2089
		parent::__construct( $attributes, $content );
2090
2091
		// Store parent form
2092
		$this->form = $form;
2093
	}
2094
2095
	/**
2096
	 * This field's input is invalid.  Flag as invalid and add an error to the parent form
2097
	 *
2098
	 * @param string $message The error message to display on the form.
2099
	 */
2100
	function add_error( $message ) {
2101
		$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...
2102
2103
		if ( !is_wp_error( $this->form->errors ) ) {
2104
			$this->form->errors = new WP_Error;
2105
		}
2106
2107
		$this->form->errors->add( $this->get_attribute( 'id' ), $message );
2108
	}
2109
2110
	/**
2111
	 * Is the field input invalid?
2112
	 *
2113
	 * @see $error
2114
	 *
2115
	 * @return bool
2116
	 */
2117
	function is_error() {
2118
		return $this->error;
2119
	}
2120
2121
	/**
2122
	 * Validates the form input
2123
	 */
2124
	function validate() {
2125
		// If it's not required, there's nothing to validate
2126
		if ( !$this->get_attribute( 'required' ) ) {
2127
			return;
2128
		}
2129
2130
		$field_id    = $this->get_attribute( 'id' );
2131
		$field_type  = $this->get_attribute( 'type' );
2132
		$field_label = $this->get_attribute( 'label' );
2133
2134
		if ( isset( $_POST[ $field_id ] ) ) {
2135
			if ( is_array( $_POST[ $field_id ] ) ) {
2136
				$field_value = array_map( 'stripslashes', $_POST[ $field_id ] );
2137
			} else {
2138
				$field_value = stripslashes( $_POST[ $field_id ] );
2139
			}
2140
		} else {
2141
			$field_value = '';
2142
		}
2143
2144
		switch ( $field_type ) {
2145
		case 'email' :
2146
			// Make sure the email address is valid
2147
			if ( !is_email( $field_value ) ) {
2148
				$this->add_error( sprintf( __( '%s requires a valid email address', 'jetpack' ), $field_label ) );
2149
			}
2150
			break;
2151
		case 'checkbox-multiple' :
2152
			// Check that there is at least one option selected
2153
			if ( empty( $field_value ) ) {
2154
				$this->add_error( sprintf( __( '%s requires at least one selection', 'jetpack' ), $field_label ) );
2155
			}
2156
			break;
2157
		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...
2158
			// Just check for presence of any text
2159
			if ( !strlen( trim( $field_value ) ) ) {
2160
				$this->add_error( sprintf( __( '%s is required', 'jetpack' ), $field_label ) );
2161
			}
2162
		}
2163
	}
2164
2165
	/**
2166
	 * Outputs the HTML for this form field
2167
	 *
2168
	 * @return string HTML
2169
	 */
2170
	function render() {
2171
		global $current_user, $user_identity;
2172
2173
		$r = '';
2174
2175
		$field_id          = $this->get_attribute( 'id' );
2176
		$field_type        = $this->get_attribute( 'type' );
2177
		$field_label       = $this->get_attribute( 'label' );
2178
		$field_required    = $this->get_attribute( 'required' );
2179
		$placeholder       = $this->get_attribute( 'placeholder' );
2180
		$class             = $this->get_attribute( 'class' );
2181
		$field_placeholder = ( ! empty( $placeholder ) ) ? "placeholder='" . esc_attr( $placeholder ) . "'" : '';
2182
		$field_class       = "class='" . trim( esc_attr( $field_type ) . " " . esc_attr( $class ) ) . "' ";
2183
2184
		if ( isset( $_POST[ $field_id ] ) ) {
2185
			if ( is_array( $_POST[ $field_id ] ) ) {
2186
				$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...
2187
			} else {
2188
				$this->value = stripslashes( (string) $_POST[ $field_id ] );
2189
			}
2190
		} elseif ( isset( $_GET[ $field_id ] ) ) {
2191
			$this->value = stripslashes( (string) $_GET[ $field_id ] );
2192
		} elseif (
2193
			is_user_logged_in() &&
2194
			( ( defined( 'IS_WPCOM' ) && IS_WPCOM ) ||
2195
			/**
2196
			 * Allow third-party tools to prefill the contact form with the user's details when they're logged in.
2197
			 *
2198
			 * @module contact-form
2199
			 *
2200
			 * @since 3.2.0
2201
			 *
2202
			 * @param bool false Should the Contact Form be prefilled with your details when you're logged in. Default to false.
2203
			 */
2204
			true === apply_filters( 'jetpack_auto_fill_logged_in_user', false )
2205
			)
2206
		) {
2207
			// Special defaults for logged-in users
2208
			switch ( $this->get_attribute( 'type' ) ) {
2209
			case 'email' :
2210
				$this->value = $current_user->data->user_email;
2211
				break;
2212
			case 'name' :
2213
				$this->value = $user_identity;
2214
				break;
2215
			case 'url' :
2216
				$this->value = $current_user->data->user_url;
2217
				break;
2218
			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...
2219
				$this->value = $this->get_attribute( 'default' );
2220
			}
2221
		} else {
2222
			$this->value = $this->get_attribute( 'default' );
2223
		}
2224
2225
		$field_value = Grunion_Contact_Form_Plugin::strip_tags( $this->value );
2226
		$field_label = Grunion_Contact_Form_Plugin::strip_tags( $field_label );
2227
2228
		/**
2229
		 * Filter the Contact Form required field text
2230
		 *
2231
		 * @module contact-form
2232
		 *
2233
		 * @since 3.8.0
2234
		 *
2235
		 * @param string $var Required field text. Default is "(required)".
2236
		 */
2237
		$required_field_text = esc_html( apply_filters( 'jetpack_required_field_text', __( "(required)", 'jetpack' ) ) );
2238
2239
		switch ( $field_type ) {
2240 View Code Duplication
		case 'email' :
2241
			$r .= "\n<div>\n";
2242
			$r .= "\t\t<label for='" . esc_attr( $field_id ) . "' class='grunion-field-label email" . ( $this->is_error() ? ' form-error' : '' ) . "'>" . esc_html( $field_label ) . ( $field_required ? '<span>' . $required_field_text . '</span>' : '' ) . "</label>\n";
2243
			$r .= "\t\t<input type='email' name='" . esc_attr( $field_id ) . "' id='" . esc_attr( $field_id ) . "' value='" . esc_attr( $field_value ) . "' " . $field_class . $field_placeholder . " " . ( $field_required ? "required aria-required='true'" : "" ) . "/>\n";
2244
			$r .= "\t</div>\n";
2245
			break;
2246
		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...
2247
			$r .= "\n<div>\n";
2248
			$r .= "\t\t<label for='" . esc_attr( $field_id ) . "' class='grunion-field-label telephone" . ( $this->is_error() ? ' form-error' : '' ) . "'>" . esc_html( $field_label ) . ( $field_required ? '<span>' . $required_field_text . '</span>' : '' ) . "</label>\n";
2249
			$r .= "\t\t<input type='tel' name='" . esc_attr( $field_id ) . "' id='" . esc_attr( $field_id ) . "' value='" . esc_attr( $field_value ) . "' " . $field_class . $field_placeholder . "/>\n";
2250 View Code Duplication
		case 'textarea' :
2251
			$r .= "\n<div>\n";
2252
			$r .= "\t\t<label for='contact-form-comment-" . esc_attr( $field_id ) . "' class='grunion-field-label textarea" . ( $this->is_error() ? ' form-error' : '' ) . "'>" . esc_html( $field_label ) . ( $field_required ? '<span>' . $required_field_text . '</span>' : '' ) . "</label>\n";
2253
			$r .= "\t\t<textarea name='" . esc_attr( $field_id ) . "' id='contact-form-comment-" . esc_attr( $field_id ) . "' rows='20' " . $field_class . $field_placeholder . " " . ( $field_required ? "required aria-required='true'" : "" ) . ">" . esc_textarea( $field_value ) . "</textarea>\n";
2254
			$r .= "\t</div>\n";
2255
			break;
2256 View Code Duplication
		case 'radio' :
2257
			$r .= "\t<div><label class='grunion-field-label" . ( $this->is_error() ? ' form-error' : '' ) . "'>" . esc_html( $field_label ) . ( $field_required ? '<span>' . $required_field_text . '</span>' : '' ) . "</label>\n";
2258
			foreach ( $this->get_attribute( 'options' ) as $option ) {
2259
				$option = Grunion_Contact_Form_Plugin::strip_tags( $option );
2260
				$r .= "\t\t<label class='grunion-radio-label radio" . ( $this->is_error() ? ' form-error' : '' ) . "'>";
2261
				$r .= "<input type='radio' name='" . esc_attr( $field_id ) . "' value='" . esc_attr( $option ) . "' " . $field_class . checked( $option, $field_value, false ) . " " . ( $field_required ? "required aria-required='true'" : "" ) . "/> ";
2262
				$r .= esc_html( $option ) . "</label>\n";
2263
				$r .= "\t\t<div class='clear-form'></div>\n";
2264
			}
2265
			$r .= "\t\t</div>\n";
2266
			break;
2267
		case 'checkbox' :
2268
			$r .= "\t<div>\n";
2269
			$r .= "\t\t<label class='grunion-field-label checkbox" . ( $this->is_error() ? ' form-error' : '' ) . "'>\n";
2270
			$r .= "\t\t<input type='checkbox' name='" . esc_attr( $field_id ) . "' value='" . esc_attr__( 'Yes', 'jetpack' ) . "' " . $field_class . checked( (bool) $field_value, true, false ) . " " . ( $field_required ? "required aria-required='true'" : "" ) . "/> \n";
2271
			$r .= "\t\t" . esc_html( $field_label ) . ( $field_required ? '<span>'. $required_field_text . '</span>' : '' ) . "</label>\n";
2272
			$r .= "\t\t<div class='clear-form'></div>\n";
2273
			$r .= "\t</div>\n";
2274
			break;
2275
		case 'checkbox-multiple' :
2276
			$r .= "\t<div><label class='grunion-field-label" . ( $this->is_error() ? ' form-error' : '' ) . "'>" . esc_html( $field_label ) . ( $field_required ? '<span>' . $required_field_text . '</span>' : '' ) . "</label>\n";
2277
			foreach ( $this->get_attribute( 'options' ) as $option ) {
2278
				$option = Grunion_Contact_Form_Plugin::strip_tags( $option );
2279
				$r .= "\t\t<label class='grunion-checkbox-multiple-label checkbox-multiple" . ( $this->is_error() ? ' form-error' : '' ) . "'>";
2280
				$r .= "<input type='checkbox' name='" . esc_attr( $field_id ) . "[]' value='" . esc_attr( $option ) . "' " . $field_class . checked( in_array( $option, (array) $field_value ), true, false ) . " /> ";
2281
				$r .= esc_html( $option ) . "</label>\n";
2282
				$r .= "\t\t<div class='clear-form'></div>\n";
2283
			}
2284
			$r .= "\t\t</div>\n";
2285
			break;
2286 View Code Duplication
		case 'select' :
2287
			$r .= "\n<div>\n";
2288
			$r .= "\t\t<label for='" . esc_attr( $field_id ) . "' class='grunion-field-label select" . ( $this->is_error() ? ' form-error' : '' ) . "'>" . esc_html( $field_label ) . ( $field_required ? '<span>'. $required_field_text . '</span>' : '' ) . "</label>\n";
2289
			$r .= "\t<select name='" . esc_attr( $field_id ) . "' id='" . esc_attr( $field_id ) . "' " . $field_class . ( $field_required ? "required aria-required='true'" : "" ) . ">\n";
2290
			foreach ( $this->get_attribute( 'options' ) as $option ) {
2291
				$option = Grunion_Contact_Form_Plugin::strip_tags( $option );
2292
				$r .= "\t\t<option" . selected( $option, $field_value, false ) . ">" . esc_html( $option ) . "</option>\n";
2293
			}
2294
			$r .= "\t</select>\n";
2295
			$r .= "\t</div>\n";
2296
			break;
2297
		case 'date' :
2298
			$r .= "\n<div>\n";
2299
			$r .= "\t\t<label for='" . esc_attr( $field_id ) . "' class='grunion-field-label " . esc_attr( $field_type ) . ( $this->is_error() ? ' form-error' : '' ) . "'>" . esc_html( $field_label ) . ( $field_required ? '<span>' . $required_field_text . '</span>' : '' ) . "</label>\n";
2300
			$r .= "\t\t<input type='date' name='" . esc_attr( $field_id ) . "' id='" . esc_attr( $field_id ) . "' value='" . esc_attr( $field_value ) . "' " . $field_class . ( $field_required ? "required aria-required='true'" : "" ) . "/>\n";
2301
			$r .= "\t</div>\n";
2302
2303
			wp_enqueue_script( 'grunion-frontend', plugins_url( 'js/grunion-frontend.js', __FILE__ ), array( 'jquery', 'jquery-ui-datepicker' ) );
2304
			break;
2305 View Code Duplication
		default : // text field
0 ignored issues
show
Coding Style introduced by
There must be no space before the colon in a DEFAULT statement

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

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

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

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

Loading history...
2306
			// note that any unknown types will produce a text input, so we can use arbitrary type names to handle
2307
			// input fields like name, email, url that require special validation or handling at POST
2308
			$r .= "\n<div>\n";
2309
			$r .= "\t\t<label for='" . esc_attr( $field_id ) . "' class='grunion-field-label " . esc_attr( $field_type ) . ( $this->is_error() ? ' form-error' : '' ) . "'>" . esc_html( $field_label ) . ( $field_required ? '<span>' . $required_field_text . '</span>' : '' ) . "</label>\n";
2310
			$r .= "\t\t<input type='text' name='" . esc_attr( $field_id ) . "' id='" . esc_attr( $field_id ) . "' value='" . esc_attr( $field_value ) . "' " . $field_class . $field_placeholder . " " . ( $field_required ? "required aria-required='true'" : "" ) . "/>\n";
2311
			$r .= "\t</div>\n";
2312
		}
2313
2314
		/**
2315
		 * Filter the HTML of the Contact Form.
2316
		 *
2317
		 * @module contact-form
2318
		 *
2319
		 * @since 2.6.0
2320
		 *
2321
		 * @param string $r Contact Form HTML output.
2322
		 * @param string $field_label Field label.
2323
		 * @param int|null $id Post ID.
2324
		 */
2325
		return apply_filters( 'grunion_contact_form_field_html', $r, $field_label, ( in_the_loop() ? get_the_ID() : null ) );
2326
	}
2327
}
2328
2329
add_action( 'init', array( 'Grunion_Contact_Form_Plugin', 'init' ) );
2330
2331
add_action( 'grunion_scheduled_delete', 'grunion_delete_old_spam' );
2332
2333
/**
2334
 * Deletes old spam feedbacks to keep the posts table size under control
2335
 */
2336
function grunion_delete_old_spam() {
2337
	global $wpdb;
2338
2339
	$grunion_delete_limit = 100;
2340
2341
	$now_gmt = current_time( 'mysql', 1 );
2342
	$sql = $wpdb->prepare( "
2343
		SELECT `ID`
2344
		FROM $wpdb->posts
2345
		WHERE DATE_SUB( %s, INTERVAL 15 DAY ) > `post_date_gmt`
2346
			AND `post_type` = 'feedback'
2347
			AND `post_status` = 'spam'
2348
		LIMIT %d
2349
	", $now_gmt, $grunion_delete_limit );
2350
	$post_ids = $wpdb->get_col( $sql );
2351
2352
	foreach ( (array) $post_ids as $post_id ) {
2353
		# force a full delete, skip the trash
2354
		wp_delete_post( $post_id, TRUE );
2355
	}
2356
2357
	# Arbitrary check points for running OPTIMIZE
2358
	# nothing special about 5000 or 11
2359
	# just trying to periodically recover deleted rows
2360
	$random_num = mt_rand( 1, 5000 );
2361
	if (
2362
		/**
2363
		 * Filter how often the module run OPTIMIZE TABLE on the core WP tables.
2364
		 *
2365
		 * @module contact-form
2366
		 *
2367
		 * @since 1.3.1
2368
		 *
2369
		 * @param int $random_num Random number.
2370
		 */
2371
		apply_filters( 'grunion_optimize_table', ( $random_num == 11 ) )
2372
	) {
2373
		$wpdb->query( "OPTIMIZE TABLE $wpdb->posts" );
2374
	}
2375
2376
	# if we hit the max then schedule another run
2377
	if ( count( $post_ids ) >= $grunion_delete_limit ) {
2378
		wp_schedule_single_event( time() + 700, 'grunion_scheduled_delete' );
2379
	}
2380
}
2381