Completed
Push — branch-6.5 ( a9f5bf...d726ff )
by
unknown
12:33 queued 05:51
created

personal_data_eraser()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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