Completed
Push — fix/7003-mail-back-compat ( cbbe89...7f3a7b )
by
unknown
429:10 queued 402:02
created

Grunion_Contact_Form::parse()   D

Complexity

Conditions 21
Paths 52

Size

Total Lines 120
Code Lines 53

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 21
eloc 53
nc 52
nop 2
dl 0
loc 120
rs 4.6955
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
3
/*
4
Plugin Name: Grunion Contact Form
5
Description: Add a contact form to any post, page or text widget.  Emails will be sent to the post's author by default, or any email address you choose.  As seen on WordPress.com.
6
Plugin URI: http://automattic.com/#
7
AUthor: Automattic, Inc.
8
Author URI: http://automattic.com/
9
Version: 2.4
10
License: GPLv2 or later
11
*/
12
13
define( 'GRUNION_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
14
define( 'GRUNION_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
15
16
if ( is_admin() ) {
17
	require_once GRUNION_PLUGIN_DIR . 'admin.php';
18
}
19
20
add_action( 'rest_api_init', 'grunion_contact_form_require_endpoint' );
21
function grunion_contact_form_require_endpoint() {
22
	require_once GRUNION_PLUGIN_DIR . 'class-grunion-contact-form-endpoint.php';
23
}
24
25
/**
26
 * Sets up various actions, filters, post types, post statuses, shortcodes.
27
 */
28
class Grunion_Contact_Form_Plugin {
29
30
	/**
31
	 * @var string The Widget ID of the widget currently being processed.  Used to build the unique contact-form ID for forms embedded in widgets.
32
	 */
33
	public $current_widget_id;
34
35
	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...
36
37
	static function init() {
38
		static $instance = false;
39
40
		if ( ! $instance ) {
41
			$instance = new Grunion_Contact_Form_Plugin;
42
43
			// Schedule our daily cleanup
44
			add_action( 'wp_scheduled_delete', array( $instance, 'daily_akismet_meta_cleanup' ) );
45
		}
46
47
		return $instance;
48
	}
49
50
	/**
51
	 * Runs daily to clean up spam detection metadata after 15 days.  Keeps your DB squeaky clean.
52
	 */
53
	public function daily_akismet_meta_cleanup() {
54
		global $wpdb;
55
56
		$feedback_ids = $wpdb->get_col( "SELECT p.ID FROM {$wpdb->posts} as p INNER JOIN {$wpdb->postmeta} as m on m.post_id = p.ID WHERE p.post_type = 'feedback' AND m.meta_key = '_feedback_akismet_values' AND DATE_SUB(NOW(), INTERVAL 15 DAY) > p.post_date_gmt LIMIT 10000" );
57
58
		if ( empty( $feedback_ids ) ) {
59
			return;
60
		}
61
62
		foreach ( $feedback_ids as $feedback_id ) {
63
			delete_post_meta( $feedback_id, '_feedback_akismet_values' );
64
		}
65
	}
66
67
	/**
68
	 * Strips HTML tags from input.  Output is NOT HTML safe.
69
	 *
70
	 * @param mixed $data_with_tags
71
	 * @return mixed
72
	 */
73
	public static function strip_tags( $data_with_tags ) {
74
		if ( is_array( $data_with_tags ) ) {
75
			foreach ( $data_with_tags as $index => $value ) {
76
				$index = sanitize_text_field( strval( $index ) );
77
				$value = wp_kses( strval( $value ), array() );
78
				$value = str_replace( '&amp;', '&', $value ); // undo damage done by wp_kses_normalize_entities()
79
80
				$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...
81
			}
82
		} else {
83
			$data_without_tags = wp_kses( $data_with_tags, array() );
84
			$data_without_tags = str_replace( '&amp;', '&', $data_without_tags ); // undo damage done by wp_kses_normalize_entities()
85
		}
86
87
		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...
88
	}
89
90
	function __construct() {
91
		$this->add_shortcode();
92
93
		// While generating the output of a text widget with a contact-form shortcode, we need to know its widget ID.
94
		add_action( 'dynamic_sidebar', array( $this, 'track_current_widget' ) );
95
96
		// Add a "widget" shortcode attribute to all contact-form shortcodes embedded in widgets
97
		add_filter( 'widget_text', array( $this, 'widget_atts' ), 0 );
98
99
		// If Text Widgets don't get shortcode processed, hack ours into place.
100
		if ( ! has_filter( 'widget_text', 'do_shortcode' ) ) {
101
			add_filter( 'widget_text', array( $this, 'widget_shortcode_hack' ), 5 );
102
		}
103
104
		// Akismet to the rescue
105
		if ( defined( 'AKISMET_VERSION' ) || function_exists( 'akismet_http_post' ) ) {
106
			add_filter( 'jetpack_contact_form_is_spam', array( $this, 'is_spam_akismet' ), 10, 2 );
107
			add_action( 'contact_form_akismet', array( $this, 'akismet_submit' ), 10, 2 );
108
		}
109
110
		add_action( 'loop_start', array( 'Grunion_Contact_Form', '_style_on' ) );
111
112
		add_action( 'wp_ajax_grunion-contact-form', array( $this, 'ajax_request' ) );
113
		add_action( 'wp_ajax_nopriv_grunion-contact-form', array( $this, 'ajax_request' ) );
114
115
		// Export to CSV feature
116
		if ( is_admin() ) {
117
			add_action( 'admin_init',            array( $this, 'download_feedback_as_csv' ) );
118
			add_action( 'admin_footer-edit.php', array( $this, 'export_form' ) );
119
			add_action( 'admin_menu',            array( $this, 'admin_menu' ) );
120
			add_action( 'current_screen',        array( $this, 'unread_count' ) );
121
		}
122
123
		// custom post type we'll use to keep copies of the feedback items
124
		register_post_type( 'feedback', array(
125
			'labels'            => array(
126
				'name'               => __( 'Feedback', 'jetpack' ),
127
				'singular_name'      => __( 'Feedback', 'jetpack' ),
128
				'search_items'       => __( 'Search Feedback', 'jetpack' ),
129
				'not_found'          => __( 'No feedback found', 'jetpack' ),
130
				'not_found_in_trash' => __( 'No feedback found', 'jetpack' ),
131
			),
132
			'menu_icon'         	=> 'dashicons-feedback',
133
			'show_ui'           	=> TRUE,
134
			'show_in_admin_bar' 	=> FALSE,
135
			'public'            	=> FALSE,
136
			'rewrite'           	=> FALSE,
137
			'query_var'         	=> FALSE,
138
			'capability_type'   	=> 'page',
139
			'show_in_rest'      	=> true,
140
			'rest_controller_class' => 'Grunion_Contact_Form_Endpoint',
141
			'capabilities'			=> array(
142
				'create_posts'        => false,
143
				'publish_posts'       => 'publish_pages',
144
				'edit_posts'          => 'edit_pages',
145
				'edit_others_posts'   => 'edit_others_pages',
146
				'delete_posts'        => 'delete_pages',
147
				'delete_others_posts' => 'delete_others_pages',
148
				'read_private_posts'  => 'read_private_pages',
149
				'edit_post'           => 'edit_page',
150
				'delete_post'         => 'delete_page',
151
				'read_post'           => 'read_page',
152
			),
153
			'map_meta_cap'			=> true,
154
		) );
155
156
		// Add to REST API post type whitelist
157
		add_filter( 'rest_api_allowed_post_types', array( $this, 'allow_feedback_rest_api_type' ) );
158
159
		// Add "spam" as a post status
160
		register_post_status( 'spam', array(
161
			'label'                  => 'Spam',
162
			'public'                 => false,
163
			'exclude_from_search'    => true,
164
			'show_in_admin_all_list' => false,
165
			'label_count'            => _n_noop( 'Spam <span class="count">(%s)</span>', 'Spam <span class="count">(%s)</span>', 'jetpack' ),
166
			'protected'              => true,
167
			'_builtin'               => false,
168
		) );
169
170
		// POST handler
171
		if (
172
			isset( $_SERVER['REQUEST_METHOD'] ) && 'POST' == strtoupper( $_SERVER['REQUEST_METHOD'] )
173
		&&
174
			isset( $_POST['action'] ) && 'grunion-contact-form' == $_POST['action']
175
		&&
176
			isset( $_POST['contact-form-id'] )
177
		) {
178
			add_action( 'template_redirect', array( $this, 'process_form_submission' ) );
179
		}
180
181
		/*
182
		 Can be dequeued by placing the following in wp-content/themes/yourtheme/functions.php
183
		 *
184
		 * 	function remove_grunion_style() {
185
		 *		wp_deregister_style('grunion.css');
186
		 *	}
187
		 *	add_action('wp_print_styles', 'remove_grunion_style');
188
		 */
189
		if ( is_rtl() ) {
190
			wp_register_style( 'grunion.css', GRUNION_PLUGIN_URL . 'css/rtl/grunion-rtl.css', array(), JETPACK__VERSION );
191
		} else {
192
			wp_register_style( 'grunion.css', GRUNION_PLUGIN_URL . 'css/grunion.css', array(), JETPACK__VERSION );
193
		}
194
	}
195
196
	/**
197
	 * Add the 'Export' menu item as a submenu of Feedback.
198
	 */
199
	public function admin_menu() {
200
		add_submenu_page(
201
			'edit.php?post_type=feedback',
202
			__( 'Export feedback as CSV', 'jetpack' ),
203
			__( 'Export CSV', 'jetpack' ),
204
			'export',
205
			'feedback-export',
206
			array( $this, 'export_form' )
207
		);
208
	}
209
210
	/**
211
	 * Add to REST API post type whitelist
212
	 */
213
	function allow_feedback_rest_api_type( $post_types ) {
214
		$post_types[] = 'feedback';
215
		return $post_types;
216
	}
217
218
	/**
219
	 * Display the count of new feedback entries received. It's reset when user visits the Feedback screen.
220
	 *
221
	 * @since 4.1.0
222
	 *
223
	 * @param object $screen Information about the current screen.
224
	 */
225
	function unread_count( $screen ) {
226
		if ( isset( $screen->post_type ) && 'feedback' == $screen->post_type ) {
227
			update_option( 'feedback_unread_count', 0 );
228
		} else {
229
			global $menu;
230
			if ( isset( $menu ) && is_array( $menu ) && ! empty( $menu ) ) {
231
				foreach ( $menu as $index => $menu_item ) {
232
					if ( 'edit.php?post_type=feedback' == $menu_item[2] ) {
233
						$unread = get_option( 'feedback_unread_count', 0 );
234
						if ( $unread > 0 ) {
235
							$unread_count = current_user_can( 'publish_pages' ) ? " <span class='feedback-unread count-{$unread} awaiting-mod'><span class='feedback-unread-count'>" . number_format_i18n( $unread ) . '</span></span>' : '';
236
							$menu[ $index ][0] .= $unread_count;
237
						}
238
						break;
239
					}
240
				}
241
			}
242
		}
243
	}
244
245
	/**
246
	 * Handles all contact-form POST submissions
247
	 *
248
	 * Conditionally attached to `template_redirect`
249
	 */
250
	function process_form_submission() {
251
		// Add a filter to replace tokens in the subject field with sanitized field values
252
		add_filter( 'contact_form_subject', array( $this, 'replace_tokens_with_input' ), 10, 2 );
253
254
		$id = stripslashes( $_POST['contact-form-id'] );
255
256
		if ( is_user_logged_in() ) {
257
			check_admin_referer( "contact-form_{$id}" );
258
		}
259
260
		$is_widget = 0 === strpos( $id, 'widget-' );
261
262
		$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...
263
264
		if ( $is_widget ) {
265
			// It's a form embedded in a text widget
266
			$this->current_widget_id = substr( $id, 7 ); // remove "widget-"
267
			$widget_type = implode( '-', array_slice( explode( '-', $this->current_widget_id ), 0, -1 ) ); // Remove trailing -#
268
269
			// Is the widget active?
270
			$sidebar = is_active_widget( false, $this->current_widget_id, $widget_type );
271
272
			// This is lame - no core API for getting a widget by ID
273
			$widget = isset( $GLOBALS['wp_registered_widgets'][ $this->current_widget_id ] ) ? $GLOBALS['wp_registered_widgets'][ $this->current_widget_id ] : false;
274
275
			if ( $sidebar && $widget && isset( $widget['callback'] ) ) {
276
				// prevent PHP notices by populating widget args
277
				$widget_args = array(
278
					'before_widget' => '',
279
					'after_widget' => '',
280
					'before_title' => '',
281
					'after_title' => '',
282
				);
283
				// This is lamer - no API for outputting a given widget by ID
284
				ob_start();
285
				// Process the widget to populate Grunion_Contact_Form::$last
286
				call_user_func( $widget['callback'], $widget_args, $widget['params'][0] );
287
				ob_end_clean();
288
			}
289
		} else {
290
			// It's a form embedded in a post
291
			$post = get_post( $id );
292
293
			// Process the content to populate Grunion_Contact_Form::$last
294
			/** This filter is already documented in core. wp-includes/post-template.php */
295
			apply_filters( 'the_content', $post->post_content );
296
		}
297
298
		$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...
299
300
		// No form may mean user is using do_shortcode, grab the form using the stored post meta
301
		if ( ! $form ) {
302
303
			// Get shortcode from post meta
304
			$shortcode = get_post_meta( $_POST['contact-form-id'], '_g_feedback_shortcode', true );
305
306
			// Format it
307
			if ( $shortcode != '' ) {
308
309
				// Get attributes from post meta.
310
				$parameters = '';
311
				$attributes = get_post_meta( $_POST['contact-form-id'], '_g_feedback_shortcode_atts', true );
312
				if ( ! empty( $attributes ) && is_array( $attributes ) ) {
313
					foreach( array_filter( $attributes ) as $param => $value  ) {
314
						$parameters .= " $param=\"$value\"";
315
					}
316
				}
317
318
				$shortcode = '[contact-form' . $parameters . ']' . $shortcode . '[/contact-form]';
319
				do_shortcode( $shortcode );
320
321
				// Recreate form
322
				$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...
323
			}
324
325
			if ( ! $form ) {
326
				return false;
327
			}
328
		}
329
330
		if ( is_wp_error( $form->errors ) && $form->errors->get_error_codes() ) {
331
			return $form->errors;
332
		}
333
334
		// Process the form
335
		return $form->process_submission();
336
	}
337
338
	function ajax_request() {
339
		$submission_result = self::process_form_submission();
340
341
		if ( ! $submission_result ) {
342
			header( 'HTTP/1.1 500 Server Error', 500, true );
343
			echo '<div class="form-error"><ul class="form-errors"><li class="form-error-message">';
344
			esc_html_e( 'An error occurred. Please try again later.', 'jetpack' );
345
			echo '</li></ul></div>';
346
		} elseif ( is_wp_error( $submission_result ) ) {
347
			header( 'HTTP/1.1 400 Bad Request', 403, true );
348
			echo '<div class="form-error"><ul class="form-errors"><li class="form-error-message">';
349
			echo esc_html( $submission_result->get_error_message() );
350
			echo '</li></ul></div>';
351
		} else {
352
			echo '<h3>' . esc_html__( 'Message Sent', 'jetpack' ) . '</h3>' . $submission_result;
353
		}
354
355
		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...
356
	}
357
358
	/**
359
	 * Ensure the post author is always zero for contact-form feedbacks
360
	 * Attached to `wp_insert_post_data`
361
	 *
362
	 * @see Grunion_Contact_Form::process_submission()
363
	 *
364
	 * @param array $data the data to insert
365
	 * @param array $postarr the data sent to wp_insert_post()
366
	 * @return array The filtered $data to insert
367
	 */
368
	function insert_feedback_filter( $data, $postarr ) {
369
		if ( $data['post_type'] == 'feedback' && $postarr['post_type'] == 'feedback' ) {
370
			$data['post_author'] = 0;
371
		}
372
373
		return $data;
374
	}
375
	/*
376
	 * Adds our contact-form shortcode
377
	 * The "child" contact-field shortcode is enabled as needed by the contact-form shortcode handler
378
	 */
379
	function add_shortcode() {
380
		add_shortcode( 'contact-form',         array( 'Grunion_Contact_Form', 'parse' ) );
381
		add_shortcode( 'contact-field',        array( 'Grunion_Contact_Form', 'parse_contact_field' ) );
382
	}
383
384
	static function tokenize_label( $label ) {
385
		return '{' . trim( preg_replace( '#^\d+_#', '', $label ) ) . '}';
386
	}
387
388
	static function sanitize_value( $value ) {
389
		return preg_replace( '=((<CR>|<LF>|0x0A/%0A|0x0D/%0D|\\n|\\r)\S).*=i', null, $value );
390
	}
391
392
	/**
393
	 * Replaces tokens like {city} or {City} (case insensitive) with the value
394
	 * of an input field of that name
395
	 *
396
	 * @param string $subject
397
	 * @param array  $field_values Array with field label => field value associations
398
	 *
399
	 * @return string The filtered $subject with the tokens replaced
400
	 */
401
	function replace_tokens_with_input( $subject, $field_values ) {
402
		// Wrap labels into tokens (inside {})
403
		$wrapped_labels = array_map( array( 'Grunion_Contact_Form_Plugin', 'tokenize_label' ), array_keys( $field_values ) );
404
		// Sanitize all values
405
		$sanitized_values = array_map( array( 'Grunion_Contact_Form_Plugin', 'sanitize_value' ), array_values( $field_values ) );
406
407
		foreach ( $sanitized_values as $k => $sanitized_value ) {
408
			if ( is_array( $sanitized_value ) ) {
409
				$sanitized_values[ $k ] = implode( ', ', $sanitized_value );
410
			}
411
		}
412
413
		// Search for all valid tokens (based on existing fields) and replace with the field's value
414
		$subject = str_ireplace( $wrapped_labels, $sanitized_values, $subject );
415
		return $subject;
416
	}
417
418
	/**
419
	 * Tracks the widget currently being processed.
420
	 * Attached to `dynamic_sidebar`
421
	 *
422
	 * @see $current_widget_id
423
	 *
424
	 * @param array $widget The widget data
425
	 */
426
	function track_current_widget( $widget ) {
427
		$this->current_widget_id = $widget['id'];
428
	}
429
430
	/**
431
	 * Adds a "widget" attribute to every contact-form embedded in a text widget.
432
	 * Used to tell the difference between post-embedded contact-forms and widget-embedded contact-forms
433
	 * Attached to `widget_text`
434
	 *
435
	 * @param string $text The widget text
436
	 * @return string The filtered widget text
437
	 */
438
	function widget_atts( $text ) {
439
		Grunion_Contact_Form::style( true );
440
441
		return preg_replace( '/\[contact-form([^a-zA-Z_-])/', '[contact-form widget="' . $this->current_widget_id . '"\\1', $text );
442
	}
443
444
	/**
445
	 * For sites where text widgets are not processed for shortcodes, we add this hack to process just our shortcode
446
	 * Attached to `widget_text`
447
	 *
448
	 * @param string $text The widget text
449
	 * @return string The contact-form filtered widget text
450
	 */
451
	function widget_shortcode_hack( $text ) {
452
		if ( ! preg_match( '/\[contact-form([^a-zA-Z_-])/', $text ) ) {
453
			return $text;
454
		}
455
456
		$old = $GLOBALS['shortcode_tags'];
457
		remove_all_shortcodes();
458
		Grunion_Contact_Form_Plugin::$using_contact_form_field = true;
459
		$this->add_shortcode();
460
461
		$text = do_shortcode( $text );
462
463
		Grunion_Contact_Form_Plugin::$using_contact_form_field = false;
464
		$GLOBALS['shortcode_tags'] = $old;
465
466
		return $text;
467
	}
468
469
	/**
470
	 * Populate an array with all values necessary to submit a NEW contact-form feedback to Akismet.
471
	 * Note that this includes the current user_ip etc, so this should only be called when accepting a new item via $_POST
472
	 *
473
	 * @param array $form Contact form feedback array
474
	 * @return array feedback array with additional data ready for submission to Akismet
475
	 */
476
	function prepare_for_akismet( $form ) {
477
		$form['comment_type'] = 'contact_form';
478
		$form['user_ip']      = $_SERVER['REMOTE_ADDR'];
479
		$form['user_agent']   = $_SERVER['HTTP_USER_AGENT'];
480
		$form['referrer']     = $_SERVER['HTTP_REFERER'];
481
		$form['blog']         = get_option( 'home' );
482
483
		foreach ( $_SERVER as $key => $value ) {
484
			if ( ! is_string( $value ) ) {
485
				continue;
486
			}
487
			if ( in_array( $key, array( 'HTTP_COOKIE', 'HTTP_COOKIE2', 'HTTP_USER_AGENT', 'HTTP_REFERER' ) ) ) {
488
				// We don't care about cookies, and the UA and Referrer were caught above.
489
				continue;
490
			} elseif ( in_array( $key, array( 'REMOTE_ADDR', 'REQUEST_URI', 'DOCUMENT_URI' ) ) ) {
491
				// All three of these are relevant indicators and should be passed along.
492
				$form[ $key ] = $value;
493
			} elseif ( wp_startswith( $key, 'HTTP_' ) ) {
494
				// Any other HTTP header indicators.
495
				// `wp_startswith()` is a wpcom helper function and is included in Jetpack via `functions.compat.php`
496
				$form[ $key ] = $value;
497
			}
498
		}
499
500
		return $form;
501
	}
502
503
	/**
504
	 * Submit contact-form data to Akismet to check for spam.
505
	 * If you're accepting a new item via $_POST, run it Grunion_Contact_Form_Plugin::prepare_for_akismet() first
506
	 * Attached to `jetpack_contact_form_is_spam`
507
	 *
508
	 * @param bool  $is_spam
509
	 * @param array $form
510
	 * @return bool|WP_Error TRUE => spam, FALSE => not spam, WP_Error => stop processing entirely
511
	 */
512
	function is_spam_akismet( $is_spam, $form = array() ) {
513
		global $akismet_api_host, $akismet_api_port;
514
515
		// The signature of this function changed from accepting just $form.
516
		// If something only sends an array, assume it's still using the old
517
		// signature and work around it.
518
		if ( empty( $form ) && is_array( $is_spam ) ) {
519
			$form = $is_spam;
520
			$is_spam = false;
521
		}
522
523
		// If a previous filter has alrady marked this as spam, trust that and move on.
524
		if ( $is_spam ) {
525
			return $is_spam;
526
		}
527
528
		if ( ! function_exists( 'akismet_http_post' ) && ! defined( 'AKISMET_VERSION' ) ) {
529
			return false;
530
		}
531
532
		$query_string = http_build_query( $form );
533
534
		if ( method_exists( 'Akismet', 'http_post' ) ) {
535
			$response = Akismet::http_post( $query_string, 'comment-check' );
536
		} else {
537
			$response = akismet_http_post( $query_string, $akismet_api_host, '/1.1/comment-check', $akismet_api_port );
538
		}
539
540
		$result = false;
541
542
		if ( isset( $response[0]['x-akismet-pro-tip'] ) && 'discard' === trim( $response[0]['x-akismet-pro-tip'] ) && get_option( 'akismet_strictness' ) === '1' ) {
543
			$result = new WP_Error( 'feedback-discarded', __( 'Feedback discarded.', 'jetpack' ) );
544
		} elseif ( isset( $response[1] ) && 'true' == trim( $response[1] ) ) { // 'true' is spam
545
			$result = true;
546
		}
547
548
		/**
549
		 * Filter the results returned by Akismet for each submitted contact form.
550
		 *
551
		 * @module contact-form
552
		 *
553
		 * @since 1.3.1
554
		 *
555
		 * @param WP_Error|bool $result Is the submitted feedback spam.
556
		 * @param array|bool $form Submitted feedback.
557
		 */
558
		return apply_filters( 'contact_form_is_spam_akismet', $result, $form );
559
	}
560
561
	/**
562
	 * Submit a feedback as either spam or ham
563
	 *
564
	 * @param string $as Either 'spam' or 'ham'.
565
	 * @param array  $form the contact-form data
566
	 */
567
	function akismet_submit( $as, $form ) {
568
		global $akismet_api_host, $akismet_api_port;
569
570
		if ( ! in_array( $as, array( 'ham', 'spam' ) ) ) {
571
			return false;
572
		}
573
574
		$query_string = '';
575
		if ( is_array( $form ) ) {
576
			$query_string = http_build_query( $form );
577
		}
578
		if ( method_exists( 'Akismet', 'http_post' ) ) {
579
		    $response = Akismet::http_post( $query_string, "submit-{$as}" );
580
		} else {
581
		    $response = akismet_http_post( $query_string, $akismet_api_host, "/1.1/submit-{$as}", $akismet_api_port );
582
		}
583
584
		return trim( $response[1] );
585
	}
586
587
	/**
588
	 * Prints the menu
589
	 */
590
	function export_form() {
591
		$current_screen = get_current_screen();
592
		if ( ! in_array( $current_screen->id, array( 'edit-feedback', 'feedback_page_feedback-export' ) ) ) {
593
			return;
594
		}
595
596
		if ( ! current_user_can( 'export' ) ) {
597
			return;
598
		}
599
600
		// if there aren't any feedbacks, bail out
601
		if ( ! (int) wp_count_posts( 'feedback' )->publish ) {
602
			return;
603
		}
604
		?>
605
606
		<div id="feedback-export" style="display:none">
607
			<h2><?php _e( 'Export feedback as CSV', 'jetpack' ) ?></h2>
608
			<div class="clear"></div>
609
			<form action="<?php echo admin_url( 'admin-post.php' ); ?>" method="post" class="form">
610
				<?php wp_nonce_field( 'feedback_export','feedback_export_nonce' ); ?>
611
612
				<input name="action" value="feedback_export" type="hidden">
613
				<label for="post"><?php _e( 'Select feedback to download', 'jetpack' ) ?></label>
614
				<select name="post">
615
					<option value="all"><?php esc_html_e( 'All posts', 'jetpack' ) ?></option>
616
					<?php echo $this->get_feedbacks_as_options() ?>
617
				</select>
618
619
				<br><br>
620
				<input type="submit" name="submit" id="submit" class="button button-primary" value="<?php esc_html_e( 'Download', 'jetpack' ); ?>">
621
			</form>
622
		</div>
623
624
		<?php
625
		// There aren't any usable actions in core to output the "export feedback" form in the correct place,
626
		// so this inline JS moves it from the top of the page to the bottom.
627
		?>
628
		<script type='text/javascript'>
629
		var menu = document.getElementById( 'feedback-export' ),
630
		wrapper = document.getElementsByClassName( 'wrap' )[0];
631
		<?php if ( 'edit-feedback' === $current_screen->id ) : ?>
632
		wrapper.appendChild(menu);
633
		<?php endif; ?>
634
		menu.style.display = 'block';
635
		</script>
636
		<?php
637
	}
638
639
	/**
640
	 * Fetch post content for a post and extract just the comment.
641
	 *
642
	 * @param int $post_id The post id to fetch the content for.
643
	 *
644
	 * @return string Trimmed post comment.
645
	 *
646
	 * @codeCoverageIgnore
647
	 */
648
	public function get_post_content_for_csv_export( $post_id ) {
649
		$post_content = get_post_field( 'post_content', $post_id );
650
		$content      = explode( '<!--more-->', $post_content );
651
652
		return trim( $content[0] );
653
	}
654
655
	/**
656
	 * Get `_feedback_extra_fields` field from post meta data.
657
	 *
658
	 * @param int $post_id Id of the post to fetch meta data for.
659
	 *
660
	 * @return mixed
661
	 *
662
	 * @codeCoverageIgnore - No need to be covered.
663
	 */
664
	public function get_post_meta_for_csv_export( $post_id ) {
665
		return get_post_meta( $post_id, '_feedback_extra_fields', true );
666
	}
667
668
	/**
669
	 * Get parsed feedback post fields.
670
	 *
671
	 * @param int $post_id Id of the post to fetch parsed contents for.
672
	 *
673
	 * @return array
674
	 *
675
	 * @codeCoverageIgnore - No need to be covered.
676
	 */
677
	public function get_parsed_field_contents_of_post( $post_id ) {
678
		return self::parse_fields_from_content( $post_id );
679
	}
680
681
	/**
682
	 * Properly maps fields that are missing from the post meta data
683
	 * to names, that are similar to those of the post meta.
684
	 *
685
	 * @param array $parsed_post_content Parsed post content
686
	 *
687
	 * @see parse_fields_from_content for how the input data is generated.
688
	 *
689
	 * @return array Mapped fields.
690
	 */
691
	public function map_parsed_field_contents_of_post_to_field_names( $parsed_post_content ) {
692
693
		$mapped_fields = array();
694
695
		$field_mapping = array(
696
			'_feedback_subject'      => __( 'Contact Form', 'jetpack' ),
697
			'_feedback_author'       => '1_Name',
698
			'_feedback_author_email' => '2_Email',
699
			'_feedback_author_url'   => '3_Website',
700
			'_feedback_main_comment' => '4_Comment',
701
		);
702
703
		foreach ( $field_mapping as $parsed_field_name => $field_name ) {
704
			if (
705
				isset( $parsed_post_content[ $parsed_field_name ] )
706
				&& ! empty( $parsed_post_content[ $parsed_field_name ] )
707
			) {
708
				$mapped_fields[ $field_name ] = $parsed_post_content[ $parsed_field_name ];
709
			}
710
		}
711
712
		return $mapped_fields;
713
	}
714
715
716
	/**
717
	 * Prepares feedback post data for CSV export.
718
	 *
719
	 * @param array $post_ids Post IDs to fetch the data for. These need to be Feedback posts.
720
	 *
721
	 * @return array
722
	 */
723
	public function get_export_data_for_posts( $post_ids ) {
724
725
		$posts_data  = array();
726
		$field_names = array();
727
		$result      = array();
728
729
		/**
730
		 * Fetch posts and get the possible field names for later use
731
		 */
732
		foreach ( $post_ids as $post_id ) {
733
734
			/**
735
			 * Fetch post main data, because we need the subject and author data for the feedback form.
736
			 */
737
			$post_real_data = $this->get_parsed_field_contents_of_post( $post_id );
738
739
			/**
740
			 * If `$post_real_data` is not an array or there is no `_feedback_subject` set,
741
			 * then something must be wrong with the feedback post. Skip it.
742
			 */
743
			if ( ! is_array( $post_real_data ) || ! isset( $post_real_data['_feedback_subject'] ) ) {
744
				continue;
745
			}
746
747
			/**
748
			 * Fetch main post comment. This is from the default textarea fields.
749
			 * If it is non-empty, then we add it to data, otherwise skip it.
750
			 */
751
			$post_comment_content = $this->get_post_content_for_csv_export( $post_id );
752
			if ( ! empty( $post_comment_content ) ) {
753
				$post_real_data['_feedback_main_comment'] = $post_comment_content;
754
			}
755
756
			/**
757
			 * Map parsed fields to proper field names
758
			 */
759
			$mapped_fields = $this->map_parsed_field_contents_of_post_to_field_names( $post_real_data );
760
761
			/**
762
			 * Fetch post meta data.
763
			 */
764
			$post_meta_data = $this->get_post_meta_for_csv_export( $post_id );
765
766
			/**
767
			 * If `$post_meta_data` is not an array or if it is empty, then there is no
768
			 * extra feedback to work with. Create an empty array.
769
			 */
770
			if ( ! is_array( $post_meta_data ) || empty( $post_meta_data ) ) {
771
				$post_meta_data = array();
772
			}
773
774
			/**
775
			 * Prepend the feedback subject to the list of fields.
776
			 */
777
			$post_meta_data = array_merge(
778
				$mapped_fields,
779
				$post_meta_data
780
			);
781
782
			/**
783
			 * Save post metadata for later usage.
784
			 */
785
			$posts_data[ $post_id ] = $post_meta_data;
786
787
			/**
788
			 * Save field names, so we can use them as header fields later in the CSV.
789
			 */
790
			$field_names = array_merge( $field_names, array_keys( $post_meta_data ) );
791
		}
792
793
		/**
794
		 * Make sure the field names are unique, because we don't want duplicate data.
795
		 */
796
		$field_names = array_unique( $field_names );
797
798
		/**
799
		 * Sort the field names by the field id number
800
		 */
801
		sort( $field_names, SORT_NUMERIC );
802
803
		/**
804
		 * Loop through every post, which is essentially CSV row.
805
		 */
806
		foreach ( $posts_data as $post_id => $single_post_data ) {
807
808
			/**
809
			 * Go through all the possible fields and check if the field is available
810
			 * in the current post.
811
			 *
812
			 * If it is - add the data as a value.
813
			 * If it is not - add an empty string, which is just a placeholder in the CSV.
814
			 */
815
			foreach ( $field_names as $single_field_name ) {
816
				if (
817
					isset( $single_post_data[ $single_field_name ] )
818
					&& ! empty( $single_post_data[ $single_field_name ] )
819
				) {
820
					$result[ $single_field_name ][] = trim( $single_post_data[ $single_field_name ] );
821
				} else {
822
					$result[ $single_field_name ][] = '';
823
				}
824
			}
825
		}
826
827
		return $result;
828
	}
829
830
	/**
831
	 * download as a csv a contact form or all of them in a csv file
832
	 */
833
	function download_feedback_as_csv() {
834
		if ( empty( $_POST['feedback_export_nonce'] ) ) {
835
			return;
836
		}
837
838
		check_admin_referer( 'feedback_export', 'feedback_export_nonce' );
839
840
		if ( ! current_user_can( 'export' ) ) {
841
			return;
842
		}
843
844
		$args = array(
845
			'posts_per_page'   => -1,
846
			'post_type'        => 'feedback',
847
			'post_status'      => 'publish',
848
			'order'            => 'ASC',
849
			'fields'           => 'ids',
850
			'suppress_filters' => false,
851
		);
852
853
		$filename = date( 'Y-m-d' ) . '-feedback-export.csv';
854
855
		// Check if we want to download all the feedbacks or just a certain contact form
856
		if ( ! empty( $_POST['post'] ) && $_POST['post'] !== 'all' ) {
857
			$args['post_parent'] = (int) $_POST['post'];
858
			$filename            = date( 'Y-m-d' ) . '-' . str_replace( '&nbsp;', '-', get_the_title( (int) $_POST['post'] ) ) . '.csv';
859
		}
860
861
		$feedbacks = get_posts( $args );
862
863
		if ( empty( $feedbacks ) ) {
864
			return;
865
		}
866
867
		$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...
868
869
		/**
870
		 * Prepare data for export.
871
		 */
872
		$data = $this->get_export_data_for_posts( $feedbacks );
873
874
		/**
875
		 * If `$data` is empty, there's nothing we can do below.
876
		 */
877
		if ( ! is_array( $data ) || empty( $data ) ) {
878
			return;
879
		}
880
881
		/**
882
		 * Extract field names from `$data` for later use.
883
		 */
884
		$fields = array_keys( $data );
885
886
		/**
887
		 * Count how many rows will be exported.
888
		 */
889
		$row_count = count( reset( $data ) );
890
891
		// Forces the download of the CSV instead of echoing
892
		header( 'Content-Disposition: attachment; filename=' . $filename );
893
		header( 'Pragma: no-cache' );
894
		header( 'Expires: 0' );
895
		header( 'Content-Type: text/csv; charset=utf-8' );
896
897
		$output = fopen( 'php://output', 'w' );
898
899
		/**
900
		 * Print CSV headers
901
		 */
902
		fputcsv( $output, $fields );
903
904
		/**
905
		 * Print rows to the output.
906
		 */
907
		for ( $i = 0; $i < $row_count; $i ++ ) {
908
909
			$current_row = array();
910
911
			/**
912
			 * Put all the fields in `$current_row` array.
913
			 */
914
			foreach ( $fields as $single_field_name ) {
915
				$current_row[] = $this->esc_csv( $data[ $single_field_name ][ $i ] );
916
			}
917
918
			/**
919
			 * Output the complete CSV row
920
			 */
921
			fputcsv( $output, $current_row );
922
		}
923
924
		fclose( $output );
925
	}
926
927
	/**
928
	 * Escape a string to be used in a CSV context
929
	 *
930
	 * Malicious input can inject formulas into CSV files, opening up the possibility for phishing attacks and
931
	 * disclosure of sensitive information.
932
	 *
933
	 * Additionally, Excel exposes the ability to launch arbitrary commands through the DDE protocol.
934
	 *
935
	 * @see http://www.contextis.com/resources/blog/comma-separated-vulnerabilities/
936
	 *
937
	 * @param string $field
938
	 *
939
	 * @return string
940
	 */
941
	function esc_csv( $field ) {
942
		$active_content_triggers = array( '=', '+', '-', '@' );
943
944
		if ( in_array( mb_substr( $field, 0, 1 ), $active_content_triggers, true ) ) {
945
			$field = "'" . $field;
946
		}
947
948
		return $field;
949
	}
950
951
	/**
952
	 * Returns a string of HTML <option> items from an array of posts
953
	 *
954
	 * @return string a string of HTML <option> items
955
	 */
956
	protected function get_feedbacks_as_options() {
957
		$options = '';
958
959
		// Get the feedbacks' parents' post IDs
960
		$feedbacks = get_posts( array(
961
			'fields'           => 'id=>parent',
962
			'posts_per_page'   => 100000,
963
			'post_type'        => 'feedback',
964
			'post_status'      => 'publish',
965
			'suppress_filters' => false,
966
		) );
967
		$parents = array_unique( array_values( $feedbacks ) );
968
969
		$posts = get_posts( array(
970
			'orderby'          => 'ID',
971
			'posts_per_page'   => 1000,
972
			'post_type'        => 'any',
973
			'post__in'         => array_values( $parents ),
974
			'suppress_filters' => false,
975
		) );
976
977
		// creates the string of <option> elements
978
		foreach ( $posts as $post ) {
979
			$options .= sprintf( '<option value="%s">%s</option>', esc_attr( $post->ID ), esc_html( $post->post_title ) );
980
		}
981
982
		return $options;
983
	}
984
985
	/**
986
	 * Get the names of all the form's fields
987
	 *
988
	 * @param  array|int $posts the post we want the fields of
989
	 *
990
	 * @return array     the array of fields
991
	 *
992
	 * @deprecated As this is no longer necessary as of the CSV export rewrite. - 2015-12-29
993
	 */
994
	protected function get_field_names( $posts ) {
995
		$posts = (array) $posts;
996
		$all_fields = array();
997
998
		foreach ( $posts as $post ) {
999
			$fields = self::parse_fields_from_content( $post );
1000
1001
			if ( isset( $fields['_feedback_all_fields'] ) ) {
1002
				$extra_fields = array_keys( $fields['_feedback_all_fields'] );
1003
				$all_fields = array_merge( $all_fields, $extra_fields );
1004
			}
1005
		}
1006
1007
		$all_fields = array_unique( $all_fields );
1008
		return $all_fields;
1009
	}
1010
1011
	public static function parse_fields_from_content( $post_id ) {
1012
		static $post_fields;
1013
1014
		if ( ! is_array( $post_fields ) ) {
1015
			$post_fields = array();
1016
		}
1017
1018
		if ( isset( $post_fields[ $post_id ] ) ) {
1019
			return $post_fields[ $post_id ];
1020
		}
1021
1022
		$all_values   = array();
1023
		$post_content = get_post_field( 'post_content', $post_id );
1024
		$content      = explode( '<!--more-->', $post_content );
1025
		$lines        = array();
1026
1027
		if ( count( $content ) > 1 ) {
1028
			$content  = str_ireplace( array( '<br />', ')</p>' ), '', $content[1] );
1029
			$one_line = preg_replace( '/\s+/', ' ', $content );
1030
			$one_line = preg_replace( '/.*Array \( (.*)\)/', '$1', $one_line );
1031
1032
			preg_match_all( '/\[([^\]]+)\] =\&gt\; ([^\[]+)/', $one_line, $matches );
1033
1034
			if ( count( $matches ) > 1 ) {
1035
				$all_values = array_combine( array_map( 'trim', $matches[1] ), array_map( 'trim', $matches[2] ) );
1036
			}
1037
1038
			$lines = array_filter( explode( "\n", $content ) );
1039
		}
1040
1041
		$var_map = array(
1042
			'AUTHOR'       => '_feedback_author',
1043
			'AUTHOR EMAIL' => '_feedback_author_email',
1044
			'AUTHOR URL'   => '_feedback_author_url',
1045
			'SUBJECT'      => '_feedback_subject',
1046
			'IP'           => '_feedback_ip',
1047
		);
1048
1049
		$fields = array();
1050
1051
		foreach ( $lines as $line ) {
1052
			$vars = explode( ': ', $line, 2 );
1053
			if ( ! empty( $vars ) ) {
1054
				if ( isset( $var_map[ $vars[0] ] ) ) {
1055
					$fields[ $var_map[ $vars[0] ] ] = self::strip_tags( trim( $vars[1] ) );
1056
				}
1057
			}
1058
		}
1059
1060
		$fields['_feedback_all_fields'] = $all_values;
1061
1062
		$post_fields[ $post_id ] = $fields;
1063
1064
		return $fields;
1065
	}
1066
1067
	/**
1068
	 * Creates a valid csv row from a post id
1069
	 *
1070
	 * @param  int   $post_id The id of the post
1071
	 * @param  array $fields  An array containing the names of all the fields of the csv
1072
	 * @return String The csv row
1073
	 *
1074
	 * @deprecated This is no longer needed, as of the CSV export rewrite.
1075
	 */
1076
	protected static function make_csv_row_from_feedback( $post_id, $fields ) {
1077
		$content_fields = self::parse_fields_from_content( $post_id );
1078
		$all_fields     = array();
1079
1080
		if ( isset( $content_fields['_feedback_all_fields'] ) ) {
1081
			$all_fields = $content_fields['_feedback_all_fields'];
1082
		}
1083
1084
		// Overwrite the parsed content with the content we stored in post_meta in a better format.
1085
		$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...
1086
		foreach ( $extra_fields as $extra_field => $extra_value ) {
1087
			$all_fields[ $extra_field ] = $extra_value;
1088
		}
1089
1090
		// The first element in all of the exports will be the subject
1091
		$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...
1092
1093
		// Loop the fields array in order to fill the $row_items array correctly
1094
		foreach ( $fields as $field ) {
1095
			if ( $field === __( 'Contact Form', 'jetpack' ) ) { // the first field will ever be the contact form, so we can continue
1096
				continue;
1097
			} elseif ( array_key_exists( $field, $all_fields ) ) {
1098
				$row_items[] = $all_fields[ $field ];
1099
			} else { $row_items[] = '';
1100
			}
1101
		}
1102
1103
		return $row_items;
1104
	}
1105
1106
	public static function get_ip_address() {
1107
		return isset( $_SERVER['REMOTE_ADDR'] ) ? $_SERVER['REMOTE_ADDR'] : null;
1108
	}
1109
}
1110
1111
/**
1112
 * Generic shortcode class.
1113
 * Does nothing other than store structured data and output the shortcode as a string
1114
 *
1115
 * Not very general - specific to Grunion.
1116
 */
1117
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...
1118
	/**
1119
	 * @var string the name of the shortcode: [$shortcode_name /]
1120
	 */
1121
	public $shortcode_name;
1122
1123
	/**
1124
	 * @var array key => value pairs for the shortcode's attributes: [$shortcode_name key="value" ... /]
1125
	 */
1126
	public $attributes;
1127
1128
	/**
1129
	 * @var array key => value pair for attribute defaults
1130
	 */
1131
	public $defaults = array();
1132
1133
	/**
1134
	 * @var null|string Null for selfclosing shortcodes.  Hhe inner content of otherwise: [$shortcode_name]$content[/$shortcode_name]
1135
	 */
1136
	public $content;
1137
1138
	/**
1139
	 * @var array Associative array of inner "child" shortcodes equivalent to the $content: [$shortcode_name][child 1/][child 2/][/$shortcode_name]
1140
	 */
1141
	public $fields;
1142
1143
	/**
1144
	 * @var null|string The HTML of the parsed inner "child" shortcodes".  Null for selfclosing shortcodes.
1145
	 */
1146
	public $body;
1147
1148
	/**
1149
	 * @param array       $attributes An associative array of shortcode attributes.  @see shortcode_atts()
1150
	 * @param null|string $content Null for selfclosing shortcodes.  The inner content otherwise.
1151
	 */
1152
	function __construct( $attributes, $content = null ) {
1153
		$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...
1154
		if ( is_array( $content ) ) {
1155
			$string_content = '';
1156
			foreach ( $content as $field ) {
1157
				$string_content .= (string) $field;
1158
			}
1159
1160
			$this->content = $string_content;
1161
		} else {
1162
			$this->content = $content;
1163
		}
1164
1165
		$this->parse_content( $this->content );
1166
	}
1167
1168
	/**
1169
	 * Processes the shortcode's inner content for "child" shortcodes
1170
	 *
1171
	 * @param string $content The shortcode's inner content: [shortcode]$content[/shortcode]
1172
	 */
1173
	function parse_content( $content ) {
1174
		if ( is_null( $content ) ) {
1175
			$this->body = null;
1176
		}
1177
1178
		$this->body = do_shortcode( $content );
1179
	}
1180
1181
	/**
1182
	 * Returns the value of the requested attribute.
1183
	 *
1184
	 * @param string $key The attribute to retrieve
1185
	 * @return mixed
1186
	 */
1187
	function get_attribute( $key ) {
1188
		return isset( $this->attributes[ $key ] ) ? $this->attributes[ $key ] : null;
1189
	}
1190
1191
	function esc_attr( $value ) {
1192
		if ( is_array( $value ) ) {
1193
			return array_map( array( $this, 'esc_attr' ), $value );
1194
		}
1195
1196
		$value = Grunion_Contact_Form_Plugin::strip_tags( $value );
1197
		$value = _wp_specialchars( $value, ENT_QUOTES, false, true );
1198
1199
		// Shortcode attributes can't contain "]"
1200
		$value = str_replace( ']', '', $value );
1201
		$value = str_replace( ',', '&#x002c;', $value ); // store commas encoded
1202
		$value = strtr( $value, array( '%' => '%25', '&' => '%26' ) );
1203
1204
		// shortcode_parse_atts() does stripcslashes()
1205
		$value = addslashes( $value );
1206
		return $value;
1207
	}
1208
1209
	function unesc_attr( $value ) {
1210
		if ( is_array( $value ) ) {
1211
			return array_map( array( $this, 'unesc_attr' ), $value );
1212
		}
1213
1214
		// For back-compat with old Grunion encoding
1215
		// Also, unencode commas
1216
		$value = strtr( $value, array( '%26' => '&', '%25' => '%' ) );
1217
		$value = preg_replace( array( '/&#x0*22;/i', '/&#x0*27;/i', '/&#x0*26;/i', '/&#x0*2c;/i' ), array( '"', "'", '&', ',' ), $value );
1218
		$value = htmlspecialchars_decode( $value, ENT_QUOTES );
1219
		$value = Grunion_Contact_Form_Plugin::strip_tags( $value );
1220
1221
		return $value;
1222
	}
1223
1224
	/**
1225
	 * Generates the shortcode
1226
	 */
1227
	function __toString() {
1228
		$r = "[{$this->shortcode_name} ";
1229
1230
		foreach ( $this->attributes as $key => $value ) {
1231
			if ( ! $value ) {
1232
				continue;
1233
			}
1234
1235
			if ( isset( $this->defaults[ $key ] ) && $this->defaults[ $key ] == $value ) {
1236
				continue;
1237
			}
1238
1239
			if ( 'id' == $key ) {
1240
				continue;
1241
			}
1242
1243
			$value = $this->esc_attr( $value );
1244
1245
			if ( is_array( $value ) ) {
1246
				$value = join( ',', $value );
1247
			}
1248
1249
			if ( false === strpos( $value, "'" ) ) {
1250
				$value = "'$value'";
1251
			} elseif ( false === strpos( $value, '"' ) ) {
1252
				$value = '"' . $value . '"';
1253
			} else {
1254
				// Shortcodes can't contain both '"' and "'".  Strip one.
1255
				$value = str_replace( "'", '', $value );
1256
				$value = "'$value'";
1257
			}
1258
1259
			$r .= "{$key}={$value} ";
1260
		}
1261
1262
		$r = rtrim( $r );
1263
1264
		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...
1265
			$r .= ']';
1266
1267
			foreach ( $this->fields as $field ) {
1268
				$r .= (string) $field;
1269
			}
1270
1271
			$r .= "[/{$this->shortcode_name}]";
1272
		} else {
1273
			$r .= '/]';
1274
		}
1275
1276
		return $r;
1277
	}
1278
}
1279
1280
/**
1281
 * Class for the contact-form shortcode.
1282
 * Parses shortcode to output the contact form as HTML
1283
 * Sends email and stores the contact form response (a.k.a. "feedback")
1284
 */
1285
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...
1286
	public $shortcode_name = 'contact-form';
1287
1288
	/**
1289
	 * @var WP_Error stores form submission errors
1290
	 */
1291
	public $errors;
1292
1293
	/**
1294
	 * @var Grunion_Contact_Form The most recent (inclusive) contact-form shortcode processed
1295
	 */
1296
	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...
1297
1298
	/**
1299
	 * @var Whatever form we are currently looking at. If processed, will become $last
1300
	 */
1301
	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...
1302
1303
	/**
1304
	 * @var bool Whether to print the grunion.css style when processing the contact-form shortcode
1305
	 */
1306
	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...
1307
1308
	function __construct( $attributes, $content = null ) {
1309
		global $post;
1310
1311
		// Set up the default subject and recipient for this form
1312
		$default_to = '';
1313
		$default_subject = '[' . get_option( 'blogname' ) . ']';
1314
1315
		if ( ! isset( $attributes ) || ! is_array( $attributes ) ) {
1316
			$attributes = array();
1317
		}
1318
1319
		if ( ! empty( $attributes['widget'] ) && $attributes['widget'] ) {
1320
			$default_to .= get_option( 'admin_email' );
1321
			$attributes['id'] = 'widget-' . $attributes['widget'];
1322
			$default_subject = sprintf( _x( '%1$s Sidebar', '%1$s = blog name', 'jetpack' ), $default_subject );
1323
		} elseif ( $post ) {
1324
			$attributes['id'] = $post->ID;
1325
			$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 ) );
1326
			$post_author = get_userdata( $post->post_author );
1327
			$default_to .= $post_author->user_email;
1328
		}
1329
1330
		// Keep reference to $this for parsing form fields
1331
		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...
1332
1333
		$this->defaults = array(
1334
			'to'                 => $default_to,
1335
			'subject'            => $default_subject,
1336
			'show_subject'       => 'no', // only used in back-compat mode
1337
			'widget'             => 0,    // Not exposed to the user. Works with Grunion_Contact_Form_Plugin::widget_atts()
1338
			'id'                 => null, // Not exposed to the user. Set above.
1339
			'submit_button_text' => __( 'Submit &#187;', 'jetpack' ),
1340
		);
1341
1342
		$attributes = shortcode_atts( $this->defaults, $attributes, 'contact-form' );
1343
1344
		// We only enable the contact-field shortcode temporarily while processing the contact-form shortcode
1345
		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...
1346
1347
		parent::__construct( $attributes, $content );
1348
1349
		// There were no fields in the contact form. The form was probably just [contact-form /]. Build a default form.
1350
		if ( empty( $this->fields ) ) {
1351
			// same as the original Grunion v1 form
1352
			$default_form = '
1353
				[contact-field label="' . __( 'Name', 'jetpack' ) . '" type="name"  required="true" /]
1354
				[contact-field label="' . __( 'Email', 'jetpack' ) . '" type="email" required="true" /]
1355
				[contact-field label="' . __( 'Website', 'jetpack' ) . '" type="url" /]';
1356
1357
			if ( 'yes' == strtolower( $this->get_attribute( 'show_subject' ) ) ) {
1358
				$default_form .= '
1359
					[contact-field label="' . __( 'Subject', 'jetpack' ) . '" type="subject" /]';
1360
			}
1361
1362
			$default_form .= '
1363
				[contact-field label="' . __( 'Message', 'jetpack' ) . '" type="textarea" /]';
1364
1365
			$this->parse_content( $default_form );
1366
1367
			// Store the shortcode
1368
			$this->store_shortcode( $default_form, $attributes );
1369
		} else {
1370
			// Store the shortcode
1371
			$this->store_shortcode( $content, $attributes );
1372
		}
1373
1374
		// $this->body and $this->fields have been setup.  We no longer need the contact-field shortcode.
1375
		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...
1376
	}
1377
1378
	/**
1379
	 * Store shortcode content for recall later
1380
	 *	- used to receate shortcode when user uses do_shortcode
1381
	 *
1382
	 * @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...
1383
	 */
1384
	static function store_shortcode( $content = null, $attributes = null ) {
1385
1386
		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...
1387
1388
			$shortcode_meta = get_post_meta( $attributes['id'], '_g_feedback_shortcode', true );
1389
1390
			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...
1391
				update_post_meta( $attributes['id'], '_g_feedback_shortcode', $content );
1392
1393
				// Save attributes to post_meta for later use. They're not available later in do_shortcode situations.
1394
				update_post_meta( $attributes['id'], '_g_feedback_shortcode_atts', $attributes );
1395
			}
1396
		}
1397
	}
1398
1399
	/**
1400
	 * Toggle for printing the grunion.css stylesheet
1401
	 *
1402
	 * @param bool $style
1403
	 */
1404
	static function style( $style ) {
1405
		$previous_style = self::$style;
1406
		self::$style = (bool) $style;
1407
		return $previous_style;
1408
	}
1409
1410
	/**
1411
	 * Turn on printing of grunion.css stylesheet
1412
	 *
1413
	 * @see ::style()
1414
	 * @internal
1415
	 * @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...
1416
	 */
1417
	static function _style_on() {
1418
		return self::style( true );
1419
	}
1420
1421
	/**
1422
	 * The contact-form shortcode processor
1423
	 *
1424
	 * @param array       $attributes Key => Value pairs as parsed by shortcode_parse_atts()
1425
	 * @param string|null $content The shortcode's inner content: [contact-form]$content[/contact-form]
1426
	 * @return string HTML for the concat form.
1427
	 */
1428
	static function parse( $attributes, $content ) {
1429
		require_once JETPACK__PLUGIN_DIR . '/sync/class.jetpack-sync-settings.php';
1430
		if ( Jetpack_Sync_Settings::is_syncing() ) {
1431
			return '';
1432
		}
1433
		// Create a new Grunion_Contact_Form object (this class)
1434
		$form = new Grunion_Contact_Form( $attributes, $content );
1435
1436
		$id = $form->get_attribute( 'id' );
1437
1438
		if ( ! $id ) { // something terrible has happened
1439
			return '[contact-form]';
1440
		}
1441
1442
		if ( is_feed() ) {
1443
			return '[contact-form]';
1444
		}
1445
1446
		// Only allow one contact form per post/widget
1447
		if ( self::$last && $id == self::$last->get_attribute( 'id' ) ) {
1448
			// We're processing the same post
1449
			if ( self::$last->attributes != $form->attributes || self::$last->content != $form->content ) {
1450
				// And we're processing a different shortcode;
1451
				return '';
1452
			} // else, we're processing the same shortcode - probably a separate run of do_shortcode() - let it through
1453
1454
		} else {
1455
			self::$last = $form;
1456
		}
1457
1458
		// Enqueue the grunion.css stylesheet if self::$style allows it
1459
		if ( self::$style && ( empty( $_REQUEST['action'] ) || $_REQUEST['action'] != 'grunion_shortcode_to_json' ) ) {
1460
			// Enqueue the style here instead of printing it, because if some other plugin has run the_post()+rewind_posts(),
1461
			// (like VideoPress does), the style tag gets "printed" the first time and discarded, leaving the contact form unstyled.
1462
			// when WordPress does the real loop.
1463
			wp_enqueue_style( 'grunion.css' );
1464
		}
1465
1466
		$r = '';
1467
		$r .= "<div id='contact-form-$id'>\n";
1468
1469
		if ( is_wp_error( $form->errors ) && $form->errors->get_error_codes() ) {
1470
			// There are errors.  Display them
1471
			$r .= "<div class='form-error'>\n<h3>" . __( 'Error!', 'jetpack' ) . "</h3>\n<ul class='form-errors'>\n";
1472
			foreach ( $form->errors->get_error_messages() as $message ) {
1473
				$r .= "\t<li class='form-error-message'>" . esc_html( $message ) . "</li>\n";
1474
			}
1475
			$r .= "</ul>\n</div>\n\n";
1476
		}
1477
1478
		if ( isset( $_GET['contact-form-id'] ) && $_GET['contact-form-id'] == self::$last->get_attribute( 'id' ) && isset( $_GET['contact-form-sent'] ) ) {
1479
			// The contact form was submitted.  Show the success message/results
1480
			$feedback_id = (int) $_GET['contact-form-sent'];
1481
1482
			$back_url = remove_query_arg( array( 'contact-form-id', 'contact-form-sent', '_wpnonce' ) );
1483
1484
			$r_success_message =
1485
				'<h3>' . __( 'Message Sent', 'jetpack' ) .
1486
				' (<a href="' . esc_url( $back_url ) . '">' . esc_html__( 'go back', 'jetpack' ) . '</a>)' .
1487
				"</h3>\n\n";
1488
1489
			// Don't show the feedback details unless the nonce matches
1490
			if ( $feedback_id && wp_verify_nonce( stripslashes( $_GET['_wpnonce'] ), "contact-form-sent-{$feedback_id}" ) ) {
1491
				$r_success_message .= self::success_message( $feedback_id, $form );
1492
			}
1493
1494
			/**
1495
			 * Filter the message returned after a successfull contact form submission.
1496
			 *
1497
			 * @module contact-form
1498
			 *
1499
			 * @since 1.3.1
1500
			 *
1501
			 * @param string $r_success_message Success message.
1502
			 */
1503
			$r .= apply_filters( 'grunion_contact_form_success_message', $r_success_message );
1504
		} else {
1505
			// Nothing special - show the normal contact form
1506
			if ( $form->get_attribute( 'widget' ) ) {
1507
				// Submit form to the current URL
1508
				$url = remove_query_arg( array( 'contact-form-id', 'contact-form-sent', 'action', '_wpnonce' ) );
1509
			} else {
1510
				// Submit form to the post permalink
1511
				$url = get_permalink();
1512
			}
1513
1514
			// For SSL/TLS page. See RFC 3986 Section 4.2
1515
			$url = set_url_scheme( $url );
1516
1517
			// May eventually want to send this to admin-post.php...
1518
			/**
1519
			 * Filter the contact form action URL.
1520
			 *
1521
			 * @module contact-form
1522
			 *
1523
			 * @since 1.3.1
1524
			 *
1525
			 * @param string $contact_form_id Contact form post URL.
1526
			 * @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...
1527
			 * @param int $id Contact Form ID.
1528
			 */
1529
			$url = apply_filters( 'grunion_contact_form_form_action', "{$url}#contact-form-{$id}", $GLOBALS['post'], $id );
1530
1531
			$r .= "<form action='" . esc_url( $url ) . "' method='post' class='contact-form commentsblock'>\n";
1532
			$r .= $form->body;
1533
			$r .= "\t<p class='contact-submit'>\n";
1534
			$r .= "\t\t<input type='submit' value='" . esc_attr( $form->get_attribute( 'submit_button_text' ) ) . "' class='pushbutton-wide'/>\n";
1535
			if ( is_user_logged_in() ) {
1536
				$r .= "\t\t" . wp_nonce_field( 'contact-form_' . $id, '_wpnonce', true, false ) . "\n"; // nonce and referer
1537
			}
1538
			$r .= "\t\t<input type='hidden' name='contact-form-id' value='$id' />\n";
1539
			$r .= "\t\t<input type='hidden' name='action' value='grunion-contact-form' />\n";
1540
			$r .= "\t</p>\n";
1541
			$r .= "</form>\n";
1542
		}
1543
1544
		$r .= '</div>';
1545
1546
		return $r;
1547
	}
1548
1549
	/**
1550
	 * Returns a success message to be returned if the form is sent via AJAX.
1551
	 *
1552
	 * @param int                         $feedback_id
1553
	 * @param object Grunion_Contact_Form $form
1554
	 *
1555
	 * @return string $message
1556
	 */
1557
	static function success_message( $feedback_id, $form ) {
1558
		return wp_kses(
1559
			'<blockquote class="contact-form-submission">'
1560
			. '<p>' . join( self::get_compiled_form( $feedback_id, $form ), '</p><p>' ) . '</p>'
1561
			. '</blockquote>',
1562
			array( 'br' => array(), 'blockquote' => array( 'class' => array() ), 'p' => array() )
1563
		);
1564
	}
1565
1566
	/**
1567
	 * Returns a compiled form with labels and values in a form of  an array
1568
	 * of lines.
1569
	 *
1570
	 * @param int                         $feedback_id
1571
	 * @param object Grunion_Contact_Form $form
1572
	 *
1573
	 * @return array $lines
1574
	 */
1575
	static function get_compiled_form( $feedback_id, $form ) {
1576
		$feedback       = get_post( $feedback_id );
1577
		$field_ids      = $form->get_field_ids();
1578
		$content_fields = Grunion_Contact_Form_Plugin::parse_fields_from_content( $feedback_id );
1579
1580
		// Maps field_ids to post_meta keys
1581
		$field_value_map = array(
1582
			'name'     => 'author',
1583
			'email'    => 'author_email',
1584
			'url'      => 'author_url',
1585
			'subject'  => 'subject',
1586
			'textarea' => false, // not a post_meta key.  This is stored in post_content
1587
		);
1588
1589
		$compiled_form = array();
1590
1591
		// "Standard" field whitelist
1592
		foreach ( $field_value_map as $type => $meta_key ) {
1593
			if ( isset( $field_ids[ $type ] ) ) {
1594
				$field = $form->fields[ $field_ids[ $type ] ];
1595
1596
				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...
1597
					if ( isset( $content_fields[ "_feedback_{$meta_key}" ] ) ) {
1598
						$value = $content_fields[ "_feedback_{$meta_key}" ];
1599
					}
1600
				} else {
1601
					// The feedback content is stored as the first "half" of post_content
1602
					$value = $feedback->post_content;
1603
					list( $value ) = explode( '<!--more-->', $value );
1604
					$value = trim( $value );
1605
				}
1606
1607
				$field_index = array_search( $field_ids[ $type ], $field_ids['all'] );
1608
				$compiled_form[ $field_index ] = sprintf(
1609
					'<b>%1$s:</b> %2$s<br /><br />',
1610
					wp_kses( $field->get_attribute( 'label' ), array() ),
1611
					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...
1612
				);
1613
			}
1614
		}
1615
1616
		// "Non-standard" fields
1617
		if ( $field_ids['extra'] ) {
1618
			// array indexed by field label (not field id)
1619
			$extra_fields = get_post_meta( $feedback_id, '_feedback_extra_fields', true );
1620
1621
			/**
1622
			 * Only get data for the compiled form if `$extra_fields` is a valid and non-empty array.
1623
			 */
1624
			if ( is_array( $extra_fields ) && ! empty( $extra_fields ) ) {
1625
1626
				$extra_field_keys = array_keys( $extra_fields );
1627
1628
				$i = 0;
1629
				foreach ( $field_ids['extra'] as $field_id ) {
1630
					$field       = $form->fields[ $field_id ];
1631
					$field_index = array_search( $field_id, $field_ids['all'] );
1632
1633
					$label = $field->get_attribute( 'label' );
1634
1635
					$compiled_form[ $field_index ] = sprintf(
1636
						'<b>%1$s:</b> %2$s<br /><br />',
1637
						wp_kses( $label, array() ),
1638
						nl2br( wp_kses( $extra_fields[ $extra_field_keys[ $i ] ], array() ) )
1639
					);
1640
1641
					$i++;
1642
				}
1643
			}
1644
		}
1645
1646
		// Sorting lines by the field index
1647
		ksort( $compiled_form );
1648
1649
		return $compiled_form;
1650
	}
1651
1652
	/**
1653
	 * The contact-field shortcode processor
1654
	 * 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.
1655
	 *
1656
	 * @param array       $attributes Key => Value pairs as parsed by shortcode_parse_atts()
1657
	 * @param string|null $content The shortcode's inner content: [contact-field]$content[/contact-field]
1658
	 * @return HTML for the contact form field
1659
	 */
1660
	static function parse_contact_field( $attributes, $content ) {
1661
		// Don't try to parse contact form fields if not inside a contact form
1662
		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...
1663
			$att_strs = array();
1664
			foreach ( $attributes as $att => $val ) {
1665
				if ( is_numeric( $att ) ) { // Is a valueless attribute
1666
					$att_strs[] = esc_html( $val );
1667
				} elseif ( isset( $val ) ) { // A regular attr - value pair
1668
					$att_strs[] = esc_html( $att ) . '=\'' . esc_html( $val ) . '\'';
1669
				}
1670
			}
1671
1672
			$html = '[contact-field ' . implode( ' ', $att_strs );
1673
1674
			if ( isset( $content ) && ! empty( $content ) ) { // If there is content, let's add a closing tag
1675
				$html .= ']' . esc_html( $content ) . '[/contact-field]';
1676
			} else { // Otherwise let's add a closing slash in the first tag
1677
				$html .= '/]';
1678
			}
1679
1680
			return $html;
1681
		}
1682
1683
		$form = Grunion_Contact_Form::$current_form;
1684
1685
		$field = new Grunion_Contact_Form_Field( $attributes, $content, $form );
1686
1687
		$field_id = $field->get_attribute( 'id' );
1688
		if ( $field_id ) {
1689
			$form->fields[ $field_id ] = $field;
1690
		} else {
1691
			$form->fields[] = $field;
1692
		}
1693
1694
		if (
1695
			isset( $_POST['action'] ) && 'grunion-contact-form' === $_POST['action']
1696
		&&
1697
			isset( $_POST['contact-form-id'] ) && $form->get_attribute( 'id' ) == $_POST['contact-form-id']
1698
		) {
1699
			// If we're processing a POST submission for this contact form, validate the field value so we can show errors as necessary.
1700
			$field->validate();
1701
		}
1702
1703
		// Output HTML
1704
		return $field->render();
1705
	}
1706
1707
	/**
1708
	 * Loops through $this->fields to generate a (structured) list of field IDs.
1709
	 *
1710
	 * Important: Currently the whitelisted fields are defined as follows:
1711
	 *  `name`, `email`, `url`, `subject`, `textarea`
1712
	 *
1713
	 * If you need to add new fields to the Contact Form, please don't add them
1714
	 * to the whitelisted fields and leave them as extra fields.
1715
	 *
1716
	 * The reasoning behind this is that both the admin Feedback view and the CSV
1717
	 * export will not include any fields that are added to the list of
1718
	 * whitelisted fields without taking proper care to add them to all the
1719
	 * other places where they accessed/used/saved.
1720
	 *
1721
	 * The safest way to add new fields is to add them to the dropdown and the
1722
	 * HTML list ( @see Grunion_Contact_Form_Field::render ) and don't add them
1723
	 * to the list of whitelisted fields. This way they will become a part of the
1724
	 * `extra fields` which are saved in the post meta and will be properly
1725
	 * handled by the admin Feedback view and the CSV Export without any extra
1726
	 * work.
1727
	 *
1728
	 * If there is need to add a field to the whitelisted fields, then please
1729
	 * take proper care to add logic to handle the field in the following places:
1730
	 *
1731
	 *  - Below in the switch statement - so the field is recognized as whitelisted.
1732
	 *
1733
	 *  - Grunion_Contact_Form::process_submission - validation and logic.
1734
	 *
1735
	 *  - Grunion_Contact_Form::process_submission - add the field as an additional
1736
	 *      field in the `post_content` when saving the feedback content.
1737
	 *
1738
	 *  - Grunion_Contact_Form_Plugin::parse_fields_from_content - add mapping
1739
	 *      for the field, defined in the above method.
1740
	 *
1741
	 *  - Grunion_Contact_Form_Plugin::map_parsed_field_contents_of_post_to_field_names -
1742
	 *      add mapping of the field for the CSV Export. Otherwise it will be missing
1743
	 *      from the exported data.
1744
	 *
1745
	 *  - admin.php / grunion_manage_post_columns - add the field to the render logic.
1746
	 *      Otherwise it will be missing from the admin Feedback view.
1747
	 *
1748
	 * @return array
1749
	 */
1750
	function get_field_ids() {
1751
		$field_ids = array(
1752
			'all'   => array(), // array of all field_ids
1753
			'extra' => array(), // array of all non-whitelisted field IDs
1754
1755
			// Whitelisted "standard" field IDs:
1756
			// 'email'    => field_id,
1757
			// 'name'     => field_id,
1758
			// 'url'      => field_id,
1759
			// 'subject'  => field_id,
1760
			// 'textarea' => field_id,
1761
		);
1762
1763
		foreach ( $this->fields as $id => $field ) {
1764
			$field_ids['all'][] = $id;
1765
1766
			$type = $field->get_attribute( 'type' );
1767
			if ( isset( $field_ids[ $type ] ) ) {
1768
				// This type of field is already present in our whitelist of "standard" fields for this form
1769
				// Put it in extra
1770
				$field_ids['extra'][] = $id;
1771
				continue;
1772
			}
1773
1774
			/**
1775
			 * See method description before modifying the switch cases.
1776
			 */
1777
			switch ( $type ) {
1778
				case 'email' :
1779
				case 'name' :
1780
				case 'url' :
1781
				case 'subject' :
1782
				case 'textarea' :
1783
					$field_ids[ $type ] = $id;
1784
					break;
1785
				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...
1786
					// Put everything else in extra
1787
					$field_ids['extra'][] = $id;
1788
			}
1789
		}
1790
1791
		return $field_ids;
1792
	}
1793
1794
	/**
1795
	 * Process the contact form's POST submission
1796
	 * Stores feedback.  Sends email.
1797
	 */
1798
	function process_submission() {
1799
		global $post;
1800
1801
		$plugin = Grunion_Contact_Form_Plugin::init();
1802
1803
		$id     = $this->get_attribute( 'id' );
1804
		$to     = $this->get_attribute( 'to' );
1805
		$widget = $this->get_attribute( 'widget' );
1806
1807
		$contact_form_subject = $this->get_attribute( 'subject' );
1808
1809
		$to = str_replace( ' ', '', $to );
1810
		$emails = explode( ',', $to );
1811
1812
		$valid_emails = array();
1813
1814
		foreach ( (array) $emails as $email ) {
1815
			if ( ! is_email( $email ) ) {
1816
				continue;
1817
			}
1818
1819
			if ( function_exists( 'is_email_address_unsafe' ) && is_email_address_unsafe( $email ) ) {
1820
				continue;
1821
			}
1822
1823
			$valid_emails[] = $email;
1824
		}
1825
1826
		// No one to send it to, which means none of the "to" attributes are valid emails.
1827
		// Use default email instead.
1828
		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...
1829
			$valid_emails = $this->defaults['to'];
1830
		}
1831
1832
		$to = $valid_emails;
1833
1834
		// Last ditch effort to set a recipient if somehow none have been set.
1835
		if ( empty( $to ) ) {
1836
			$to = get_option( 'admin_email' );
1837
		}
1838
1839
		// Make sure we're processing the form we think we're processing... probably a redundant check.
1840
		if ( $widget ) {
1841
			if ( 'widget-' . $widget != $_POST['contact-form-id'] ) {
1842
				return false;
1843
			}
1844
		} else {
1845
			if ( $post->ID != $_POST['contact-form-id'] ) {
1846
				return false;
1847
			}
1848
		}
1849
1850
		$field_ids = $this->get_field_ids();
1851
1852
		// Initialize all these "standard" fields to null
1853
		$comment_author_email = $comment_author_email_label = // v
1854
		$comment_author       = $comment_author_label       = // v
1855
		$comment_author_url   = $comment_author_url_label   = // v
1856
		$comment_content      = $comment_content_label      = null;
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned correctly; expected 1 space but found 6 spaces

This check looks for improperly formatted assignments.

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

To illustrate:

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

will have no issues, while

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

will report issues in lines 1 and 2.

Loading history...
1857
1858
		// For each of the "standard" fields, grab their field label and value.
1859 View Code Duplication
		if ( isset( $field_ids['name'] ) ) {
1860
			$field = $this->fields[ $field_ids['name'] ];
1861
			$comment_author = Grunion_Contact_Form_Plugin::strip_tags(
1862
				stripslashes(
1863
					/** This filter is already documented in core/wp-includes/comment-functions.php */
1864
					apply_filters( 'pre_comment_author_name', addslashes( $field->value ) )
1865
				)
1866
			);
1867
			$comment_author_label = Grunion_Contact_Form_Plugin::strip_tags( $field->get_attribute( 'label' ) );
1868
		}
1869
1870 View Code Duplication
		if ( isset( $field_ids['email'] ) ) {
1871
			$field = $this->fields[ $field_ids['email'] ];
1872
			$comment_author_email = Grunion_Contact_Form_Plugin::strip_tags(
1873
				stripslashes(
1874
					/** This filter is already documented in core/wp-includes/comment-functions.php */
1875
					apply_filters( 'pre_comment_author_email', addslashes( $field->value ) )
1876
				)
1877
			);
1878
			$comment_author_email_label = Grunion_Contact_Form_Plugin::strip_tags( $field->get_attribute( 'label' ) );
1879
		}
1880
1881
		if ( isset( $field_ids['url'] ) ) {
1882
			$field = $this->fields[ $field_ids['url'] ];
1883
			$comment_author_url = Grunion_Contact_Form_Plugin::strip_tags(
1884
				stripslashes(
1885
					/** This filter is already documented in core/wp-includes/comment-functions.php */
1886
					apply_filters( 'pre_comment_author_url', addslashes( $field->value ) )
1887
				)
1888
			);
1889
			if ( 'http://' == $comment_author_url ) {
1890
				$comment_author_url = '';
1891
			}
1892
			$comment_author_url_label = Grunion_Contact_Form_Plugin::strip_tags( $field->get_attribute( 'label' ) );
1893
		}
1894
1895
		if ( isset( $field_ids['textarea'] ) ) {
1896
			$field = $this->fields[ $field_ids['textarea'] ];
1897
			$comment_content = trim( Grunion_Contact_Form_Plugin::strip_tags( $field->value ) );
1898
			$comment_content_label = Grunion_Contact_Form_Plugin::strip_tags( $field->get_attribute( 'label' ) );
1899
		}
1900
1901
		if ( isset( $field_ids['subject'] ) ) {
1902
			$field = $this->fields[ $field_ids['subject'] ];
1903
			if ( $field->value ) {
1904
				$contact_form_subject = Grunion_Contact_Form_Plugin::strip_tags( $field->value );
1905
			}
1906
		}
1907
1908
		$all_values = $extra_values = array();
1909
		$i = 1; // Prefix counter for stored metadata
1910
1911
		// For all fields, grab label and value
1912
		foreach ( $field_ids['all'] as $field_id ) {
1913
			$field = $this->fields[ $field_id ];
1914
			$label = $i . '_' . $field->get_attribute( 'label' );
1915
			$value = $field->value;
1916
1917
			$all_values[ $label ] = $value;
1918
			$i++; // Increment prefix counter for the next field
1919
		}
1920
1921
		// For the "non-standard" fields, grab label and value
1922
		// Extra fields have their prefix starting from count( $all_values ) + 1
1923
		foreach ( $field_ids['extra'] as $field_id ) {
1924
			$field = $this->fields[ $field_id ];
1925
			$label = $i . '_' . $field->get_attribute( 'label' );
1926
			$value = $field->value;
1927
1928
			if ( is_array( $value ) ) {
1929
				$value = implode( ', ', $value );
1930
			}
1931
1932
			$extra_values[ $label ] = $value;
1933
			$i++; // Increment prefix counter for the next extra field
1934
		}
1935
1936
		$contact_form_subject = trim( $contact_form_subject );
1937
1938
		$comment_author_IP = Grunion_Contact_Form_Plugin::get_ip_address();
1939
1940
		$vars = array( 'comment_author', 'comment_author_email', 'comment_author_url', 'contact_form_subject', 'comment_author_IP' );
1941
		foreach ( $vars as $var ) {
1942
			$$var = str_replace( array( "\n", "\r" ), '', $$var );
1943
		}
1944
1945
		// Ensure that Akismet gets all of the relevant information from the contact form,
1946
		// not just the textarea field and predetermined subject.
1947
		$akismet_vars = compact( $vars );
1948
		$akismet_vars['comment_content'] = $comment_content;
1949
1950
		foreach ( array_merge( $field_ids['all'], $field_ids['extra'] ) as $field_id ) {
1951
			$field = $this->fields[ $field_id ];
1952
1953
			// Skip any fields that are just a choice from a pre-defined list. They wouldn't have any value
1954
			// from a spam-filtering point of view.
1955
			if ( in_array( $field->get_attribute( 'type' ), array( 'select', 'checkbox', 'checkbox-multiple', 'radio' ) ) ) {
1956
				continue;
1957
			}
1958
1959
			// Normalize the label into a slug.
1960
			$field_slug = trim( // Strip all leading/trailing dashes.
1961
				preg_replace(   // Normalize everything to a-z0-9_-
1962
					'/[^a-z0-9_]+/',
1963
					'-',
1964
					strtolower( $field->get_attribute( 'label' ) ) // Lowercase
1965
				),
1966
				'-'
1967
			);
1968
1969
			$field_value = ( is_array( $field->value ) ) ? trim( implode( ', ', $field->value ) ) : trim( $field->value );
1970
1971
			// Skip any values that are already in the array we're sending.
1972
			if ( $field_value && in_array( $field_value, $akismet_vars ) ) {
1973
				continue;
1974
			}
1975
1976
			$akismet_vars[ 'contact_form_field_' . $field_slug ] = $field_value;
1977
		}
1978
1979
		$spam = '';
1980
		$akismet_values = $plugin->prepare_for_akismet( $akismet_vars );
1981
1982
		// Is it spam?
1983
		/** This filter is already documented in modules/contact-form/admin.php */
1984
		$is_spam = apply_filters( 'jetpack_contact_form_is_spam', false, $akismet_values );
1985
		if ( is_wp_error( $is_spam ) ) { // WP_Error to abort
1986
			return $is_spam; // abort
1987
		} elseif ( $is_spam === true ) {  // TRUE to flag a spam
1988
			$spam = '***SPAM*** ';
1989
		}
1990
1991
		if ( ! $comment_author ) {
1992
			$comment_author = $comment_author_email;
1993
		}
1994
1995
		/**
1996
		 * Filter the email where a submitted feedback is sent.
1997
		 *
1998
		 * @module contact-form
1999
		 *
2000
		 * @since 1.3.1
2001
		 *
2002
		 * @param string|array $to Array of valid email addresses, or single email address.
2003
		 */
2004
		$to = (array) apply_filters( 'contact_form_to', $to );
2005
		$reply_to_addr = $to[0]; // get just the address part before the name part is added
2006
2007
		foreach ( $to as $to_key => $to_value ) {
2008
			$to[ $to_key ] = Grunion_Contact_Form_Plugin::strip_tags( $to_value );
2009
			$to[ $to_key ] = self::add_name_to_address( $to_value );
2010
		}
2011
2012
		$blog_url = parse_url( site_url() );
2013
		$from_email_addr = 'wordpress@' . $blog_url['host'];
2014
2015
		if ( ! empty( $comment_author_email ) ) {
2016
			$reply_to_addr = $comment_author_email;
2017
		}
2018
2019
		$headers = 'From: "' . $comment_author . '" <' . $from_email_addr . ">\r\n" .
2020
					'Reply-To: "' . $comment_author . '" <' . $reply_to_addr . ">\r\n";
2021
2022
		// Build feedback reference
2023
		$feedback_time  = current_time( 'mysql' );
2024
		$feedback_title = "{$comment_author} - {$feedback_time}";
2025
		$feedback_id    = md5( $feedback_title );
2026
2027
		$all_values = array_merge( $all_values, array(
2028
			'entry_title'     => the_title_attribute( 'echo=0' ),
2029
			'entry_permalink' => esc_url( get_permalink( get_the_ID() ) ),
2030
			'feedback_id'     => $feedback_id,
2031
		) );
2032
2033
		/** This filter is already documented in modules/contact-form/admin.php */
2034
		$subject = apply_filters( 'contact_form_subject', $contact_form_subject, $all_values );
2035
		$url     = $widget ? home_url( '/' ) : get_permalink( $post->ID );
2036
2037
		$date_time_format = _x( '%1$s \a\t %2$s', '{$date_format} \a\t {$time_format}', 'jetpack' );
2038
		$date_time_format = sprintf( $date_time_format, get_option( 'date_format' ), get_option( 'time_format' ) );
2039
		$time = date_i18n( $date_time_format, current_time( 'timestamp' ) );
2040
2041
		// keep a copy of the feedback as a custom post type
2042
		$feedback_status = $is_spam === true ? 'spam' : 'publish';
2043
2044
		foreach ( (array) $akismet_values as $av_key => $av_value ) {
2045
			$akismet_values[ $av_key ] = Grunion_Contact_Form_Plugin::strip_tags( $av_value );
2046
		}
2047
2048
		foreach ( (array) $all_values as $all_key => $all_value ) {
2049
			$all_values[ $all_key ] = Grunion_Contact_Form_Plugin::strip_tags( $all_value );
2050
		}
2051
2052
		foreach ( (array) $extra_values as $ev_key => $ev_value ) {
2053
			$extra_values[ $ev_key ] = Grunion_Contact_Form_Plugin::strip_tags( $ev_value );
2054
		}
2055
2056
		/*
2057
		 We need to make sure that the post author is always zero for contact
2058
		 * form submissions.  This prevents export/import from trying to create
2059
		 * new users based on form submissions from people who were logged in
2060
		 * at the time.
2061
		 *
2062
		 * Unfortunately wp_insert_post() tries very hard to make sure the post
2063
		 * author gets the currently logged in user id.  That is how we ended up
2064
		 * with this work around. */
2065
		add_filter( 'wp_insert_post_data', array( $plugin, 'insert_feedback_filter' ), 10, 2 );
2066
2067
		$post_id = wp_insert_post( array(
2068
			'post_date'    => addslashes( $feedback_time ),
2069
			'post_type'    => 'feedback',
2070
			'post_status'  => addslashes( $feedback_status ),
2071
			'post_parent'  => (int) $post->ID,
2072
			'post_title'   => addslashes( wp_kses( $feedback_title, array() ) ),
2073
			'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
2074
			'post_name'    => $feedback_id,
2075
		) );
2076
2077
		// once insert has finished we don't need this filter any more
2078
		remove_filter( 'wp_insert_post_data', array( $plugin, 'insert_feedback_filter' ), 10 );
2079
2080
		update_post_meta( $post_id, '_feedback_extra_fields', $this->addslashes_deep( $extra_values ) );
2081
2082
		if ( 'publish' == $feedback_status ) {
2083
			// Increase count of unread feedback.
2084
			$unread = get_option( 'feedback_unread_count', 0 ) + 1;
2085
			update_option( 'feedback_unread_count', $unread );
2086
		}
2087
2088
		if ( defined( 'AKISMET_VERSION' ) ) {
2089
			update_post_meta( $post_id, '_feedback_akismet_values', $this->addslashes_deep( $akismet_values ) );
2090
		}
2091
2092
		$message = self::get_compiled_form( $post_id, $this );
2093
2094
		array_push(
2095
			$message,
2096
			"<br />",
2097
			'<hr />',
2098
			__( 'Time:', 'jetpack' ) . ' ' . $time . '<br />',
2099
			__( 'IP Address:', 'jetpack' ) . ' ' . $comment_author_IP . '<br />',
2100
			__( 'Contact Form URL:', 'jetpack' ) . ' ' . $url . '<br />'
2101
		);
2102
2103
		if ( is_user_logged_in() ) {
2104
			array_push(
2105
				$message,
2106
				sprintf(
2107
					'<p>' . __( 'Sent by a verified %s user.', 'jetpack' ) . '</p>',
2108
					isset( $GLOBALS['current_site']->site_name ) && $GLOBALS['current_site']->site_name ?
2109
						$GLOBALS['current_site']->site_name : '"' . get_option( 'blogname' ) . '"'
2110
				)
2111
			);
2112
		} else {
2113
			array_push( $message, '<p>' . __( 'Sent by an unverified visitor to your site.', 'jetpack' ) . '</p>' );
2114
		}
2115
2116
		$message = join( $message, '' );
2117
2118
		/**
2119
		 * Filters the message sent via email after a successfull form submission.
2120
		 *
2121
		 * @module contact-form
2122
		 *
2123
		 * @since 1.3.1
2124
		 *
2125
		 * @param string $message Feedback email message.
2126
		 */
2127
		$message = apply_filters( 'contact_form_message', $message );
2128
2129
		// This is called after `contact_form_message`, in order to preserve back-compat
2130
		$message = self::wrap_message_in_html_tags( $message );
2131
2132
		update_post_meta( $post_id, '_feedback_email', $this->addslashes_deep( compact( 'to', 'message' ) ) );
2133
2134
		/**
2135
		 * Fires right before the contact form message is sent via email to
2136
		 * the recipient specified in the contact form.
2137
		 *
2138
		 * @module contact-form
2139
		 *
2140
		 * @since 1.3.1
2141
		 *
2142
		 * @param integer $post_id Post contact form lives on
2143
		 * @param array $all_values Contact form fields
2144
		 * @param array $extra_values Contact form fields not included in $all_values
2145
		 */
2146
		do_action( 'grunion_pre_message_sent', $post_id, $all_values, $extra_values );
2147
2148
		// schedule deletes of old spam feedbacks
2149
		if ( ! wp_next_scheduled( 'grunion_scheduled_delete' ) ) {
2150
			wp_schedule_event( time() + 250, 'daily', 'grunion_scheduled_delete' );
2151
		}
2152
2153
		add_filter( 'wp_mail_content_type', __CLASS__ . '::get_mail_content_type' );
2154
		add_action( 'phpmailer_init', __CLASS__ . '::add_plain_text_alternative' );
2155
		if (
2156
			$is_spam !== true &&
2157
			/**
2158
			 * Filter to choose whether an email should be sent after each successfull contact form submission.
2159
			 *
2160
			 * @module contact-form
2161
			 *
2162
			 * @since 2.6.0
2163
			 *
2164
			 * @param bool true Should an email be sent after a form submission. Default to true.
2165
			 * @param int $post_id Post ID.
2166
			 */
2167
			true === apply_filters( 'grunion_should_send_email', true, $post_id )
2168
		) {
2169
			wp_mail( $to, "{$spam}{$subject}", $message, $headers );
2170
		} elseif (
2171
			true === $is_spam &&
2172
			/**
2173
			 * Choose whether an email should be sent for each spam contact form submission.
2174
			 *
2175
			 * @module contact-form
2176
			 *
2177
			 * @since 1.3.1
2178
			 *
2179
			 * @param bool false Should an email be sent after a spam form submission. Default to false.
2180
			 */
2181
			apply_filters( 'grunion_still_email_spam', false ) == true
2182
		) { // don't send spam by default.  Filterable.
2183
			wp_mail( $to, "{$spam}{$subject}", $message, $headers );
2184
		}
2185
		remove_filter( 'wp_mail_content_type', __CLASS__ . '::get_mail_content_type' );
2186
		remove_action( 'phpmailer_init', __CLASS__ . '::add_plain_text_alternative' );
2187
2188
		if ( defined( 'DOING_AJAX' ) && DOING_AJAX ) {
2189
			return self::success_message( $post_id, $this );
2190
		}
2191
2192
		$redirect = wp_get_referer();
2193
		if ( ! $redirect ) { // wp_get_referer() returns false if the referer is the same as the current page
2194
			$redirect = $_SERVER['REQUEST_URI'];
2195
		}
2196
2197
		$redirect = add_query_arg( urlencode_deep( array(
2198
			'contact-form-id'   => $id,
2199
			'contact-form-sent' => $post_id,
2200
			'_wpnonce'          => wp_create_nonce( "contact-form-sent-{$post_id}" ), // wp_nonce_url HTMLencodes :(
2201
		) ), $redirect );
2202
2203
		/**
2204
		 * Filter the URL where the reader is redirected after submitting a form.
2205
		 *
2206
		 * @module contact-form
2207
		 *
2208
		 * @since 1.9.0
2209
		 *
2210
		 * @param string $redirect Post submission URL.
2211
		 * @param int $id Contact Form ID.
2212
		 * @param int $post_id Post ID.
2213
		 */
2214
		$redirect = apply_filters( 'grunion_contact_form_redirect_url', $redirect, $id, $post_id );
2215
2216
		wp_safe_redirect( $redirect );
2217
		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...
2218
	}
2219
2220
	/**
2221
	 * Add a display name part to an email address
2222
	 *
2223
	 * SpamAssassin doesn't like addresses in HTML messages that are missing display names (e.g., `[email protected]`
2224
	 * instead of `"Foo Bar" <[email protected]>`.
2225
	 *
2226
	 * @param string $address
2227
	 *
2228
	 * @return string
2229
	 */
2230
	function add_name_to_address( $address ) {
2231
		// If it's just the address, without a display name
2232
		if ( is_email( $address ) ) {
2233
			$address = sprintf( '"%s" <%s>', $address, $address );
2234
		}
2235
2236
		return $address;
2237
	}
2238
2239
	/**
2240
	 * Get the content type that should be assigned to outbound emails
2241
	 *
2242
	 * @return string
2243
	 */
2244
	static function get_mail_content_type() {
2245
		return 'text/html';
2246
	}
2247
2248
	/**
2249
	 * Wrap a message body with the appropriate in HTML tags
2250
	 *
2251
	 * This helps to ensure correct parsing by clients, and also helps avoid triggering spam filtering rules
2252
	 *
2253
	 * @param string $body
2254
	 *
2255
	 * @return string
2256
	 */
2257
	static function wrap_message_in_html_tags( $body ) {
2258
		// Don't do anything if the message was already wrapped in HTML tags
2259
		// That could have be done by a plugin via filters
2260
		if ( false !== strpos( $body, '<html' ) ) {
2261
			return $body;
2262
		}
2263
2264
		$html_message = sprintf(
2265
			// The tabs are just here so that the raw code is correctly formatted for developers
2266
			// They're removed so that they don't affect the final message sent to users
2267
			str_replace( "\t", '',
2268
				"<!doctype html>
2269
				<html xmlns=\"http://www.w3.org/1999/xhtml\">
2270
				<body>
2271
2272
				%s
2273
2274
				</body>
2275
				</html>"
2276
			),
2277
			$body
2278
		);
2279
2280
		return $html_message;
2281
	}
2282
2283
	/**
2284
	 * Add a plain-text alternative part to an outbound email
2285
	 *
2286
	 * This makes the message more accessible to mail clients that aren't HTML-aware, and decreases the likelihood
2287
	 * that the message will be flagged as spam.
2288
	 *
2289
	 * @param PHPMailer $phpmailer
2290
	 */
2291
	static function add_plain_text_alternative( $phpmailer ) {
2292
		// Add an extra break so that the extra space above the <p> is preserved after the <p> is stripped out
2293
		$alt_body = str_replace( '<p>', '<p><br />', $phpmailer->Body );
2294
2295
		// Convert <br> to \n breaks, to preserve the space between lines that we want to keep
2296
		$alt_body = str_replace( array( '<br>', '<br />' ), "\n", $alt_body );
2297
2298
		// Convert <hr> to an plain-text equivalent, to preserve the integrity of the message
2299
		$alt_body = str_replace( array( "<hr>", "<hr />" ), "----\n", $alt_body );
2300
2301
		// Trim the plain text message to remove the \n breaks that were after <doctype>, <html>, and <body>
2302
		$phpmailer->AltBody = trim( strip_tags( $alt_body ) );
2303
	}
2304
2305
	function addslashes_deep( $value ) {
2306
		if ( is_array( $value ) ) {
2307
			return array_map( array( $this, 'addslashes_deep' ), $value );
2308
		} elseif ( is_object( $value ) ) {
2309
			$vars = get_object_vars( $value );
2310
			foreach ( $vars as $key => $data ) {
2311
				$value->{$key} = $this->addslashes_deep( $data );
2312
			}
2313
			return $value;
2314
		}
2315
2316
		return addslashes( $value );
2317
	}
2318
}
2319
2320
/**
2321
 * Class for the contact-field shortcode.
2322
 * Parses shortcode to output the contact form field as HTML.
2323
 * Validates input.
2324
 */
2325
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...
2326
	public $shortcode_name = 'contact-field';
2327
2328
	/**
2329
	 * @var Grunion_Contact_Form parent form
2330
	 */
2331
	public $form;
2332
2333
	/**
2334
	 * @var string default or POSTed value
2335
	 */
2336
	public $value;
2337
2338
	/**
2339
	 * @var bool Is the input invalid?
2340
	 */
2341
	public $error = false;
2342
2343
	/**
2344
	 * @param array                $attributes An associative array of shortcode attributes.  @see shortcode_atts()
2345
	 * @param null|string          $content Null for selfclosing shortcodes.  The inner content otherwise.
2346
	 * @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...
2347
	 */
2348
	function __construct( $attributes, $content = null, $form = null ) {
2349
		$attributes = shortcode_atts( array(
2350
					'label'       => null,
2351
					'type'        => 'text',
2352
					'required'    => false,
2353
					'options'     => array(),
2354
					'id'          => null,
2355
					'default'     => null,
2356
					'values'      => null,
2357
					'placeholder' => null,
2358
					'class'       => null,
2359
		), $attributes, 'contact-field' );
2360
2361
		// special default for subject field
2362
		if ( 'subject' == $attributes['type'] && is_null( $attributes['default'] ) && ! is_null( $form ) ) {
2363
			$attributes['default'] = $form->get_attribute( 'subject' );
2364
		}
2365
2366
		// allow required=1 or required=true
2367
		if ( '1' == $attributes['required'] || 'true' == strtolower( $attributes['required'] ) ) {
2368
			$attributes['required'] = true;
2369
		} else { $attributes['required'] = false;
2370
		}
2371
2372
		// parse out comma-separated options list (for selects, radios, and checkbox-multiples)
2373
		if ( ! empty( $attributes['options'] ) && is_string( $attributes['options'] ) ) {
2374
			$attributes['options'] = array_map( 'trim', explode( ',', $attributes['options'] ) );
2375
2376 View Code Duplication
			if ( ! empty( $attributes['values'] ) && is_string( $attributes['values'] ) ) {
2377
				$attributes['values'] = array_map( 'trim', explode( ',', $attributes['values'] ) );
2378
			}
2379
		}
2380
2381
		if ( $form ) {
2382
			// make a unique field ID based on the label, with an incrementing number if needed to avoid clashes
2383
			$form_id = $form->get_attribute( 'id' );
2384
			$id = isset( $attributes['id'] ) ? $attributes['id'] : false;
2385
2386
			$unescaped_label = $this->unesc_attr( $attributes['label'] );
2387
			$unescaped_label = str_replace( '%', '-', $unescaped_label ); // jQuery doesn't like % in IDs?
2388
			$unescaped_label = preg_replace( '/[^a-zA-Z0-9.-_:]/', '', $unescaped_label );
2389
2390
			if ( empty( $id ) ) {
2391
				$id = sanitize_title_with_dashes( 'g' . $form_id . '-' . $unescaped_label );
2392
				$i = 0;
2393
				$max_tries = 99;
2394
				while ( isset( $form->fields[ $id ] ) ) {
2395
					$i++;
2396
					$id = sanitize_title_with_dashes( 'g' . $form_id . '-' . $unescaped_label . '-' . $i );
2397
2398
					if ( $i > $max_tries ) {
2399
						break;
2400
					}
2401
				}
2402
			}
2403
2404
			$attributes['id'] = $id;
2405
		}
2406
2407
		parent::__construct( $attributes, $content );
2408
2409
		// Store parent form
2410
		$this->form = $form;
2411
	}
2412
2413
	/**
2414
	 * This field's input is invalid.  Flag as invalid and add an error to the parent form
2415
	 *
2416
	 * @param string $message The error message to display on the form.
2417
	 */
2418
	function add_error( $message ) {
2419
		$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...
2420
2421
		if ( ! is_wp_error( $this->form->errors ) ) {
2422
			$this->form->errors = new WP_Error;
2423
		}
2424
2425
		$this->form->errors->add( $this->get_attribute( 'id' ), $message );
2426
	}
2427
2428
	/**
2429
	 * Is the field input invalid?
2430
	 *
2431
	 * @see $error
2432
	 *
2433
	 * @return bool
2434
	 */
2435
	function is_error() {
2436
		return $this->error;
2437
	}
2438
2439
	/**
2440
	 * Validates the form input
2441
	 */
2442
	function validate() {
2443
		// If it's not required, there's nothing to validate
2444
		if ( ! $this->get_attribute( 'required' ) ) {
2445
			return;
2446
		}
2447
2448
		$field_id    = $this->get_attribute( 'id' );
2449
		$field_type  = $this->get_attribute( 'type' );
2450
		$field_label = $this->get_attribute( 'label' );
2451
2452
		if ( isset( $_POST[ $field_id ] ) ) {
2453
			if ( is_array( $_POST[ $field_id ] ) ) {
2454
				$field_value = array_map( 'stripslashes', $_POST[ $field_id ] );
2455
			} else {
2456
				$field_value = stripslashes( $_POST[ $field_id ] );
2457
			}
2458
		} else {
2459
			$field_value = '';
2460
		}
2461
2462
		switch ( $field_type ) {
2463
			case 'email' :
2464
				// Make sure the email address is valid
2465
				if ( ! is_email( $field_value ) ) {
2466
					/* translators: %s is the name of a form field */
2467
					$this->add_error( sprintf( __( '%s requires a valid email address', 'jetpack' ), $field_label ) );
2468
				}
2469
			break;
2470
			case 'checkbox-multiple' :
2471
				// Check that there is at least one option selected
2472
				if ( empty( $field_value ) ) {
2473
					/* translators: %s is the name of a form field */
2474
					$this->add_error( sprintf( __( '%s requires at least one selection', 'jetpack' ), $field_label ) );
2475
				}
2476
			break;
2477
			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...
2478
				// Just check for presence of any text
2479
				if ( ! strlen( trim( $field_value ) ) ) {
2480
					/* translators: %s is the name of a form field */
2481
					$this->add_error( sprintf( __( '%s is required', 'jetpack' ), $field_label ) );
2482
				}
2483
		}
2484
	}
2485
2486
2487
	/**
2488
	 * Check the default value for options field
2489
	 *
2490
	 * @param string value
2491
	 * @param int index
2492
	 * @param string default value
2493
	 *
2494
	 * @return string
2495
	 */
2496
	public function get_option_value( $value, $index, $options ) {
2497
		if ( empty( $value[ $index ] ) ) {
2498
			return $options;
2499
		}
2500
		return $value[ $index ];
2501
	}
2502
2503
	/**
2504
	 * Outputs the HTML for this form field
2505
	 *
2506
	 * @return string HTML
2507
	 */
2508
	function render() {
2509
		global $current_user, $user_identity;
2510
2511
		$r = '';
2512
2513
		$field_id          = $this->get_attribute( 'id' );
2514
		$field_type        = $this->get_attribute( 'type' );
2515
		$field_label       = $this->get_attribute( 'label' );
2516
		$field_required    = $this->get_attribute( 'required' );
2517
		$placeholder       = $this->get_attribute( 'placeholder' );
2518
		$class             = $this->get_attribute( 'class' );
2519
		$field_placeholder = ( ! empty( $placeholder ) ) ? "placeholder='" . esc_attr( $placeholder ) . "'" : '';
2520
		$field_class       = "class='" . trim( esc_attr( $field_type ) . ' ' . esc_attr( $class ) ) . "' ";
2521
2522
		if ( isset( $_POST[ $field_id ] ) ) {
2523
			if ( is_array( $_POST[ $field_id ] ) ) {
2524
				$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...
2525
			} else {
2526
				$this->value = stripslashes( (string) $_POST[ $field_id ] );
2527
			}
2528
		} elseif ( isset( $_GET[ $field_id ] ) ) {
2529
			$this->value = stripslashes( (string) $_GET[ $field_id ] );
2530
		} elseif (
2531
			is_user_logged_in() &&
2532
			( ( defined( 'IS_WPCOM' ) && IS_WPCOM ) ||
2533
			/**
2534
			 * Allow third-party tools to prefill the contact form with the user's details when they're logged in.
2535
			 *
2536
			 * @module contact-form
2537
			 *
2538
			 * @since 3.2.0
2539
			 *
2540
			 * @param bool false Should the Contact Form be prefilled with your details when you're logged in. Default to false.
2541
			 */
2542
			true === apply_filters( 'jetpack_auto_fill_logged_in_user', false )
2543
			)
2544
		) {
2545
			// Special defaults for logged-in users
2546
			switch ( $this->get_attribute( 'type' ) ) {
2547
				case 'email' :
2548
					$this->value = $current_user->data->user_email;
2549
				break;
2550
				case 'name' :
2551
					$this->value = $user_identity;
2552
				break;
2553
				case 'url' :
2554
					$this->value = $current_user->data->user_url;
2555
				break;
2556
				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...
2557
					$this->value = $this->get_attribute( 'default' );
2558
			}
2559
		} else {
2560
			$this->value = $this->get_attribute( 'default' );
2561
		}
2562
2563
		$field_value = Grunion_Contact_Form_Plugin::strip_tags( $this->value );
2564
		$field_label = Grunion_Contact_Form_Plugin::strip_tags( $field_label );
2565
2566
		/**
2567
		 * Filter the Contact Form required field text
2568
		 *
2569
		 * @module contact-form
2570
		 *
2571
		 * @since 3.8.0
2572
		 *
2573
		 * @param string $var Required field text. Default is "(required)".
2574
		 */
2575
		$required_field_text = esc_html( apply_filters( 'jetpack_required_field_text', __( '(required)', 'jetpack' ) ) );
2576
2577
		switch ( $field_type ) {
2578 View Code Duplication
			case 'email' :
2579
				$r .= "\n<div>\n";
2580
				$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";
2581
				$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";
2582
				$r .= "\t</div>\n";
2583
			break;
2584
			case 'telephone' :
2585
				$r .= "\n<div>\n";
2586
				$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";
2587
				$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";
2588
			break;
2589 View Code Duplication
			case 'textarea' :
2590
				$r .= "\n<div>\n";
2591
				$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";
2592
				$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";
2593
				$r .= "\t</div>\n";
2594
			break;
2595
			case 'radio' :
2596
				$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";
2597
				foreach ( $this->get_attribute( 'options' ) as $optionIndex => $option ) {
2598
					$option = Grunion_Contact_Form_Plugin::strip_tags( $option );
2599
					$r .= "\t\t<label class='grunion-radio-label radio" . ( $this->is_error() ? ' form-error' : '' ) . "'>";
2600
					$r .= "<input type='radio' name='" . esc_attr( $field_id ) . "' value='" . esc_attr( $this->get_option_value( $this->get_attribute( 'values' ), $optionIndex, $option ) ) . "' " . $field_class . checked( $option, $field_value, false ) . ' ' . ( $field_required ? "required aria-required='true'" : '' ) . '/> ';
2601
					$r .= esc_html( $option ) . "</label>\n";
2602
					$r .= "\t\t<div class='clear-form'></div>\n";
2603
				}
2604
				$r .= "\t\t</div>\n";
2605
			break;
2606
			case 'checkbox' :
2607
				$r .= "\t<div>\n";
2608
				$r .= "\t\t<label class='grunion-field-label checkbox" . ( $this->is_error() ? ' form-error' : '' ) . "'>\n";
2609
				$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";
2610
				$r .= "\t\t" . esc_html( $field_label ) . ( $field_required ? '<span>' . $required_field_text . '</span>' : '' ) . "</label>\n";
2611
				$r .= "\t\t<div class='clear-form'></div>\n";
2612
				$r .= "\t</div>\n";
2613
			break;
2614
			case 'checkbox-multiple' :
2615
				$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";
2616
				foreach ( $this->get_attribute( 'options' ) as $optionIndex => $option ) {
2617
					$option = Grunion_Contact_Form_Plugin::strip_tags( $option );
2618
					$r .= "\t\t<label class='grunion-checkbox-multiple-label checkbox-multiple" . ( $this->is_error() ? ' form-error' : '' ) . "'>";
2619
					$r .= "<input type='checkbox' name='" . esc_attr( $field_id ) . "[]' value='" . esc_attr( $this->get_option_value( $this->get_attribute( 'values' ), $optionIndex, $option ) ) . "' " . $field_class . checked( in_array( $option, (array) $field_value ), true, false ) . ' /> ';
2620
					$r .= esc_html( $option ) . "</label>\n";
2621
					$r .= "\t\t<div class='clear-form'></div>\n";
2622
				}
2623
				$r .= "\t\t</div>\n";
2624
			break;
2625
			case 'select' :
2626
				$r .= "\n<div>\n";
2627
				$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";
2628
				$r .= "\t<select name='" . esc_attr( $field_id ) . "' id='" . esc_attr( $field_id ) . "' " . $field_class . ( $field_required ? "required aria-required='true'" : '' ) . ">\n";
2629
				foreach ( $this->get_attribute( 'options' ) as $optionIndex => $option ) {
2630
					$option = Grunion_Contact_Form_Plugin::strip_tags( $option );
2631
					$r .= "\t\t<option" . selected( $option, $field_value, false ) . " value='" . esc_attr( $this->get_option_value( $this->get_attribute( 'values' ), $optionIndex, $option ) ) . "'>" . esc_html( $option ) . "</option>\n";
2632
				}
2633
				$r .= "\t</select>\n";
2634
				$r .= "\t</div>\n";
2635
			break;
2636
			case 'date' :
2637
				$r .= "\n<div>\n";
2638
				$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";
2639
				$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";
2640
				$r .= "\t</div>\n";
2641
2642
				wp_enqueue_script( 'grunion-frontend', plugins_url( 'js/grunion-frontend.js', __FILE__ ), array( 'jquery', 'jquery-ui-datepicker' ) );
2643
			break;
2644 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...
2645
				// note that any unknown types will produce a text input, so we can use arbitrary type names to handle
2646
				// input fields like name, email, url that require special validation or handling at POST
2647
				$r .= "\n<div>\n";
2648
				$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";
2649
				$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";
2650
				$r .= "\t</div>\n";
2651
		}
2652
2653
		/**
2654
		 * Filter the HTML of the Contact Form.
2655
		 *
2656
		 * @module contact-form
2657
		 *
2658
		 * @since 2.6.0
2659
		 *
2660
		 * @param string $r Contact Form HTML output.
2661
		 * @param string $field_label Field label.
2662
		 * @param int|null $id Post ID.
2663
		 */
2664
		return apply_filters( 'grunion_contact_form_field_html', $r, $field_label, ( in_the_loop() ? get_the_ID() : null ) );
2665
	}
2666
}
2667
2668
add_action( 'init', array( 'Grunion_Contact_Form_Plugin', 'init' ) );
2669
2670
add_action( 'grunion_scheduled_delete', 'grunion_delete_old_spam' );
2671
2672
/**
2673
 * Deletes old spam feedbacks to keep the posts table size under control
2674
 */
2675
function grunion_delete_old_spam() {
2676
	global $wpdb;
2677
2678
	$grunion_delete_limit = 100;
2679
2680
	$now_gmt = current_time( 'mysql', 1 );
2681
	$sql = $wpdb->prepare( "
2682
		SELECT `ID`
2683
		FROM $wpdb->posts
2684
		WHERE DATE_SUB( %s, INTERVAL 15 DAY ) > `post_date_gmt`
2685
			AND `post_type` = 'feedback'
2686
			AND `post_status` = 'spam'
2687
		LIMIT %d
2688
	", $now_gmt, $grunion_delete_limit );
2689
	$post_ids = $wpdb->get_col( $sql );
2690
2691
	foreach ( (array) $post_ids as $post_id ) {
2692
		// force a full delete, skip the trash
2693
		wp_delete_post( $post_id, true );
2694
	}
2695
2696
	// Arbitrary check points for running OPTIMIZE
2697
	// nothing special about 5000 or 11
2698
	// just trying to periodically recover deleted rows
2699
	$random_num = mt_rand( 1, 5000 );
2700
	if (
2701
		/**
2702
		 * Filter how often the module run OPTIMIZE TABLE on the core WP tables.
2703
		 *
2704
		 * @module contact-form
2705
		 *
2706
		 * @since 1.3.1
2707
		 *
2708
		 * @param int $random_num Random number.
2709
		 */
2710
		apply_filters( 'grunion_optimize_table', ( $random_num == 11 ) )
2711
	) {
2712
		$wpdb->query( "OPTIMIZE TABLE $wpdb->posts" );
2713
	}
2714
2715
	// if we hit the max then schedule another run
2716
	if ( count( $post_ids ) >= $grunion_delete_limit ) {
2717
		wp_schedule_single_event( time() + 700, 'grunion_scheduled_delete' );
2718
	}
2719
}
2720