Completed
Push — master-stable ( 6129d5...f1df5b )
by
unknown
21:50 queued 12:42
created

Crunion_Contact_Form_Shortcode   A

Complexity

Total Complexity 22

Size/Duplication

Total Lines 162
Duplicated Lines 0 %

Coupling/Cohesion

Components 2
Dependencies 1
Metric Value
wmc 22
lcom 2
cbo 1
dl 0
loc 162
rs 10

6 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 15 3
A parse_content() 0 7 2
A get_attribute() 0 3 2
A esc_attr() 0 17 2
A unesc_attr() 0 14 2
C __toString() 0 51 11
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
			'show_in_rest'      => true,
111
			'capabilities'		=> array(
112
				'create_posts'        => false,
113
				'publish_posts'       => 'publish_pages',
114
				'edit_posts'          => 'edit_pages',
115
				'edit_others_posts'   => 'edit_others_pages',
116
				'delete_posts'        => 'delete_pages',
117
				'delete_others_posts' => 'delete_others_pages',
118
				'read_private_posts'  => 'read_private_pages',
119
				'edit_post'           => 'edit_page',
120
				'delete_post'         => 'delete_page',
121
				'read_post'           => 'read_page',
122
			),
123
			'map_meta_cap'		=> true,
124
		) );
125
126
		// Add to REST API post type whitelist
127
		add_filter( 'rest_api_allowed_post_types', array( $this, 'allow_feedback_rest_api_type' ) );
128
129
		// Add "spam" as a post status
130
		register_post_status( 'spam', array(
131
			'label'                  => 'Spam',
132
			'public'                 => FALSE,
133
			'exclude_from_search'    => TRUE,
134
			'show_in_admin_all_list' => FALSE,
135
			'label_count'            => _n_noop( 'Spam <span class="count">(%s)</span>', 'Spam <span class="count">(%s)</span>', 'jetpack' ),
136
			'protected'              => TRUE,
137
			'_builtin'               => FALSE
138
		) );
139
140
		// POST handler
141
		if (
142
			isset( $_SERVER['REQUEST_METHOD'] ) && 'POST' == strtoupper( $_SERVER['REQUEST_METHOD'] )
143
		&&
144
			isset( $_POST['action'] ) && 'grunion-contact-form' == $_POST['action']
145
		&&
146
			isset( $_POST['contact-form-id'] )
147
		) {
148
			add_action( 'template_redirect', array( $this, 'process_form_submission' ) );
149
		}
150
151
		/* Can be dequeued by placing the following in wp-content/themes/yourtheme/functions.php
152
		 *
153
		 * 	function remove_grunion_style() {
154
		 *		wp_deregister_style('grunion.css');
155
		 *	}
156
		 *	add_action('wp_print_styles', 'remove_grunion_style');
157
		 */
158
		if( is_rtl() ){
159
			wp_register_style( 'grunion.css', GRUNION_PLUGIN_URL . 'css/rtl/grunion-rtl.css', array(), JETPACK__VERSION );
160
		} else {
161
			wp_register_style( 'grunion.css', GRUNION_PLUGIN_URL . 'css/grunion.css', array(), JETPACK__VERSION );
162
		}
163
	}
164
165
	/**
166
	 * Add to REST API post type whitelist
167
	 */
168
	function allow_feedback_rest_api_type( $post_types ) {
169
		$post_types[] = 'feedback';
170
		return $post_types;
171
	}
172
173
	/**
174
	 * Handles all contact-form POST submissions
175
	 *
176
	 * Conditionally attached to `template_redirect`
177
	 */
178
	function process_form_submission() {
179
		// Add a filter to replace tokens in the subject field with sanitized field values
180
		add_filter( 'contact_form_subject', array( $this, 'replace_tokens_with_input' ), 10, 2 );
181
182
		$id = stripslashes( $_POST['contact-form-id'] );
183
184
		if ( is_user_logged_in() ) {
185
			check_admin_referer( "contact-form_{$id}" );
186
		}
187
188
		$is_widget = 0 === strpos( $id, 'widget-' );
189
190
		$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...
191
192
		if ( $is_widget ) {
193
			// It's a form embedded in a text widget
194
195
			$this->current_widget_id = substr( $id, 7 ); // remove "widget-"
196
			$widget_type = implode( '-', array_slice( explode( '-', $this->current_widget_id ), 0, -1 ) ); // Remove trailing -#
197
198
			// Is the widget active?
199
			$sidebar = is_active_widget( false, $this->current_widget_id, $widget_type );
200
201
			// This is lame - no core API for getting a widget by ID
202
			$widget = isset( $GLOBALS['wp_registered_widgets'][$this->current_widget_id] ) ? $GLOBALS['wp_registered_widgets'][$this->current_widget_id] : false;
203
204
			if ( $sidebar && $widget && isset( $widget['callback'] ) ) {
205
				// This is lamer - no API for outputting a given widget by ID
206
				ob_start();
207
				// Process the widget to populate Grunion_Contact_Form::$last
208
				call_user_func( $widget['callback'], array(), $widget['params'][0] );
209
				ob_end_clean();
210
			}
211
		} else {
212
			// It's a form embedded in a post
213
214
			$post = get_post( $id );
215
216
			// Process the content to populate Grunion_Contact_Form::$last
217
			/** This filter is already documented in core. wp-includes/post-template.php */
218
			apply_filters( 'the_content', $post->post_content );
219
		}
220
221
		$form = Grunion_Contact_Form::$last;
0 ignored issues
show
Bug introduced by
The property last cannot be accessed from this context as it is declared private in class Grunion_Contact_Form.

This check looks for access to properties that are not accessible from the current context.

If you need to make a property accessible to another context you can either raise its visibility level or provide an accessible getter in the defining class.

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