Completed
Push — fix/visibility-condition-issue... ( eee6fd...a71c15 )
by
unknown
11:07
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
104
		// Akismet to the rescue
105
		if ( defined( 'AKISMET_VERSION' ) || function_exists( 'akismet_http_post' ) ) {
106
			add_filter( 'jetpack_contact_form_is_spam', array( $this, 'is_spam_akismet' ), 10, 2 );
107
			add_action( 'contact_form_akismet', array( $this, 'akismet_submit' ), 10, 2 );
108
		}
109
110
		add_action( 'loop_start', array( 'Grunion_Contact_Form', '_style_on' ) );
111
112
		add_action( 'wp_ajax_grunion-contact-form', array( $this, 'ajax_request' ) );
113
		add_action( 'wp_ajax_nopriv_grunion-contact-form', array( $this, 'ajax_request' ) );
114
115
		// Export to CSV feature
116
		if ( is_admin() ) {
117
			add_action( 'admin_init',            array( $this, 'download_feedback_as_csv' ) );
118
			add_action( 'admin_footer-edit.php', array( $this, 'export_form' ) );
119
			add_action( 'admin_menu',            array( $this, 'admin_menu' ) );
120
			add_action( 'current_screen',        array( $this, 'unread_count' ) );
121
		}
122
123
		// custom post type we'll use to keep copies of the feedback items
124
		register_post_type( 'feedback', array(
125
			'labels'            => array(
126
				'name'               => __( 'Feedback', 'jetpack' ),
127
				'singular_name'      => __( 'Feedback', 'jetpack' ),
128
				'search_items'       => __( 'Search Feedback', 'jetpack' ),
129
				'not_found'          => __( 'No feedback found', 'jetpack' ),
130
				'not_found_in_trash' => __( 'No feedback found', 'jetpack' ),
131
			),
132
			'menu_icon'         	=> 'dashicons-feedback',
133
			'show_ui'           	=> TRUE,
134
			'show_in_admin_bar' 	=> FALSE,
135
			'public'            	=> FALSE,
136
			'rewrite'           	=> FALSE,
137
			'query_var'         	=> FALSE,
138
			'capability_type'   	=> 'page',
139
			'show_in_rest'      	=> true,
140
			'rest_controller_class' => 'Grunion_Contact_Form_Endpoint',
141
			'capabilities'			=> array(
142
				'create_posts'        => false,
143
				'publish_posts'       => 'publish_pages',
144
				'edit_posts'          => 'edit_pages',
145
				'edit_others_posts'   => 'edit_others_pages',
146
				'delete_posts'        => 'delete_pages',
147
				'delete_others_posts' => 'delete_others_pages',
148
				'read_private_posts'  => 'read_private_pages',
149
				'edit_post'           => 'edit_page',
150
				'delete_post'         => 'delete_page',
151
				'read_post'           => 'read_page',
152
			),
153
			'map_meta_cap'			=> true,
154
		) );
155
156
		// Add to REST API post type whitelist
157
		add_filter( 'rest_api_allowed_post_types', array( $this, 'allow_feedback_rest_api_type' ) );
158
159
		// Add "spam" as a post status
160
		register_post_status( 'spam', array(
161
			'label'                  => 'Spam',
162
			'public'                 => false,
163
			'exclude_from_search'    => true,
164
			'show_in_admin_all_list' => false,
165
			'label_count'            => _n_noop( 'Spam <span class="count">(%s)</span>', 'Spam <span class="count">(%s)</span>', 'jetpack' ),
166
			'protected'              => true,
167
			'_builtin'               => false,
168
		) );
169
170
		// POST handler
171
		if (
172
			isset( $_SERVER['REQUEST_METHOD'] ) && 'POST' == strtoupper( $_SERVER['REQUEST_METHOD'] )
173
		&&
174
			isset( $_POST['action'] ) && 'grunion-contact-form' == $_POST['action']
175
		&&
176
			isset( $_POST['contact-form-id'] )
177
		) {
178
			add_action( 'template_redirect', array( $this, 'process_form_submission' ) );
179
		}
180
181
		/*
182
		 Can be dequeued by placing the following in wp-content/themes/yourtheme/functions.php
183
		 *
184
		 * 	function remove_grunion_style() {
185
		 *		wp_deregister_style('grunion.css');
186
		 *	}
187
		 *	add_action('wp_print_styles', 'remove_grunion_style');
188
		 */
189
		if ( is_rtl() ) {
190
			wp_register_style( 'grunion.css', GRUNION_PLUGIN_URL . 'css/rtl/grunion-rtl.css', array(), JETPACK__VERSION );
191
		} else {
192
			wp_register_style( 'grunion.css', GRUNION_PLUGIN_URL . 'css/grunion.css', array(), JETPACK__VERSION );
193
		}
194
	}
195
196
	/**
197
	 * Add the 'Export' menu item as a submenu of Feedback.
198
	 */
199
	public function admin_menu() {
200
		add_submenu_page(
201
			'edit.php?post_type=feedback',
202
			__( 'Export feedback as CSV', 'jetpack' ),
203
			__( 'Export CSV', 'jetpack' ),
204
			'export',
205
			'feedback-export',
206
			array( $this, 'export_form' )
207
		);
208
	}
209
210
	/**
211
	 * Add to REST API post type whitelist
212
	 */
213
	function allow_feedback_rest_api_type( $post_types ) {
214
		$post_types[] = 'feedback';
215
		return $post_types;
216
	}
217
218
	/**
219
	 * Display the count of new feedback entries received. It's reset when user visits the Feedback screen.
220
	 *
221
	 * @since 4.1.0
222
	 *
223
	 * @param object $screen Information about the current screen.
224
	 */
225
	function unread_count( $screen ) {
226
		if ( isset( $screen->post_type ) && 'feedback' == $screen->post_type ) {
227
			update_option( 'feedback_unread_count', 0 );
228
		} else {
229
			global $menu;
230
			if ( isset( $menu ) && is_array( $menu ) && ! empty( $menu ) ) {
231
				foreach ( $menu as $index => $menu_item ) {
232
					if ( 'edit.php?post_type=feedback' == $menu_item[2] ) {
233
						$unread = get_option( 'feedback_unread_count', 0 );
234
						if ( $unread > 0 ) {
235
							$unread_count = current_user_can( 'publish_pages' ) ? " <span class='feedback-unread count-{$unread} awaiting-mod'><span class='feedback-unread-count'>" . number_format_i18n( $unread ) . '</span></span>' : '';
236
							$menu[ $index ][0] .= $unread_count;
237
						}
238
						break;
239
					}
240
				}
241
			}
242
		}
243
	}
244
245
	/**
246
	 * Handles all contact-form POST submissions
247
	 *
248
	 * Conditionally attached to `template_redirect`
249
	 */
250
	function process_form_submission() {
251
		// Add a filter to replace tokens in the subject field with sanitized field values
252
		add_filter( 'contact_form_subject', array( $this, 'replace_tokens_with_input' ), 10, 2 );
253
254
		$id = stripslashes( $_POST['contact-form-id'] );
255
256
		if ( is_user_logged_in() ) {
257
			check_admin_referer( "contact-form_{$id}" );
258
		}
259
260
		$is_widget = 0 === strpos( $id, 'widget-' );
261
262
		$form = false;
0 ignored issues
show
Unused Code introduced by
$form is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

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