Completed
Push — gm18/grunionblock ( dfadbe...bddab0 )
by George
11:22 queued 04:29
created

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