Completed
Push — contact-form/move-export-rebas... ( e50f9a )
by George
342:28 queued 334:16
created

Grunion_Contact_Form_Plugin::admin_menu()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 10
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

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