Completed
Push — fix/protect-math-fallback-text ( 3d091a...354abb )
by
unknown
08:00
created

Grunion_Contact_Form_Plugin::admin_menu()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
nc 1
nop 0
dl 0
loc 10
rs 9.9332
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;
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
		/**
63
		 * Fires right before deleting the _feedback_akismet_values post meta on $feedback_ids
64
		 *
65
		 * @module contact-form
66
		 *
67
		 * @since 6.1.0
68
		 *
69
		 * @param array $feedback_ids list of feedback post ID
70
		 */
71
		do_action( 'jetpack_daily_akismet_meta_cleanup_before', $feedback_ids );
72
		foreach ( $feedback_ids as $feedback_id ) {
73
			delete_post_meta( $feedback_id, '_feedback_akismet_values' );
74
		}
75
76
		/**
77
		 * Fires right after deleting the _feedback_akismet_values post meta on $feedback_ids
78
		 *
79
		 * @module contact-form
80
		 *
81
		 * @since 6.1.0
82
		 *
83
		 * @param array $feedback_ids list of feedback post ID
84
		 */
85
		do_action( 'jetpack_daily_akismet_meta_cleanup_after', $feedback_ids );
86
	}
87
88
	/**
89
	 * Strips HTML tags from input.  Output is NOT HTML safe.
90
	 *
91
	 * @param mixed $data_with_tags
92
	 * @return mixed
93
	 */
94
	public static function strip_tags( $data_with_tags ) {
95
		if ( is_array( $data_with_tags ) ) {
96
			foreach ( $data_with_tags as $index => $value ) {
97
				$index = sanitize_text_field( strval( $index ) );
98
				$value = wp_kses( strval( $value ), array() );
99
				$value = str_replace( '&amp;', '&', $value ); // undo damage done by wp_kses_normalize_entities()
100
101
				$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...
102
			}
103
		} else {
104
			$data_without_tags = wp_kses( $data_with_tags, array() );
105
			$data_without_tags = str_replace( '&amp;', '&', $data_without_tags ); // undo damage done by wp_kses_normalize_entities()
106
		}
107
108
		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...
109
	}
110
111
	function __construct() {
112
		$this->add_shortcode();
113
114
		// While generating the output of a text widget with a contact-form shortcode, we need to know its widget ID.
115
		add_action( 'dynamic_sidebar', array( $this, 'track_current_widget' ) );
116
117
		// Add a "widget" shortcode attribute to all contact-form shortcodes embedded in widgets
118
		add_filter( 'widget_text', array( $this, 'widget_atts' ), 0 );
119
120
		// If Text Widgets don't get shortcode processed, hack ours into place.
121
		if (
122
			version_compare( get_bloginfo( 'version' ), '4.9-z', '<=' )
123
			&& ! has_filter( 'widget_text', 'do_shortcode' )
124
		) {
125
			add_filter( 'widget_text', array( $this, 'widget_shortcode_hack' ), 5 );
126
		}
127
128
		// Akismet to the rescue
129
		if ( defined( 'AKISMET_VERSION' ) || function_exists( 'akismet_http_post' ) ) {
130
			add_filter( 'jetpack_contact_form_is_spam', array( $this, 'is_spam_akismet' ), 10, 2 );
131
			add_action( 'contact_form_akismet', array( $this, 'akismet_submit' ), 10, 2 );
132
		}
133
134
		add_action( 'loop_start', array( 'Grunion_Contact_Form', '_style_on' ) );
135
136
		add_action( 'wp_ajax_grunion-contact-form', array( $this, 'ajax_request' ) );
137
		add_action( 'wp_ajax_nopriv_grunion-contact-form', array( $this, 'ajax_request' ) );
138
139
		// GDPR: personal data exporter & eraser.
140
		add_filter( 'wp_privacy_personal_data_exporters', array( $this, 'register_personal_data_exporter' ) );
141
		add_filter( 'wp_privacy_personal_data_erasers', array( $this, 'register_personal_data_eraser' ) );
142
143
		// Export to CSV feature
144
		if ( is_admin() ) {
145
			add_action( 'admin_init',            array( $this, 'download_feedback_as_csv' ) );
146
			add_action( 'admin_footer-edit.php', array( $this, 'export_form' ) );
147
			add_action( 'admin_menu',            array( $this, 'admin_menu' ) );
148
			add_action( 'current_screen',        array( $this, 'unread_count' ) );
149
		}
150
151
		// custom post type we'll use to keep copies of the feedback items
152
		register_post_type( 'feedback', array(
153
			'labels'            => array(
154
				'name'               => __( 'Feedback', 'jetpack' ),
155
				'singular_name'      => __( 'Feedback', 'jetpack' ),
156
				'search_items'       => __( 'Search Feedback', 'jetpack' ),
157
				'not_found'          => __( 'No feedback found', 'jetpack' ),
158
				'not_found_in_trash' => __( 'No feedback found', 'jetpack' ),
159
			),
160
			'menu_icon'         	=> 'dashicons-feedback',
161
			'show_ui'           	=> TRUE,
162
			'show_in_admin_bar' 	=> FALSE,
163
			'public'            	=> FALSE,
164
			'rewrite'           	=> FALSE,
165
			'query_var'         	=> FALSE,
166
			'capability_type'   	=> 'page',
167
			'show_in_rest'      	=> true,
168
			'rest_controller_class' => 'Grunion_Contact_Form_Endpoint',
169
			'capabilities'			=> array(
170
				'create_posts'        => false,
171
				'publish_posts'       => 'publish_pages',
172
				'edit_posts'          => 'edit_pages',
173
				'edit_others_posts'   => 'edit_others_pages',
174
				'delete_posts'        => 'delete_pages',
175
				'delete_others_posts' => 'delete_others_pages',
176
				'read_private_posts'  => 'read_private_pages',
177
				'edit_post'           => 'edit_page',
178
				'delete_post'         => 'delete_page',
179
				'read_post'           => 'read_page',
180
			),
181
			'map_meta_cap'			=> true,
182
		) );
183
184
		// Add to REST API post type whitelist
185
		add_filter( 'rest_api_allowed_post_types', array( $this, 'allow_feedback_rest_api_type' ) );
186
187
		// Add "spam" as a post status
188
		register_post_status( 'spam', array(
189
			'label'                  => 'Spam',
190
			'public'                 => false,
191
			'exclude_from_search'    => true,
192
			'show_in_admin_all_list' => false,
193
			'label_count'            => _n_noop( 'Spam <span class="count">(%s)</span>', 'Spam <span class="count">(%s)</span>', 'jetpack' ),
194
			'protected'              => true,
195
			'_builtin'               => false,
196
		) );
197
198
		// POST handler
199
		if (
200
			isset( $_SERVER['REQUEST_METHOD'] ) && 'POST' == strtoupper( $_SERVER['REQUEST_METHOD'] )
201
		&&
202
			isset( $_POST['action'] ) && 'grunion-contact-form' == $_POST['action']
203
		&&
204
			isset( $_POST['contact-form-id'] )
205
		) {
206
			add_action( 'template_redirect', array( $this, 'process_form_submission' ) );
207
		}
208
209
		/*
210
		 Can be dequeued by placing the following in wp-content/themes/yourtheme/functions.php
211
		 *
212
		 * 	function remove_grunion_style() {
213
		 *		wp_deregister_style('grunion.css');
214
		 *	}
215
		 *	add_action('wp_print_styles', 'remove_grunion_style');
216
		 */
217
		wp_register_style( 'grunion.css', GRUNION_PLUGIN_URL . 'css/grunion.css', array(), JETPACK__VERSION );
218
		wp_style_add_data( 'grunion.css', 'rtl', 'replace' );
219
	}
220
221
	/**
222
	 * Add the 'Export' menu item as a submenu of Feedback.
223
	 */
224
	public function admin_menu() {
225
		add_submenu_page(
226
			'edit.php?post_type=feedback',
227
			__( 'Export feedback as CSV', 'jetpack' ),
228
			__( 'Export CSV', 'jetpack' ),
229
			'export',
230
			'feedback-export',
231
			array( $this, 'export_form' )
232
		);
233
	}
234
235
	/**
236
	 * Add to REST API post type whitelist
237
	 */
238
	function allow_feedback_rest_api_type( $post_types ) {
239
		$post_types[] = 'feedback';
240
		return $post_types;
241
	}
242
243
	/**
244
	 * Display the count of new feedback entries received. It's reset when user visits the Feedback screen.
245
	 *
246
	 * @since 4.1.0
247
	 *
248
	 * @param object $screen Information about the current screen.
249
	 */
250
	function unread_count( $screen ) {
251
		if ( isset( $screen->post_type ) && 'feedback' == $screen->post_type ) {
252
			update_option( 'feedback_unread_count', 0 );
253
		} else {
254
			global $menu;
255
			if ( isset( $menu ) && is_array( $menu ) && ! empty( $menu ) ) {
256
				foreach ( $menu as $index => $menu_item ) {
257
					if ( 'edit.php?post_type=feedback' == $menu_item[2] ) {
258
						$unread = get_option( 'feedback_unread_count', 0 );
259
						if ( $unread > 0 ) {
260
							$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>' : '';
261
							$menu[ $index ][0] .= $unread_count;
262
						}
263
						break;
264
					}
265
				}
266
			}
267
		}
268
	}
269
270
	/**
271
	 * Handles all contact-form POST submissions
272
	 *
273
	 * Conditionally attached to `template_redirect`
274
	 */
275
	function process_form_submission() {
276
		// Add a filter to replace tokens in the subject field with sanitized field values
277
		add_filter( 'contact_form_subject', array( $this, 'replace_tokens_with_input' ), 10, 2 );
278
279
		$id = stripslashes( $_POST['contact-form-id'] );
280
		$hash = isset( $_POST['contact-form-hash'] ) ? $_POST['contact-form-hash'] : null;
281
		$hash = preg_replace( '/[^\da-f]/i', '', $hash );
282
283
		if ( is_user_logged_in() ) {
284
			check_admin_referer( "contact-form_{$id}" );
285
		}
286
287
		$is_widget = 0 === strpos( $id, 'widget-' );
288
289
		$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...
290
291
		if ( $is_widget ) {
292
			// It's a form embedded in a text widget
293
			$this->current_widget_id = substr( $id, 7 ); // remove "widget-"
294
			$widget_type = implode( '-', array_slice( explode( '-', $this->current_widget_id ), 0, -1 ) ); // Remove trailing -#
295
296
			// Is the widget active?
297
			$sidebar = is_active_widget( false, $this->current_widget_id, $widget_type );
298
299
			// This is lame - no core API for getting a widget by ID
300
			$widget = isset( $GLOBALS['wp_registered_widgets'][ $this->current_widget_id ] ) ? $GLOBALS['wp_registered_widgets'][ $this->current_widget_id ] : false;
301
302
			if ( $sidebar && $widget && isset( $widget['callback'] ) ) {
303
				// prevent PHP notices by populating widget args
304
				$widget_args = array(
305
					'before_widget' => '',
306
					'after_widget' => '',
307
					'before_title' => '',
308
					'after_title' => '',
309
				);
310
				// This is lamer - no API for outputting a given widget by ID
311
				ob_start();
312
				// Process the widget to populate Grunion_Contact_Form::$last
313
				call_user_func( $widget['callback'], $widget_args, $widget['params'][0] );
314
				ob_end_clean();
315
			}
316
		} else {
317
			// It's a form embedded in a post
318
			$post = get_post( $id );
319
320
			// Process the content to populate Grunion_Contact_Form::$last
321
			/** This filter is already documented in core. wp-includes/post-template.php */
322
			apply_filters( 'the_content', $post->post_content );
323
		}
324
325
		$form = isset( Grunion_Contact_Form::$forms[ $hash ] ) ? Grunion_Contact_Form::$forms[ $hash ] : null;
0 ignored issues
show
Bug introduced by
The property forms 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...
326
327
		// No form may mean user is using do_shortcode, grab the form using the stored post meta
328
		if ( ! $form ) {
329
330
			// Get shortcode from post meta
331
			$shortcode = get_post_meta( $_POST['contact-form-id'], "_g_feedback_shortcode_{$hash}", true );
332
333
			// Format it
334
			if ( $shortcode != '' ) {
335
336
				// Get attributes from post meta.
337
				$parameters = '';
338
				$attributes = get_post_meta( $_POST['contact-form-id'], "_g_feedback_shortcode_atts_{$hash}", true );
339
				if ( ! empty( $attributes ) && is_array( $attributes ) ) {
340
					foreach( array_filter( $attributes ) as $param => $value  ) {
341
						$parameters .= " $param=\"$value\"";
342
					}
343
				}
344
345
				$shortcode = '[contact-form' . $parameters . ']' . $shortcode . '[/contact-form]';
346
				do_shortcode( $shortcode );
347
348
				// Recreate form
349
				$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...
350
			}
351
352
			if ( ! $form ) {
353
				return false;
354
			}
355
		}
356
357
		if ( is_wp_error( $form->errors ) && $form->errors->get_error_codes() ) {
358
			return $form->errors;
359
		}
360
361
		// Process the form
362
		return $form->process_submission();
363
	}
364
365
	function ajax_request() {
366
		$submission_result = self::process_form_submission();
367
368
		if ( ! $submission_result ) {
369
			header( 'HTTP/1.1 500 Server Error', 500, true );
370
			echo '<div class="form-error"><ul class="form-errors"><li class="form-error-message">';
371
			esc_html_e( 'An error occurred. Please try again later.', 'jetpack' );
372
			echo '</li></ul></div>';
373
		} elseif ( is_wp_error( $submission_result ) ) {
374
			header( 'HTTP/1.1 400 Bad Request', 403, true );
375
			echo '<div class="form-error"><ul class="form-errors"><li class="form-error-message">';
376
			echo esc_html( $submission_result->get_error_message() );
377
			echo '</li></ul></div>';
378
		} else {
379
			echo '<h3>' . esc_html__( 'Message Sent', 'jetpack' ) . '</h3>' . $submission_result;
380
		}
381
382
		die;
383
	}
384
385
	/**
386
	 * Ensure the post author is always zero for contact-form feedbacks
387
	 * Attached to `wp_insert_post_data`
388
	 *
389
	 * @see Grunion_Contact_Form::process_submission()
390
	 *
391
	 * @param array $data the data to insert
392
	 * @param array $postarr the data sent to wp_insert_post()
393
	 * @return array The filtered $data to insert
394
	 */
395
	function insert_feedback_filter( $data, $postarr ) {
396
		if ( $data['post_type'] == 'feedback' && $postarr['post_type'] == 'feedback' ) {
397
			$data['post_author'] = 0;
398
		}
399
400
		return $data;
401
	}
402
	/*
403
	 * Adds our contact-form shortcode
404
	 * The "child" contact-field shortcode is enabled as needed by the contact-form shortcode handler
405
	 */
406
	function add_shortcode() {
407
		add_shortcode( 'contact-form',         array( 'Grunion_Contact_Form', 'parse' ) );
408
		add_shortcode( 'contact-field',        array( 'Grunion_Contact_Form', 'parse_contact_field' ) );
409
	}
410
411
	static function tokenize_label( $label ) {
412
		return '{' . trim( preg_replace( '#^\d+_#', '', $label ) ) . '}';
413
	}
414
415
	static function sanitize_value( $value ) {
416
		return preg_replace( '=((<CR>|<LF>|0x0A/%0A|0x0D/%0D|\\n|\\r)\S).*=i', null, $value );
417
	}
418
419
	/**
420
	 * Replaces tokens like {city} or {City} (case insensitive) with the value
421
	 * of an input field of that name
422
	 *
423
	 * @param string $subject
424
	 * @param array  $field_values Array with field label => field value associations
425
	 *
426
	 * @return string The filtered $subject with the tokens replaced
427
	 */
428
	function replace_tokens_with_input( $subject, $field_values ) {
429
		// Wrap labels into tokens (inside {})
430
		$wrapped_labels = array_map( array( 'Grunion_Contact_Form_Plugin', 'tokenize_label' ), array_keys( $field_values ) );
431
		// Sanitize all values
432
		$sanitized_values = array_map( array( 'Grunion_Contact_Form_Plugin', 'sanitize_value' ), array_values( $field_values ) );
433
434
		foreach ( $sanitized_values as $k => $sanitized_value ) {
435
			if ( is_array( $sanitized_value ) ) {
436
				$sanitized_values[ $k ] = implode( ', ', $sanitized_value );
437
			}
438
		}
439
440
		// Search for all valid tokens (based on existing fields) and replace with the field's value
441
		$subject = str_ireplace( $wrapped_labels, $sanitized_values, $subject );
442
		return $subject;
443
	}
444
445
	/**
446
	 * Tracks the widget currently being processed.
447
	 * Attached to `dynamic_sidebar`
448
	 *
449
	 * @see $current_widget_id
450
	 *
451
	 * @param array $widget The widget data
452
	 */
453
	function track_current_widget( $widget ) {
454
		$this->current_widget_id = $widget['id'];
455
	}
456
457
	/**
458
	 * Adds a "widget" attribute to every contact-form embedded in a text widget.
459
	 * Used to tell the difference between post-embedded contact-forms and widget-embedded contact-forms
460
	 * Attached to `widget_text`
461
	 *
462
	 * @param string $text The widget text
463
	 * @return string The filtered widget text
464
	 */
465
	function widget_atts( $text ) {
466
		Grunion_Contact_Form::style( true );
467
468
		return preg_replace( '/\[contact-form([^a-zA-Z_-])/', '[contact-form widget="' . $this->current_widget_id . '"\\1', $text );
469
	}
470
471
	/**
472
	 * For sites where text widgets are not processed for shortcodes, we add this hack to process just our shortcode
473
	 * Attached to `widget_text`
474
	 *
475
	 * @param string $text The widget text
476
	 * @return string The contact-form filtered widget text
477
	 */
478
	function widget_shortcode_hack( $text ) {
479
		if ( ! preg_match( '/\[contact-form([^a-zA-Z_-])/', $text ) ) {
480
			return $text;
481
		}
482
483
		$old = $GLOBALS['shortcode_tags'];
484
		remove_all_shortcodes();
485
		Grunion_Contact_Form_Plugin::$using_contact_form_field = true;
486
		$this->add_shortcode();
487
488
		$text = do_shortcode( $text );
489
490
		Grunion_Contact_Form_Plugin::$using_contact_form_field = false;
491
		$GLOBALS['shortcode_tags'] = $old;
492
493
		return $text;
494
	}
495
496
	/**
497
	 * Populate an array with all values necessary to submit a NEW contact-form feedback to Akismet.
498
	 * Note that this includes the current user_ip etc, so this should only be called when accepting a new item via $_POST
499
	 *
500
	 * @param array $form Contact form feedback array
501
	 * @return array feedback array with additional data ready for submission to Akismet
502
	 */
503
	function prepare_for_akismet( $form ) {
504
		$form['comment_type'] = 'contact_form';
505
		$form['user_ip']      = $_SERVER['REMOTE_ADDR'];
506
		$form['user_agent']   = $_SERVER['HTTP_USER_AGENT'];
507
		$form['referrer']     = $_SERVER['HTTP_REFERER'];
508
		$form['blog']         = get_option( 'home' );
509
510
		foreach ( $_SERVER as $key => $value ) {
511
			if ( ! is_string( $value ) ) {
512
				continue;
513
			}
514
			if ( in_array( $key, array( 'HTTP_COOKIE', 'HTTP_COOKIE2', 'HTTP_USER_AGENT', 'HTTP_REFERER' ) ) ) {
515
				// We don't care about cookies, and the UA and Referrer were caught above.
516
				continue;
517
			} elseif ( in_array( $key, array( 'REMOTE_ADDR', 'REQUEST_URI', 'DOCUMENT_URI' ) ) ) {
518
				// All three of these are relevant indicators and should be passed along.
519
				$form[ $key ] = $value;
520
			} elseif ( wp_startswith( $key, 'HTTP_' ) ) {
521
				// Any other HTTP header indicators.
522
				// `wp_startswith()` is a wpcom helper function and is included in Jetpack via `functions.compat.php`
523
				$form[ $key ] = $value;
524
			}
525
		}
526
527
		return $form;
528
	}
529
530
	/**
531
	 * Submit contact-form data to Akismet to check for spam.
532
	 * If you're accepting a new item via $_POST, run it Grunion_Contact_Form_Plugin::prepare_for_akismet() first
533
	 * Attached to `jetpack_contact_form_is_spam`
534
	 *
535
	 * @param bool  $is_spam
536
	 * @param array $form
537
	 * @return bool|WP_Error TRUE => spam, FALSE => not spam, WP_Error => stop processing entirely
538
	 */
539
	function is_spam_akismet( $is_spam, $form = array() ) {
540
		global $akismet_api_host, $akismet_api_port;
541
542
		// The signature of this function changed from accepting just $form.
543
		// If something only sends an array, assume it's still using the old
544
		// signature and work around it.
545
		if ( empty( $form ) && is_array( $is_spam ) ) {
546
			$form = $is_spam;
547
			$is_spam = false;
548
		}
549
550
		// If a previous filter has alrady marked this as spam, trust that and move on.
551
		if ( $is_spam ) {
552
			return $is_spam;
553
		}
554
555
		if ( ! function_exists( 'akismet_http_post' ) && ! defined( 'AKISMET_VERSION' ) ) {
556
			return false;
557
		}
558
559
		$query_string = http_build_query( $form );
560
561
		if ( method_exists( 'Akismet', 'http_post' ) ) {
562
			$response = Akismet::http_post( $query_string, 'comment-check' );
563
		} else {
564
			$response = akismet_http_post( $query_string, $akismet_api_host, '/1.1/comment-check', $akismet_api_port );
565
		}
566
567
		$result = false;
568
569
		if ( isset( $response[0]['x-akismet-pro-tip'] ) && 'discard' === trim( $response[0]['x-akismet-pro-tip'] ) && get_option( 'akismet_strictness' ) === '1' ) {
570
			$result = new WP_Error( 'feedback-discarded', __( 'Feedback discarded.', 'jetpack' ) );
571
		} elseif ( isset( $response[1] ) && 'true' == trim( $response[1] ) ) { // 'true' is spam
572
			$result = true;
573
		}
574
575
		/**
576
		 * Filter the results returned by Akismet for each submitted contact form.
577
		 *
578
		 * @module contact-form
579
		 *
580
		 * @since 1.3.1
581
		 *
582
		 * @param WP_Error|bool $result Is the submitted feedback spam.
583
		 * @param array|bool $form Submitted feedback.
584
		 */
585
		return apply_filters( 'contact_form_is_spam_akismet', $result, $form );
586
	}
587
588
	/**
589
	 * Submit a feedback as either spam or ham
590
	 *
591
	 * @param string $as Either 'spam' or 'ham'.
592
	 * @param array  $form the contact-form data
593
	 */
594
	function akismet_submit( $as, $form ) {
595
		global $akismet_api_host, $akismet_api_port;
596
597
		if ( ! in_array( $as, array( 'ham', 'spam' ) ) ) {
598
			return false;
599
		}
600
601
		$query_string = '';
602
		if ( is_array( $form ) ) {
603
			$query_string = http_build_query( $form );
604
		}
605
		if ( method_exists( 'Akismet', 'http_post' ) ) {
606
			$response = Akismet::http_post( $query_string, "submit-{$as}" );
607
		} else {
608
			$response = akismet_http_post( $query_string, $akismet_api_host, "/1.1/submit-{$as}", $akismet_api_port );
609
		}
610
611
		return trim( $response[1] );
612
	}
613
614
	/**
615
	 * Prints the menu
616
	 */
617
	function export_form() {
618
		$current_screen = get_current_screen();
619
		if ( ! in_array( $current_screen->id, array( 'edit-feedback', 'feedback_page_feedback-export' ) ) ) {
620
			return;
621
		}
622
623
		if ( ! current_user_can( 'export' ) ) {
624
			return;
625
		}
626
627
		// if there aren't any feedbacks, bail out
628
		if ( ! (int) wp_count_posts( 'feedback' )->publish ) {
629
			return;
630
		}
631
		?>
632
633
		<div id="feedback-export" style="display:none">
634
			<h2><?php _e( 'Export feedback as CSV', 'jetpack' ) ?></h2>
635
			<div class="clear"></div>
636
			<form action="<?php echo admin_url( 'admin-post.php' ); ?>" method="post" class="form">
637
				<?php wp_nonce_field( 'feedback_export','feedback_export_nonce' ); ?>
638
639
				<input name="action" value="feedback_export" type="hidden">
640
				<label for="post"><?php _e( 'Select feedback to download', 'jetpack' ) ?></label>
641
				<select name="post">
642
					<option value="all"><?php esc_html_e( 'All posts', 'jetpack' ) ?></option>
643
					<?php echo $this->get_feedbacks_as_options() ?>
644
				</select>
645
646
				<br><br>
647
				<input type="submit" name="submit" id="submit" class="button button-primary" value="<?php esc_html_e( 'Download', 'jetpack' ); ?>">
648
			</form>
649
		</div>
650
651
		<?php
652
		// There aren't any usable actions in core to output the "export feedback" form in the correct place,
653
		// so this inline JS moves it from the top of the page to the bottom.
654
		?>
655
		<script type='text/javascript'>
656
		var menu = document.getElementById( 'feedback-export' ),
657
		wrapper = document.getElementsByClassName( 'wrap' )[0];
658
		<?php if ( 'edit-feedback' === $current_screen->id ) : ?>
659
		wrapper.appendChild(menu);
660
		<?php endif; ?>
661
		menu.style.display = 'block';
662
		</script>
663
		<?php
664
	}
665
666
	/**
667
	 * Fetch post content for a post and extract just the comment.
668
	 *
669
	 * @param int $post_id The post id to fetch the content for.
670
	 *
671
	 * @return string Trimmed post comment.
672
	 *
673
	 * @codeCoverageIgnore
674
	 */
675
	public function get_post_content_for_csv_export( $post_id ) {
676
		$post_content = get_post_field( 'post_content', $post_id );
677
		$content      = explode( '<!--more-->', $post_content );
678
679
		return trim( $content[0] );
680
	}
681
682
	/**
683
	 * Get `_feedback_extra_fields` field from post meta data.
684
	 *
685
	 * @param int $post_id Id of the post to fetch meta data for.
686
	 *
687
	 * @return mixed
688
	 *
689
	 * @codeCoverageIgnore - No need to be covered.
690
	 */
691
	public function get_post_meta_for_csv_export( $post_id ) {
692
		return get_post_meta( $post_id, '_feedback_extra_fields', true );
693
	}
694
695
	/**
696
	 * Get parsed feedback post fields.
697
	 *
698
	 * @param int $post_id Id of the post to fetch parsed contents for.
699
	 *
700
	 * @return array
701
	 *
702
	 * @codeCoverageIgnore - No need to be covered.
703
	 */
704
	public function get_parsed_field_contents_of_post( $post_id ) {
705
		return self::parse_fields_from_content( $post_id );
706
	}
707
708
	/**
709
	 * Properly maps fields that are missing from the post meta data
710
	 * to names, that are similar to those of the post meta.
711
	 *
712
	 * @param array $parsed_post_content Parsed post content
713
	 *
714
	 * @see parse_fields_from_content for how the input data is generated.
715
	 *
716
	 * @return array Mapped fields.
717
	 */
718
	public function map_parsed_field_contents_of_post_to_field_names( $parsed_post_content ) {
719
720
		$mapped_fields = array();
721
722
		$field_mapping = array(
723
			'_feedback_subject'      => __( 'Contact Form', 'jetpack' ),
724
			'_feedback_author'       => '1_Name',
725
			'_feedback_author_email' => '2_Email',
726
			'_feedback_author_url'   => '3_Website',
727
			'_feedback_main_comment' => '4_Comment',
728
		);
729
730
		foreach ( $field_mapping as $parsed_field_name => $field_name ) {
731
			if (
732
				isset( $parsed_post_content[ $parsed_field_name ] )
733
				&& ! empty( $parsed_post_content[ $parsed_field_name ] )
734
			) {
735
				$mapped_fields[ $field_name ] = $parsed_post_content[ $parsed_field_name ];
736
			}
737
		}
738
739
		return $mapped_fields;
740
	}
741
742
	/**
743
	 * Registers the personal data exporter.
744
	 *
745
	 * @since 6.1.1
746
	 *
747
	 * @param  array $exporters An array of personal data exporters.
748
	 *
749
	 * @return array $exporters An array of personal data exporters.
750
	 */
751
	public function register_personal_data_exporter( $exporters ) {
752
		$exporters['jetpack-feedback'] = array(
753
			'exporter_friendly_name' => __( 'Feedback', 'jetpack' ),
754
			'callback'               => array( $this, 'personal_data_exporter' ),
755
		);
756
757
		return $exporters;
758
	}
759
760
	/**
761
	 * Registers the personal data eraser.
762
	 *
763
	 * @since 6.1.1
764
	 *
765
	 * @param  array $erasers An array of personal data erasers.
766
	 *
767
	 * @return array $erasers An array of personal data erasers.
768
	 */
769
	public function register_personal_data_eraser( $erasers ) {
770
		$erasers['jetpack-feedback'] = array(
771
			'eraser_friendly_name' => __( 'Feedback', 'jetpack' ),
772
			'callback'             => array( $this, 'personal_data_eraser' ),
773
		);
774
775
		return $erasers;
776
	}
777
778
	/**
779
	 * Exports personal data.
780
	 *
781
	 * @since 6.1.1
782
	 *
783
	 * @param  string $email  Email address.
784
	 * @param  int    $page   Page to export.
785
	 *
786
	 * @return array  $return Associative array with keys expected by core.
787
	 */
788
	public function personal_data_exporter( $email, $page = 1 ) {
789
		$per_page    = 250;
790
		$export_data = array();
791
		$post_ids    = $this->personal_data_post_ids_by_email( $email, $per_page, $page );
792
793
		foreach ( $post_ids as $post_id ) {
794
			$post_fields = $this->get_parsed_field_contents_of_post( $post_id );
795
796
			if ( ! is_array( $post_fields ) || empty( $post_fields['_feedback_subject'] ) ) {
797
				continue; // Corrupt data.
798
			}
799
800
			$post_fields['_feedback_main_comment'] = $this->get_post_content_for_csv_export( $post_id );
801
			$post_fields                           = $this->map_parsed_field_contents_of_post_to_field_names( $post_fields );
802
803
			if ( ! is_array( $post_fields ) || empty( $post_fields ) ) {
804
				continue; // No fields to export.
805
			}
806
807
			$post_meta   = $this->get_post_meta_for_csv_export( $post_id );
808
			$post_meta   = is_array( $post_meta ) ? $post_meta : array();
809
810
			$post_export_data = array();
811
			$post_data        = array_merge( $post_fields, $post_meta );
812
			ksort( $post_data );
813
814
			foreach ( $post_data as $post_data_key => $post_data_value ) {
815
				$post_export_data[] = array(
816
					'name'  => preg_replace( '/^[0-9]+_/', '', $post_data_key ),
817
					'value' => $post_data_value,
818
				);
819
			}
820
821
			$export_data[] = array(
822
				'group_id'    => 'feedback',
823
				'group_label' => __( 'Feedback', 'jetpack' ),
824
				'item_id'     => 'feedback-' . $post_id,
825
				'data'        => $post_export_data,
826
			);
827
		}
828
829
		return array(
830
			'data' => $export_data,
831
			'done' => count( $post_ids ) < $per_page,
832
		);
833
	}
834
835
	/**
836
	 * Erases personal data.
837
	 *
838
	 * @since 6.1.1
839
	 *
840
	 * @param  string $email Email address.
841
	 * @param  int    $page  Page to erase.
842
	 *
843
	 * @return array         Associative array with keys expected by core.
844
	 */
845
	public function personal_data_eraser( $email, $page = 1 ) {
846
		$per_page = 250;
847
		$removed  = false;
848
		$retained = false;
849
		$messages = array();
850
		$option_name = sprintf( '_jetpack_pde_feedback_%s', md5( $email ) );
851
		$last_post_id = 1 === $page ? 0 : get_option( $option_name, 0 );
852
		$post_ids = $this->personal_data_post_ids_by_email( $email, $per_page, $page, $last_post_id );
853
854
		foreach ( $post_ids as $post_id ) {
855
			/**
856
			 * Filters whether to erase a particular Feedback post.
857
			 *
858
			 * @since 6.3.0
859
			 *
860
			 * @param bool|string $prevention_message Whether to apply erase the Feedback post (bool).
861
			 *                                        Custom prevention message (string). Default true.
862
			 * @param int         $post_id            Feedback post ID.
863
			 */
864
			$prevention_message = apply_filters( 'grunion_contact_form_delete_feedback_post', true, $post_id );
865
866
			if ( true !== $prevention_message ) {
867
				if ( $prevention_message && is_string( $prevention_message ) ) {
868
					$messages[] = esc_html( $prevention_message );
869
				} else {
870
					$messages[] = sprintf(
871
						// translators: %d: Post ID.
872
						__( 'Feedback ID %d could not be removed at this time.', 'jetpack' ),
873
						$post_id
874
					);
875
				}
876
877
				$retained = true;
878
879
				continue;
880
			}
881
882
			if ( wp_delete_post( $post_id, true ) ) {
883
				$removed = true;
884
			} else {
885
				$retained = true;
886
				$messages[] = sprintf(
887
					// translators: %d: Post ID.
888
					__( 'Feedback ID %d could not be removed at this time.', 'jetpack' ),
889
					$post_id
890
				);
891
			}
892
		}
893
894
		$done = count( $post_ids ) < $per_page;
895
896
		if ( $done ) {
897
			delete_option( $option_name );
898
		} else {
899
			update_option( $option_name, (int) $post_id );
0 ignored issues
show
Bug introduced by
The variable $post_id seems to be defined by a foreach iteration on line 854. Are you sure the iterator is never empty, otherwise this variable is not defined?

It seems like you are relying on a variable being defined by an iteration:

foreach ($a as $b) {
}

// $b is defined here only if $a has elements, for example if $a is array()
// then $b would not be defined here. To avoid that, we recommend to set a
// default value for $b.


// Better
$b = 0; // or whatever default makes sense in your context
foreach ($a as $b) {
}

// $b is now guaranteed to be defined here.
Loading history...
900
		}
901
902
		return array(
903
			'items_removed'  => $removed,
904
			'items_retained' => $retained,
905
			'messages'       => $messages,
906
			'done'           => $done,
907
		);
908
	}
909
910
	/**
911
	 * Queries personal data by email address.
912
	 *
913
	 * @since 6.1.1
914
	 *
915
	 * @param  string $email        Email address.
916
	 * @param  int    $per_page     Post IDs per page. Default is `250`.
917
	 * @param  int    $page         Page to query. Default is `1`.
918
	 * @param  int    $last_post_id Page to query. Default is `0`. If non-zero, used instead of $page.
919
	 *
920
	 * @return array An array of post IDs.
921
	 */
922
	public function personal_data_post_ids_by_email( $email, $per_page = 250, $page = 1, $last_post_id = 0 ) {
923
		add_filter( 'posts_search', array( $this, 'personal_data_search_filter' ) );
924
925
		$post_ids = get_posts( array(
926
			'post_type'        => 'feedback',
927
			'post_status'      => 'publish',
928
			's'                => 'AUTHOR EMAIL: ' . $email,
929
			'sentence'         => true,
930
			'order'            => 'ASC',
931
			'orderby'          => 'ID',
932
			'fields'           => 'ids',
933
			'posts_per_page'   => $per_page,
934
			'paged'            => $last_post_id ? 1 : $page,
935
			'suppress_filters' => false,
936
		) );
937
938
		remove_filter( 'posts_search', array( $this, 'personal_data_search_filter' ) );
939
940
		return $post_ids;
941
	}
942
943
	/**
944
	 * Filters searches by email address.
945
	 *
946
	 * @since 6.1.1
947
	 *
948
	 * @param  string $search SQL where clause.
949
	 *
950
	 * @return array          Filtered SQL where clause.
951
	 */
952
	public function personal_data_search_filter( $search ) {
953
		global $wpdb;
954
955
		/*
956
		 * Limits search to `post_content` only, and we only match the
957
		 * author's email address whenever it's on a line by itself.
958
		 * `CHAR(13)` = `\r`, `CHAR(10)` = `\n`
959
		 */
960
		if ( preg_match( '/AUTHOR EMAIL\: ([^{\s]+)/', $search, $m ) ) {
961
			$option_name = sprintf( '_jetpack_pde_feedback_%s', md5( $m[1] ) );
962
			$last_post_id = get_option( $option_name, 0 );
963
964
			$esc_like_email = esc_sql( $wpdb->esc_like( 'AUTHOR EMAIL: ' . $m[1] ) );
965
			$search         = " AND (
966
				{$wpdb->posts}.post_content LIKE CONCAT('%', CHAR(13), '{$esc_like_email}', CHAR(13), '%')
967
				OR {$wpdb->posts}.post_content LIKE CONCAT('%', CHAR(10), '{$esc_like_email}', CHAR(10), '%')
968
			)";
969
970
			if ( $last_post_id ) {
971
				$search .= $wpdb->prepare( " AND {$wpdb->posts}.ID > %d", $last_post_id );
972
			}
973
		}
974
975
		return $search;
976
	}
977
978
	/**
979
	 * Prepares feedback post data for CSV export.
980
	 *
981
	 * @param array $post_ids Post IDs to fetch the data for. These need to be Feedback posts.
982
	 *
983
	 * @return array
984
	 */
985
	public function get_export_data_for_posts( $post_ids ) {
986
987
		$posts_data  = array();
988
		$field_names = array();
989
		$result      = array();
990
991
		/**
992
		 * Fetch posts and get the possible field names for later use
993
		 */
994
		foreach ( $post_ids as $post_id ) {
995
996
			/**
997
			 * Fetch post main data, because we need the subject and author data for the feedback form.
998
			 */
999
			$post_real_data = $this->get_parsed_field_contents_of_post( $post_id );
1000
1001
			/**
1002
			 * If `$post_real_data` is not an array or there is no `_feedback_subject` set,
1003
			 * then something must be wrong with the feedback post. Skip it.
1004
			 */
1005
			if ( ! is_array( $post_real_data ) || ! isset( $post_real_data['_feedback_subject'] ) ) {
1006
				continue;
1007
			}
1008
1009
			/**
1010
			 * Fetch main post comment. This is from the default textarea fields.
1011
			 * If it is non-empty, then we add it to data, otherwise skip it.
1012
			 */
1013
			$post_comment_content = $this->get_post_content_for_csv_export( $post_id );
1014
			if ( ! empty( $post_comment_content ) ) {
1015
				$post_real_data['_feedback_main_comment'] = $post_comment_content;
1016
			}
1017
1018
			/**
1019
			 * Map parsed fields to proper field names
1020
			 */
1021
			$mapped_fields = $this->map_parsed_field_contents_of_post_to_field_names( $post_real_data );
1022
1023
			/**
1024
			 * Fetch post meta data.
1025
			 */
1026
			$post_meta_data = $this->get_post_meta_for_csv_export( $post_id );
1027
1028
			/**
1029
			 * If `$post_meta_data` is not an array or if it is empty, then there is no
1030
			 * extra feedback to work with. Create an empty array.
1031
			 */
1032
			if ( ! is_array( $post_meta_data ) || empty( $post_meta_data ) ) {
1033
				$post_meta_data = array();
1034
			}
1035
1036
			/**
1037
			 * Prepend the feedback subject to the list of fields.
1038
			 */
1039
			$post_meta_data = array_merge(
1040
				$mapped_fields,
1041
				$post_meta_data
1042
			);
1043
1044
			/**
1045
			 * Save post metadata for later usage.
1046
			 */
1047
			$posts_data[ $post_id ] = $post_meta_data;
1048
1049
			/**
1050
			 * Save field names, so we can use them as header fields later in the CSV.
1051
			 */
1052
			$field_names = array_merge( $field_names, array_keys( $post_meta_data ) );
1053
		}
1054
1055
		/**
1056
		 * Make sure the field names are unique, because we don't want duplicate data.
1057
		 */
1058
		$field_names = array_unique( $field_names );
1059
1060
		/**
1061
		 * Sort the field names by the field id number
1062
		 */
1063
		sort( $field_names, SORT_NUMERIC );
1064
1065
		/**
1066
		 * Loop through every post, which is essentially CSV row.
1067
		 */
1068
		foreach ( $posts_data as $post_id => $single_post_data ) {
1069
1070
			/**
1071
			 * Go through all the possible fields and check if the field is available
1072
			 * in the current post.
1073
			 *
1074
			 * If it is - add the data as a value.
1075
			 * If it is not - add an empty string, which is just a placeholder in the CSV.
1076
			 */
1077
			foreach ( $field_names as $single_field_name ) {
1078
				if (
1079
					isset( $single_post_data[ $single_field_name ] )
1080
					&& ! empty( $single_post_data[ $single_field_name ] )
1081
				) {
1082
					$result[ $single_field_name ][] = trim( $single_post_data[ $single_field_name ] );
1083
				} else {
1084
					$result[ $single_field_name ][] = '';
1085
				}
1086
			}
1087
		}
1088
1089
		return $result;
1090
	}
1091
1092
	/**
1093
	 * download as a csv a contact form or all of them in a csv file
1094
	 */
1095
	function download_feedback_as_csv() {
1096
		if ( empty( $_POST['feedback_export_nonce'] ) ) {
1097
			return;
1098
		}
1099
1100
		check_admin_referer( 'feedback_export', 'feedback_export_nonce' );
1101
1102
		if ( ! current_user_can( 'export' ) ) {
1103
			return;
1104
		}
1105
1106
		$args = array(
1107
			'posts_per_page'   => -1,
1108
			'post_type'        => 'feedback',
1109
			'post_status'      => 'publish',
1110
			'order'            => 'ASC',
1111
			'fields'           => 'ids',
1112
			'suppress_filters' => false,
1113
		);
1114
1115
		$filename = date( 'Y-m-d' ) . '-feedback-export.csv';
1116
1117
		// Check if we want to download all the feedbacks or just a certain contact form
1118
		if ( ! empty( $_POST['post'] ) && $_POST['post'] !== 'all' ) {
1119
			$args['post_parent'] = (int) $_POST['post'];
1120
			$filename            = date( 'Y-m-d' ) . '-' . str_replace( '&nbsp;', '-', get_the_title( (int) $_POST['post'] ) ) . '.csv';
1121
		}
1122
1123
		$feedbacks = get_posts( $args );
1124
1125
		if ( empty( $feedbacks ) ) {
1126
			return;
1127
		}
1128
1129
		$filename  = sanitize_file_name( $filename );
1130
1131
		/**
1132
		 * Prepare data for export.
1133
		 */
1134
		$data = $this->get_export_data_for_posts( $feedbacks );
1135
1136
		/**
1137
		 * If `$data` is empty, there's nothing we can do below.
1138
		 */
1139
		if ( ! is_array( $data ) || empty( $data ) ) {
1140
			return;
1141
		}
1142
1143
		/**
1144
		 * Extract field names from `$data` for later use.
1145
		 */
1146
		$fields = array_keys( $data );
1147
1148
		/**
1149
		 * Count how many rows will be exported.
1150
		 */
1151
		$row_count = count( reset( $data ) );
1152
1153
		// Forces the download of the CSV instead of echoing
1154
		header( 'Content-Disposition: attachment; filename=' . $filename );
1155
		header( 'Pragma: no-cache' );
1156
		header( 'Expires: 0' );
1157
		header( 'Content-Type: text/csv; charset=utf-8' );
1158
1159
		$output = fopen( 'php://output', 'w' );
1160
1161
		/**
1162
		 * Print CSV headers
1163
		 */
1164
		fputcsv( $output, $fields );
1165
1166
		/**
1167
		 * Print rows to the output.
1168
		 */
1169
		for ( $i = 0; $i < $row_count; $i ++ ) {
1170
1171
			$current_row = array();
1172
1173
			/**
1174
			 * Put all the fields in `$current_row` array.
1175
			 */
1176
			foreach ( $fields as $single_field_name ) {
1177
				$current_row[] = $this->esc_csv( $data[ $single_field_name ][ $i ] );
1178
			}
1179
1180
			/**
1181
			 * Output the complete CSV row
1182
			 */
1183
			fputcsv( $output, $current_row );
1184
		}
1185
1186
		fclose( $output );
1187
	}
1188
1189
	/**
1190
	 * Escape a string to be used in a CSV context
1191
	 *
1192
	 * Malicious input can inject formulas into CSV files, opening up the possibility for phishing attacks and
1193
	 * disclosure of sensitive information.
1194
	 *
1195
	 * Additionally, Excel exposes the ability to launch arbitrary commands through the DDE protocol.
1196
	 *
1197
	 * @see http://www.contextis.com/resources/blog/comma-separated-vulnerabilities/
1198
	 *
1199
	 * @param string $field
1200
	 *
1201
	 * @return string
1202
	 */
1203
	public function esc_csv( $field ) {
1204
		$active_content_triggers = array( '=', '+', '-', '@' );
1205
1206
		if ( in_array( mb_substr( $field, 0, 1 ), $active_content_triggers, true ) ) {
1207
			$field = "'" . $field;
1208
		}
1209
1210
		return $field;
1211
	}
1212
1213
	/**
1214
	 * Returns a string of HTML <option> items from an array of posts
1215
	 *
1216
	 * @return string a string of HTML <option> items
1217
	 */
1218
	protected function get_feedbacks_as_options() {
1219
		$options = '';
1220
1221
		// Get the feedbacks' parents' post IDs
1222
		$feedbacks = get_posts( array(
1223
			'fields'           => 'id=>parent',
1224
			'posts_per_page'   => 100000,
1225
			'post_type'        => 'feedback',
1226
			'post_status'      => 'publish',
1227
			'suppress_filters' => false,
1228
		) );
1229
		$parents = array_unique( array_values( $feedbacks ) );
1230
1231
		$posts = get_posts( array(
1232
			'orderby'          => 'ID',
1233
			'posts_per_page'   => 1000,
1234
			'post_type'        => 'any',
1235
			'post__in'         => array_values( $parents ),
1236
			'suppress_filters' => false,
1237
		) );
1238
1239
		// creates the string of <option> elements
1240
		foreach ( $posts as $post ) {
1241
			$options .= sprintf( '<option value="%s">%s</option>', esc_attr( $post->ID ), esc_html( $post->post_title ) );
1242
		}
1243
1244
		return $options;
1245
	}
1246
1247
	/**
1248
	 * Get the names of all the form's fields
1249
	 *
1250
	 * @param  array|int $posts the post we want the fields of
1251
	 *
1252
	 * @return array     the array of fields
1253
	 *
1254
	 * @deprecated As this is no longer necessary as of the CSV export rewrite. - 2015-12-29
1255
	 */
1256
	protected function get_field_names( $posts ) {
1257
		$posts = (array) $posts;
1258
		$all_fields = array();
1259
1260
		foreach ( $posts as $post ) {
1261
			$fields = self::parse_fields_from_content( $post );
1262
1263
			if ( isset( $fields['_feedback_all_fields'] ) ) {
1264
				$extra_fields = array_keys( $fields['_feedback_all_fields'] );
1265
				$all_fields = array_merge( $all_fields, $extra_fields );
1266
			}
1267
		}
1268
1269
		$all_fields = array_unique( $all_fields );
1270
		return $all_fields;
1271
	}
1272
1273
	public static function parse_fields_from_content( $post_id ) {
1274
		static $post_fields;
1275
1276
		if ( ! is_array( $post_fields ) ) {
1277
			$post_fields = array();
1278
		}
1279
1280
		if ( isset( $post_fields[ $post_id ] ) ) {
1281
			return $post_fields[ $post_id ];
1282
		}
1283
1284
		$all_values   = array();
1285
		$post_content = get_post_field( 'post_content', $post_id );
1286
		$content      = explode( '<!--more-->', $post_content );
1287
		$lines        = array();
1288
1289
		if ( count( $content ) > 1 ) {
1290
			$content  = str_ireplace( array( '<br />', ')</p>' ), '', $content[1] );
1291
			$one_line = preg_replace( '/\s+/', ' ', $content );
1292
			$one_line = preg_replace( '/.*Array \( (.*)\)/', '$1', $one_line );
1293
1294
			preg_match_all( '/\[([^\]]+)\] =\&gt\; ([^\[]+)/', $one_line, $matches );
1295
1296
			if ( count( $matches ) > 1 ) {
1297
				$all_values = array_combine( array_map( 'trim', $matches[1] ), array_map( 'trim', $matches[2] ) );
1298
			}
1299
1300
			$lines = array_filter( explode( "\n", $content ) );
1301
		}
1302
1303
		$var_map = array(
1304
			'AUTHOR'       => '_feedback_author',
1305
			'AUTHOR EMAIL' => '_feedback_author_email',
1306
			'AUTHOR URL'   => '_feedback_author_url',
1307
			'SUBJECT'      => '_feedback_subject',
1308
			'IP'           => '_feedback_ip',
1309
		);
1310
1311
		$fields = array();
1312
1313
		foreach ( $lines as $line ) {
1314
			$vars = explode( ': ', $line, 2 );
1315
			if ( ! empty( $vars ) ) {
1316
				if ( isset( $var_map[ $vars[0] ] ) ) {
1317
					$fields[ $var_map[ $vars[0] ] ] = self::strip_tags( trim( $vars[1] ) );
1318
				}
1319
			}
1320
		}
1321
1322
		$fields['_feedback_all_fields'] = $all_values;
1323
1324
		$post_fields[ $post_id ] = $fields;
1325
1326
		return $fields;
1327
	}
1328
1329
	/**
1330
	 * Creates a valid csv row from a post id
1331
	 *
1332
	 * @param  int   $post_id The id of the post
1333
	 * @param  array $fields  An array containing the names of all the fields of the csv
1334
	 * @return String The csv row
1335
	 *
1336
	 * @deprecated This is no longer needed, as of the CSV export rewrite.
1337
	 */
1338
	protected static function make_csv_row_from_feedback( $post_id, $fields ) {
1339
		$content_fields = self::parse_fields_from_content( $post_id );
1340
		$all_fields     = array();
1341
1342
		if ( isset( $content_fields['_feedback_all_fields'] ) ) {
1343
			$all_fields = $content_fields['_feedback_all_fields'];
1344
		}
1345
1346
		// Overwrite the parsed content with the content we stored in post_meta in a better format.
1347
		$extra_fields   = get_post_meta( $post_id, '_feedback_extra_fields', true );
1348
		foreach ( $extra_fields as $extra_field => $extra_value ) {
1349
			$all_fields[ $extra_field ] = $extra_value;
1350
		}
1351
1352
		// The first element in all of the exports will be the subject
1353
		$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...
1354
1355
		// Loop the fields array in order to fill the $row_items array correctly
1356
		foreach ( $fields as $field ) {
1357
			if ( $field === __( 'Contact Form', 'jetpack' ) ) { // the first field will ever be the contact form, so we can continue
1358
				continue;
1359
			} elseif ( array_key_exists( $field, $all_fields ) ) {
1360
				$row_items[] = $all_fields[ $field ];
1361
			} else { $row_items[] = '';
1362
			}
1363
		}
1364
1365
		return $row_items;
1366
	}
1367
1368
	public static function get_ip_address() {
1369
		return isset( $_SERVER['REMOTE_ADDR'] ) ? $_SERVER['REMOTE_ADDR'] : null;
1370
	}
1371
}
1372
1373
/**
1374
 * Generic shortcode class.
1375
 * Does nothing other than store structured data and output the shortcode as a string
1376
 *
1377
 * Not very general - specific to Grunion.
1378
 */
1379
class Crunion_Contact_Form_Shortcode {
1380
	/**
1381
	 * @var string the name of the shortcode: [$shortcode_name /]
1382
	 */
1383
	public $shortcode_name;
1384
1385
	/**
1386
	 * @var array key => value pairs for the shortcode's attributes: [$shortcode_name key="value" ... /]
1387
	 */
1388
	public $attributes;
1389
1390
	/**
1391
	 * @var array key => value pair for attribute defaults
1392
	 */
1393
	public $defaults = array();
1394
1395
	/**
1396
	 * @var null|string Null for selfclosing shortcodes.  Hhe inner content of otherwise: [$shortcode_name]$content[/$shortcode_name]
1397
	 */
1398
	public $content;
1399
1400
	/**
1401
	 * @var array Associative array of inner "child" shortcodes equivalent to the $content: [$shortcode_name][child 1/][child 2/][/$shortcode_name]
1402
	 */
1403
	public $fields;
1404
1405
	/**
1406
	 * @var null|string The HTML of the parsed inner "child" shortcodes".  Null for selfclosing shortcodes.
1407
	 */
1408
	public $body;
1409
1410
	/**
1411
	 * @param array       $attributes An associative array of shortcode attributes.  @see shortcode_atts()
1412
	 * @param null|string $content Null for selfclosing shortcodes.  The inner content otherwise.
1413
	 */
1414
	function __construct( $attributes, $content = null ) {
1415
		$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...
1416
		if ( is_array( $content ) ) {
1417
			$string_content = '';
1418
			foreach ( $content as $field ) {
1419
				$string_content .= (string) $field;
1420
			}
1421
1422
			$this->content = $string_content;
1423
		} else {
1424
			$this->content = $content;
1425
		}
1426
1427
		$this->parse_content( $this->content );
1428
	}
1429
1430
	/**
1431
	 * Processes the shortcode's inner content for "child" shortcodes
1432
	 *
1433
	 * @param string $content The shortcode's inner content: [shortcode]$content[/shortcode]
1434
	 */
1435
	function parse_content( $content ) {
1436
		if ( is_null( $content ) ) {
1437
			$this->body = null;
1438
		}
1439
1440
		$this->body = do_shortcode( $content );
1441
	}
1442
1443
	/**
1444
	 * Returns the value of the requested attribute.
1445
	 *
1446
	 * @param string $key The attribute to retrieve
1447
	 * @return mixed
1448
	 */
1449
	function get_attribute( $key ) {
1450
		return isset( $this->attributes[ $key ] ) ? $this->attributes[ $key ] : null;
1451
	}
1452
1453
	function esc_attr( $value ) {
1454
		if ( is_array( $value ) ) {
1455
			return array_map( array( $this, 'esc_attr' ), $value );
1456
		}
1457
1458
		$value = Grunion_Contact_Form_Plugin::strip_tags( $value );
1459
		$value = _wp_specialchars( $value, ENT_QUOTES, false, true );
1460
1461
		// Shortcode attributes can't contain "]"
1462
		$value = str_replace( ']', '', $value );
1463
		$value = str_replace( ',', '&#x002c;', $value ); // store commas encoded
1464
		$value = strtr( $value, array( '%' => '%25', '&' => '%26' ) );
1465
1466
		// shortcode_parse_atts() does stripcslashes()
1467
		$value = addslashes( $value );
1468
		return $value;
1469
	}
1470
1471
	function unesc_attr( $value ) {
1472
		if ( is_array( $value ) ) {
1473
			return array_map( array( $this, 'unesc_attr' ), $value );
1474
		}
1475
1476
		// For back-compat with old Grunion encoding
1477
		// Also, unencode commas
1478
		$value = strtr( $value, array( '%26' => '&', '%25' => '%' ) );
1479
		$value = preg_replace( array( '/&#x0*22;/i', '/&#x0*27;/i', '/&#x0*26;/i', '/&#x0*2c;/i' ), array( '"', "'", '&', ',' ), $value );
1480
		$value = htmlspecialchars_decode( $value, ENT_QUOTES );
1481
		$value = Grunion_Contact_Form_Plugin::strip_tags( $value );
1482
1483
		return $value;
1484
	}
1485
1486
	/**
1487
	 * Generates the shortcode
1488
	 */
1489
	function __toString() {
1490
		$r = "[{$this->shortcode_name} ";
1491
1492
		foreach ( $this->attributes as $key => $value ) {
1493
			if ( ! $value ) {
1494
				continue;
1495
			}
1496
1497
			if ( isset( $this->defaults[ $key ] ) && $this->defaults[ $key ] == $value ) {
1498
				continue;
1499
			}
1500
1501
			if ( 'id' == $key ) {
1502
				continue;
1503
			}
1504
1505
			$value = $this->esc_attr( $value );
1506
1507
			if ( is_array( $value ) ) {
1508
				$value = join( ',', $value );
1509
			}
1510
1511
			if ( false === strpos( $value, "'" ) ) {
1512
				$value = "'$value'";
1513
			} elseif ( false === strpos( $value, '"' ) ) {
1514
				$value = '"' . $value . '"';
1515
			} else {
1516
				// Shortcodes can't contain both '"' and "'".  Strip one.
1517
				$value = str_replace( "'", '', $value );
1518
				$value = "'$value'";
1519
			}
1520
1521
			$r .= "{$key}={$value} ";
1522
		}
1523
1524
		$r = rtrim( $r );
1525
1526
		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...
1527
			$r .= ']';
1528
1529
			foreach ( $this->fields as $field ) {
1530
				$r .= (string) $field;
1531
			}
1532
1533
			$r .= "[/{$this->shortcode_name}]";
1534
		} else {
1535
			$r .= '/]';
1536
		}
1537
1538
		return $r;
1539
	}
1540
}
1541
1542
/**
1543
 * Class for the contact-form shortcode.
1544
 * Parses shortcode to output the contact form as HTML
1545
 * Sends email and stores the contact form response (a.k.a. "feedback")
1546
 */
1547
class Grunion_Contact_Form extends Crunion_Contact_Form_Shortcode {
1548
	public $shortcode_name = 'contact-form';
1549
1550
	/**
1551
	 * @var WP_Error stores form submission errors
1552
	 */
1553
	public $errors;
1554
1555
	/**
1556
	 * @var string The SHA1 hash of the attributes that comprise the form.
1557
	 */
1558
	public $hash;
1559
1560
	/**
1561
	 * @var Grunion_Contact_Form The most recent (inclusive) contact-form shortcode processed
1562
	 */
1563
	static $last;
1564
1565
	/**
1566
	 * @var Whatever form we are currently looking at. If processed, will become $last
1567
	 */
1568
	static $current_form;
1569
1570
	/**
1571
	 * @var array All found forms, indexed by hash.
1572
	 */
1573
	static $forms = array();
1574
1575
	/**
1576
	 * @var bool Whether to print the grunion.css style when processing the contact-form shortcode
1577
	 */
1578
	static $style = false;
1579
1580
	function __construct( $attributes, $content = null ) {
1581
		global $post;
1582
1583
		$this->hash = sha1( json_encode( $attributes ) . $content );
1584
		self::$forms[ $this->hash ] = $this;
1585
1586
		// Set up the default subject and recipient for this form
1587
		$default_to = '';
1588
		$default_subject = '[' . get_option( 'blogname' ) . ']';
1589
1590
		if ( ! isset( $attributes ) || ! is_array( $attributes ) ) {
1591
			$attributes = array();
1592
		}
1593
1594
		if ( ! empty( $attributes['widget'] ) && $attributes['widget'] ) {
1595
			$default_to .= get_option( 'admin_email' );
1596
			$attributes['id'] = 'widget-' . $attributes['widget'];
1597
			$default_subject = sprintf( _x( '%1$s Sidebar', '%1$s = blog name', 'jetpack' ), $default_subject );
1598
		} elseif ( $post ) {
1599
			$attributes['id'] = $post->ID;
1600
			$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 ) );
1601
			$post_author = get_userdata( $post->post_author );
1602
			$default_to .= $post_author->user_email;
1603
		}
1604
1605
		// Keep reference to $this for parsing form fields
1606
		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...
1607
1608
		$this->defaults = array(
1609
			'to'                 => $default_to,
1610
			'subject'            => $default_subject,
1611
			'show_subject'       => 'no', // only used in back-compat mode
1612
			'widget'             => 0,    // Not exposed to the user. Works with Grunion_Contact_Form_Plugin::widget_atts()
1613
			'id'                 => null, // Not exposed to the user. Set above.
1614
			'submit_button_text' => __( 'Submit &#187;', 'jetpack' ),
1615
		);
1616
1617
		$attributes = shortcode_atts( $this->defaults, $attributes, 'contact-form' );
1618
1619
		// We only enable the contact-field shortcode temporarily while processing the contact-form shortcode
1620
		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...
1621
1622
		parent::__construct( $attributes, $content );
1623
1624
		// There were no fields in the contact form. The form was probably just [contact-form /]. Build a default form.
1625
		if ( empty( $this->fields ) ) {
1626
			// same as the original Grunion v1 form
1627
			$default_form = '
1628
				[contact-field label="' . __( 'Name', 'jetpack' ) . '" type="name"  required="true" /]
1629
				[contact-field label="' . __( 'Email', 'jetpack' ) . '" type="email" required="true" /]
1630
				[contact-field label="' . __( 'Website', 'jetpack' ) . '" type="url" /]';
1631
1632
			if ( 'yes' == strtolower( $this->get_attribute( 'show_subject' ) ) ) {
1633
				$default_form .= '
1634
					[contact-field label="' . __( 'Subject', 'jetpack' ) . '" type="subject" /]';
1635
			}
1636
1637
			$default_form .= '
1638
				[contact-field label="' . __( 'Message', 'jetpack' ) . '" type="textarea" /]';
1639
1640
			$this->parse_content( $default_form );
1641
1642
			// Store the shortcode
1643
			$this->store_shortcode( $default_form, $attributes, $this->hash );
1644
		} else {
1645
			// Store the shortcode
1646
			$this->store_shortcode( $content, $attributes, $this->hash );
1647
		}
1648
1649
		// $this->body and $this->fields have been setup.  We no longer need the contact-field shortcode.
1650
		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...
1651
	}
1652
1653
	/**
1654
	 * Store shortcode content for recall later
1655
	 *	- used to receate shortcode when user uses do_shortcode
1656
	 *
1657
	 * @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...
1658
	 * @param array $attributes
0 ignored issues
show
Documentation introduced by
Should the type for parameter $attributes not be array|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...
1659
	 * @param string $hash
0 ignored issues
show
Documentation introduced by
Should the type for parameter $hash 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...
1660
	 */
1661
	static function store_shortcode( $content = null, $attributes = null, $hash = null ) {
1662
1663
		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...
1664
1665
			if ( empty( $hash ) ) {
1666
				$hash = sha1( json_encode( $attributes ) . $content );
1667
			}
1668
1669
			$shortcode_meta = get_post_meta( $attributes['id'], "_g_feedback_shortcode_{$hash}", true );
1670
1671
			if ( $shortcode_meta != '' or $shortcode_meta != $content ) {
1672
				update_post_meta( $attributes['id'], "_g_feedback_shortcode_{$hash}", $content );
1673
1674
				// Save attributes to post_meta for later use. They're not available later in do_shortcode situations.
1675
				update_post_meta( $attributes['id'], "_g_feedback_shortcode_atts_{$hash}", $attributes );
1676
			}
1677
		}
1678
	}
1679
1680
	/**
1681
	 * Toggle for printing the grunion.css stylesheet
1682
	 *
1683
	 * @param bool $style
1684
	 */
1685
	static function style( $style ) {
1686
		$previous_style = self::$style;
1687
		self::$style = (bool) $style;
1688
		return $previous_style;
1689
	}
1690
1691
	/**
1692
	 * Turn on printing of grunion.css stylesheet
1693
	 *
1694
	 * @see ::style()
1695
	 * @internal
1696
	 * @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...
1697
	 */
1698
	static function _style_on() {
1699
		return self::style( true );
1700
	}
1701
1702
	/**
1703
	 * The contact-form shortcode processor
1704
	 *
1705
	 * @param array       $attributes Key => Value pairs as parsed by shortcode_parse_atts()
1706
	 * @param string|null $content The shortcode's inner content: [contact-form]$content[/contact-form]
1707
	 * @return string HTML for the concat form.
1708
	 */
1709
	static function parse( $attributes, $content ) {
1710
		require_once JETPACK__PLUGIN_DIR . '/sync/class.jetpack-sync-settings.php';
1711
		if ( Jetpack_Sync_Settings::is_syncing() ) {
1712
			return '';
1713
		}
1714
		// Create a new Grunion_Contact_Form object (this class)
1715
		$form = new Grunion_Contact_Form( $attributes, $content );
1716
1717
		$id = $form->get_attribute( 'id' );
1718
1719
		if ( ! $id ) { // something terrible has happened
1720
			return '[contact-form]';
1721
		}
1722
1723
		if ( is_feed() ) {
1724
			return '[contact-form]';
1725
		}
1726
1727
		self::$last = $form;
1728
1729
		// Enqueue the grunion.css stylesheet if self::$style allows it
1730
		if ( self::$style && ( empty( $_REQUEST['action'] ) || $_REQUEST['action'] != 'grunion_shortcode_to_json' ) ) {
1731
			// Enqueue the style here instead of printing it, because if some other plugin has run the_post()+rewind_posts(),
1732
			// (like VideoPress does), the style tag gets "printed" the first time and discarded, leaving the contact form unstyled.
1733
			// when WordPress does the real loop.
1734
			wp_enqueue_style( 'grunion.css' );
1735
		}
1736
1737
		$r = '';
1738
		$r .= "<div id='contact-form-$id'>\n";
1739
1740
		if ( is_wp_error( $form->errors ) && $form->errors->get_error_codes() ) {
1741
			// There are errors.  Display them
1742
			$r .= "<div class='form-error'>\n<h3>" . __( 'Error!', 'jetpack' ) . "</h3>\n<ul class='form-errors'>\n";
1743
			foreach ( $form->errors->get_error_messages() as $message ) {
1744
				$r .= "\t<li class='form-error-message'>" . esc_html( $message ) . "</li>\n";
1745
			}
1746
			$r .= "</ul>\n</div>\n\n";
1747
		}
1748
1749
		if ( isset( $_GET['contact-form-id'] )
1750
			&& $_GET['contact-form-id'] == self::$last->get_attribute( 'id' )
1751
			&& isset( $_GET['contact-form-sent'], $_GET['contact-form-hash'] )
1752
			&& hash_equals( $form->hash, $_GET['contact-form-hash'] ) ) { // phpcs:ignore PHPCompatibility -- skipping since `hash_equals` is part of WP core
1753
			// The contact form was submitted.  Show the success message/results
1754
			$feedback_id = (int) $_GET['contact-form-sent'];
1755
1756
			$back_url = remove_query_arg( array( 'contact-form-id', 'contact-form-sent', '_wpnonce' ) );
1757
1758
			$r_success_message =
1759
				'<h3>' . __( 'Message Sent', 'jetpack' ) .
1760
				' (<a href="' . esc_url( $back_url ) . '">' . esc_html__( 'go back', 'jetpack' ) . '</a>)' .
1761
				"</h3>\n\n";
1762
1763
			// Don't show the feedback details unless the nonce matches
1764
			if ( $feedback_id && wp_verify_nonce( stripslashes( $_GET['_wpnonce'] ), "contact-form-sent-{$feedback_id}" ) ) {
1765
				$r_success_message .= self::success_message( $feedback_id, $form );
1766
			}
1767
1768
			/**
1769
			 * Filter the message returned after a successful contact form submission.
1770
			 *
1771
			 * @module contact-form
1772
			 *
1773
			 * @since 1.3.1
1774
			 *
1775
			 * @param string $r_success_message Success message.
1776
			 */
1777
			$r .= apply_filters( 'grunion_contact_form_success_message', $r_success_message );
1778
		} else {
1779
			// Nothing special - show the normal contact form
1780
			if ( $form->get_attribute( 'widget' ) ) {
1781
				// Submit form to the current URL
1782
				$url = remove_query_arg( array( 'contact-form-id', 'contact-form-sent', 'action', '_wpnonce' ) );
1783
			} else {
1784
				// Submit form to the post permalink
1785
				$url = get_permalink();
1786
			}
1787
1788
			// For SSL/TLS page. See RFC 3986 Section 4.2
1789
			$url = set_url_scheme( $url );
1790
1791
			// May eventually want to send this to admin-post.php...
1792
			/**
1793
			 * Filter the contact form action URL.
1794
			 *
1795
			 * @module contact-form
1796
			 *
1797
			 * @since 1.3.1
1798
			 *
1799
			 * @param string $contact_form_id Contact form post URL.
1800
			 * @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...
1801
			 * @param int $id Contact Form ID.
1802
			 */
1803
			$url = apply_filters( 'grunion_contact_form_form_action', "{$url}#contact-form-{$id}", $GLOBALS['post'], $id );
1804
1805
			$r .= "<form action='" . esc_url( $url ) . "' method='post' class='contact-form commentsblock'>\n";
1806
			$r .= $form->body;
1807
			$r .= "\t<p class='contact-submit'>\n";
1808
			$r .= "\t\t<input type='submit' value='" . esc_attr( $form->get_attribute( 'submit_button_text' ) ) . "' class='pushbutton-wide'/>\n";
1809
			if ( is_user_logged_in() ) {
1810
				$r .= "\t\t" . wp_nonce_field( 'contact-form_' . $id, '_wpnonce', true, false ) . "\n"; // nonce and referer
1811
			}
1812
			$r .= "\t\t<input type='hidden' name='contact-form-id' value='$id' />\n";
1813
			$r .= "\t\t<input type='hidden' name='action' value='grunion-contact-form' />\n";
1814
			$r .= "\t\t<input type='hidden' name='contact-form-hash' value='" . esc_attr( $form->hash ) . "' />\n";
1815
			$r .= "\t</p>\n";
1816
			$r .= "</form>\n";
1817
		}
1818
1819
		$r .= '</div>';
1820
1821
		return $r;
1822
	}
1823
1824
	/**
1825
	 * Returns a success message to be returned if the form is sent via AJAX.
1826
	 *
1827
	 * @param int                         $feedback_id
1828
	 * @param object Grunion_Contact_Form $form
1829
	 *
1830
	 * @return string $message
1831
	 */
1832
	static function success_message( $feedback_id, $form ) {
1833
		return wp_kses(
1834
			'<blockquote class="contact-form-submission">'
1835
			. '<p>' . join( self::get_compiled_form( $feedback_id, $form ), '</p><p>' ) . '</p>'
1836
			. '</blockquote>',
1837
			array( 'br' => array(), 'blockquote' => array( 'class' => array() ), 'p' => array() )
1838
		);
1839
	}
1840
1841
	/**
1842
	 * Returns a compiled form with labels and values in a form of  an array
1843
	 * of lines.
1844
	 *
1845
	 * @param int                         $feedback_id
1846
	 * @param object Grunion_Contact_Form $form
1847
	 *
1848
	 * @return array $lines
1849
	 */
1850
	static function get_compiled_form( $feedback_id, $form ) {
1851
		$feedback       = get_post( $feedback_id );
1852
		$field_ids      = $form->get_field_ids();
1853
		$content_fields = Grunion_Contact_Form_Plugin::parse_fields_from_content( $feedback_id );
1854
1855
		// Maps field_ids to post_meta keys
1856
		$field_value_map = array(
1857
			'name'     => 'author',
1858
			'email'    => 'author_email',
1859
			'url'      => 'author_url',
1860
			'subject'  => 'subject',
1861
			'textarea' => false, // not a post_meta key.  This is stored in post_content
1862
		);
1863
1864
		$compiled_form = array();
1865
1866
		// "Standard" field whitelist
1867
		foreach ( $field_value_map as $type => $meta_key ) {
1868
			if ( isset( $field_ids[ $type ] ) ) {
1869
				$field = $form->fields[ $field_ids[ $type ] ];
1870
1871
				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...
1872
					if ( isset( $content_fields[ "_feedback_{$meta_key}" ] ) ) {
1873
						$value = $content_fields[ "_feedback_{$meta_key}" ];
1874
					}
1875
				} else {
1876
					// The feedback content is stored as the first "half" of post_content
1877
					$value = $feedback->post_content;
1878
					list( $value ) = explode( '<!--more-->', $value );
1879
					$value = trim( $value );
1880
				}
1881
1882
				$field_index = array_search( $field_ids[ $type ], $field_ids['all'] );
1883
				$compiled_form[ $field_index ] = sprintf(
1884
					'<b>%1$s:</b> %2$s<br /><br />',
1885
					wp_kses( $field->get_attribute( 'label' ), array() ),
1886
					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...
1887
				);
1888
			}
1889
		}
1890
1891
		// "Non-standard" fields
1892
		if ( $field_ids['extra'] ) {
1893
			// array indexed by field label (not field id)
1894
			$extra_fields = get_post_meta( $feedback_id, '_feedback_extra_fields', true );
1895
1896
			/**
1897
			 * Only get data for the compiled form if `$extra_fields` is a valid and non-empty array.
1898
			 */
1899
			if ( is_array( $extra_fields ) && ! empty( $extra_fields ) ) {
1900
1901
				$extra_field_keys = array_keys( $extra_fields );
1902
1903
				$i = 0;
1904
				foreach ( $field_ids['extra'] as $field_id ) {
1905
					$field       = $form->fields[ $field_id ];
1906
					$field_index = array_search( $field_id, $field_ids['all'] );
1907
1908
					$label = $field->get_attribute( 'label' );
1909
1910
					$compiled_form[ $field_index ] = sprintf(
1911
						'<b>%1$s:</b> %2$s<br /><br />',
1912
						wp_kses( $label, array() ),
1913
						nl2br( wp_kses( $extra_fields[ $extra_field_keys[ $i ] ], array() ) )
1914
					);
1915
1916
					$i++;
1917
				}
1918
			}
1919
		}
1920
1921
		// Sorting lines by the field index
1922
		ksort( $compiled_form );
1923
1924
		return $compiled_form;
1925
	}
1926
1927
	/**
1928
	 * The contact-field shortcode processor
1929
	 * 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.
1930
	 *
1931
	 * @param array       $attributes Key => Value pairs as parsed by shortcode_parse_atts()
1932
	 * @param string|null $content The shortcode's inner content: [contact-field]$content[/contact-field]
1933
	 * @return HTML for the contact form field
1934
	 */
1935
	static function parse_contact_field( $attributes, $content ) {
1936
		// Don't try to parse contact form fields if not inside a contact form
1937
		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...
1938
			$att_strs = array();
1939
			foreach ( $attributes as $att => $val ) {
1940
				if ( is_numeric( $att ) ) { // Is a valueless attribute
1941
					$att_strs[] = esc_html( $val );
1942
				} elseif ( isset( $val ) ) { // A regular attr - value pair
1943
					$att_strs[] = esc_html( $att ) . '=\'' . esc_html( $val ) . '\'';
1944
				}
1945
			}
1946
1947
			$html = '[contact-field ' . implode( ' ', $att_strs );
1948
1949
			if ( isset( $content ) && ! empty( $content ) ) { // If there is content, let's add a closing tag
1950
				$html .= ']' . esc_html( $content ) . '[/contact-field]';
1951
			} else { // Otherwise let's add a closing slash in the first tag
1952
				$html .= '/]';
1953
			}
1954
1955
			return $html;
1956
		}
1957
1958
		$form = Grunion_Contact_Form::$current_form;
1959
1960
		$field = new Grunion_Contact_Form_Field( $attributes, $content, $form );
1961
1962
		$field_id = $field->get_attribute( 'id' );
1963
		if ( $field_id ) {
1964
			$form->fields[ $field_id ] = $field;
1965
		} else {
1966
			$form->fields[] = $field;
1967
		}
1968
1969
		if (
1970
			isset( $_POST['action'] ) && 'grunion-contact-form' === $_POST['action']
1971
		&&
1972
			isset( $_POST['contact-form-id'] ) && $form->get_attribute( 'id' ) == $_POST['contact-form-id']
1973
		&&
1974
			isset( $_POST['contact-form-hash'] ) && hash_equals( $form->hash, $_POST['contact-form-hash'] ) // phpcs:ignore PHPCompatibility -- skipping since `hash_equals` is part of WP core
1975
		) {
1976
			// If we're processing a POST submission for this contact form, validate the field value so we can show errors as necessary.
1977
			$field->validate();
1978
		}
1979
1980
		// Output HTML
1981
		return $field->render();
1982
	}
1983
1984
	/**
1985
	 * Loops through $this->fields to generate a (structured) list of field IDs.
1986
	 *
1987
	 * Important: Currently the whitelisted fields are defined as follows:
1988
	 *  `name`, `email`, `url`, `subject`, `textarea`
1989
	 *
1990
	 * If you need to add new fields to the Contact Form, please don't add them
1991
	 * to the whitelisted fields and leave them as extra fields.
1992
	 *
1993
	 * The reasoning behind this is that both the admin Feedback view and the CSV
1994
	 * export will not include any fields that are added to the list of
1995
	 * whitelisted fields without taking proper care to add them to all the
1996
	 * other places where they accessed/used/saved.
1997
	 *
1998
	 * The safest way to add new fields is to add them to the dropdown and the
1999
	 * HTML list ( @see Grunion_Contact_Form_Field::render ) and don't add them
2000
	 * to the list of whitelisted fields. This way they will become a part of the
2001
	 * `extra fields` which are saved in the post meta and will be properly
2002
	 * handled by the admin Feedback view and the CSV Export without any extra
2003
	 * work.
2004
	 *
2005
	 * If there is need to add a field to the whitelisted fields, then please
2006
	 * take proper care to add logic to handle the field in the following places:
2007
	 *
2008
	 *  - Below in the switch statement - so the field is recognized as whitelisted.
2009
	 *
2010
	 *  - Grunion_Contact_Form::process_submission - validation and logic.
2011
	 *
2012
	 *  - Grunion_Contact_Form::process_submission - add the field as an additional
2013
	 *      field in the `post_content` when saving the feedback content.
2014
	 *
2015
	 *  - Grunion_Contact_Form_Plugin::parse_fields_from_content - add mapping
2016
	 *      for the field, defined in the above method.
2017
	 *
2018
	 *  - Grunion_Contact_Form_Plugin::map_parsed_field_contents_of_post_to_field_names -
2019
	 *      add mapping of the field for the CSV Export. Otherwise it will be missing
2020
	 *      from the exported data.
2021
	 *
2022
	 *  - admin.php / grunion_manage_post_columns - add the field to the render logic.
2023
	 *      Otherwise it will be missing from the admin Feedback view.
2024
	 *
2025
	 * @return array
2026
	 */
2027
	function get_field_ids() {
2028
		$field_ids = array(
2029
			'all'   => array(), // array of all field_ids
2030
			'extra' => array(), // array of all non-whitelisted field IDs
2031
2032
			// Whitelisted "standard" field IDs:
2033
			// 'email'    => field_id,
2034
			// 'name'     => field_id,
2035
			// 'url'      => field_id,
2036
			// 'subject'  => field_id,
2037
			// 'textarea' => field_id,
2038
		);
2039
2040
		foreach ( $this->fields as $id => $field ) {
2041
			$field_ids['all'][] = $id;
2042
2043
			$type = $field->get_attribute( 'type' );
2044
			if ( isset( $field_ids[ $type ] ) ) {
2045
				// This type of field is already present in our whitelist of "standard" fields for this form
2046
				// Put it in extra
2047
				$field_ids['extra'][] = $id;
2048
				continue;
2049
			}
2050
2051
			/**
2052
			 * See method description before modifying the switch cases.
2053
			 */
2054
			switch ( $type ) {
2055
				case 'email' :
2056
				case 'name' :
2057
				case 'url' :
2058
				case 'subject' :
2059
				case 'textarea' :
2060
					$field_ids[ $type ] = $id;
2061
					break;
2062
				default :
2063
					// Put everything else in extra
2064
					$field_ids['extra'][] = $id;
2065
			}
2066
		}
2067
2068
		return $field_ids;
2069
	}
2070
2071
	/**
2072
	 * Process the contact form's POST submission
2073
	 * Stores feedback.  Sends email.
2074
	 */
2075
	function process_submission() {
2076
		global $post;
2077
2078
		$plugin = Grunion_Contact_Form_Plugin::init();
2079
2080
		$id     = $this->get_attribute( 'id' );
2081
		$to     = $this->get_attribute( 'to' );
2082
		$widget = $this->get_attribute( 'widget' );
2083
2084
		$contact_form_subject = $this->get_attribute( 'subject' );
2085
2086
		$to = str_replace( ' ', '', $to );
2087
		$emails = explode( ',', $to );
2088
2089
		$valid_emails = array();
2090
2091
		foreach ( (array) $emails as $email ) {
2092
			if ( ! is_email( $email ) ) {
2093
				continue;
2094
			}
2095
2096
			if ( function_exists( 'is_email_address_unsafe' ) && is_email_address_unsafe( $email ) ) {
2097
				continue;
2098
			}
2099
2100
			$valid_emails[] = $email;
2101
		}
2102
2103
		// No one to send it to, which means none of the "to" attributes are valid emails.
2104
		// Use default email instead.
2105
		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...
2106
			$valid_emails = $this->defaults['to'];
2107
		}
2108
2109
		$to = $valid_emails;
2110
2111
		// Last ditch effort to set a recipient if somehow none have been set.
2112
		if ( empty( $to ) ) {
2113
			$to = get_option( 'admin_email' );
2114
		}
2115
2116
		// Make sure we're processing the form we think we're processing... probably a redundant check.
2117
		if ( $widget ) {
2118
			if ( 'widget-' . $widget != $_POST['contact-form-id'] ) {
2119
				return false;
2120
			}
2121
		} else {
2122
			if ( $post->ID != $_POST['contact-form-id'] ) {
2123
				return false;
2124
			}
2125
		}
2126
2127
		$field_ids = $this->get_field_ids();
2128
2129
		// Initialize all these "standard" fields to null
2130
		$comment_author_email = $comment_author_email_label = // v
2131
		$comment_author       = $comment_author_label       = // v
2132
		$comment_author_url   = $comment_author_url_label   = // v
2133
		$comment_content      = $comment_content_label      = null;
2134
2135
		// For each of the "standard" fields, grab their field label and value.
2136 View Code Duplication
		if ( isset( $field_ids['name'] ) ) {
2137
			$field = $this->fields[ $field_ids['name'] ];
2138
			$comment_author = Grunion_Contact_Form_Plugin::strip_tags(
2139
				stripslashes(
2140
					/** This filter is already documented in core/wp-includes/comment-functions.php */
2141
					apply_filters( 'pre_comment_author_name', addslashes( $field->value ) )
2142
				)
2143
			);
2144
			$comment_author_label = Grunion_Contact_Form_Plugin::strip_tags( $field->get_attribute( 'label' ) );
2145
		}
2146
2147 View Code Duplication
		if ( isset( $field_ids['email'] ) ) {
2148
			$field = $this->fields[ $field_ids['email'] ];
2149
			$comment_author_email = Grunion_Contact_Form_Plugin::strip_tags(
2150
				stripslashes(
2151
					/** This filter is already documented in core/wp-includes/comment-functions.php */
2152
					apply_filters( 'pre_comment_author_email', addslashes( $field->value ) )
2153
				)
2154
			);
2155
			$comment_author_email_label = Grunion_Contact_Form_Plugin::strip_tags( $field->get_attribute( 'label' ) );
2156
		}
2157
2158
		if ( isset( $field_ids['url'] ) ) {
2159
			$field = $this->fields[ $field_ids['url'] ];
2160
			$comment_author_url = Grunion_Contact_Form_Plugin::strip_tags(
2161
				stripslashes(
2162
					/** This filter is already documented in core/wp-includes/comment-functions.php */
2163
					apply_filters( 'pre_comment_author_url', addslashes( $field->value ) )
2164
				)
2165
			);
2166
			if ( 'http://' == $comment_author_url ) {
2167
				$comment_author_url = '';
2168
			}
2169
			$comment_author_url_label = Grunion_Contact_Form_Plugin::strip_tags( $field->get_attribute( 'label' ) );
2170
		}
2171
2172
		if ( isset( $field_ids['textarea'] ) ) {
2173
			$field = $this->fields[ $field_ids['textarea'] ];
2174
			$comment_content = trim( Grunion_Contact_Form_Plugin::strip_tags( $field->value ) );
2175
			$comment_content_label = Grunion_Contact_Form_Plugin::strip_tags( $field->get_attribute( 'label' ) );
2176
		}
2177
2178
		if ( isset( $field_ids['subject'] ) ) {
2179
			$field = $this->fields[ $field_ids['subject'] ];
2180
			if ( $field->value ) {
2181
				$contact_form_subject = Grunion_Contact_Form_Plugin::strip_tags( $field->value );
2182
			}
2183
		}
2184
2185
		$all_values = $extra_values = array();
2186
		$i = 1; // Prefix counter for stored metadata
2187
2188
		// For all fields, grab label and value
2189
		foreach ( $field_ids['all'] as $field_id ) {
2190
			$field = $this->fields[ $field_id ];
2191
			$label = $i . '_' . $field->get_attribute( 'label' );
2192
			$value = $field->value;
2193
2194
			$all_values[ $label ] = $value;
2195
			$i++; // Increment prefix counter for the next field
2196
		}
2197
2198
		// For the "non-standard" fields, grab label and value
2199
		// Extra fields have their prefix starting from count( $all_values ) + 1
2200
		foreach ( $field_ids['extra'] as $field_id ) {
2201
			$field = $this->fields[ $field_id ];
2202
			$label = $i . '_' . $field->get_attribute( 'label' );
2203
			$value = $field->value;
2204
2205
			if ( is_array( $value ) ) {
2206
				$value = implode( ', ', $value );
2207
			}
2208
2209
			$extra_values[ $label ] = $value;
2210
			$i++; // Increment prefix counter for the next extra field
2211
		}
2212
2213
		$contact_form_subject = trim( $contact_form_subject );
2214
2215
		$comment_author_IP = Grunion_Contact_Form_Plugin::get_ip_address();
2216
2217
		$vars = array( 'comment_author', 'comment_author_email', 'comment_author_url', 'contact_form_subject', 'comment_author_IP' );
2218
		foreach ( $vars as $var ) {
2219
			$$var = str_replace( array( "\n", "\r" ), '', $$var );
2220
		}
2221
2222
		// Ensure that Akismet gets all of the relevant information from the contact form,
2223
		// not just the textarea field and predetermined subject.
2224
		$akismet_vars = compact( $vars );
2225
		$akismet_vars['comment_content'] = $comment_content;
2226
2227
		foreach ( array_merge( $field_ids['all'], $field_ids['extra'] ) as $field_id ) {
2228
			$field = $this->fields[ $field_id ];
2229
2230
			// Skip any fields that are just a choice from a pre-defined list. They wouldn't have any value
2231
			// from a spam-filtering point of view.
2232
			if ( in_array( $field->get_attribute( 'type' ), array( 'select', 'checkbox', 'checkbox-multiple', 'radio' ) ) ) {
2233
				continue;
2234
			}
2235
2236
			// Normalize the label into a slug.
2237
			$field_slug = trim( // Strip all leading/trailing dashes.
2238
				preg_replace(   // Normalize everything to a-z0-9_-
2239
					'/[^a-z0-9_]+/',
2240
					'-',
2241
					strtolower( $field->get_attribute( 'label' ) ) // Lowercase
2242
				),
2243
				'-'
2244
			);
2245
2246
			$field_value = ( is_array( $field->value ) ) ? trim( implode( ', ', $field->value ) ) : trim( $field->value );
2247
2248
			// Skip any values that are already in the array we're sending.
2249
			if ( $field_value && in_array( $field_value, $akismet_vars ) ) {
2250
				continue;
2251
			}
2252
2253
			$akismet_vars[ 'contact_form_field_' . $field_slug ] = $field_value;
2254
		}
2255
2256
		$spam = '';
2257
		$akismet_values = $plugin->prepare_for_akismet( $akismet_vars );
2258
2259
		// Is it spam?
2260
		/** This filter is already documented in modules/contact-form/admin.php */
2261
		$is_spam = apply_filters( 'jetpack_contact_form_is_spam', false, $akismet_values );
2262
		if ( is_wp_error( $is_spam ) ) { // WP_Error to abort
2263
			return $is_spam; // abort
2264
		} elseif ( $is_spam === true ) {  // TRUE to flag a spam
2265
			$spam = '***SPAM*** ';
2266
		}
2267
2268
		if ( ! $comment_author ) {
2269
			$comment_author = $comment_author_email;
2270
		}
2271
2272
		/**
2273
		 * Filter the email where a submitted feedback is sent.
2274
		 *
2275
		 * @module contact-form
2276
		 *
2277
		 * @since 1.3.1
2278
		 *
2279
		 * @param string|array $to Array of valid email addresses, or single email address.
2280
		 */
2281
		$to = (array) apply_filters( 'contact_form_to', $to );
2282
		$reply_to_addr = $to[0]; // get just the address part before the name part is added
2283
2284
		foreach ( $to as $to_key => $to_value ) {
2285
			$to[ $to_key ] = Grunion_Contact_Form_Plugin::strip_tags( $to_value );
2286
			$to[ $to_key ] = self::add_name_to_address( $to_value );
2287
		}
2288
2289
		$blog_url = parse_url( site_url() );
2290
		$from_email_addr = 'wordpress@' . $blog_url['host'];
2291
2292
		if ( ! empty( $comment_author_email ) ) {
2293
			$reply_to_addr = $comment_author_email;
2294
		}
2295
2296
		$headers = 'From: "' . $comment_author . '" <' . $from_email_addr . ">\r\n" .
2297
					'Reply-To: "' . $comment_author . '" <' . $reply_to_addr . ">\r\n";
2298
2299
		// Build feedback reference
2300
		$feedback_time  = current_time( 'mysql' );
2301
		$feedback_title = "{$comment_author} - {$feedback_time}";
2302
		$feedback_id    = md5( $feedback_title );
2303
2304
		$all_values = array_merge( $all_values, array(
2305
			'entry_title'     => the_title_attribute( 'echo=0' ),
2306
			'entry_permalink' => esc_url( get_permalink( get_the_ID() ) ),
2307
			'feedback_id'     => $feedback_id,
2308
		) );
2309
2310
		/** This filter is already documented in modules/contact-form/admin.php */
2311
		$subject = apply_filters( 'contact_form_subject', $contact_form_subject, $all_values );
2312
		$url     = $widget ? home_url( '/' ) : get_permalink( $post->ID );
2313
2314
		$date_time_format = _x( '%1$s \a\t %2$s', '{$date_format} \a\t {$time_format}', 'jetpack' );
2315
		$date_time_format = sprintf( $date_time_format, get_option( 'date_format' ), get_option( 'time_format' ) );
2316
		$time = date_i18n( $date_time_format, current_time( 'timestamp' ) );
2317
2318
		// keep a copy of the feedback as a custom post type
2319
		$feedback_status = $is_spam === true ? 'spam' : 'publish';
2320
2321
		foreach ( (array) $akismet_values as $av_key => $av_value ) {
2322
			$akismet_values[ $av_key ] = Grunion_Contact_Form_Plugin::strip_tags( $av_value );
2323
		}
2324
2325
		foreach ( (array) $all_values as $all_key => $all_value ) {
2326
			$all_values[ $all_key ] = Grunion_Contact_Form_Plugin::strip_tags( $all_value );
2327
		}
2328
2329
		foreach ( (array) $extra_values as $ev_key => $ev_value ) {
2330
			$extra_values[ $ev_key ] = Grunion_Contact_Form_Plugin::strip_tags( $ev_value );
2331
		}
2332
2333
		/*
2334
		 We need to make sure that the post author is always zero for contact
2335
		 * form submissions.  This prevents export/import from trying to create
2336
		 * new users based on form submissions from people who were logged in
2337
		 * at the time.
2338
		 *
2339
		 * Unfortunately wp_insert_post() tries very hard to make sure the post
2340
		 * author gets the currently logged in user id.  That is how we ended up
2341
		 * with this work around. */
2342
		add_filter( 'wp_insert_post_data', array( $plugin, 'insert_feedback_filter' ), 10, 2 );
2343
2344
		$post_id = wp_insert_post( array(
2345
			'post_date'    => addslashes( $feedback_time ),
2346
			'post_type'    => 'feedback',
2347
			'post_status'  => addslashes( $feedback_status ),
2348
			'post_parent'  => (int) $post->ID,
2349
			'post_title'   => addslashes( wp_kses( $feedback_title, array() ) ),
2350
			'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
2351
			'post_name'    => $feedback_id,
2352
		) );
2353
2354
		// once insert has finished we don't need this filter any more
2355
		remove_filter( 'wp_insert_post_data', array( $plugin, 'insert_feedback_filter' ), 10 );
2356
2357
		update_post_meta( $post_id, '_feedback_extra_fields', $this->addslashes_deep( $extra_values ) );
2358
2359
		if ( 'publish' == $feedback_status ) {
2360
			// Increase count of unread feedback.
2361
			$unread = get_option( 'feedback_unread_count', 0 ) + 1;
2362
			update_option( 'feedback_unread_count', $unread );
2363
		}
2364
2365
		if ( defined( 'AKISMET_VERSION' ) ) {
2366
			update_post_meta( $post_id, '_feedback_akismet_values', $this->addslashes_deep( $akismet_values ) );
2367
		}
2368
2369
		$message = self::get_compiled_form( $post_id, $this );
2370
2371
		array_push(
2372
			$message,
2373
			"<br />",
2374
			'<hr />',
2375
			__( 'Time:', 'jetpack' ) . ' ' . $time . '<br />',
2376
			__( 'IP Address:', 'jetpack' ) . ' ' . $comment_author_IP . '<br />',
2377
			__( 'Contact Form URL:', 'jetpack' ) . ' ' . $url . '<br />'
2378
		);
2379
2380
		if ( is_user_logged_in() ) {
2381
			array_push(
2382
				$message,
2383
				sprintf(
2384
					'<p>' . __( 'Sent by a verified %s user.', 'jetpack' ) . '</p>',
2385
					isset( $GLOBALS['current_site']->site_name ) && $GLOBALS['current_site']->site_name ?
2386
						$GLOBALS['current_site']->site_name : '"' . get_option( 'blogname' ) . '"'
2387
				)
2388
			);
2389
		} else {
2390
			array_push( $message, '<p>' . __( 'Sent by an unverified visitor to your site.', 'jetpack' ) . '</p>' );
2391
		}
2392
2393
		$message = join( $message, '' );
2394
2395
		/**
2396
		 * Filters the message sent via email after a successful form submission.
2397
		 *
2398
		 * @module contact-form
2399
		 *
2400
		 * @since 1.3.1
2401
		 *
2402
		 * @param string $message Feedback email message.
2403
		 */
2404
		$message = apply_filters( 'contact_form_message', $message );
2405
2406
		// This is called after `contact_form_message`, in order to preserve back-compat
2407
		$message = self::wrap_message_in_html_tags( $message );
2408
2409
		update_post_meta( $post_id, '_feedback_email', $this->addslashes_deep( compact( 'to', 'message' ) ) );
2410
2411
		/**
2412
		 * Fires right before the contact form message is sent via email to
2413
		 * the recipient specified in the contact form.
2414
		 *
2415
		 * @module contact-form
2416
		 *
2417
		 * @since 1.3.1
2418
		 *
2419
		 * @param integer $post_id Post contact form lives on
2420
		 * @param array $all_values Contact form fields
2421
		 * @param array $extra_values Contact form fields not included in $all_values
2422
		 */
2423
		do_action( 'grunion_pre_message_sent', $post_id, $all_values, $extra_values );
2424
2425
		// schedule deletes of old spam feedbacks
2426
		if ( ! wp_next_scheduled( 'grunion_scheduled_delete' ) ) {
2427
			wp_schedule_event( time() + 250, 'daily', 'grunion_scheduled_delete' );
2428
		}
2429
2430
		if (
2431
			$is_spam !== true &&
2432
			/**
2433
			 * Filter to choose whether an email should be sent after each successful contact form submission.
2434
			 *
2435
			 * @module contact-form
2436
			 *
2437
			 * @since 2.6.0
2438
			 *
2439
			 * @param bool true Should an email be sent after a form submission. Default to true.
2440
			 * @param int $post_id Post ID.
2441
			 */
2442
			true === apply_filters( 'grunion_should_send_email', true, $post_id )
2443
		) {
2444
			self::wp_mail( $to, "{$spam}{$subject}", $message, $headers );
2445
		} elseif (
2446
			true === $is_spam &&
2447
			/**
2448
			 * Choose whether an email should be sent for each spam contact form submission.
2449
			 *
2450
			 * @module contact-form
2451
			 *
2452
			 * @since 1.3.1
2453
			 *
2454
			 * @param bool false Should an email be sent after a spam form submission. Default to false.
2455
			 */
2456
			apply_filters( 'grunion_still_email_spam', false ) == true
2457
		) { // don't send spam by default.  Filterable.
2458
			self::wp_mail( $to, "{$spam}{$subject}", $message, $headers );
2459
		}
2460
2461
		if ( defined( 'DOING_AJAX' ) && DOING_AJAX ) {
2462
			return self::success_message( $post_id, $this );
2463
		}
2464
2465
		$redirect = wp_get_referer();
2466
		if ( ! $redirect ) { // wp_get_referer() returns false if the referer is the same as the current page
2467
			$redirect = $_SERVER['REQUEST_URI'];
2468
		}
2469
2470
		$redirect = add_query_arg( urlencode_deep( array(
2471
			'contact-form-id'   => $id,
2472
			'contact-form-sent' => $post_id,
2473
			'contact-form-hash' => $this->hash,
2474
			'_wpnonce'          => wp_create_nonce( "contact-form-sent-{$post_id}" ), // wp_nonce_url HTMLencodes :(
2475
		) ), $redirect );
2476
2477
		/**
2478
		 * Filter the URL where the reader is redirected after submitting a form.
2479
		 *
2480
		 * @module contact-form
2481
		 *
2482
		 * @since 1.9.0
2483
		 *
2484
		 * @param string $redirect Post submission URL.
2485
		 * @param int $id Contact Form ID.
2486
		 * @param int $post_id Post ID.
2487
		 */
2488
		$redirect = apply_filters( 'grunion_contact_form_redirect_url', $redirect, $id, $post_id );
2489
2490
		wp_safe_redirect( $redirect );
2491
		exit;
2492
	}
2493
2494
	/**
2495
	 * Wrapper for wp_mail() that enables HTML messages with text alternatives
2496
	 *
2497
	 * @param string|array $to          Array or comma-separated list of email addresses to send message.
2498
	 * @param string       $subject     Email subject.
2499
	 * @param string       $message     Message contents.
2500
	 * @param string|array $headers     Optional. Additional headers.
2501
	 * @param string|array $attachments Optional. Files to attach.
2502
	 *
2503
	 * @return bool Whether the email contents were sent successfully.
2504
	 */
2505
	public static function wp_mail( $to, $subject, $message, $headers = '', $attachments = array() ) {
2506
		add_filter( 'wp_mail_content_type', __CLASS__ . '::get_mail_content_type' );
2507
		add_action( 'phpmailer_init',       __CLASS__ . '::add_plain_text_alternative' );
2508
2509
		$result = wp_mail( $to, $subject, $message, $headers, $attachments );
2510
2511
		remove_filter( 'wp_mail_content_type', __CLASS__ . '::get_mail_content_type' );
2512
		remove_action( 'phpmailer_init',       __CLASS__ . '::add_plain_text_alternative' );
2513
2514
		return $result;
2515
	}
2516
2517
	/**
2518
	 * Add a display name part to an email address
2519
	 *
2520
	 * SpamAssassin doesn't like addresses in HTML messages that are missing display names (e.g., `[email protected]`
2521
	 * instead of `"Foo Bar" <[email protected]>`.
2522
	 *
2523
	 * @param string $address
2524
	 *
2525
	 * @return string
2526
	 */
2527
	function add_name_to_address( $address ) {
2528
		// If it's just the address, without a display name
2529
		if ( is_email( $address ) ) {
2530
			$address_parts = explode( '@', $address );
2531
			$address = sprintf( '"%s" <%s>', $address_parts[0], $address );
2532
		}
2533
2534
		return $address;
2535
	}
2536
2537
	/**
2538
	 * Get the content type that should be assigned to outbound emails
2539
	 *
2540
	 * @return string
2541
	 */
2542
	static function get_mail_content_type() {
2543
		return 'text/html';
2544
	}
2545
2546
	/**
2547
	 * Wrap a message body with the appropriate in HTML tags
2548
	 *
2549
	 * This helps to ensure correct parsing by clients, and also helps avoid triggering spam filtering rules
2550
	 *
2551
	 * @param string $body
2552
	 *
2553
	 * @return string
2554
	 */
2555
	static function wrap_message_in_html_tags( $body ) {
2556
		// Don't do anything if the message was already wrapped in HTML tags
2557
		// That could have be done by a plugin via filters
2558
		if ( false !== strpos( $body, '<html' ) ) {
2559
			return $body;
2560
		}
2561
2562
		$html_message = sprintf(
2563
			// The tabs are just here so that the raw code is correctly formatted for developers
2564
			// They're removed so that they don't affect the final message sent to users
2565
			str_replace( "\t", '',
2566
				"<!doctype html>
2567
				<html xmlns=\"http://www.w3.org/1999/xhtml\">
2568
				<body>
2569
2570
				%s
2571
2572
				</body>
2573
				</html>"
2574
			),
2575
			$body
2576
		);
2577
2578
		return $html_message;
2579
	}
2580
2581
	/**
2582
	 * Add a plain-text alternative part to an outbound email
2583
	 *
2584
	 * This makes the message more accessible to mail clients that aren't HTML-aware, and decreases the likelihood
2585
	 * that the message will be flagged as spam.
2586
	 *
2587
	 * @param PHPMailer $phpmailer
2588
	 */
2589
	static function add_plain_text_alternative( $phpmailer ) {
2590
		// Add an extra break so that the extra space above the <p> is preserved after the <p> is stripped out
2591
		$alt_body = str_replace( '<p>', '<p><br />', $phpmailer->Body );
2592
2593
		// Convert <br> to \n breaks, to preserve the space between lines that we want to keep
2594
		$alt_body = str_replace( array( '<br>', '<br />' ), "\n", $alt_body );
2595
2596
		// Convert <hr> to an plain-text equivalent, to preserve the integrity of the message
2597
		$alt_body = str_replace( array( "<hr>", "<hr />" ), "----\n", $alt_body );
2598
2599
		// Trim the plain text message to remove the \n breaks that were after <doctype>, <html>, and <body>
2600
		$phpmailer->AltBody = trim( strip_tags( $alt_body ) );
2601
	}
2602
2603
	function addslashes_deep( $value ) {
2604
		if ( is_array( $value ) ) {
2605
			return array_map( array( $this, 'addslashes_deep' ), $value );
2606
		} elseif ( is_object( $value ) ) {
2607
			$vars = get_object_vars( $value );
2608
			foreach ( $vars as $key => $data ) {
2609
				$value->{$key} = $this->addslashes_deep( $data );
2610
			}
2611
			return $value;
2612
		}
2613
2614
		return addslashes( $value );
2615
	}
2616
}
2617
2618
/**
2619
 * Class for the contact-field shortcode.
2620
 * Parses shortcode to output the contact form field as HTML.
2621
 * Validates input.
2622
 */
2623
class Grunion_Contact_Form_Field extends Crunion_Contact_Form_Shortcode {
2624
	public $shortcode_name = 'contact-field';
2625
2626
	/**
2627
	 * @var Grunion_Contact_Form parent form
2628
	 */
2629
	public $form;
2630
2631
	/**
2632
	 * @var string default or POSTed value
2633
	 */
2634
	public $value;
2635
2636
	/**
2637
	 * @var bool Is the input invalid?
2638
	 */
2639
	public $error = false;
2640
2641
	/**
2642
	 * @param array                $attributes An associative array of shortcode attributes.  @see shortcode_atts()
2643
	 * @param null|string          $content Null for selfclosing shortcodes.  The inner content otherwise.
2644
	 * @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...
2645
	 */
2646
	function __construct( $attributes, $content = null, $form = null ) {
2647
		$attributes = shortcode_atts( array(
2648
					'label'       => null,
2649
					'type'        => 'text',
2650
					'required'    => false,
2651
					'options'     => array(),
2652
					'id'          => null,
2653
					'default'     => null,
2654
					'values'      => null,
2655
					'placeholder' => null,
2656
					'class'       => null,
2657
		), $attributes, 'contact-field' );
2658
2659
		// special default for subject field
2660
		if ( 'subject' == $attributes['type'] && is_null( $attributes['default'] ) && ! is_null( $form ) ) {
2661
			$attributes['default'] = $form->get_attribute( 'subject' );
2662
		}
2663
2664
		// allow required=1 or required=true
2665
		if ( '1' == $attributes['required'] || 'true' == strtolower( $attributes['required'] ) ) {
2666
			$attributes['required'] = true;
2667
		} else { $attributes['required'] = false;
2668
		}
2669
2670
		// parse out comma-separated options list (for selects, radios, and checkbox-multiples)
2671
		if ( ! empty( $attributes['options'] ) && is_string( $attributes['options'] ) ) {
2672
			$attributes['options'] = array_map( 'trim', explode( ',', $attributes['options'] ) );
2673
2674 View Code Duplication
			if ( ! empty( $attributes['values'] ) && is_string( $attributes['values'] ) ) {
2675
				$attributes['values'] = array_map( 'trim', explode( ',', $attributes['values'] ) );
2676
			}
2677
		}
2678
2679
		if ( $form ) {
2680
			// make a unique field ID based on the label, with an incrementing number if needed to avoid clashes
2681
			$form_id = $form->get_attribute( 'id' );
2682
			$id = isset( $attributes['id'] ) ? $attributes['id'] : false;
2683
2684
			$unescaped_label = $this->unesc_attr( $attributes['label'] );
2685
			$unescaped_label = str_replace( '%', '-', $unescaped_label ); // jQuery doesn't like % in IDs?
2686
			$unescaped_label = preg_replace( '/[^a-zA-Z0-9.-_:]/', '', $unescaped_label );
2687
2688
			if ( empty( $id ) ) {
2689
				$id = sanitize_title_with_dashes( 'g' . $form_id . '-' . $unescaped_label );
2690
				$i = 0;
2691
				$max_tries = 99;
2692
				while ( isset( $form->fields[ $id ] ) ) {
2693
					$i++;
2694
					$id = sanitize_title_with_dashes( 'g' . $form_id . '-' . $unescaped_label . '-' . $i );
2695
2696
					if ( $i > $max_tries ) {
2697
						break;
2698
					}
2699
				}
2700
			}
2701
2702
			$attributes['id'] = $id;
2703
		}
2704
2705
		parent::__construct( $attributes, $content );
2706
2707
		// Store parent form
2708
		$this->form = $form;
2709
	}
2710
2711
	/**
2712
	 * This field's input is invalid.  Flag as invalid and add an error to the parent form
2713
	 *
2714
	 * @param string $message The error message to display on the form.
2715
	 */
2716
	function add_error( $message ) {
2717
		$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...
2718
2719
		if ( ! is_wp_error( $this->form->errors ) ) {
2720
			$this->form->errors = new WP_Error;
2721
		}
2722
2723
		$this->form->errors->add( $this->get_attribute( 'id' ), $message );
2724
	}
2725
2726
	/**
2727
	 * Is the field input invalid?
2728
	 *
2729
	 * @see $error
2730
	 *
2731
	 * @return bool
2732
	 */
2733
	function is_error() {
2734
		return $this->error;
2735
	}
2736
2737
	/**
2738
	 * Validates the form input
2739
	 */
2740
	function validate() {
2741
		// If it's not required, there's nothing to validate
2742
		if ( ! $this->get_attribute( 'required' ) ) {
2743
			return;
2744
		}
2745
2746
		$field_id    = $this->get_attribute( 'id' );
2747
		$field_type  = $this->get_attribute( 'type' );
2748
		$field_label = $this->get_attribute( 'label' );
2749
2750
		if ( isset( $_POST[ $field_id ] ) ) {
2751
			if ( is_array( $_POST[ $field_id ] ) ) {
2752
				$field_value = array_map( 'stripslashes', $_POST[ $field_id ] );
2753
			} else {
2754
				$field_value = stripslashes( $_POST[ $field_id ] );
2755
			}
2756
		} else {
2757
			$field_value = '';
2758
		}
2759
2760
		switch ( $field_type ) {
2761
			case 'email' :
2762
				// Make sure the email address is valid
2763
				if ( ! is_email( $field_value ) ) {
2764
					/* translators: %s is the name of a form field */
2765
					$this->add_error( sprintf( __( '%s requires a valid email address', 'jetpack' ), $field_label ) );
2766
				}
2767
			break;
2768
			case 'checkbox-multiple' :
2769
				// Check that there is at least one option selected
2770
				if ( empty( $field_value ) ) {
2771
					/* translators: %s is the name of a form field */
2772
					$this->add_error( sprintf( __( '%s requires at least one selection', 'jetpack' ), $field_label ) );
2773
				}
2774
			break;
2775
			default :
2776
				// Just check for presence of any text
2777
				if ( ! strlen( trim( $field_value ) ) ) {
2778
					/* translators: %s is the name of a form field */
2779
					$this->add_error( sprintf( __( '%s is required', 'jetpack' ), $field_label ) );
2780
				}
2781
		}
2782
	}
2783
2784
2785
	/**
2786
	 * Check the default value for options field
2787
	 *
2788
	 * @param string value
2789
	 * @param int index
2790
	 * @param string default value
2791
	 *
2792
	 * @return string
2793
	 */
2794
	public function get_option_value( $value, $index, $options ) {
2795
		if ( empty( $value[ $index ] ) ) {
2796
			return $options;
2797
		}
2798
		return $value[ $index ];
2799
	}
2800
2801
	/**
2802
	 * Outputs the HTML for this form field
2803
	 *
2804
	 * @return string HTML
2805
	 */
2806
	function render() {
2807
		global $current_user, $user_identity;
2808
2809
		$r = '';
2810
2811
		$field_id          = $this->get_attribute( 'id' );
2812
		$field_type        = $this->get_attribute( 'type' );
2813
		$field_label       = $this->get_attribute( 'label' );
2814
		$field_required    = $this->get_attribute( 'required' );
2815
		$placeholder       = $this->get_attribute( 'placeholder' );
2816
		$class             = 'date' === $field_type ? 'jp-contact-form-date' : $this->get_attribute( 'class' );
2817
		$field_placeholder = ( ! empty( $placeholder ) ) ? "placeholder='" . esc_attr( $placeholder ) . "'" : '';
2818
		$field_class       = "class='" . trim( esc_attr( $field_type ) . ' ' . esc_attr( $class ) ) . "' ";
2819
2820
		if ( isset( $_POST[ $field_id ] ) ) {
2821
			if ( is_array( $_POST[ $field_id ] ) ) {
2822
				$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...
2823
			} else {
2824
				$this->value = stripslashes( (string) $_POST[ $field_id ] );
2825
			}
2826
		} elseif ( isset( $_GET[ $field_id ] ) ) {
2827
			$this->value = stripslashes( (string) $_GET[ $field_id ] );
2828
		} elseif (
2829
			is_user_logged_in() &&
2830
			( ( defined( 'IS_WPCOM' ) && IS_WPCOM ) ||
2831
			/**
2832
			 * Allow third-party tools to prefill the contact form with the user's details when they're logged in.
2833
			 *
2834
			 * @module contact-form
2835
			 *
2836
			 * @since 3.2.0
2837
			 *
2838
			 * @param bool false Should the Contact Form be prefilled with your details when you're logged in. Default to false.
2839
			 */
2840
			true === apply_filters( 'jetpack_auto_fill_logged_in_user', false )
2841
			)
2842
		) {
2843
			// Special defaults for logged-in users
2844
			switch ( $this->get_attribute( 'type' ) ) {
2845
				case 'email' :
2846
					$this->value = $current_user->data->user_email;
2847
				break;
2848
				case 'name' :
2849
					$this->value = $user_identity;
2850
				break;
2851
				case 'url' :
2852
					$this->value = $current_user->data->user_url;
2853
				break;
2854
				default :
2855
					$this->value = $this->get_attribute( 'default' );
2856
			}
2857
		} else {
2858
			$this->value = $this->get_attribute( 'default' );
2859
		}
2860
2861
		$field_value = Grunion_Contact_Form_Plugin::strip_tags( $this->value );
2862
		$field_label = Grunion_Contact_Form_Plugin::strip_tags( $field_label );
2863
2864
		/**
2865
		 * Filter the Contact Form required field text
2866
		 *
2867
		 * @module contact-form
2868
		 *
2869
		 * @since 3.8.0
2870
		 *
2871
		 * @param string $var Required field text. Default is "(required)".
2872
		 */
2873
		$required_field_text = esc_html( apply_filters( 'jetpack_required_field_text', __( '(required)', 'jetpack' ) ) );
2874
2875
		switch ( $field_type ) {
2876 View Code Duplication
			case 'email' :
2877
				$r .= "\n<div>\n";
2878
				$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";
2879
				$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";
2880
				$r .= "\t</div>\n";
2881
			break;
2882
			case 'telephone' :
2883
				$r .= "\n<div>\n";
2884
				$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";
2885
				$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";
2886
			break;
2887
			case 'url' :
2888
				$r .= "\n<div>\n";
2889
				$r .= "\t\t<label for='" . esc_attr( $field_id ) . "' class='grunion-field-label url" . ( $this->is_error() ? ' form-error' : '' ) . "'>" . esc_html( $field_label ) . ( $field_required ? '<span>' . $required_field_text . '</span>' : '' ) . "</label>\n";
2890
				$r .= "\t\t<input type='url' 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";
2891
				$r .= "\t</div>\n";
2892
			break;
2893 View Code Duplication
			case 'textarea' :
2894
				$r .= "\n<div>\n";
2895
				$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";
2896
				$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";
2897
				$r .= "\t</div>\n";
2898
			break;
2899
			case 'radio' :
2900
				$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";
2901
				foreach ( $this->get_attribute( 'options' ) as $optionIndex => $option ) {
2902
					$option = Grunion_Contact_Form_Plugin::strip_tags( $option );
2903
					$r .= "\t\t<label class='grunion-radio-label radio" . ( $this->is_error() ? ' form-error' : '' ) . "'>";
2904
					$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'" : '' ) . '/> ';
2905
					$r .= esc_html( $option ) . "</label>\n";
2906
					$r .= "\t\t<div class='clear-form'></div>\n";
2907
				}
2908
				$r .= "\t\t</div>\n";
2909
			break;
2910
			case 'checkbox' :
2911
				$r .= "\t<div>\n";
2912
				$r .= "\t\t<label class='grunion-field-label checkbox" . ( $this->is_error() ? ' form-error' : '' ) . "'>\n";
2913
				$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";
2914
				$r .= "\t\t" . esc_html( $field_label ) . ( $field_required ? '<span>' . $required_field_text . '</span>' : '' ) . "</label>\n";
2915
				$r .= "\t\t<div class='clear-form'></div>\n";
2916
				$r .= "\t</div>\n";
2917
			break;
2918
			case 'checkbox-multiple' :
2919
				$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";
2920
				foreach ( $this->get_attribute( 'options' ) as $optionIndex => $option ) {
2921
					$option = Grunion_Contact_Form_Plugin::strip_tags( $option );
2922
					$r .= "\t\t<label class='grunion-checkbox-multiple-label checkbox-multiple" . ( $this->is_error() ? ' form-error' : '' ) . "'>";
2923
					$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 ) . ' /> ';
2924
					$r .= esc_html( $option ) . "</label>\n";
2925
					$r .= "\t\t<div class='clear-form'></div>\n";
2926
				}
2927
				$r .= "\t\t</div>\n";
2928
			break;
2929
			case 'select' :
2930
				$r .= "\n<div>\n";
2931
				$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";
2932
				$r .= "\t<select name='" . esc_attr( $field_id ) . "' id='" . esc_attr( $field_id ) . "' " . $field_class . ( $field_required ? "required aria-required='true'" : '' ) . ">\n";
2933
				foreach ( $this->get_attribute( 'options' ) as $optionIndex => $option ) {
2934
					$option = Grunion_Contact_Form_Plugin::strip_tags( $option );
2935
					$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";
2936
				}
2937
				$r .= "\t</select>\n";
2938
				$r .= "\t</div>\n";
2939
			break;
2940
			case 'date' :
2941
				$r .= "\n<div>\n";
2942
				$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";
2943
				$r .= "\t\t<input type='text' name='" . esc_attr( $field_id ) . "' id='" . esc_attr( $field_id ) . "' value='" . esc_attr( $field_value ) . "' " . $field_class . ( $field_required ? "required aria-required='true'" : '' ) . "/>\n";
2944
				$r .= "\t</div>\n";
2945
2946
				wp_enqueue_script(
2947
					'grunion-frontend',
2948
					Jetpack::get_file_url_for_environment(
2949
						'_inc/build/contact-form/js/grunion-frontend.min.js',
2950
						'modules/contact-form/js/grunion-frontend.js'
2951
					),
2952
					array( 'jquery', 'jquery-ui-datepicker' )
2953
				);
2954
				wp_enqueue_style( 'jp-jquery-ui-datepicker', plugins_url( 'css/jquery-ui-datepicker.css', __FILE__ ), array( 'dashicons' ), '1.0' );
2955
2956
				// Using Core's built-in datepicker localization routine
2957
				wp_localize_jquery_ui_datepicker();
2958
			break;
2959 View Code Duplication
			default : // text field
2960
				// note that any unknown types will produce a text input, so we can use arbitrary type names to handle
2961
				// input fields like name, email, url that require special validation or handling at POST
2962
				$r .= "\n<div>\n";
2963
				$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";
2964
				$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";
2965
				$r .= "\t</div>\n";
2966
		}
2967
2968
		/**
2969
		 * Filter the HTML of the Contact Form.
2970
		 *
2971
		 * @module contact-form
2972
		 *
2973
		 * @since 2.6.0
2974
		 *
2975
		 * @param string $r Contact Form HTML output.
2976
		 * @param string $field_label Field label.
2977
		 * @param int|null $id Post ID.
2978
		 */
2979
		return apply_filters( 'grunion_contact_form_field_html', $r, $field_label, ( in_the_loop() ? get_the_ID() : null ) );
2980
	}
2981
}
2982
2983
add_action( 'init', array( 'Grunion_Contact_Form_Plugin', 'init' ) );
2984
2985
add_action( 'grunion_scheduled_delete', 'grunion_delete_old_spam' );
2986
2987
/**
2988
 * Deletes old spam feedbacks to keep the posts table size under control
2989
 */
2990
function grunion_delete_old_spam() {
2991
	global $wpdb;
2992
2993
	$grunion_delete_limit = 100;
2994
2995
	$now_gmt = current_time( 'mysql', 1 );
2996
	$sql = $wpdb->prepare( "
2997
		SELECT `ID`
2998
		FROM $wpdb->posts
2999
		WHERE DATE_SUB( %s, INTERVAL 15 DAY ) > `post_date_gmt`
3000
			AND `post_type` = 'feedback'
3001
			AND `post_status` = 'spam'
3002
		LIMIT %d
3003
	", $now_gmt, $grunion_delete_limit );
3004
	$post_ids = $wpdb->get_col( $sql );
3005
3006
	foreach ( (array) $post_ids as $post_id ) {
3007
		// force a full delete, skip the trash
3008
		wp_delete_post( $post_id, true );
3009
	}
3010
3011
	if (
3012
		/**
3013
		 * Filter if the module run OPTIMIZE TABLE on the core WP tables.
3014
		 *
3015
		 * @module contact-form
3016
		 *
3017
		 * @since 1.3.1
3018
		 * @since 6.4.0 Set to false by default.
3019
		 *
3020
		 * @param bool $filter Should Jetpack optimize the table, defaults to false.
3021
		 */
3022
		apply_filters( 'grunion_optimize_table', false )
3023
	) {
3024
		$wpdb->query( "OPTIMIZE TABLE $wpdb->posts" );
3025
	}
3026
3027
	// if we hit the max then schedule another run
3028
	if ( count( $post_ids ) >= $grunion_delete_limit ) {
3029
		wp_schedule_single_event( time() + 700, 'grunion_scheduled_delete' );
3030
	}
3031
}
3032