Completed
Push — renovate/react-monorepo ( 545e89...65a81e )
by
unknown
268:48 queued 253:47
created

Grunion_Contact_Form_Plugin::sanitize_value()   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 1
dl 0
loc 3
rs 10
c 0
b 0
f 0
1
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
2
/**
3
 * Grunion Contact Form
4
 * Add a contact form to any post, page or text widget.
5
 * Emails will be sent to the post's author by default, or any email address you choose.
6
 *
7
 * @package automattic/jetpack
8
 */
9
10
use Automattic\Jetpack\Assets;
11
use Automattic\Jetpack\Blocks;
12
use Automattic\Jetpack\Sync\Settings;
13
14
define( 'GRUNION_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
15
define( 'GRUNION_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
16
17
if ( is_admin() ) {
18
	require_once GRUNION_PLUGIN_DIR . 'admin.php';
19
}
20
21
add_action( 'rest_api_init', 'grunion_contact_form_require_endpoint' );
22
function grunion_contact_form_require_endpoint() {
23
	require_once GRUNION_PLUGIN_DIR . 'class-grunion-contact-form-endpoint.php';
24
}
25
26
/**
27
 * Sets up various actions, filters, post types, post statuses, shortcodes.
28
 */
29
class Grunion_Contact_Form_Plugin {
30
31
	/**
32
	 * @var string The Widget ID of the widget currently being processed.  Used to build the unique contact-form ID for forms embedded in widgets.
33
	 */
34
	public $current_widget_id;
35
36
	static $using_contact_form_field = false;
37
38
	/**
39
	 * @var int The last Feedback Post ID Erased as part of the Personal Data Eraser.
40
	 * Helps with pagination.
41
	 */
42
	private $pde_last_post_id_erased = 0;
43
44
	/**
45
	 * @var string The email address for which we are deleting/exporting all feedbacks
46
	 * as part of a Personal Data Eraser or Personal Data Exporter request.
47
	 */
48
	private $pde_email_address = '';
49
50
	static function init() {
51
		static $instance = false;
52
53
		if ( ! $instance ) {
54
			$instance = new Grunion_Contact_Form_Plugin();
55
56
			// Schedule our daily cleanup
57
			add_action( 'wp_scheduled_delete', array( $instance, 'daily_akismet_meta_cleanup' ) );
58
		}
59
60
		return $instance;
61
	}
62
63
	/**
64
	 * Runs daily to clean up spam detection metadata after 15 days.  Keeps your DB squeaky clean.
65
	 */
66
	public function daily_akismet_meta_cleanup() {
67
		global $wpdb;
68
69
		$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" );
70
71
		if ( empty( $feedback_ids ) ) {
72
			return;
73
		}
74
75
		/**
76
		 * Fires right before deleting the _feedback_akismet_values post meta on $feedback_ids
77
		 *
78
		 * @module contact-form
79
		 *
80
		 * @since 6.1.0
81
		 *
82
		 * @param array $feedback_ids list of feedback post ID
83
		 */
84
		do_action( 'jetpack_daily_akismet_meta_cleanup_before', $feedback_ids );
85
		foreach ( $feedback_ids as $feedback_id ) {
86
			delete_post_meta( $feedback_id, '_feedback_akismet_values' );
87
		}
88
89
		/**
90
		 * Fires right after deleting the _feedback_akismet_values post meta on $feedback_ids
91
		 *
92
		 * @module contact-form
93
		 *
94
		 * @since 6.1.0
95
		 *
96
		 * @param array $feedback_ids list of feedback post ID
97
		 */
98
		do_action( 'jetpack_daily_akismet_meta_cleanup_after', $feedback_ids );
99
	}
100
101
	/**
102
	 * Strips HTML tags from input.  Output is NOT HTML safe.
103
	 *
104
	 * @param mixed $data_with_tags
105
	 * @return mixed
106
	 */
107
	public static function strip_tags( $data_with_tags ) {
108
		if ( is_array( $data_with_tags ) ) {
109
			foreach ( $data_with_tags as $index => $value ) {
110
				$index = sanitize_text_field( (string) $index );
111
				$value = wp_kses( (string) $value, array() );
112
				$value = str_replace( '&amp;', '&', $value ); // undo damage done by wp_kses_normalize_entities()
113
114
				$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...
115
			}
116
		} else {
117
			$data_without_tags = wp_kses( $data_with_tags, array() );
118
			$data_without_tags = str_replace( '&amp;', '&', $data_without_tags ); // undo damage done by wp_kses_normalize_entities()
119
		}
120
121
		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...
122
	}
123
124
	/**
125
	 * Class uses singleton pattern; use Grunion_Contact_Form_Plugin::init() to initialize.
126
	 */
127
	protected function __construct() {
128
		$this->add_shortcode();
129
130
		// While generating the output of a text widget with a contact-form shortcode, we need to know its widget ID.
131
		add_action( 'dynamic_sidebar', array( $this, 'track_current_widget' ) );
132
133
		// Add a "widget" shortcode attribute to all contact-form shortcodes embedded in widgets
134
		add_filter( 'widget_text', array( $this, 'widget_atts' ), 0 );
135
136
		// If Text Widgets don't get shortcode processed, hack ours into place.
137
		if (
138
			version_compare( get_bloginfo( 'version' ), '4.9-z', '<=' )
139
			&& ! has_filter( 'widget_text', 'do_shortcode' )
140
		) {
141
			add_filter( 'widget_text', array( $this, 'widget_shortcode_hack' ), 5 );
142
		}
143
144
		add_filter( 'jetpack_contact_form_is_spam', array( $this, 'is_spam_blocklist' ), 10, 2 );
145
		add_filter( 'jetpack_contact_form_in_comment_disallowed_list', array( $this, 'is_in_disallowed_list' ), 10, 2 );
146
		// Akismet to the rescue
147
		if ( defined( 'AKISMET_VERSION' ) || function_exists( 'akismet_http_post' ) ) {
148
			add_filter( 'jetpack_contact_form_is_spam', array( $this, 'is_spam_akismet' ), 10, 2 );
149
			add_action( 'contact_form_akismet', array( $this, 'akismet_submit' ), 10, 2 );
150
		}
151
152
		add_action( 'loop_start', array( 'Grunion_Contact_Form', '_style_on' ) );
153
		add_action( 'pre_amp_render_post', array( 'Grunion_Contact_Form', '_style_on' ) );
154
155
		add_action( 'wp_ajax_grunion-contact-form', array( $this, 'ajax_request' ) );
156
		add_action( 'wp_ajax_nopriv_grunion-contact-form', array( $this, 'ajax_request' ) );
157
158
		// GDPR: personal data exporter & eraser.
159
		add_filter( 'wp_privacy_personal_data_exporters', array( $this, 'register_personal_data_exporter' ) );
160
		add_filter( 'wp_privacy_personal_data_erasers', array( $this, 'register_personal_data_eraser' ) );
161
162
		// Export to CSV feature
163
		if ( is_admin() ) {
164
			add_action( 'admin_init', array( $this, 'download_feedback_as_csv' ) );
165
			add_action( 'admin_footer-edit.php', array( $this, 'export_form' ) );
166
			add_action( 'admin_menu', array( $this, 'admin_menu' ) );
167
			add_action( 'current_screen', array( $this, 'unread_count' ) );
168
		}
169
170
		// custom post type we'll use to keep copies of the feedback items
171
		register_post_type(
172
			'feedback', array(
173
				'labels'                => array(
174
					'name'               => __( 'Feedback', 'jetpack' ),
175
					'singular_name'      => __( 'Feedback', 'jetpack' ),
176
					'search_items'       => __( 'Search Feedback', 'jetpack' ),
177
					'not_found'          => __( 'No feedback found', 'jetpack' ),
178
					'not_found_in_trash' => __( 'No feedback found', 'jetpack' ),
179
				),
180
				'menu_icon'             => 'dashicons-feedback',
181
				'show_ui'               => true,
182
				'show_in_admin_bar'     => false,
183
				'public'                => false,
184
				'rewrite'               => false,
185
				'query_var'             => false,
186
				'capability_type'       => 'page',
187
				'show_in_rest'          => true,
188
				'rest_controller_class' => 'Grunion_Contact_Form_Endpoint',
189
				'capabilities'          => array(
190
					'create_posts'        => 'do_not_allow',
191
					'publish_posts'       => 'publish_pages',
192
					'edit_posts'          => 'edit_pages',
193
					'edit_others_posts'   => 'edit_others_pages',
194
					'delete_posts'        => 'delete_pages',
195
					'delete_others_posts' => 'delete_others_pages',
196
					'read_private_posts'  => 'read_private_pages',
197
					'edit_post'           => 'edit_page',
198
					'delete_post'         => 'delete_page',
199
					'read_post'           => 'read_page',
200
				),
201
				'map_meta_cap'          => true,
202
			)
203
		);
204
205
		// Add to REST API post type allowed list.
206
		add_filter( 'rest_api_allowed_post_types', array( $this, 'allow_feedback_rest_api_type' ) );
207
208
		// Add "spam" as a post status
209
		register_post_status(
210
			'spam', array(
211
				'label'                  => 'Spam',
212
				'public'                 => false,
213
				'exclude_from_search'    => true,
214
				'show_in_admin_all_list' => false,
215
				'label_count'            => _n_noop( 'Spam <span class="count">(%s)</span>', 'Spam <span class="count">(%s)</span>', 'jetpack' ),
216
				'protected'              => true,
217
				'_builtin'               => false,
218
			)
219
		);
220
221
		// POST handler
222
		if (
223
			isset( $_SERVER['REQUEST_METHOD'] ) && 'POST' == strtoupper( $_SERVER['REQUEST_METHOD'] )
224
			&&
225
			isset( $_POST['action'] ) && 'grunion-contact-form' == $_POST['action']
226
			&&
227
			isset( $_POST['contact-form-id'] )
228
		) {
229
			add_action( 'template_redirect', array( $this, 'process_form_submission' ) );
230
		}
231
232
		/*
233
		 Can be dequeued by placing the following in wp-content/themes/yourtheme/functions.php
234
		 *
235
		 * 	function remove_grunion_style() {
236
		 *		wp_deregister_style('grunion.css');
237
		 *	}
238
		 *	add_action('wp_print_styles', 'remove_grunion_style');
239
		 */
240
		wp_register_style( 'grunion.css', GRUNION_PLUGIN_URL . 'css/grunion.css', array(), JETPACK__VERSION );
241
		wp_style_add_data( 'grunion.css', 'rtl', 'replace' );
242
243
		self::register_contact_form_blocks();
244
	}
245
246
	private static function register_contact_form_blocks() {
247
		Blocks::jetpack_register_block(
248
			'jetpack/contact-form',
249
			array(
250
				'render_callback' => array( __CLASS__, 'gutenblock_render_form' ),
251
			)
252
		);
253
254
		// Field render methods.
255
		Blocks::jetpack_register_block(
256
			'jetpack/field-text',
257
			array(
258
				'parent'          => array( 'jetpack/contact-form' ),
259
				'render_callback' => array( __CLASS__, 'gutenblock_render_field_text' ),
260
			)
261
		);
262
		Blocks::jetpack_register_block(
263
			'jetpack/field-name',
264
			array(
265
				'parent'          => array( 'jetpack/contact-form' ),
266
				'render_callback' => array( __CLASS__, 'gutenblock_render_field_name' ),
267
			)
268
		);
269
		Blocks::jetpack_register_block(
270
			'jetpack/field-email',
271
			array(
272
				'parent'          => array( 'jetpack/contact-form' ),
273
				'render_callback' => array( __CLASS__, 'gutenblock_render_field_email' ),
274
			)
275
		);
276
		Blocks::jetpack_register_block(
277
			'jetpack/field-url',
278
			array(
279
				'parent'          => array( 'jetpack/contact-form' ),
280
				'render_callback' => array( __CLASS__, 'gutenblock_render_field_url' ),
281
			)
282
		);
283
		Blocks::jetpack_register_block(
284
			'jetpack/field-date',
285
			array(
286
				'parent'          => array( 'jetpack/contact-form' ),
287
				'render_callback' => array( __CLASS__, 'gutenblock_render_field_date' ),
288
			)
289
		);
290
		Blocks::jetpack_register_block(
291
			'jetpack/field-telephone',
292
			array(
293
				'parent'          => array( 'jetpack/contact-form' ),
294
				'render_callback' => array( __CLASS__, 'gutenblock_render_field_telephone' ),
295
			)
296
		);
297
		Blocks::jetpack_register_block(
298
			'jetpack/field-textarea',
299
			array(
300
				'parent'          => array( 'jetpack/contact-form' ),
301
				'render_callback' => array( __CLASS__, 'gutenblock_render_field_textarea' ),
302
			)
303
		);
304
		Blocks::jetpack_register_block(
305
			'jetpack/field-checkbox',
306
			array(
307
				'parent'          => array( 'jetpack/contact-form' ),
308
				'render_callback' => array( __CLASS__, 'gutenblock_render_field_checkbox' ),
309
			)
310
		);
311
		Blocks::jetpack_register_block(
312
			'jetpack/field-checkbox-multiple',
313
			array(
314
				'parent'          => array( 'jetpack/contact-form' ),
315
				'render_callback' => array( __CLASS__, 'gutenblock_render_field_checkbox_multiple' ),
316
			)
317
		);
318
		Blocks::jetpack_register_block(
319
			'jetpack/field-radio',
320
			array(
321
				'parent'          => array( 'jetpack/contact-form' ),
322
				'render_callback' => array( __CLASS__, 'gutenblock_render_field_radio' ),
323
			)
324
		);
325
		Blocks::jetpack_register_block(
326
			'jetpack/field-select',
327
			array(
328
				'parent'          => array( 'jetpack/contact-form' ),
329
				'render_callback' => array( __CLASS__, 'gutenblock_render_field_select' ),
330
			)
331
		);
332
		Blocks::jetpack_register_block(
333
			'jetpack/field-consent',
334
			array(
335
				'parent'          => array( 'jetpack/contact-form' ),
336
				'render_callback' => array( __CLASS__, 'gutenblock_render_field_consent' ),
337
			)
338
		);
339
	}
340
341
	public static function gutenblock_render_form( $atts, $content ) {
342
343
		// Render fallback in other contexts than frontend (i.e. feed, emails, API, etc.), unless the form is being submitted.
344
		if ( ! jetpack_is_frontend() && ! isset( $_POST['contact-form-id'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
345
			return sprintf(
346
				'<div class="%1$s"><a href="%2$s" target="_blank" rel="noopener noreferrer">%3$s</a></div>',
347
				esc_attr( Blocks::classes( 'contact-form', $atts ) ),
348
				esc_url( get_the_permalink() ),
349
				esc_html__( 'Submit a form.', 'jetpack' )
350
			);
351
		}
352
353
		return Grunion_Contact_Form::parse( $atts, do_blocks( $content ) );
354
	}
355
356
	public static function block_attributes_to_shortcode_attributes( $atts, $type ) {
357
		$atts['type'] = $type;
358
		if ( isset( $atts['className'] ) ) {
359
			$atts['class'] = $atts['className'];
360
			unset( $atts['className'] );
361
		}
362
363
		if ( isset( $atts['defaultValue'] ) ) {
364
			$atts['default'] = $atts['defaultValue'];
365
			unset( $atts['defaultValue'] );
366
		}
367
368
		return $atts;
369
	}
370
371
	public static function gutenblock_render_field_text( $atts, $content ) {
372
		$atts = self::block_attributes_to_shortcode_attributes( $atts, 'text' );
373
		return Grunion_Contact_Form::parse_contact_field( $atts, $content );
374
	}
375
	public static function gutenblock_render_field_name( $atts, $content ) {
376
		$atts = self::block_attributes_to_shortcode_attributes( $atts, 'name' );
377
		return Grunion_Contact_Form::parse_contact_field( $atts, $content );
378
	}
379
	public static function gutenblock_render_field_email( $atts, $content ) {
380
		$atts = self::block_attributes_to_shortcode_attributes( $atts, 'email' );
381
		return Grunion_Contact_Form::parse_contact_field( $atts, $content );
382
	}
383
	public static function gutenblock_render_field_url( $atts, $content ) {
384
		$atts = self::block_attributes_to_shortcode_attributes( $atts, 'url' );
385
		return Grunion_Contact_Form::parse_contact_field( $atts, $content );
386
	}
387
	public static function gutenblock_render_field_date( $atts, $content ) {
388
		$atts = self::block_attributes_to_shortcode_attributes( $atts, 'date' );
389
		return Grunion_Contact_Form::parse_contact_field( $atts, $content );
390
	}
391
	public static function gutenblock_render_field_telephone( $atts, $content ) {
392
		$atts = self::block_attributes_to_shortcode_attributes( $atts, 'telephone' );
393
		return Grunion_Contact_Form::parse_contact_field( $atts, $content );
394
	}
395
	public static function gutenblock_render_field_textarea( $atts, $content ) {
396
		$atts = self::block_attributes_to_shortcode_attributes( $atts, 'textarea' );
397
		return Grunion_Contact_Form::parse_contact_field( $atts, $content );
398
	}
399
	public static function gutenblock_render_field_checkbox( $atts, $content ) {
400
		$atts = self::block_attributes_to_shortcode_attributes( $atts, 'checkbox' );
401
		return Grunion_Contact_Form::parse_contact_field( $atts, $content );
402
	}
403
	public static function gutenblock_render_field_checkbox_multiple( $atts, $content ) {
404
		$atts = self::block_attributes_to_shortcode_attributes( $atts, 'checkbox-multiple' );
405
		return Grunion_Contact_Form::parse_contact_field( $atts, $content );
406
	}
407
	public static function gutenblock_render_field_radio( $atts, $content ) {
408
		$atts = self::block_attributes_to_shortcode_attributes( $atts, 'radio' );
409
		return Grunion_Contact_Form::parse_contact_field( $atts, $content );
410
	}
411
	public static function gutenblock_render_field_select( $atts, $content ) {
412
		$atts = self::block_attributes_to_shortcode_attributes( $atts, 'select' );
413
		return Grunion_Contact_Form::parse_contact_field( $atts, $content );
414
	}
415
416
	/**
417
	 * Render the consent field.
418
	 *
419
	 * @param string $atts consent attributes.
420
	 * @param string $content html content.
421
	 */
422
	public static function gutenblock_render_field_consent( $atts, $content ) {
423
		$atts = self::block_attributes_to_shortcode_attributes( $atts, 'consent' );
424
425
		if ( ! isset( $atts['implicitConsentMessage'] ) ) {
426
			$atts['implicitConsentMessage'] = __( "By submitting your information, you're giving us permission to email you. You may unsubscribe at any time.", 'jetpack' );
427
		}
428
429
		if ( ! isset( $atts['explicitConsentMessage'] ) ) {
430
			$atts['explicitConsentMessage'] = __( 'Can we send you an email from time to time?', 'jetpack' );
431
		}
432
433
		return Grunion_Contact_Form::parse_contact_field( $atts, $content );
434
	}
435
436
	/**
437
	 * Add the 'Export' menu item as a submenu of Feedback.
438
	 */
439
	public function admin_menu() {
440
		add_submenu_page(
441
			'edit.php?post_type=feedback',
442
			__( 'Export feedback as CSV', 'jetpack' ),
443
			__( 'Export CSV', 'jetpack' ),
444
			'export',
445
			'feedback-export',
446
			array( $this, 'export_form' )
447
		);
448
	}
449
450
	/**
451
	 * Add to REST API post type allowed list.
452
	 */
453
	function allow_feedback_rest_api_type( $post_types ) {
454
		$post_types[] = 'feedback';
455
		return $post_types;
456
	}
457
458
	/**
459
	 * Display the count of new feedback entries received. It's reset when user visits the Feedback screen.
460
	 *
461
	 * @since 4.1.0
462
	 *
463
	 * @param object $screen Information about the current screen.
464
	 */
465
	function unread_count( $screen ) {
466
		if ( isset( $screen->post_type ) && 'feedback' == $screen->post_type ) {
467
			update_option( 'feedback_unread_count', 0 );
468
		} else {
469
			global $menu;
470
			if ( isset( $menu ) && is_array( $menu ) && ! empty( $menu ) ) {
471
				foreach ( $menu as $index => $menu_item ) {
472
					if ( 'edit.php?post_type=feedback' == $menu_item[2] ) {
473
						$unread = get_option( 'feedback_unread_count', 0 );
474
						if ( $unread > 0 ) {
475
							$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>' : '';
476
							$menu[ $index ][0] .= $unread_count;
477
						}
478
						break;
479
					}
480
				}
481
			}
482
		}
483
	}
484
485
	/**
486
	 * Handles all contact-form POST submissions
487
	 *
488
	 * Conditionally attached to `template_redirect`
489
	 */
490
	function process_form_submission() {
491
		// Add a filter to replace tokens in the subject field with sanitized field values
492
		add_filter( 'contact_form_subject', array( $this, 'replace_tokens_with_input' ), 10, 2 );
493
494
		$id   = stripslashes( $_POST['contact-form-id'] );
495
		$hash = isset( $_POST['contact-form-hash'] ) ? $_POST['contact-form-hash'] : '';
496
		$hash = preg_replace( '/[^\da-f]/i', '', $hash );
497
498
		if ( ! is_string( $id ) || ! is_string( $hash ) ) {
499
			return false;
500
		}
501
502
		if ( is_user_logged_in() ) {
503
			check_admin_referer( "contact-form_{$id}" );
504
		}
505
506
		$is_widget = 0 === strpos( $id, 'widget-' );
507
508
		$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...
509
510
		if ( $is_widget ) {
511
			// It's a form embedded in a text widget
512
			$this->current_widget_id = substr( $id, 7 ); // remove "widget-"
513
			$widget_type             = implode( '-', array_slice( explode( '-', $this->current_widget_id ), 0, -1 ) ); // Remove trailing -#
514
515
			// Is the widget active?
516
			$sidebar = is_active_widget( false, $this->current_widget_id, $widget_type );
517
518
			// This is lame - no core API for getting a widget by ID
519
			$widget = isset( $GLOBALS['wp_registered_widgets'][ $this->current_widget_id ] ) ? $GLOBALS['wp_registered_widgets'][ $this->current_widget_id ] : false;
520
521
			if ( $sidebar && $widget && isset( $widget['callback'] ) ) {
522
				// prevent PHP notices by populating widget args
523
				$widget_args = array(
524
					'before_widget' => '',
525
					'after_widget'  => '',
526
					'before_title'  => '',
527
					'after_title'   => '',
528
				);
529
				// This is lamer - no API for outputting a given widget by ID
530
				ob_start();
531
				// Process the widget to populate Grunion_Contact_Form::$last
532
				call_user_func( $widget['callback'], $widget_args, $widget['params'][0] );
533
				ob_end_clean();
534
			}
535
		} else {
536
			// It's a form embedded in a post
537
			$post = get_post( $id );
538
539
			// Process the content to populate Grunion_Contact_Form::$last
540
			if ( $post ) {
541
				/** This filter is already documented in core. wp-includes/post-template.php */
542
				apply_filters( 'the_content', $post->post_content );
0 ignored issues
show
Unused Code introduced by
The call to the function apply_filters() seems unnecessary as the function has no side-effects.
Loading history...
543
			}
544
		}
545
546
		$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...
547
548
		// No form may mean user is using do_shortcode, grab the form using the stored post meta
549
		if ( ! $form && is_numeric( $id ) && $hash ) {
550
551
			// Get shortcode from post meta
552
			$shortcode = get_post_meta( $id, "_g_feedback_shortcode_{$hash}", true );
553
554
			// Format it
555
			if ( $shortcode != '' ) {
556
557
				// Get attributes from post meta.
558
				$parameters = '';
559
				$attributes = get_post_meta( $id, "_g_feedback_shortcode_atts_{$hash}", true );
560
				if ( ! empty( $attributes ) && is_array( $attributes ) ) {
561
					foreach ( array_filter( $attributes ) as $param => $value ) {
562
						$parameters .= " $param=\"$value\"";
563
					}
564
				}
565
566
				$shortcode = '[contact-form' . $parameters . ']' . $shortcode . '[/contact-form]';
567
				do_shortcode( $shortcode );
568
569
				// Recreate form
570
				$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...
571
			}
572
		}
573
574
		if ( ! $form ) {
575
			return false;
576
		}
577
578
		if ( is_wp_error( $form->errors ) && $form->errors->get_error_codes() ) {
579
			return $form->errors;
580
		}
581
582
		// Process the form
583
		return $form->process_submission();
584
	}
585
586
	function ajax_request() {
587
		$submission_result = self::process_form_submission();
588
589
		if ( ! $submission_result ) {
590
			header( 'HTTP/1.1 500 Server Error', 500, true );
591
			echo '<div class="form-error"><ul class="form-errors"><li class="form-error-message">';
592
			esc_html_e( 'An error occurred. Please try again later.', 'jetpack' );
593
			echo '</li></ul></div>';
594
		} elseif ( is_wp_error( $submission_result ) ) {
595
			header( 'HTTP/1.1 400 Bad Request', 403, true );
596
			echo '<div class="form-error"><ul class="form-errors"><li class="form-error-message">';
597
			echo esc_html( $submission_result->get_error_message() );
598
			echo '</li></ul></div>';
599
		} else {
600
			echo '<h3>' . esc_html__( 'Message Sent', 'jetpack' ) . '</h3>' . $submission_result;
601
		}
602
603
		die;
604
	}
605
606
	/**
607
	 * Ensure the post author is always zero for contact-form feedbacks
608
	 * Attached to `wp_insert_post_data`
609
	 *
610
	 * @see Grunion_Contact_Form::process_submission()
611
	 *
612
	 * @param array $data the data to insert
613
	 * @param array $postarr the data sent to wp_insert_post()
614
	 * @return array The filtered $data to insert
615
	 */
616
	function insert_feedback_filter( $data, $postarr ) {
617
		if ( $data['post_type'] == 'feedback' && $postarr['post_type'] == 'feedback' ) {
618
			$data['post_author'] = 0;
619
		}
620
621
		return $data;
622
	}
623
	/*
624
	 * Adds our contact-form shortcode
625
	 * The "child" contact-field shortcode is enabled as needed by the contact-form shortcode handler
626
	 */
627
	function add_shortcode() {
628
		add_shortcode( 'contact-form', array( 'Grunion_Contact_Form', 'parse' ) );
629
		add_shortcode( 'contact-field', array( 'Grunion_Contact_Form', 'parse_contact_field' ) );
630
	}
631
632
	static function tokenize_label( $label ) {
633
		return '{' . trim( preg_replace( '#^\d+_#', '', $label ) ) . '}';
634
	}
635
636
	static function sanitize_value( $value ) {
637
		return preg_replace( '=((<CR>|<LF>|0x0A/%0A|0x0D/%0D|\\n|\\r)\S).*=i', null, $value );
638
	}
639
640
	/**
641
	 * Replaces tokens like {city} or {City} (case insensitive) with the value
642
	 * of an input field of that name
643
	 *
644
	 * @param string $subject
645
	 * @param array  $field_values Array with field label => field value associations
646
	 *
647
	 * @return string The filtered $subject with the tokens replaced
648
	 */
649
	function replace_tokens_with_input( $subject, $field_values ) {
650
		// Wrap labels into tokens (inside {})
651
		$wrapped_labels = array_map( array( 'Grunion_Contact_Form_Plugin', 'tokenize_label' ), array_keys( $field_values ) );
652
		// Sanitize all values
653
		$sanitized_values = array_map( array( 'Grunion_Contact_Form_Plugin', 'sanitize_value' ), array_values( $field_values ) );
654
655
		foreach ( $sanitized_values as $k => $sanitized_value ) {
656
			if ( is_array( $sanitized_value ) ) {
657
				$sanitized_values[ $k ] = implode( ', ', $sanitized_value );
658
			}
659
		}
660
661
		// Search for all valid tokens (based on existing fields) and replace with the field's value
662
		$subject = str_ireplace( $wrapped_labels, $sanitized_values, $subject );
663
		return $subject;
664
	}
665
666
	/**
667
	 * Tracks the widget currently being processed.
668
	 * Attached to `dynamic_sidebar`
669
	 *
670
	 * @see $current_widget_id
671
	 *
672
	 * @param array $widget The widget data
673
	 */
674
	function track_current_widget( $widget ) {
675
		$this->current_widget_id = $widget['id'];
676
	}
677
678
	/**
679
	 * Adds a "widget" attribute to every contact-form embedded in a text widget.
680
	 * Used to tell the difference between post-embedded contact-forms and widget-embedded contact-forms
681
	 * Attached to `widget_text`
682
	 *
683
	 * @param string $text The widget text
684
	 * @return string The filtered widget text
685
	 */
686
	function widget_atts( $text ) {
687
		Grunion_Contact_Form::style( true );
688
689
		return preg_replace( '/\[contact-form([^a-zA-Z_-])/', '[contact-form widget="' . $this->current_widget_id . '"\\1', $text );
690
	}
691
692
	/**
693
	 * For sites where text widgets are not processed for shortcodes, we add this hack to process just our shortcode
694
	 * Attached to `widget_text`
695
	 *
696
	 * @param string $text The widget text
697
	 * @return string The contact-form filtered widget text
698
	 */
699
	function widget_shortcode_hack( $text ) {
700
		if ( ! preg_match( '/\[contact-form([^a-zA-Z_-])/', $text ) ) {
701
			return $text;
702
		}
703
704
		$old = $GLOBALS['shortcode_tags'];
705
		remove_all_shortcodes();
706
		Grunion_Contact_Form_Plugin::$using_contact_form_field = true;
707
		$this->add_shortcode();
708
709
		$text = do_shortcode( $text );
710
711
		Grunion_Contact_Form_Plugin::$using_contact_form_field = false;
712
		$GLOBALS['shortcode_tags']                             = $old;
713
714
		return $text;
715
	}
716
717
	/**
718
	 * Check if a submission matches the Comment Blocklist.
719
	 * The Comment Blocklist is a means to moderate discussion, and contact
720
	 * forms are 1:1 discussion forums, ripe for abuse by users who are being
721
	 * removed from the public discussion.
722
	 * Attached to `jetpack_contact_form_is_spam`
723
	 *
724
	 * @param bool  $is_spam
725
	 * @param array $form
726
	 * @return bool TRUE => spam, FALSE => not spam
727
	 */
728
	public function is_spam_blocklist( $is_spam, $form = array() ) {
729
		if ( $is_spam ) {
730
			return $is_spam;
731
		}
732
733
		return $this->is_in_disallowed_list( false, $form );
734
	}
735
736
	/**
737
	 * Check if a submission matches the comment disallowed list.
738
	 * Attached to `jetpack_contact_form_in_comment_disallowed_list`.
739
	 *
740
	 * @param boolean $in_disallowed_list Whether the feedback is in the disallowed list.
741
	 * @param array   $form The form array.
742
	 * @return bool Returns true if the form submission matches the disallowed list and false if it doesn't.
743
	 */
744
	public function is_in_disallowed_list( $in_disallowed_list, $form = array() ) {
745
		if ( $in_disallowed_list ) {
746
			return $in_disallowed_list;
747
		}
748
749
		if (
750
			wp_check_comment_disallowed_list(
751
				$form['comment_author'],
752
				$form['comment_author_email'],
753
				$form['comment_author_url'],
754
				$form['comment_content'],
755
				$form['user_ip'],
756
				$form['user_agent']
757
			)
758
		) {
759
			return true;
760
		}
761
762
		return false;
763
	}
764
765
	/**
766
	 * Populate an array with all values necessary to submit a NEW contact-form feedback to Akismet.
767
	 * Note that this includes the current user_ip etc, so this should only be called when accepting a new item via $_POST
768
	 *
769
	 * @param array $form Contact form feedback array
770
	 * @return array feedback array with additional data ready for submission to Akismet
771
	 */
772
	function prepare_for_akismet( $form ) {
773
		$form['comment_type'] = 'contact_form';
774
		$form['user_ip']      = $_SERVER['REMOTE_ADDR'];
775
		$form['user_agent']   = isset( $_SERVER['HTTP_USER_AGENT'] ) ? $_SERVER['HTTP_USER_AGENT'] : '';
776
		$form['referrer']     = isset( $_SERVER['HTTP_REFERER'] ) ? $_SERVER['HTTP_REFERER'] : '';
777
		$form['blog']         = get_option( 'home' );
778
779
		foreach ( $_SERVER as $key => $value ) {
780
			if ( ! is_string( $value ) ) {
781
				continue;
782
			}
783
			if ( in_array( $key, array( 'HTTP_COOKIE', 'HTTP_COOKIE2', 'HTTP_USER_AGENT', 'HTTP_REFERER' ) ) ) {
784
				// We don't care about cookies, and the UA and Referrer were caught above.
785
				continue;
786
			} elseif ( in_array( $key, array( 'REMOTE_ADDR', 'REQUEST_URI', 'DOCUMENT_URI' ) ) ) {
787
				// All three of these are relevant indicators and should be passed along.
788
				$form[ $key ] = $value;
789
			} elseif ( wp_startswith( $key, 'HTTP_' ) ) {
790
				// Any other HTTP header indicators.
791
				// `wp_startswith()` is a wpcom helper function and is included in Jetpack via `functions.compat.php`
792
				$form[ $key ] = $value;
793
			}
794
		}
795
796
		return $form;
797
	}
798
799
	/**
800
	 * Submit contact-form data to Akismet to check for spam.
801
	 * If you're accepting a new item via $_POST, run it Grunion_Contact_Form_Plugin::prepare_for_akismet() first
802
	 * Attached to `jetpack_contact_form_is_spam`
803
	 *
804
	 * @param bool  $is_spam
805
	 * @param array $form
806
	 * @return bool|WP_Error TRUE => spam, FALSE => not spam, WP_Error => stop processing entirely
807
	 */
808
	function is_spam_akismet( $is_spam, $form = array() ) {
809
		global $akismet_api_host, $akismet_api_port;
810
811
		// The signature of this function changed from accepting just $form.
812
		// If something only sends an array, assume it's still using the old
813
		// signature and work around it.
814
		if ( empty( $form ) && is_array( $is_spam ) ) {
815
			$form    = $is_spam;
816
			$is_spam = false;
817
		}
818
819
		// If a previous filter has alrady marked this as spam, trust that and move on.
820
		if ( $is_spam ) {
821
			return $is_spam;
822
		}
823
824
		if ( ! function_exists( 'akismet_http_post' ) && ! defined( 'AKISMET_VERSION' ) ) {
825
			return false;
826
		}
827
828
		$query_string = http_build_query( $form );
829
830
		if ( method_exists( 'Akismet', 'http_post' ) ) {
831
			$response = Akismet::http_post( $query_string, 'comment-check' );
832
		} else {
833
			$response = akismet_http_post( $query_string, $akismet_api_host, '/1.1/comment-check', $akismet_api_port );
834
		}
835
836
		$result = false;
837
838
		if ( isset( $response[0]['x-akismet-pro-tip'] ) && 'discard' === trim( $response[0]['x-akismet-pro-tip'] ) && get_option( 'akismet_strictness' ) === '1' ) {
839
			$result = new WP_Error( 'feedback-discarded', __( 'Feedback discarded.', 'jetpack' ) );
0 ignored issues
show
Unused Code introduced by
The call to WP_Error::__construct() has too many arguments starting with 'feedback-discarded'.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
840
		} elseif ( isset( $response[1] ) && 'true' == trim( $response[1] ) ) { // 'true' is spam
841
			$result = true;
842
		}
843
844
		/**
845
		 * Filter the results returned by Akismet for each submitted contact form.
846
		 *
847
		 * @module contact-form
848
		 *
849
		 * @since 1.3.1
850
		 *
851
		 * @param WP_Error|bool $result Is the submitted feedback spam.
852
		 * @param array|bool $form Submitted feedback.
853
		 */
854
		return apply_filters( 'contact_form_is_spam_akismet', $result, $form );
0 ignored issues
show
Unused Code introduced by
The call to apply_filters() has too many arguments starting with $form.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
855
	}
856
857
	/**
858
	 * Submit a feedback as either spam or ham
859
	 *
860
	 * @param string $as Either 'spam' or 'ham'.
861
	 * @param array  $form the contact-form data
862
	 */
863
	function akismet_submit( $as, $form ) {
864
		global $akismet_api_host, $akismet_api_port;
865
866
		if ( ! in_array( $as, array( 'ham', 'spam' ) ) ) {
867
			return false;
868
		}
869
870
		$query_string = '';
871
		if ( is_array( $form ) ) {
872
			$query_string = http_build_query( $form );
873
		}
874
		if ( method_exists( 'Akismet', 'http_post' ) ) {
875
			$response = Akismet::http_post( $query_string, "submit-{$as}" );
876
		} else {
877
			$response = akismet_http_post( $query_string, $akismet_api_host, "/1.1/submit-{$as}", $akismet_api_port );
878
		}
879
880
		return trim( $response[1] );
881
	}
882
883
	/**
884
	 * Prints the menu
885
	 */
886
	function export_form() {
887
		$current_screen = get_current_screen();
888
		if ( ! in_array( $current_screen->id, array( 'edit-feedback', 'feedback_page_feedback-export' ) ) ) {
889
			return;
890
		}
891
892
		if ( ! current_user_can( 'export' ) ) {
893
			return;
894
		}
895
896
		// if there aren't any feedbacks, bail out
897
		if ( ! (int) wp_count_posts( 'feedback' )->publish ) {
898
			return;
899
		}
900
		?>
901
902
		<div id="feedback-export" style="display:none">
903
			<h2><?php _e( 'Export feedback as CSV', 'jetpack' ); ?></h2>
904
			<div class="clear"></div>
905
			<form action="<?php echo admin_url( 'admin-post.php' ); ?>" method="post" class="form">
906
				<?php wp_nonce_field( 'feedback_export', 'feedback_export_nonce' ); ?>
907
908
				<input name="action" value="feedback_export" type="hidden">
909
				<label for="post"><?php _e( 'Select feedback to download', 'jetpack' ); ?></label>
910
				<select name="post">
911
					<option value="all"><?php esc_html_e( 'All posts', 'jetpack' ); ?></option>
912
					<?php echo $this->get_feedbacks_as_options(); ?>
913
				</select>
914
915
				<br><br>
916
				<input type="submit" name="submit" id="submit" class="button button-primary" value="<?php esc_html_e( 'Download', 'jetpack' ); ?>">
917
			</form>
918
		</div>
919
920
		<?php
921
		// There aren't any usable actions in core to output the "export feedback" form in the correct place,
922
		// so this inline JS moves it from the top of the page to the bottom.
923
		?>
924
		<script type='text/javascript'>
925
		    var menu = document.getElementById( 'feedback-export' ),
926
                wrapper = document.getElementsByClassName( 'wrap' )[0];
927
            <?php if ( 'edit-feedback' === $current_screen->id ) : ?>
928
            wrapper.appendChild(menu);
929
            <?php endif; ?>
930
            menu.style.display = 'block';
931
		</script>
932
		<?php
933
	}
934
935
	/**
936
	 * Fetch post content for a post and extract just the comment.
937
	 *
938
	 * @param int $post_id The post id to fetch the content for.
939
	 *
940
	 * @return string Trimmed post comment.
941
	 *
942
	 * @codeCoverageIgnore
943
	 */
944
	public function get_post_content_for_csv_export( $post_id ) {
945
		$post_content = get_post_field( 'post_content', $post_id );
946
		$content      = explode( '<!--more-->', $post_content );
947
948
		return trim( $content[0] );
949
	}
950
951
	/**
952
	 * Get `_feedback_extra_fields` field from post meta data.
953
	 *
954
	 * @param int $post_id Id of the post to fetch meta data for.
955
	 *
956
	 * @return mixed
957
	 */
958
	public function get_post_meta_for_csv_export( $post_id ) {
959
		$md                  = get_post_meta( $post_id, '_feedback_extra_fields', true );
960
		$md['feedback_date'] = get_the_date( DATE_RFC3339, $post_id );
961
		$content_fields      = self::parse_fields_from_content( $post_id );
962
		$md['feedback_ip']   = ( isset( $content_fields['_feedback_ip'] ) ) ? $content_fields['_feedback_ip'] : 0;
963
964
		// add the email_marketing_consent to the post meta.
965
		$md['email_marketing_consent'] = 0;
966
		if ( isset( $content_fields['_feedback_all_fields'] ) ) {
967
			$all_fields = $content_fields['_feedback_all_fields'];
968
			// check if the email_marketing_consent field exists.
969
			if ( isset( $all_fields['email_marketing_consent'] ) ) {
970
				$md['email_marketing_consent'] = $all_fields['email_marketing_consent'];
971
			}
972
		}
973
974
		return $md;
975
	}
976
977
	/**
978
	 * Get parsed feedback post fields.
979
	 *
980
	 * @param int $post_id Id of the post to fetch parsed contents for.
981
	 *
982
	 * @return array
983
	 *
984
	 * @codeCoverageIgnore - No need to be covered.
985
	 */
986
	public function get_parsed_field_contents_of_post( $post_id ) {
987
		return self::parse_fields_from_content( $post_id );
988
	}
989
990
	/**
991
	 * Properly maps fields that are missing from the post meta data
992
	 * to names, that are similar to those of the post meta.
993
	 *
994
	 * @param array $parsed_post_content Parsed post content
995
	 *
996
	 * @see parse_fields_from_content for how the input data is generated.
997
	 *
998
	 * @return array Mapped fields.
999
	 */
1000
	public function map_parsed_field_contents_of_post_to_field_names( $parsed_post_content ) {
1001
1002
		$mapped_fields = array();
1003
1004
		$field_mapping = array(
1005
			'_feedback_subject'      => __( 'Contact Form', 'jetpack' ),
1006
			'_feedback_author'       => '1_Name',
1007
			'_feedback_author_email' => '2_Email',
1008
			'_feedback_author_url'   => '3_Website',
1009
			'_feedback_main_comment' => '4_Comment',
1010
			'_feedback_author_ip'    => '5_IP',
1011
		);
1012
1013
		foreach ( $field_mapping as $parsed_field_name => $field_name ) {
1014
			if (
1015
				isset( $parsed_post_content[ $parsed_field_name ] )
1016
				&& ! empty( $parsed_post_content[ $parsed_field_name ] )
1017
			) {
1018
				$mapped_fields[ $field_name ] = $parsed_post_content[ $parsed_field_name ];
1019
			}
1020
		}
1021
1022
		return $mapped_fields;
1023
	}
1024
1025
	/**
1026
	 * Registers the personal data exporter.
1027
	 *
1028
	 * @since 6.1.1
1029
	 *
1030
	 * @param  array $exporters An array of personal data exporters.
1031
	 *
1032
	 * @return array $exporters An array of personal data exporters.
1033
	 */
1034
	public function register_personal_data_exporter( $exporters ) {
1035
		$exporters['jetpack-feedback'] = array(
1036
			'exporter_friendly_name' => __( 'Feedback', 'jetpack' ),
1037
			'callback'               => array( $this, 'personal_data_exporter' ),
1038
		);
1039
1040
		return $exporters;
1041
	}
1042
1043
	/**
1044
	 * Registers the personal data eraser.
1045
	 *
1046
	 * @since 6.1.1
1047
	 *
1048
	 * @param  array $erasers An array of personal data erasers.
1049
	 *
1050
	 * @return array $erasers An array of personal data erasers.
1051
	 */
1052
	public function register_personal_data_eraser( $erasers ) {
1053
		$erasers['jetpack-feedback'] = array(
1054
			'eraser_friendly_name' => __( 'Feedback', 'jetpack' ),
1055
			'callback'             => array( $this, 'personal_data_eraser' ),
1056
		);
1057
1058
		return $erasers;
1059
	}
1060
1061
	/**
1062
	 * Exports personal data.
1063
	 *
1064
	 * @since 6.1.1
1065
	 *
1066
	 * @param  string $email  Email address.
1067
	 * @param  int    $page   Page to export.
1068
	 *
1069
	 * @return array  $return Associative array with keys expected by core.
1070
	 */
1071
	public function personal_data_exporter( $email, $page = 1 ) {
1072
		return $this->_internal_personal_data_exporter( $email, $page );
1073
	}
1074
1075
	/**
1076
	 * Internal method for exporting personal data.
1077
	 *
1078
	 * Allows us to have a different signature than core expects
1079
	 * while protecting against future core API changes.
1080
	 *
1081
	 * @internal
1082
	 * @since 6.5
1083
	 *
1084
	 * @param  string $email    Email address.
1085
	 * @param  int    $page     Page to export.
1086
	 * @param  int    $per_page Number of feedbacks to process per page. Internal use only (testing)
1087
	 *
1088
	 * @return array            Associative array with keys expected by core.
1089
	 */
1090
	public function _internal_personal_data_exporter( $email, $page = 1, $per_page = 250 ) {
1091
		$export_data = array();
1092
		$post_ids    = $this->personal_data_post_ids_by_email( $email, $per_page, $page );
1093
1094
		foreach ( $post_ids as $post_id ) {
1095
			$post_fields = $this->get_parsed_field_contents_of_post( $post_id );
1096
1097
			if ( ! is_array( $post_fields ) || empty( $post_fields['_feedback_subject'] ) ) {
1098
				continue; // Corrupt data.
1099
			}
1100
1101
			$post_fields['_feedback_main_comment'] = $this->get_post_content_for_csv_export( $post_id );
1102
			$post_fields                           = $this->map_parsed_field_contents_of_post_to_field_names( $post_fields );
1103
1104
			if ( ! is_array( $post_fields ) || empty( $post_fields ) ) {
1105
				continue; // No fields to export.
1106
			}
1107
1108
			$post_meta = $this->get_post_meta_for_csv_export( $post_id );
1109
			$post_meta = is_array( $post_meta ) ? $post_meta : array();
1110
1111
			$post_export_data = array();
1112
			$post_data        = array_merge( $post_fields, $post_meta );
1113
			ksort( $post_data );
1114
1115
			foreach ( $post_data as $post_data_key => $post_data_value ) {
1116
				$post_export_data[] = array(
1117
					'name'  => preg_replace( '/^[0-9]+_/', '', $post_data_key ),
1118
					'value' => $post_data_value,
1119
				);
1120
			}
1121
1122
			$export_data[] = array(
1123
				'group_id'    => 'feedback',
1124
				'group_label' => __( 'Feedback', 'jetpack' ),
1125
				'item_id'     => 'feedback-' . $post_id,
1126
				'data'        => $post_export_data,
1127
			);
1128
		}
1129
1130
		return array(
1131
			'data' => $export_data,
1132
			'done' => count( $post_ids ) < $per_page,
1133
		);
1134
	}
1135
1136
	/**
1137
	 * Erases personal data.
1138
	 *
1139
	 * @since 6.1.1
1140
	 *
1141
	 * @param  string $email Email address.
1142
	 * @param  int    $page  Page to erase.
1143
	 *
1144
	 * @return array         Associative array with keys expected by core.
1145
	 */
1146
	public function personal_data_eraser( $email, $page = 1 ) {
1147
		return $this->_internal_personal_data_eraser( $email, $page );
1148
	}
1149
1150
	/**
1151
	 * Internal method for erasing personal data.
1152
	 *
1153
	 * Allows us to have a different signature than core expects
1154
	 * while protecting against future core API changes.
1155
	 *
1156
	 * @internal
1157
	 * @since 6.5
1158
	 *
1159
	 * @param  string $email    Email address.
1160
	 * @param  int    $page     Page to erase.
1161
	 * @param  int    $per_page Number of feedbacks to process per page. Internal use only (testing)
1162
	 *
1163
	 * @return array            Associative array with keys expected by core.
1164
	 */
1165
	public function _internal_personal_data_eraser( $email, $page = 1, $per_page = 250 ) {
1166
		$removed      = false;
1167
		$retained     = false;
1168
		$messages     = array();
1169
		$option_name  = sprintf( '_jetpack_pde_feedback_%s', md5( $email ) );
1170
		$last_post_id = 1 === $page ? 0 : get_option( $option_name, 0 );
1171
		$post_ids     = $this->personal_data_post_ids_by_email( $email, $per_page, $page, $last_post_id );
1172
1173
		foreach ( $post_ids as $post_id ) {
1174
			/**
1175
			 * Filters whether to erase a particular Feedback post.
1176
			 *
1177
			 * @since 6.3.0
1178
			 *
1179
			 * @param bool|string $prevention_message Whether to apply erase the Feedback post (bool).
1180
			 *                                        Custom prevention message (string). Default true.
1181
			 * @param int         $post_id            Feedback post ID.
1182
			 */
1183
			$prevention_message = apply_filters( 'grunion_contact_form_delete_feedback_post', true, $post_id );
0 ignored issues
show
Unused Code introduced by
The call to apply_filters() has too many arguments starting with $post_id.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
1184
1185
			if ( true !== $prevention_message ) {
1186
				if ( $prevention_message && is_string( $prevention_message ) ) {
1187
					$messages[] = esc_html( $prevention_message );
1188
				} else {
1189
					$messages[] = sprintf(
1190
					// translators: %d: Post ID.
1191
						__( 'Feedback ID %d could not be removed at this time.', 'jetpack' ),
1192
						$post_id
1193
					);
1194
				}
1195
1196
				$retained = true;
1197
1198
				continue;
1199
			}
1200
1201
			if ( wp_delete_post( $post_id, true ) ) {
1202
				$removed = true;
1203
			} else {
1204
				$retained   = true;
1205
				$messages[] = sprintf(
1206
				// translators: %d: Post ID.
1207
					__( 'Feedback ID %d could not be removed at this time.', 'jetpack' ),
1208
					$post_id
1209
				);
1210
			}
1211
		}
1212
1213
		$done = count( $post_ids ) < $per_page;
1214
1215
		if ( $done ) {
1216
			delete_option( $option_name );
1217
		} else {
1218
			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 1173. 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...
1219
		}
1220
1221
		return array(
1222
			'items_removed'  => $removed,
1223
			'items_retained' => $retained,
1224
			'messages'       => $messages,
1225
			'done'           => $done,
1226
		);
1227
	}
1228
1229
	/**
1230
	 * Queries personal data by email address.
1231
	 *
1232
	 * @since 6.1.1
1233
	 *
1234
	 * @param  string $email        Email address.
1235
	 * @param  int    $per_page     Post IDs per page. Default is `250`.
1236
	 * @param  int    $page         Page to query. Default is `1`.
1237
	 * @param  int    $last_post_id Page to query. Default is `0`. If non-zero, used instead of $page.
1238
	 *
1239
	 * @return array An array of post IDs.
1240
	 */
1241
	public function personal_data_post_ids_by_email( $email, $per_page = 250, $page = 1, $last_post_id = 0 ) {
1242
		add_filter( 'posts_search', array( $this, 'personal_data_search_filter' ) );
1243
1244
		$this->pde_last_post_id_erased = $last_post_id;
1245
		$this->pde_email_address       = $email;
1246
1247
		$post_ids = get_posts(
1248
			array(
1249
				'post_type'        => 'feedback',
1250
				'post_status'      => 'publish',
1251
				// This search parameter gets overwritten in ->personal_data_search_filter()
1252
				's'                => '..PDE..AUTHOR EMAIL:..PDE..',
1253
				'sentence'         => true,
1254
				'order'            => 'ASC',
1255
				'orderby'          => 'ID',
1256
				'fields'           => 'ids',
1257
				'posts_per_page'   => $per_page,
1258
				'paged'            => $last_post_id ? 1 : $page,
1259
				'suppress_filters' => false,
1260
			)
1261
		);
1262
1263
		$this->pde_last_post_id_erased = 0;
1264
		$this->pde_email_address       = '';
1265
1266
		remove_filter( 'posts_search', array( $this, 'personal_data_search_filter' ) );
1267
1268
		return $post_ids;
1269
	}
1270
1271
	/**
1272
	 * Filters searches by email address.
1273
	 *
1274
	 * @since 6.1.1
1275
	 *
1276
	 * @param  string $search SQL where clause.
1277
	 *
1278
	 * @return array          Filtered SQL where clause.
1279
	 */
1280
	public function personal_data_search_filter( $search ) {
1281
		global $wpdb;
1282
1283
		/*
1284
		 * Limits search to `post_content` only, and we only match the
1285
		 * author's email address whenever it's on a line by itself.
1286
		 */
1287
		if ( $this->pde_email_address && false !== strpos( $search, '..PDE..AUTHOR EMAIL:..PDE..' ) ) {
1288
			$search = $wpdb->prepare(
1289
				" AND (
1290
					{$wpdb->posts}.post_content LIKE %s
1291
					OR {$wpdb->posts}.post_content LIKE %s
1292
				)",
1293
				// `chr( 10 )` = `\n`, `chr( 13 )` = `\r`
1294
				'%' . $wpdb->esc_like( chr( 10 ) . 'AUTHOR EMAIL: ' . $this->pde_email_address . chr( 10 ) ) . '%',
1295
				'%' . $wpdb->esc_like( chr( 13 ) . 'AUTHOR EMAIL: ' . $this->pde_email_address . chr( 13 ) ) . '%'
1296
			);
1297
1298
			if ( $this->pde_last_post_id_erased ) {
1299
				$search .= $wpdb->prepare( " AND {$wpdb->posts}.ID > %d", $this->pde_last_post_id_erased );
1300
			}
1301
		}
1302
1303
		return $search;
1304
	}
1305
1306
	/**
1307
	 * Prepares feedback post data for CSV export.
1308
	 *
1309
	 * @param array $post_ids Post IDs to fetch the data for. These need to be Feedback posts.
1310
	 *
1311
	 * @return array
1312
	 */
1313
	public function get_export_data_for_posts( $post_ids ) {
1314
1315
		$posts_data  = array();
1316
		$field_names = array();
1317
		$result      = array();
1318
1319
		/**
1320
		 * Fetch posts and get the possible field names for later use
1321
		 */
1322
		foreach ( $post_ids as $post_id ) {
1323
1324
			/**
1325
			 * Fetch post main data, because we need the subject and author data for the feedback form.
1326
			 */
1327
			$post_real_data = $this->get_parsed_field_contents_of_post( $post_id );
1328
1329
			/**
1330
			 * If `$post_real_data` is not an array or there is no `_feedback_subject` set,
1331
			 * then something must be wrong with the feedback post. Skip it.
1332
			 */
1333
			if ( ! is_array( $post_real_data ) || ! isset( $post_real_data['_feedback_subject'] ) ) {
1334
				continue;
1335
			}
1336
1337
			/**
1338
			 * Fetch main post comment. This is from the default textarea fields.
1339
			 * If it is non-empty, then we add it to data, otherwise skip it.
1340
			 */
1341
			$post_comment_content = $this->get_post_content_for_csv_export( $post_id );
1342
			if ( ! empty( $post_comment_content ) ) {
1343
				$post_real_data['_feedback_main_comment'] = $post_comment_content;
1344
			}
1345
1346
			/**
1347
			 * Map parsed fields to proper field names
1348
			 */
1349
			$mapped_fields = $this->map_parsed_field_contents_of_post_to_field_names( $post_real_data );
1350
1351
			/**
1352
			 * Fetch post meta data.
1353
			 */
1354
			$post_meta_data = $this->get_post_meta_for_csv_export( $post_id );
1355
1356
			/**
1357
			 * If `$post_meta_data` is not an array or if it is empty, then there is no
1358
			 * extra feedback to work with. Create an empty array.
1359
			 */
1360
			if ( ! is_array( $post_meta_data ) || empty( $post_meta_data ) ) {
1361
				$post_meta_data = array();
1362
			}
1363
1364
			/**
1365
			 * Prepend the feedback subject to the list of fields.
1366
			 */
1367
			$post_meta_data = array_merge(
1368
				$mapped_fields,
1369
				$post_meta_data
1370
			);
1371
1372
			/**
1373
			 * Save post metadata for later usage.
1374
			 */
1375
			$posts_data[ $post_id ] = $post_meta_data;
1376
1377
			/**
1378
			 * Save field names, so we can use them as header fields later in the CSV.
1379
			 */
1380
			$field_names = array_merge( $field_names, array_keys( $post_meta_data ) );
1381
		}
1382
1383
		/**
1384
		 * Make sure the field names are unique, because we don't want duplicate data.
1385
		 */
1386
		$field_names = array_unique( $field_names );
1387
1388
		/**
1389
		 * Sort the field names by the field id number
1390
		 */
1391
		sort( $field_names, SORT_NUMERIC );
1392
1393
		/**
1394
		 * Loop through every post, which is essentially CSV row.
1395
		 */
1396
		foreach ( $posts_data as $post_id => $single_post_data ) {
1397
1398
			/**
1399
			 * Go through all the possible fields and check if the field is available
1400
			 * in the current post.
1401
			 *
1402
			 * If it is - add the data as a value.
1403
			 * If it is not - add an empty string, which is just a placeholder in the CSV.
1404
			 */
1405
			foreach ( $field_names as $single_field_name ) {
1406
				if (
1407
					isset( $single_post_data[ $single_field_name ] )
1408
					&& ! empty( $single_post_data[ $single_field_name ] )
1409
				) {
1410
					$result[ $single_field_name ][] = trim( $single_post_data[ $single_field_name ] );
1411
				} else {
1412
					$result[ $single_field_name ][] = '';
1413
				}
1414
			}
1415
		}
1416
1417
		return $result;
1418
	}
1419
1420
	/**
1421
	 * download as a csv a contact form or all of them in a csv file
1422
	 */
1423
	function download_feedback_as_csv() {
1424
		if ( empty( $_POST['feedback_export_nonce'] ) ) {
1425
			return;
1426
		}
1427
1428
		check_admin_referer( 'feedback_export', 'feedback_export_nonce' );
1429
1430
		if ( ! current_user_can( 'export' ) ) {
1431
			return;
1432
		}
1433
1434
		$args = array(
1435
			'posts_per_page'   => -1,
1436
			'post_type'        => 'feedback',
1437
			'post_status'      => 'publish',
1438
			'order'            => 'ASC',
1439
			'fields'           => 'ids',
1440
			'suppress_filters' => false,
1441
		);
1442
1443
		$filename = date( 'Y-m-d' ) . '-feedback-export.csv';
1444
1445
		// Check if we want to download all the feedbacks or just a certain contact form
1446
		if ( ! empty( $_POST['post'] ) && $_POST['post'] !== 'all' ) {
1447
			$args['post_parent'] = (int) $_POST['post'];
1448
			$filename            = date( 'Y-m-d' ) . '-' . str_replace( '&nbsp;', '-', get_the_title( (int) $_POST['post'] ) ) . '.csv';
1449
		}
1450
1451
		$feedbacks = get_posts( $args );
1452
1453
		if ( empty( $feedbacks ) ) {
1454
			return;
1455
		}
1456
1457
		$filename = sanitize_file_name( $filename );
1458
1459
		/**
1460
		 * Prepare data for export.
1461
		 */
1462
		$data = $this->get_export_data_for_posts( $feedbacks );
1463
1464
		/**
1465
		 * If `$data` is empty, there's nothing we can do below.
1466
		 */
1467
		if ( ! is_array( $data ) || empty( $data ) ) {
1468
			return;
1469
		}
1470
1471
		/**
1472
		 * Extract field names from `$data` for later use.
1473
		 */
1474
		$fields = array_keys( $data );
1475
1476
		/**
1477
		 * Count how many rows will be exported.
1478
		 */
1479
		$row_count = count( reset( $data ) );
1480
1481
		// Forces the download of the CSV instead of echoing
1482
		header( 'Content-Disposition: attachment; filename=' . $filename );
1483
		header( 'Pragma: no-cache' );
1484
		header( 'Expires: 0' );
1485
		header( 'Content-Type: text/csv; charset=utf-8' );
1486
1487
		$output = fopen( 'php://output', 'w' );
1488
1489
		/**
1490
		 * Print CSV headers
1491
		 */
1492
		fputcsv( $output, $fields );
1493
1494
		/**
1495
		 * Print rows to the output.
1496
		 */
1497
		for ( $i = 0; $i < $row_count; $i ++ ) {
1498
1499
			$current_row = array();
1500
1501
			/**
1502
			 * Put all the fields in `$current_row` array.
1503
			 */
1504
			foreach ( $fields as $single_field_name ) {
1505
				$current_row[] = $this->esc_csv( $data[ $single_field_name ][ $i ] );
1506
			}
1507
1508
			/**
1509
			 * Output the complete CSV row
1510
			 */
1511
			fputcsv( $output, $current_row );
1512
		}
1513
1514
		fclose( $output );
1515
	}
1516
1517
	/**
1518
	 * Escape a string to be used in a CSV context
1519
	 *
1520
	 * Malicious input can inject formulas into CSV files, opening up the possibility for phishing attacks and
1521
	 * disclosure of sensitive information.
1522
	 *
1523
	 * Additionally, Excel exposes the ability to launch arbitrary commands through the DDE protocol.
1524
	 *
1525
	 * @see https://www.contextis.com/en/blog/comma-separated-vulnerabilities
1526
	 *
1527
	 * @param string $field
1528
	 *
1529
	 * @return string
1530
	 */
1531
	public function esc_csv( $field ) {
1532
		$active_content_triggers = array( '=', '+', '-', '@' );
1533
1534
		if ( in_array( mb_substr( $field, 0, 1 ), $active_content_triggers, true ) ) {
1535
			$field = "'" . $field;
1536
		}
1537
1538
		return $field;
1539
	}
1540
1541
	/**
1542
	 * Returns a string of HTML <option> items from an array of posts
1543
	 *
1544
	 * @return string a string of HTML <option> items
1545
	 */
1546
	protected function get_feedbacks_as_options() {
1547
		$options = '';
1548
1549
		// Get the feedbacks' parents' post IDs
1550
		$feedbacks = get_posts(
1551
			array(
1552
				'fields'           => 'id=>parent',
1553
				'posts_per_page'   => 100000,
1554
				'post_type'        => 'feedback',
1555
				'post_status'      => 'publish',
1556
				'suppress_filters' => false,
1557
			)
1558
		);
1559
		$parents   = array_unique( array_values( $feedbacks ) );
1560
1561
		$posts = get_posts(
1562
			array(
1563
				'orderby'          => 'ID',
1564
				'posts_per_page'   => 1000,
1565
				'post_type'        => 'any',
1566
				'post__in'         => array_values( $parents ),
1567
				'suppress_filters' => false,
1568
			)
1569
		);
1570
1571
		// creates the string of <option> elements
1572
		foreach ( $posts as $post ) {
1573
			$options .= sprintf( '<option value="%s">%s</option>', esc_attr( $post->ID ), esc_html( $post->post_title ) );
1574
		}
1575
1576
		return $options;
1577
	}
1578
1579
	/**
1580
	 * Get the names of all the form's fields
1581
	 *
1582
	 * @param  array|int $posts the post we want the fields of
1583
	 *
1584
	 * @return array     the array of fields
1585
	 *
1586
	 * @deprecated As this is no longer necessary as of the CSV export rewrite. - 2015-12-29
1587
	 */
1588
	protected function get_field_names( $posts ) {
1589
		$posts      = (array) $posts;
1590
		$all_fields = array();
1591
1592
		foreach ( $posts as $post ) {
1593
			$fields = self::parse_fields_from_content( $post );
1594
1595
			if ( isset( $fields['_feedback_all_fields'] ) ) {
1596
				$extra_fields = array_keys( $fields['_feedback_all_fields'] );
1597
				$all_fields   = array_merge( $all_fields, $extra_fields );
1598
			}
1599
		}
1600
1601
		$all_fields = array_unique( $all_fields );
1602
		return $all_fields;
1603
	}
1604
1605
	public static function parse_fields_from_content( $post_id ) {
1606
		static $post_fields;
1607
1608
		if ( ! is_array( $post_fields ) ) {
1609
			$post_fields = array();
1610
		}
1611
1612
		if ( isset( $post_fields[ $post_id ] ) ) {
1613
			return $post_fields[ $post_id ];
1614
		}
1615
1616
		$all_values   = array();
1617
		$post_content = get_post_field( 'post_content', $post_id );
1618
		$content      = explode( '<!--more-->', $post_content );
1619
		$lines        = array();
1620
1621
		if ( count( $content ) > 1 ) {
1622
			$content  = str_ireplace( array( '<br />', ')</p>' ), '', $content[1] );
1623
			$one_line = preg_replace( '/\s+/', ' ', $content );
1624
			$one_line = preg_replace( '/.*Array \( (.*)\)/', '$1', $one_line );
1625
1626
			preg_match_all( '/\[([^\]]+)\] =\&gt\; ([^\[]+)/', $one_line, $matches );
1627
1628
			if ( count( $matches ) > 1 ) {
1629
				$all_values = array_combine( array_map( 'trim', $matches[1] ), array_map( 'trim', $matches[2] ) );
1630
			}
1631
1632
			$lines = array_filter( explode( "\n", $content ) );
1633
		}
1634
1635
		$var_map = array(
1636
			'AUTHOR'       => '_feedback_author',
1637
			'AUTHOR EMAIL' => '_feedback_author_email',
1638
			'AUTHOR URL'   => '_feedback_author_url',
1639
			'SUBJECT'      => '_feedback_subject',
1640
			'IP'           => '_feedback_ip',
1641
		);
1642
1643
		$fields = array();
1644
1645
		foreach ( $lines as $line ) {
1646
			$vars = explode( ': ', $line, 2 );
1647
			if ( ! empty( $vars ) ) {
1648
				if ( isset( $var_map[ $vars[0] ] ) ) {
1649
					$fields[ $var_map[ $vars[0] ] ] = self::strip_tags( trim( $vars[1] ) );
1650
				}
1651
			}
1652
		}
1653
1654
		$fields['_feedback_all_fields'] = $all_values;
1655
1656
		$post_fields[ $post_id ] = $fields;
1657
1658
		return $fields;
1659
	}
1660
1661
	/**
1662
	 * Creates a valid csv row from a post id
1663
	 *
1664
	 * @param  int   $post_id The id of the post
1665
	 * @param  array $fields  An array containing the names of all the fields of the csv
1666
	 * @return String The csv row
1667
	 *
1668
	 * @deprecated This is no longer needed, as of the CSV export rewrite.
1669
	 */
1670
	protected static function make_csv_row_from_feedback( $post_id, $fields ) {
1671
		$content_fields = self::parse_fields_from_content( $post_id );
1672
		$all_fields     = array();
1673
1674
		if ( isset( $content_fields['_feedback_all_fields'] ) ) {
1675
			$all_fields = $content_fields['_feedback_all_fields'];
1676
		}
1677
1678
		// Overwrite the parsed content with the content we stored in post_meta in a better format.
1679
		$extra_fields = get_post_meta( $post_id, '_feedback_extra_fields', true );
1680
		foreach ( $extra_fields as $extra_field => $extra_value ) {
1681
			$all_fields[ $extra_field ] = $extra_value;
1682
		}
1683
1684
		// The first element in all of the exports will be the subject
1685
		$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...
1686
1687
		// Loop the fields array in order to fill the $row_items array correctly
1688
		foreach ( $fields as $field ) {
1689
			if ( $field === __( 'Contact Form', 'jetpack' ) ) { // the first field will ever be the contact form, so we can continue
1690
				continue;
1691
			} elseif ( array_key_exists( $field, $all_fields ) ) {
1692
				$row_items[] = $all_fields[ $field ];
1693
			} else {
1694
				$row_items[] = '';
1695
			}
1696
		}
1697
1698
		return $row_items;
1699
	}
1700
1701
	public static function get_ip_address() {
1702
		return isset( $_SERVER['REMOTE_ADDR'] ) ? $_SERVER['REMOTE_ADDR'] : null;
1703
	}
1704
}
1705
1706
/**
1707
 * Generic shortcode class.
1708
 * Does nothing other than store structured data and output the shortcode as a string
1709
 *
1710
 * Not very general - specific to Grunion.
1711
 */
1712
class Crunion_Contact_Form_Shortcode {
1713
	/**
1714
	 * @var string the name of the shortcode: [$shortcode_name /]
1715
	 */
1716
	public $shortcode_name;
1717
1718
	/**
1719
	 * @var array key => value pairs for the shortcode's attributes: [$shortcode_name key="value" ... /]
1720
	 */
1721
	public $attributes;
1722
1723
	/**
1724
	 * @var array key => value pair for attribute defaults
1725
	 */
1726
	public $defaults = array();
1727
1728
	/**
1729
	 * @var null|string Null for selfclosing shortcodes.  Hhe inner content of otherwise: [$shortcode_name]$content[/$shortcode_name]
1730
	 */
1731
	public $content;
1732
1733
	/**
1734
	 * @var array Associative array of inner "child" shortcodes equivalent to the $content: [$shortcode_name][child 1/][child 2/][/$shortcode_name]
1735
	 */
1736
	public $fields;
1737
1738
	/**
1739
	 * @var null|string The HTML of the parsed inner "child" shortcodes".  Null for selfclosing shortcodes.
1740
	 */
1741
	public $body;
1742
1743
	/**
1744
	 * @param array       $attributes An associative array of shortcode attributes.  @see shortcode_atts()
1745
	 * @param null|string $content Null for selfclosing shortcodes.  The inner content otherwise.
1746
	 */
1747
	function __construct( $attributes, $content = null ) {
1748
		$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...
1749
		if ( is_array( $content ) ) {
1750
			$string_content = '';
1751
			foreach ( $content as $field ) {
1752
				$string_content .= (string) $field;
1753
			}
1754
1755
			$this->content = $string_content;
1756
		} else {
1757
			$this->content = $content;
1758
		}
1759
1760
		$this->parse_content( $this->content );
1761
	}
1762
1763
	/**
1764
	 * Processes the shortcode's inner content for "child" shortcodes
1765
	 *
1766
	 * @param string $content The shortcode's inner content: [shortcode]$content[/shortcode]
1767
	 */
1768
	function parse_content( $content ) {
1769
		if ( is_null( $content ) ) {
1770
			$this->body = null;
1771
		}
1772
1773
		$this->body = do_shortcode( $content );
1774
	}
1775
1776
	/**
1777
	 * Returns the value of the requested attribute.
1778
	 *
1779
	 * @param string $key The attribute to retrieve
1780
	 * @return mixed
1781
	 */
1782
	function get_attribute( $key ) {
1783
		return isset( $this->attributes[ $key ] ) ? $this->attributes[ $key ] : null;
1784
	}
1785
1786
	function esc_attr( $value ) {
1787
		if ( is_array( $value ) ) {
1788
			return array_map( array( $this, 'esc_attr' ), $value );
1789
		}
1790
1791
		$value = Grunion_Contact_Form_Plugin::strip_tags( $value );
1792
		$value = _wp_specialchars( $value, ENT_QUOTES, false, true );
1793
1794
		// Shortcode attributes can't contain "]"
1795
		$value = str_replace( ']', '', $value );
1796
		$value = str_replace( ',', '&#x002c;', $value ); // store commas encoded
1797
		$value = strtr(
1798
			$value, array(
1799
				'%' => '%25',
1800
				'&' => '%26',
1801
			)
1802
		);
1803
1804
		// shortcode_parse_atts() does stripcslashes()
1805
		$value = addslashes( $value );
1806
		return $value;
1807
	}
1808
1809
	function unesc_attr( $value ) {
1810
		if ( is_array( $value ) ) {
1811
			return array_map( array( $this, 'unesc_attr' ), $value );
1812
		}
1813
1814
		// For back-compat with old Grunion encoding
1815
		// Also, unencode commas
1816
		$value = strtr(
1817
			$value, array(
1818
				'%26' => '&',
1819
				'%25' => '%',
1820
			)
1821
		);
1822
		$value = preg_replace( array( '/&#x0*22;/i', '/&#x0*27;/i', '/&#x0*26;/i', '/&#x0*2c;/i' ), array( '"', "'", '&', ',' ), $value );
1823
		$value = htmlspecialchars_decode( $value, ENT_QUOTES );
1824
		$value = Grunion_Contact_Form_Plugin::strip_tags( $value );
1825
1826
		return $value;
1827
	}
1828
1829
	/**
1830
	 * Generates the shortcode
1831
	 */
1832
	function __toString() {
1833
		$r = "[{$this->shortcode_name} ";
1834
1835
		foreach ( $this->attributes as $key => $value ) {
1836
			if ( ! $value ) {
1837
				continue;
1838
			}
1839
1840
			if ( isset( $this->defaults[ $key ] ) && $this->defaults[ $key ] == $value ) {
1841
				continue;
1842
			}
1843
1844
			if ( 'id' == $key ) {
1845
				continue;
1846
			}
1847
1848
			$value = $this->esc_attr( $value );
1849
1850
			if ( is_array( $value ) ) {
1851
				$value = join( ',', $value );
1852
			}
1853
1854
			if ( false === strpos( $value, "'" ) ) {
1855
				$value = "'$value'";
1856
			} elseif ( false === strpos( $value, '"' ) ) {
1857
				$value = '"' . $value . '"';
1858
			} else {
1859
				// Shortcodes can't contain both '"' and "'".  Strip one.
1860
				$value = str_replace( "'", '', $value );
1861
				$value = "'$value'";
1862
			}
1863
1864
			$r .= "{$key}={$value} ";
1865
		}
1866
1867
		$r = rtrim( $r );
1868
1869
		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...
1870
			$r .= ']';
1871
1872
			foreach ( $this->fields as $field ) {
1873
				$r .= (string) $field;
1874
			}
1875
1876
			$r .= "[/{$this->shortcode_name}]";
1877
		} else {
1878
			$r .= '/]';
1879
		}
1880
1881
		return $r;
1882
	}
1883
}
1884
1885
/**
1886
 * Class for the contact-form shortcode.
1887
 * Parses shortcode to output the contact form as HTML
1888
 * Sends email and stores the contact form response (a.k.a. "feedback")
1889
 */
1890
class Grunion_Contact_Form extends Crunion_Contact_Form_Shortcode {
1891
	public $shortcode_name = 'contact-form';
1892
1893
	/**
1894
	 * @var WP_Error stores form submission errors
1895
	 */
1896
	public $errors;
1897
1898
	/**
1899
	 * @var string The SHA1 hash of the attributes that comprise the form.
1900
	 */
1901
	public $hash;
1902
1903
	/**
1904
	 * @var Grunion_Contact_Form The most recent (inclusive) contact-form shortcode processed
1905
	 */
1906
	static $last;
1907
1908
	/**
1909
	 * @var Whatever form we are currently looking at. If processed, will become $last
1910
	 */
1911
	static $current_form;
1912
1913
	/**
1914
	 * @var array All found forms, indexed by hash.
1915
	 */
1916
	static $forms = array();
1917
1918
	/**
1919
	 * @var bool Whether to print the grunion.css style when processing the contact-form shortcode
1920
	 */
1921
	static $style = false;
1922
1923
	/**
1924
	 * @var array When printing the submit button, what tags are allowed
1925
	 */
1926
	static $allowed_html_tags_for_submit_button = array( 'br' => array() );
1927
1928
	function __construct( $attributes, $content = null ) {
1929
		global $post;
1930
1931
		$this->hash                 = sha1( json_encode( $attributes ) . $content );
1932
		self::$forms[ $this->hash ] = $this;
1933
1934
		// Set up the default subject and recipient for this form.
1935
		$default_to      = '';
1936
		$default_subject = '[' . get_option( 'blogname' ) . ']';
1937
1938
		if ( ! isset( $attributes ) || ! is_array( $attributes ) ) {
1939
			$attributes = array();
1940
		}
1941
1942
		if ( ! empty( $attributes['widget'] ) && $attributes['widget'] ) {
1943
			$default_to      .= get_option( 'admin_email' );
1944
			$attributes['id'] = 'widget-' . $attributes['widget'];
1945
			$default_subject  = sprintf( _x( '%1$s Sidebar', '%1$s = blog name', 'jetpack' ), $default_subject );
1946
		} elseif ( $post ) {
1947
			$attributes['id'] = $post->ID;
1948
			$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 ) );
1949
			$post_author      = get_userdata( $post->post_author );
1950
			$default_to      .= $post_author->user_email;
1951
		}
1952
1953
		// Keep reference to $this for parsing form fields.
1954
		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...
1955
1956
		$this->defaults = array(
1957
			'to'                     => $default_to,
1958
			'subject'                => $default_subject,
1959
			'show_subject'           => 'no', // only used in back-compat mode
1960
			'widget'                 => 0,    // Not exposed to the user. Works with Grunion_Contact_Form_Plugin::widget_atts()
1961
			'id'                     => null, // Not exposed to the user. Set above.
1962
			'submit_button_text'     => __( 'Submit', 'jetpack' ),
1963
			// These attributes come from the block editor, so use camel case instead of snake case.
1964
			'customThankyou'         => '', // Whether to show a custom thankyou response after submitting a form. '' for no, 'message' for a custom message, 'redirect' to redirect to a new URL.
1965
			'customThankyouMessage'  => __( 'Thank you for your submission!', 'jetpack' ), // The message to show when customThankyou is set to 'message'.
1966
			'customThankyouRedirect' => '', // The URL to redirect to when customThankyou is set to 'redirect'.
1967
			'jetpackCRM'             => true, // Whether Jetpack CRM should store the form submission.
1968
		);
1969
1970
		$attributes = shortcode_atts( $this->defaults, $attributes, 'contact-form' );
1971
1972
		// We only enable the contact-field shortcode temporarily while processing the contact-form shortcode.
1973
		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...
1974
1975
		parent::__construct( $attributes, $content );
1976
1977
		// There were no fields in the contact form. The form was probably just [contact-form /]. Build a default form.
1978
		if ( empty( $this->fields ) ) {
1979
			// same as the original Grunion v1 form.
1980
			$default_form = '
1981
				[contact-field label="' . __( 'Name', 'jetpack' ) . '" type="name"  required="true" /]
1982
				[contact-field label="' . __( 'Email', 'jetpack' ) . '" type="email" required="true" /]
1983
				[contact-field label="' . __( 'Website', 'jetpack' ) . '" type="url" /]';
1984
1985
			if ( 'yes' == strtolower( $this->get_attribute( 'show_subject' ) ) ) {
1986
				$default_form .= '
1987
					[contact-field label="' . __( 'Subject', 'jetpack' ) . '" type="subject" /]';
1988
			}
1989
1990
			$default_form .= '
1991
				[contact-field label="' . __( 'Message', 'jetpack' ) . '" type="textarea" /]';
1992
1993
			$this->parse_content( $default_form );
1994
1995
			// Store the shortcode.
1996
			$this->store_shortcode( $default_form, $attributes, $this->hash );
1997
		} else {
1998
			// Store the shortcode.
1999
			$this->store_shortcode( $content, $attributes, $this->hash );
2000
		}
2001
2002
		// $this->body and $this->fields have been setup.  We no longer need the contact-field shortcode.
2003
		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...
2004
	}
2005
2006
	/**
2007
	 * Store shortcode content for recall later
2008
	 *  - used to receate shortcode when user uses do_shortcode
2009
	 *
2010
	 * @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...
2011
	 * @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...
2012
	 * @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...
2013
	 */
2014
	static function store_shortcode( $content = null, $attributes = null, $hash = null ) {
2015
2016
		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...
2017
2018
			if ( empty( $hash ) ) {
2019
				$hash = sha1( json_encode( $attributes ) . $content );
2020
			}
2021
2022
			$shortcode_meta = get_post_meta( $attributes['id'], "_g_feedback_shortcode_{$hash}", true );
2023
2024
			if ( $shortcode_meta != '' or $shortcode_meta != $content ) {
2025
				update_post_meta( $attributes['id'], "_g_feedback_shortcode_{$hash}", $content );
2026
2027
				// Save attributes to post_meta for later use. They're not available later in do_shortcode situations.
2028
				update_post_meta( $attributes['id'], "_g_feedback_shortcode_atts_{$hash}", $attributes );
2029
			}
2030
		}
2031
	}
2032
2033
	/**
2034
	 * Toggle for printing the grunion.css stylesheet
2035
	 *
2036
	 * @param bool $style
2037
	 */
2038
	static function style( $style ) {
2039
		$previous_style = self::$style;
2040
		self::$style    = (bool) $style;
2041
		return $previous_style;
2042
	}
2043
2044
	/**
2045
	 * Turn on printing of grunion.css stylesheet
2046
	 *
2047
	 * @see ::style()
2048
	 * @internal
2049
	 * @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...
2050
	 */
2051
	static function _style_on() {
2052
		return self::style( true );
2053
	}
2054
2055
	/**
2056
	 * The contact-form shortcode processor
2057
	 *
2058
	 * @param array       $attributes Key => Value pairs as parsed by shortcode_parse_atts()
2059
	 * @param string|null $content The shortcode's inner content: [contact-form]$content[/contact-form]
2060
	 * @return string HTML for the concat form.
2061
	 */
2062
	static function parse( $attributes, $content ) {
2063
		if ( Settings::is_syncing() ) {
2064
			return '';
2065
		}
2066
		// Create a new Grunion_Contact_Form object (this class)
2067
		$form = new Grunion_Contact_Form( $attributes, $content );
2068
2069
		$id = $form->get_attribute( 'id' );
2070
2071
		if ( ! $id ) { // something terrible has happened
2072
			return '[contact-form]';
2073
		}
2074
2075
		if ( is_feed() ) {
2076
			return '[contact-form]';
2077
		}
2078
2079
		self::$last = $form;
2080
2081
		// Enqueue the grunion.css stylesheet if self::$style allows it
2082
		if ( self::$style && ( empty( $_REQUEST['action'] ) || $_REQUEST['action'] != 'grunion_shortcode_to_json' ) ) {
2083
			// Enqueue the style here instead of printing it, because if some other plugin has run the_post()+rewind_posts(),
2084
			// (like VideoPress does), the style tag gets "printed" the first time and discarded, leaving the contact form unstyled.
2085
			// when WordPress does the real loop.
2086
			wp_enqueue_style( 'grunion.css' );
2087
		}
2088
2089
		$r  = '';
2090
		$r .= "<div id='contact-form-$id'>\n";
2091
2092
		if ( is_wp_error( $form->errors ) && $form->errors->get_error_codes() ) {
0 ignored issues
show
Bug introduced by
The method get_error_codes() does not seem to exist on object<WP_Error>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
2093
			// There are errors.  Display them
2094
			$r .= "<div class='form-error'>\n<h3>" . __( 'Error!', 'jetpack' ) . "</h3>\n<ul class='form-errors'>\n";
2095
			foreach ( $form->errors->get_error_messages() as $message ) {
0 ignored issues
show
Bug introduced by
The method get_error_messages() does not seem to exist on object<WP_Error>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
2096
				$r .= "\t<li class='form-error-message'>" . esc_html( $message ) . "</li>\n";
2097
			}
2098
			$r .= "</ul>\n</div>\n\n";
2099
		}
2100
2101
		if ( isset( $_GET['contact-form-id'] )
2102
			&& (int) $_GET['contact-form-id'] === (int) self::$last->get_attribute( 'id' )
2103
			&& isset( $_GET['contact-form-sent'], $_GET['contact-form-hash'] )
2104
			&& is_string( $_GET['contact-form-hash'] )
2105
			&& hash_equals( $form->hash, $_GET['contact-form-hash'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
2106
			// The contact form was submitted.  Show the success message/results.
2107
			$feedback_id = (int) $_GET['contact-form-sent'];
2108
2109
			$back_url = remove_query_arg( array( 'contact-form-id', 'contact-form-sent', '_wpnonce' ) );
2110
2111
			$r_success_message =
2112
				'<h3>' . __( 'Message Sent', 'jetpack' ) .
2113
				' (<a href="' . esc_url( $back_url ) . '">' . esc_html__( 'go back', 'jetpack' ) . '</a>)' .
2114
				"</h3>\n\n";
2115
2116
			// Don't show the feedback details unless the nonce matches
2117
			if ( $feedback_id && wp_verify_nonce( stripslashes( $_GET['_wpnonce'] ), "contact-form-sent-{$feedback_id}" ) ) {
2118
				$r_success_message .= self::success_message( $feedback_id, $form );
2119
			}
2120
2121
			/**
2122
			 * Filter the message returned after a successful contact form submission.
2123
			 *
2124
			 * @module contact-form
2125
			 *
2126
			 * @since 1.3.1
2127
			 *
2128
			 * @param string $r_success_message Success message.
2129
			 */
2130
			$r .= apply_filters( 'grunion_contact_form_success_message', $r_success_message );
2131
		} else {
2132
			// Nothing special - show the normal contact form
2133
			if ( $form->get_attribute( 'widget' ) ) {
2134
				// Submit form to the current URL
2135
				$url = remove_query_arg( array( 'contact-form-id', 'contact-form-sent', 'action', '_wpnonce' ) );
2136
			} else {
2137
				// Submit form to the post permalink
2138
				$url = get_permalink();
2139
			}
2140
2141
			// For SSL/TLS page. See RFC 3986 Section 4.2
2142
			$url = set_url_scheme( $url );
2143
2144
			// May eventually want to send this to admin-post.php...
2145
			/**
2146
			 * Filter the contact form action URL.
2147
			 *
2148
			 * @module contact-form
2149
			 *
2150
			 * @since 1.3.1
2151
			 *
2152
			 * @param string $contact_form_id Contact form post URL.
2153
			 * @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...
2154
			 * @param int $id Contact Form ID.
2155
			 */
2156
			$url                     = apply_filters( 'grunion_contact_form_form_action', "{$url}#contact-form-{$id}", $GLOBALS['post'], $id );
0 ignored issues
show
Unused Code introduced by
The call to apply_filters() has too many arguments starting with $GLOBALS['post'].

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
2157
			$has_submit_button_block = ! ( false === strpos( $content, 'wp-block-jetpack-button' ) );
2158
			$form_classes            = 'contact-form commentsblock';
2159
2160
			if ( $has_submit_button_block ) {
2161
				$form_classes .= ' wp-block-jetpack-contact-form';
2162
			}
2163
2164
			$r .= "<form action='" . esc_url( $url ) . "' method='post' class='" . esc_attr( $form_classes ) . "'>\n";
2165
			$r .= $form->body;
2166
2167
			// In new versions of the contact form block the button is an inner block
2168
			// so the button does not need to be constructed server-side.
2169
			if ( ! $has_submit_button_block ) {
2170
				$r .= "\t<p class='contact-submit'>\n";
2171
2172
				$gutenberg_submit_button_classes = '';
2173
				if ( ! empty( $attributes['submitButtonClasses'] ) ) {
2174
					$gutenberg_submit_button_classes = ' ' . $attributes['submitButtonClasses'];
2175
				}
2176
2177
				/**
2178
				 * Filter the contact form submit button class attribute.
2179
				 *
2180
				 * @module contact-form
2181
				 *
2182
				 * @since 6.6.0
2183
				 *
2184
				 * @param string $class Additional CSS classes for button attribute.
2185
				 */
2186
				$submit_button_class = apply_filters( 'jetpack_contact_form_submit_button_class', 'pushbutton-wide' . $gutenberg_submit_button_classes );
2187
2188
				$submit_button_styles = '';
2189
				if ( ! empty( $attributes['customBackgroundButtonColor'] ) ) {
2190
					$submit_button_styles .= 'background-color: ' . $attributes['customBackgroundButtonColor'] . '; ';
2191
				}
2192
				if ( ! empty( $attributes['customTextButtonColor'] ) ) {
2193
					$submit_button_styles .= 'color: ' . $attributes['customTextButtonColor'] . ';';
2194
				}
2195
				if ( ! empty( $attributes['submitButtonText'] ) ) {
2196
					$submit_button_text = $attributes['submitButtonText'];
2197
				} else {
2198
					$submit_button_text = $form->get_attribute( 'submit_button_text' );
2199
				}
2200
2201
				$r .= "\t\t<button type='submit' class='" . esc_attr( $submit_button_class ) . "'";
2202
				if ( ! empty( $submit_button_styles ) ) {
2203
					$r .= " style='" . esc_attr( $submit_button_styles ) . "'";
2204
				}
2205
				$r .= ">";
2206
				$r .= wp_kses(
2207
					      $submit_button_text,
2208
					      self::$allowed_html_tags_for_submit_button
2209
				      ) . "</button>";
2210
			}
2211
2212
			if ( is_user_logged_in() ) {
2213
				$r .= "\t\t" . wp_nonce_field( 'contact-form_' . $id, '_wpnonce', true, false ) . "\n"; // nonce and referer
2214
			}
2215
2216
			if ( isset( $attributes['hasFormSettingsSet'] ) && $attributes['hasFormSettingsSet'] ) {
2217
				$r .= "\t\t<input type='hidden' name='is_block' value='1' />\n";
2218
			}
2219
			$r .= "\t\t<input type='hidden' name='contact-form-id' value='$id' />\n";
2220
			$r .= "\t\t<input type='hidden' name='action' value='grunion-contact-form' />\n";
2221
			$r .= "\t\t<input type='hidden' name='contact-form-hash' value='" . esc_attr( $form->hash ) . "' />\n";
2222
2223
			if ( ! $has_submit_button_block ) {
2224
				$r .= "\t</p>\n";
2225
			}
2226
2227
			$r .= "</form>\n";
2228
		}
2229
2230
		$r .= '</div>';
2231
2232
		return $r;
2233
	}
2234
2235
	/**
2236
	 * Returns a success message to be returned if the form is sent via AJAX.
2237
	 *
2238
	 * @param int                         $feedback_id
2239
	 * @param object Grunion_Contact_Form $form
2240
	 *
2241
	 * @return string $message
2242
	 */
2243
	static function success_message( $feedback_id, $form ) {
2244
		if ( 'message' === $form->get_attribute( 'customThankyou' ) ) {
2245
			$message = wpautop( $form->get_attribute( 'customThankyouMessage' ) );
2246
		} else {
2247
			$message = '<blockquote class="contact-form-submission">'
2248
			. '<p>' . join( '</p><p>', self::get_compiled_form( $feedback_id, $form ) ) . '</p>'
2249
			. '</blockquote>';
2250
		}
2251
2252
		return wp_kses(
2253
			$message,
2254
			array(
2255
				'br'         => array(),
2256
				'blockquote' => array( 'class' => array() ),
2257
				'p'          => array(),
2258
			)
2259
		);
2260
	}
2261
2262
	/**
2263
	 * Returns a compiled form with labels and values in a form of  an array
2264
	 * of lines.
2265
	 *
2266
	 * @param int                         $feedback_id
2267
	 * @param object Grunion_Contact_Form $form
2268
	 *
2269
	 * @return array $lines
2270
	 */
2271
	static function get_compiled_form( $feedback_id, $form ) {
2272
		$feedback       = get_post( $feedback_id );
2273
		$field_ids      = $form->get_field_ids();
2274
		$content_fields = Grunion_Contact_Form_Plugin::parse_fields_from_content( $feedback_id );
2275
2276
		// Maps field_ids to post_meta keys
2277
		$field_value_map = array(
2278
			'name'     => 'author',
2279
			'email'    => 'author_email',
2280
			'url'      => 'author_url',
2281
			'subject'  => 'subject',
2282
			'textarea' => false, // not a post_meta key.  This is stored in post_content
2283
		);
2284
2285
		$compiled_form = array();
2286
2287
		// "Standard" field allowed list.
2288
		foreach ( $field_value_map as $type => $meta_key ) {
2289
			if ( isset( $field_ids[ $type ] ) ) {
2290
				$field = $form->fields[ $field_ids[ $type ] ];
2291
2292
				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...
2293
					if ( isset( $content_fields[ "_feedback_{$meta_key}" ] ) ) {
2294
						$value = $content_fields[ "_feedback_{$meta_key}" ];
2295
					}
2296
				} else {
2297
					// The feedback content is stored as the first "half" of post_content
2298
					$value         = $feedback->post_content;
2299
					list( $value ) = explode( '<!--more-->', $value );
2300
					$value         = trim( $value );
2301
				}
2302
2303
				$field_index                   = array_search( $field_ids[ $type ], $field_ids['all'] );
2304
				$compiled_form[ $field_index ] = sprintf(
2305
					'<b>%1$s:</b> %2$s<br /><br />',
2306
					wp_kses( $field->get_attribute( 'label' ), array() ),
2307
					self::escape_and_sanitize_field_value( $value )
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...
2308
				);
2309
			}
2310
		}
2311
2312
		// "Non-standard" fields
2313
		if ( $field_ids['extra'] ) {
2314
			// array indexed by field label (not field id)
2315
			$extra_fields = get_post_meta( $feedback_id, '_feedback_extra_fields', true );
2316
2317
			/**
2318
			 * Only get data for the compiled form if `$extra_fields` is a valid and non-empty array.
2319
			 */
2320
			if ( is_array( $extra_fields ) && ! empty( $extra_fields ) ) {
2321
2322
				$extra_field_keys = array_keys( $extra_fields );
2323
2324
				$i = 0;
2325
				foreach ( $field_ids['extra'] as $field_id ) {
2326
					$field       = $form->fields[ $field_id ];
2327
					$field_index = array_search( $field_id, $field_ids['all'] );
2328
2329
					$label = $field->get_attribute( 'label' );
2330
2331
					$compiled_form[ $field_index ] = sprintf(
2332
						'<b>%1$s:</b> %2$s<br /><br />',
2333
						wp_kses( $label, array() ),
2334
						self::escape_and_sanitize_field_value( $extra_fields[ $extra_field_keys[ $i ] ] )
2335
					);
2336
2337
					$i++;
2338
				}
2339
			}
2340
		}
2341
2342
		// Sorting lines by the field index
2343
		ksort( $compiled_form );
2344
2345
		return $compiled_form;
2346
	}
2347
2348
	static function escape_and_sanitize_field_value( $value ) {
2349
        $value = str_replace( array( '[' , ']' ) ,  array( '&#91;' , '&#93;' ) , $value );
2350
        return nl2br( wp_kses( $value, array() ) );
2351
    }
2352
2353
	/**
2354
	 * Only strip out empty string values and keep all the other values as they are.
2355
     *
2356
	 * @param $single_value
2357
	 *
2358
	 * @return bool
2359
	 */
2360
	static function remove_empty( $single_value ) {
2361
		return ( $single_value !== '' );
2362
	}
2363
2364
	/**
2365
	 * Escape a shortcode value.
2366
	 *
2367
	 * Shortcode attribute values have a number of unfortunate restrictions, which fortunately we
2368
	 * can get around by adding some extra HTML encoding.
2369
	 *
2370
	 * The output HTML will have a few extra escapes, but that makes no functional difference.
2371
	 *
2372
	 * @since 9.1.0
2373
	 * @param string $val Value to escape.
2374
	 * @return string
2375
	 */
2376
	private static function esc_shortcode_val( $val ) {
2377
		return strtr(
2378
			esc_html( $val ),
2379
			array(
2380
				// Brackets in attribute values break the shortcode parser.
2381
				'['  => '&#091;',
2382
				']'  => '&#093;',
2383
				// Shortcode parser screws up backslashes too, thanks to calls to `stripcslashes`.
2384
				'\\' => '&#092;',
2385
				// The existing code here represents arrays as comma-separated strings.
2386
				// Rather than trying to change representations now, just escape the commas in values.
2387
				','  => '&#044;',
2388
			)
2389
		);
2390
	}
2391
2392
	/**
2393
	 * The contact-field shortcode processor
2394
	 * 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.
2395
	 *
2396
	 * @param array       $attributes Key => Value pairs as parsed by shortcode_parse_atts()
2397
	 * @param string|null $content The shortcode's inner content: [contact-field]$content[/contact-field]
2398
	 * @return HTML for the contact form field
2399
	 */
2400
	static function parse_contact_field( $attributes, $content ) {
2401
		// Don't try to parse contact form fields if not inside a contact form
2402
		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...
2403
			$att_strs = array();
2404
			if ( ! isset( $attributes['label'] )  ) {
2405
				$type = isset( $attributes['type'] ) ? $attributes['type'] : null;
2406
				$attributes['label'] = self::get_default_label_from_type( $type );
2407
			}
2408
			foreach ( $attributes as $att => $val ) {
2409
				if ( is_numeric( $att ) ) { // Is a valueless attribute
2410
					$att_strs[] = self::esc_shortcode_val( $val );
2411
				} elseif ( isset( $val ) ) { // A regular attr - value pair
2412
					if ( ( $att === 'options' || $att === 'values' ) && is_string( $val ) ) { // remove any empty strings
2413
						$val = explode( ',', $val );
2414
					}
2415
					if ( is_array( $val ) ) {
2416
						$val =  array_filter( $val, array( __CLASS__, 'remove_empty' ) ); // removes any empty strings
2417
						$att_strs[] = esc_html( $att ) . '="' . implode( ',', array_map( array( __CLASS__, 'esc_shortcode_val' ), $val ) ) . '"';
2418
					} elseif ( is_bool( $val ) ) {
2419
						$att_strs[] = esc_html( $att ) . '="' . ( $val ? '1' : '' ) . '"';
2420
					} else {
2421
						$att_strs[] = esc_html( $att ) . '="' . self::esc_shortcode_val( $val ) . '"';
2422
					}
2423
				}
2424
			}
2425
2426
			$html = '[contact-field ' . implode( ' ', $att_strs );
2427
2428
			if ( isset( $content ) && ! empty( $content ) ) { // If there is content, let's add a closing tag
2429
				$html .= ']' . esc_html( $content ) . '[/contact-field]';
2430
			} else { // Otherwise let's add a closing slash in the first tag
2431
				$html .= '/]';
2432
			}
2433
2434
			return $html;
2435
		}
2436
2437
		$form = Grunion_Contact_Form::$current_form;
2438
2439
		$field = new Grunion_Contact_Form_Field( $attributes, $content, $form );
2440
2441
		$field_id = $field->get_attribute( 'id' );
2442
		if ( $field_id ) {
2443
			$form->fields[ $field_id ] = $field;
2444
		} else {
2445
			$form->fields[] = $field;
2446
		}
2447
2448
		if (
2449
			isset( $_POST['action'] ) && 'grunion-contact-form' === $_POST['action']
2450
			&&
2451
			isset( $_POST['contact-form-id'] ) && $form->get_attribute( 'id' ) == $_POST['contact-form-id']
2452
			&&
2453
			isset( $_POST['contact-form-hash'] ) && hash_equals( $form->hash, $_POST['contact-form-hash'] )
2454
		) {
2455
			// If we're processing a POST submission for this contact form, validate the field value so we can show errors as necessary.
2456
			$field->validate();
2457
		}
2458
2459
		// Output HTML
2460
		return $field->render();
2461
	}
2462
2463
	static function get_default_label_from_type( $type ) {
2464
		$str = null;
0 ignored issues
show
Unused Code introduced by
$str 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...
2465
		switch ( $type ) {
2466
			case 'text':
2467
				$str = __( 'Text', 'jetpack' );
2468
				break;
2469
			case 'name':
2470
				$str = __( 'Name', 'jetpack' );
2471
				break;
2472
			case 'email':
2473
				$str = __( 'Email', 'jetpack' );
2474
				break;
2475
			case 'url':
2476
				$str = __( 'Website', 'jetpack' );
2477
				break;
2478
			case 'date':
2479
				$str = __( 'Date', 'jetpack' );
2480
				break;
2481
			case 'telephone':
2482
				$str = __( 'Phone', 'jetpack' );
2483
				break;
2484
			case 'textarea':
2485
				$str = __( 'Message', 'jetpack' );
2486
				break;
2487
			case 'checkbox':
2488
				$str = __( 'Checkbox', 'jetpack' );
2489
				break;
2490
			case 'checkbox-multiple':
2491
				$str = __( 'Choose several', 'jetpack' );
2492
				break;
2493
			case 'radio':
2494
				$str = __( 'Choose one', 'jetpack' );
2495
				break;
2496
			case 'select':
2497
				$str = __( 'Select one', 'jetpack' );
2498
				break;
2499
			case 'consent':
2500
				$str = __( 'Consent', 'jetpack' );
2501
				break;
2502
			default:
2503
				$str = null;
2504
		}
2505
		return $str;
2506
	}
2507
2508
	/**
2509
	 * Loops through $this->fields to generate a (structured) list of field IDs.
2510
	 *
2511
	 * Important: Currently the allowed fields are defined as follows:
2512
	 *  `name`, `email`, `url`, `subject`, `textarea`
2513
	 *
2514
	 * If you need to add new fields to the Contact Form, please don't add them
2515
	 * to the allowed fields and leave them as extra fields.
2516
	 *
2517
	 * The reasoning behind this is that both the admin Feedback view and the CSV
2518
	 * export will not include any fields that are added to the list of
2519
	 * allowed fields without taking proper care to add them to all the
2520
	 * other places where they accessed/used/saved.
2521
	 *
2522
	 * The safest way to add new fields is to add them to the dropdown and the
2523
	 * HTML list ( @see Grunion_Contact_Form_Field::render ) and don't add them
2524
	 * to the list of allowed fields. This way they will become a part of the
2525
	 * `extra fields` which are saved in the post meta and will be properly
2526
	 * handled by the admin Feedback view and the CSV Export without any extra
2527
	 * work.
2528
	 *
2529
	 * If there is need to add a field to the allowed fields, then please
2530
	 * take proper care to add logic to handle the field in the following places:
2531
	 *
2532
	 *  - Below in the switch statement - so the field is recognized as allowed.
2533
	 *
2534
	 *  - Grunion_Contact_Form::process_submission - validation and logic.
2535
	 *
2536
	 *  - Grunion_Contact_Form::process_submission - add the field as an additional
2537
	 *      field in the `post_content` when saving the feedback content.
2538
	 *
2539
	 *  - Grunion_Contact_Form_Plugin::parse_fields_from_content - add mapping
2540
	 *      for the field, defined in the above method.
2541
	 *
2542
	 *  - Grunion_Contact_Form_Plugin::map_parsed_field_contents_of_post_to_field_names -
2543
	 *      add mapping of the field for the CSV Export. Otherwise it will be missing
2544
	 *      from the exported data.
2545
	 *
2546
	 *  - admin.php / grunion_manage_post_columns - add the field to the render logic.
2547
	 *      Otherwise it will be missing from the admin Feedback view.
2548
	 *
2549
	 * @return array
2550
	 */
2551
	function get_field_ids() {
2552
		$field_ids = array(
2553
			'all'   => array(), // array of all field_ids.
2554
			'extra' => array(), // array of all non-allowed field IDs.
2555
2556
			// Allowed "standard" field IDs:
2557
			// 'email'    => field_id,
2558
			// 'name'     => field_id,
2559
			// 'url'      => field_id,
2560
			// 'subject'  => field_id,
2561
			// 'textarea' => field_id,
2562
		);
2563
2564
		foreach ( $this->fields as $id => $field ) {
2565
			$field_ids['all'][] = $id;
2566
2567
			$type = $field->get_attribute( 'type' );
2568
			if ( isset( $field_ids[ $type ] ) ) {
2569
				// This type of field is already present in our allowed list of "standard" fields for this form
2570
				// Put it in extra
2571
				$field_ids['extra'][] = $id;
2572
				continue;
2573
			}
2574
2575
			/**
2576
			 * See method description before modifying the switch cases.
2577
			 */
2578
			switch ( $type ) {
2579
				case 'email':
2580
				case 'name':
2581
				case 'url':
2582
				case 'subject':
2583
				case 'textarea':
2584
				case 'consent':
2585
					$field_ids[ $type ] = $id;
2586
					break;
2587
				default:
2588
					// Put everything else in extra
2589
					$field_ids['extra'][] = $id;
2590
			}
2591
		}
2592
2593
		return $field_ids;
2594
	}
2595
2596
	/**
2597
	 * Process the contact form's POST submission
2598
	 * Stores feedback.  Sends email.
2599
	 */
2600
	function process_submission() {
2601
		global $post;
2602
2603
		$plugin = Grunion_Contact_Form_Plugin::init();
2604
2605
		$id     = $this->get_attribute( 'id' );
2606
		$to     = $this->get_attribute( 'to' );
2607
		$widget = $this->get_attribute( 'widget' );
2608
2609
		$contact_form_subject    = $this->get_attribute( 'subject' );
2610
		$email_marketing_consent = false;
2611
2612
		$to     = str_replace( ' ', '', $to );
2613
		$emails = explode( ',', $to );
2614
2615
		$valid_emails = array();
2616
2617
		foreach ( (array) $emails as $email ) {
2618
			if ( ! is_email( $email ) ) {
2619
				continue;
2620
			}
2621
2622
			if ( function_exists( 'is_email_address_unsafe' ) && is_email_address_unsafe( $email ) ) {
2623
				continue;
2624
			}
2625
2626
			$valid_emails[] = $email;
2627
		}
2628
2629
		// No one to send it to, which means none of the "to" attributes are valid emails.
2630
		// Use default email instead.
2631
		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...
2632
			$valid_emails = $this->defaults['to'];
2633
		}
2634
2635
		$to = $valid_emails;
2636
2637
		// Last ditch effort to set a recipient if somehow none have been set.
2638
		if ( empty( $to ) ) {
2639
			$to = get_option( 'admin_email' );
2640
		}
2641
2642
		// Make sure we're processing the form we think we're processing... probably a redundant check.
2643
		if ( $widget ) {
2644
			if ( 'widget-' . $widget != $_POST['contact-form-id'] ) {
2645
				return false;
2646
			}
2647
		} else {
2648
			if ( $post->ID != $_POST['contact-form-id'] ) {
2649
				return false;
2650
			}
2651
		}
2652
2653
		$field_ids = $this->get_field_ids();
2654
2655
		// Initialize all these "standard" fields to null
2656
		$comment_author_email = $comment_author_email_label = // v
2657
		$comment_author       = $comment_author_label       = // v
2658
		$comment_author_url   = $comment_author_url_label   = // v
2659
		$comment_content      = $comment_content_label = null;
2660
2661
		// For each of the "standard" fields, grab their field label and value.
2662 View Code Duplication
		if ( isset( $field_ids['name'] ) ) {
2663
			$field          = $this->fields[ $field_ids['name'] ];
2664
			$comment_author = Grunion_Contact_Form_Plugin::strip_tags(
2665
				stripslashes(
2666
					/** This filter is already documented in core/wp-includes/comment-functions.php */
2667
					apply_filters( 'pre_comment_author_name', addslashes( $field->value ) )
2668
				)
2669
			);
2670
			$comment_author_label = Grunion_Contact_Form_Plugin::strip_tags( $field->get_attribute( 'label' ) );
2671
		}
2672
2673 View Code Duplication
		if ( isset( $field_ids['email'] ) ) {
2674
			$field                = $this->fields[ $field_ids['email'] ];
2675
			$comment_author_email = Grunion_Contact_Form_Plugin::strip_tags(
2676
				stripslashes(
2677
					/** This filter is already documented in core/wp-includes/comment-functions.php */
2678
					apply_filters( 'pre_comment_author_email', addslashes( $field->value ) )
2679
				)
2680
			);
2681
			$comment_author_email_label = Grunion_Contact_Form_Plugin::strip_tags( $field->get_attribute( 'label' ) );
2682
		}
2683
2684
		if ( isset( $field_ids['url'] ) ) {
2685
			$field              = $this->fields[ $field_ids['url'] ];
2686
			$comment_author_url = Grunion_Contact_Form_Plugin::strip_tags(
2687
				stripslashes(
2688
					/** This filter is already documented in core/wp-includes/comment-functions.php */
2689
					apply_filters( 'pre_comment_author_url', addslashes( $field->value ) )
2690
				)
2691
			);
2692
			if ( 'http://' == $comment_author_url ) {
2693
				$comment_author_url = '';
2694
			}
2695
			$comment_author_url_label = Grunion_Contact_Form_Plugin::strip_tags( $field->get_attribute( 'label' ) );
2696
		}
2697
2698
		if ( isset( $field_ids['textarea'] ) ) {
2699
			$field                 = $this->fields[ $field_ids['textarea'] ];
2700
			$comment_content       = trim( Grunion_Contact_Form_Plugin::strip_tags( $field->value ) );
2701
			$comment_content_label = Grunion_Contact_Form_Plugin::strip_tags( $field->get_attribute( 'label' ) );
2702
		}
2703
2704
		if ( isset( $field_ids['subject'] ) ) {
2705
			$field = $this->fields[ $field_ids['subject'] ];
2706
			if ( $field->value ) {
2707
				$contact_form_subject = Grunion_Contact_Form_Plugin::strip_tags( $field->value );
2708
			}
2709
		}
2710
2711
		if ( isset( $field_ids['consent'] ) ) {
2712
			$field = $this->fields[ $field_ids['consent'] ];
2713
			if ( $field->value ) {
2714
				$email_marketing_consent = true;
2715
			}
2716
		}
2717
2718
		$all_values = $extra_values = array();
2719
		$i          = 1; // Prefix counter for stored metadata
2720
2721
		// For all fields, grab label and value
2722
		foreach ( $field_ids['all'] as $field_id ) {
2723
			$field = $this->fields[ $field_id ];
2724
			$label = $i . '_' . $field->get_attribute( 'label' );
2725
			$value = $field->value;
2726
2727
			$all_values[ $label ] = $value;
2728
			$i++; // Increment prefix counter for the next field
2729
		}
2730
2731
		// For the "non-standard" fields, grab label and value
2732
		// Extra fields have their prefix starting from count( $all_values ) + 1
2733
		foreach ( $field_ids['extra'] as $field_id ) {
2734
			$field = $this->fields[ $field_id ];
2735
			$label = $i . '_' . $field->get_attribute( 'label' );
2736
			$value = $field->value;
2737
2738
			if ( is_array( $value ) ) {
2739
				$value = implode( ', ', $value );
2740
			}
2741
2742
			$extra_values[ $label ] = $value;
2743
			$i++; // Increment prefix counter for the next extra field
2744
		}
2745
2746
		if ( isset( $_REQUEST['is_block'] ) && $_REQUEST['is_block'] ) {
2747
			$extra_values['is_block'] = true;
2748
		}
2749
2750
		$contact_form_subject = trim( $contact_form_subject );
2751
2752
		$comment_author_IP = Grunion_Contact_Form_Plugin::get_ip_address();
2753
2754
		$vars = array( 'comment_author', 'comment_author_email', 'comment_author_url', 'contact_form_subject', 'comment_author_IP' );
2755
		foreach ( $vars as $var ) {
2756
			$$var = str_replace( array( "\n", "\r" ), '', $$var );
2757
		}
2758
2759
		// Ensure that Akismet gets all of the relevant information from the contact form,
2760
		// not just the textarea field and predetermined subject.
2761
		$akismet_vars                    = compact( $vars );
2762
		$akismet_vars['comment_content'] = $comment_content;
2763
2764
		foreach ( array_merge( $field_ids['all'], $field_ids['extra'] ) as $field_id ) {
2765
			$field = $this->fields[ $field_id ];
2766
2767
			// Skip any fields that are just a choice from a pre-defined list. They wouldn't have any value
2768
			// from a spam-filtering point of view.
2769
			if ( in_array( $field->get_attribute( 'type' ), array( 'select', 'checkbox', 'checkbox-multiple', 'radio' ) ) ) {
2770
				continue;
2771
			}
2772
2773
			// Normalize the label into a slug.
2774
			$field_slug = trim( // Strip all leading/trailing dashes.
2775
				preg_replace(   // Normalize everything to a-z0-9_-
2776
					'/[^a-z0-9_]+/',
2777
					'-',
2778
					strtolower( $field->get_attribute( 'label' ) ) // Lowercase
2779
				),
2780
				'-'
2781
			);
2782
2783
			$field_value = ( is_array( $field->value ) ) ? trim( implode( ', ', $field->value ) ) : trim( $field->value );
2784
2785
			// Skip any values that are already in the array we're sending.
2786
			if ( $field_value && in_array( $field_value, $akismet_vars ) ) {
2787
				continue;
2788
			}
2789
2790
			$akismet_vars[ 'contact_form_field_' . $field_slug ] = $field_value;
2791
		}
2792
2793
		$spam           = '';
2794
		$akismet_values = $plugin->prepare_for_akismet( $akismet_vars );
2795
2796
		// Is it spam?
2797
		/** This filter is already documented in modules/contact-form/admin.php */
2798
		$is_spam = apply_filters( 'jetpack_contact_form_is_spam', false, $akismet_values );
0 ignored issues
show
Unused Code introduced by
The call to apply_filters() has too many arguments starting with $akismet_values.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
2799
		if ( is_wp_error( $is_spam ) ) { // WP_Error to abort
2800
			return $is_spam; // abort
2801
		} elseif ( $is_spam === true ) {  // TRUE to flag a spam
2802
			$spam = '***SPAM*** ';
2803
		}
2804
2805
		/**
2806
		 * Filter whether a submitted contact form is in the comment disallowed list.
2807
		 *
2808
		 * @module contact-form
2809
		 *
2810
		 * @since 8.9.0
2811
		 *
2812
		 * @param bool  $result         Is the submitted feedback in the disallowed list.
2813
		 * @param array $akismet_values Feedack values returned by the Akismet plugin.
2814
		 */
2815
		$in_comment_disallowed_list = apply_filters( 'jetpack_contact_form_in_comment_disallowed_list', false, $akismet_values );
0 ignored issues
show
Unused Code introduced by
The call to apply_filters() has too many arguments starting with $akismet_values.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
2816
2817
		if ( ! $comment_author ) {
2818
			$comment_author = $comment_author_email;
2819
		}
2820
2821
		/**
2822
		 * Filter the email where a submitted feedback is sent.
2823
		 *
2824
		 * @module contact-form
2825
		 *
2826
		 * @since 1.3.1
2827
		 *
2828
		 * @param string|array $to Array of valid email addresses, or single email address.
2829
		 */
2830
		$to            = (array) apply_filters( 'contact_form_to', $to );
2831
		$reply_to_addr = $to[0]; // get just the address part before the name part is added
2832
2833
		foreach ( $to as $to_key => $to_value ) {
2834
			$to[ $to_key ] = Grunion_Contact_Form_Plugin::strip_tags( $to_value );
2835
			$to[ $to_key ] = self::add_name_to_address( $to_value );
2836
		}
2837
2838
		$blog_url        = wp_parse_url( site_url() );
2839
		$from_email_addr = 'wordpress@' . $blog_url['host'];
2840
2841
		if ( ! empty( $comment_author_email ) ) {
2842
			$reply_to_addr = $comment_author_email;
2843
		}
2844
2845
		$headers = 'From: "' . $comment_author . '" <' . $from_email_addr . ">\r\n" .
2846
		           'Reply-To: "' . $comment_author . '" <' . $reply_to_addr . ">\r\n";
2847
2848
		$all_values['email_marketing_consent'] = $email_marketing_consent;
2849
2850
		// Build feedback reference
2851
		$feedback_time  = current_time( 'mysql' );
2852
		$feedback_title = "{$comment_author} - {$feedback_time}";
2853
		$feedback_id    = md5( $feedback_title );
2854
2855
		$entry_values = array(
2856
			'entry_title'     => the_title_attribute( 'echo=0' ),
2857
			'entry_permalink' => esc_url( get_permalink( get_the_ID() ) ),
2858
			'feedback_id'     => $feedback_id,
2859
		);
2860
2861
		$all_values = array_merge( $all_values, $entry_values );
2862
2863
		/** This filter is already documented in modules/contact-form/admin.php */
2864
		$subject = apply_filters( 'contact_form_subject', $contact_form_subject, $all_values );
0 ignored issues
show
Unused Code introduced by
The call to apply_filters() has too many arguments starting with $all_values.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
2865
		$url     = $widget ? home_url( '/' ) : get_permalink( $post->ID );
2866
2867
		$date_time_format = _x( '%1$s \a\t %2$s', '{$date_format} \a\t {$time_format}', 'jetpack' );
2868
		$date_time_format = sprintf( $date_time_format, get_option( 'date_format' ), get_option( 'time_format' ) );
2869
		$time             = date_i18n( $date_time_format, current_time( 'timestamp' ) );
2870
2871
		// Keep a copy of the feedback as a custom post type.
2872
		if ( $in_comment_disallowed_list ) {
2873
			$feedback_status = 'trash';
2874
		} elseif ( $is_spam ) {
2875
			$feedback_status = 'spam';
2876
		} else {
2877
			$feedback_status = 'publish';
2878
		}
2879
2880
		foreach ( (array) $akismet_values as $av_key => $av_value ) {
2881
			$akismet_values[ $av_key ] = Grunion_Contact_Form_Plugin::strip_tags( $av_value );
2882
		}
2883
2884
		foreach ( (array) $all_values as $all_key => $all_value ) {
2885
			$all_values[ $all_key ] = Grunion_Contact_Form_Plugin::strip_tags( $all_value );
2886
		}
2887
2888
		foreach ( (array) $extra_values as $ev_key => $ev_value ) {
2889
			$extra_values[ $ev_key ] = Grunion_Contact_Form_Plugin::strip_tags( $ev_value );
2890
		}
2891
2892
		/*
2893
		 We need to make sure that the post author is always zero for contact
2894
		 * form submissions.  This prevents export/import from trying to create
2895
		 * new users based on form submissions from people who were logged in
2896
		 * at the time.
2897
		 *
2898
		 * Unfortunately wp_insert_post() tries very hard to make sure the post
2899
		 * author gets the currently logged in user id.  That is how we ended up
2900
		 * with this work around. */
2901
		add_filter( 'wp_insert_post_data', array( $plugin, 'insert_feedback_filter' ), 10, 2 );
2902
2903
		$post_id = wp_insert_post(
2904
			array(
2905
				'post_date'    => addslashes( $feedback_time ),
2906
				'post_type'    => 'feedback',
2907
				'post_status'  => addslashes( $feedback_status ),
2908
				'post_parent'  => (int) $post->ID,
2909
				'post_title'   => addslashes( wp_kses( $feedback_title, array() ) ),
2910
				'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
2911
				'post_name'    => $feedback_id,
2912
			)
2913
		);
2914
2915
		// once insert has finished we don't need this filter any more
2916
		remove_filter( 'wp_insert_post_data', array( $plugin, 'insert_feedback_filter' ), 10 );
2917
2918
		update_post_meta( $post_id, '_feedback_extra_fields', $this->addslashes_deep( $extra_values ) );
2919
2920
		if ( 'publish' == $feedback_status ) {
2921
			// Increase count of unread feedback.
2922
			$unread = get_option( 'feedback_unread_count', 0 ) + 1;
2923
			update_option( 'feedback_unread_count', $unread );
2924
		}
2925
2926
		if ( defined( 'AKISMET_VERSION' ) ) {
2927
			update_post_meta( $post_id, '_feedback_akismet_values', $this->addslashes_deep( $akismet_values ) );
2928
		}
2929
2930
		/**
2931
		 * Fires after the feedback post for the contact form submission has been inserted.
2932
		 *
2933
		 * @module contact-form
2934
		 *
2935
		 * @since 8.6.0
2936
		 *
2937
		 * @param integer $post_id The post id that contains the contact form data.
2938
		 * @param array   $this->fields An array containg the form's Grunion_Contact_Form_Field objects.
2939
		 * @param boolean $is_spam Whether the form submission has been identified as spam.
2940
		 * @param array   $entry_values The feedback entry values.
2941
		 */
2942
		do_action( 'grunion_after_feedback_post_inserted', $post_id, $this->fields, $is_spam, $entry_values );
2943
2944
		$message = self::get_compiled_form( $post_id, $this );
2945
2946
		array_push(
2947
			$message,
2948
			'<br />',
2949
			'<hr />',
2950
			__( 'Time:', 'jetpack' ) . ' ' . $time . '<br />',
2951
			__( 'IP Address:', 'jetpack' ) . ' ' . $comment_author_IP . '<br />',
2952
			__( 'Contact Form URL:', 'jetpack' ) . ' ' . $url . '<br />'
2953
		);
2954
2955
		if ( is_user_logged_in() ) {
2956
			array_push(
2957
				$message,
2958
				sprintf(
2959
					'<p>' . __( 'Sent by a verified %s user.', 'jetpack' ) . '</p>',
2960
					isset( $GLOBALS['current_site']->site_name ) && $GLOBALS['current_site']->site_name ?
2961
						$GLOBALS['current_site']->site_name : '"' . get_option( 'blogname' ) . '"'
2962
				)
2963
			);
2964
		} else {
2965
			array_push( $message, '<p>' . __( 'Sent by an unverified visitor to your site.', 'jetpack' ) . '</p>' );
2966
		}
2967
2968
		$message = join( '', $message );
2969
2970
		/**
2971
		 * Filters the message sent via email after a successful form submission.
2972
		 *
2973
		 * @module contact-form
2974
		 *
2975
		 * @since 1.3.1
2976
		 *
2977
		 * @param string $message Feedback email message.
2978
		 */
2979
		$message = apply_filters( 'contact_form_message', $message );
2980
2981
		// This is called after `contact_form_message`, in order to preserve back-compat
2982
		$message = self::wrap_message_in_html_tags( $message );
2983
2984
		update_post_meta( $post_id, '_feedback_email', $this->addslashes_deep( compact( 'to', 'message' ) ) );
2985
2986
		/**
2987
		 * Fires right before the contact form message is sent via email to
2988
		 * the recipient specified in the contact form.
2989
		 *
2990
		 * @module contact-form
2991
		 *
2992
		 * @since 1.3.1
2993
		 *
2994
		 * @param integer $post_id Post contact form lives on
2995
		 * @param array $all_values Contact form fields
2996
		 * @param array $extra_values Contact form fields not included in $all_values
2997
		 */
2998
		do_action( 'grunion_pre_message_sent', $post_id, $all_values, $extra_values );
2999
3000
		// schedule deletes of old spam feedbacks
3001
		if ( ! wp_next_scheduled( 'grunion_scheduled_delete' ) ) {
3002
			wp_schedule_event( time() + 250, 'daily', 'grunion_scheduled_delete' );
3003
		}
3004
3005
		if (
3006
			$is_spam !== true &&
3007
			/**
3008
			 * Filter to choose whether an email should be sent after each successful contact form submission.
3009
			 *
3010
			 * @module contact-form
3011
			 *
3012
			 * @since 2.6.0
3013
			 *
3014
			 * @param bool true Should an email be sent after a form submission. Default to true.
3015
			 * @param int $post_id Post ID.
3016
			 */
3017
			true === apply_filters( 'grunion_should_send_email', true, $post_id )
0 ignored issues
show
Unused Code introduced by
The call to apply_filters() has too many arguments starting with $post_id.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
3018
		) {
3019
			self::wp_mail( $to, "{$spam}{$subject}", $message, $headers );
3020
		} elseif (
3021
			true === $is_spam &&
3022
			/**
3023
			 * Choose whether an email should be sent for each spam contact form submission.
3024
			 *
3025
			 * @module contact-form
3026
			 *
3027
			 * @since 1.3.1
3028
			 *
3029
			 * @param bool false Should an email be sent after a spam form submission. Default to false.
3030
			 */
3031
			apply_filters( 'grunion_still_email_spam', false ) == true
3032
		) { // don't send spam by default.  Filterable.
3033
			self::wp_mail( $to, "{$spam}{$subject}", $message, $headers );
3034
		}
3035
3036
		/**
3037
		 * Fires an action hook right after the email(s) have been sent.
3038
		 *
3039
		 * @module contact-form
3040
		 *
3041
		 * @since 7.3.0
3042
		 *
3043
		 * @param int $post_id Post contact form lives on.
3044
		 * @param string|array $to Array of valid email addresses, or single email address.
3045
		 * @param string $subject Feedback email subject.
3046
		 * @param string $message Feedback email message.
3047
		 * @param string|array $headers Optional. Additional headers.
3048
		 * @param array $all_values Contact form fields.
3049
		 * @param array $extra_values Contact form fields not included in $all_values
3050
		 */
3051
		do_action( 'grunion_after_message_sent', $post_id, $to, $subject, $message, $headers, $all_values, $extra_values );
3052
3053
		if ( defined( 'DOING_AJAX' ) && DOING_AJAX ) {
3054
			return self::success_message( $post_id, $this );
3055
		}
3056
3057
		$redirect = '';
3058
		$custom_redirect = false;
3059
		if ( 'redirect' === $this->get_attribute( 'customThankyou' ) ) {
3060
			$custom_redirect = true;
3061
			$redirect        = esc_url( $this->get_attribute( 'customThankyouRedirect' ) );
3062
		}
3063
3064
		if ( ! $redirect ) {
3065
			$custom_redirect = false;
3066
			$redirect        = wp_get_referer();
3067
		}
3068
3069
		if ( ! $redirect ) { // wp_get_referer() returns false if the referer is the same as the current page.
3070
			$custom_redirect = false;
3071
			$redirect        = $_SERVER['REQUEST_URI'];
3072
		}
3073
3074
		if ( ! $custom_redirect ) {
3075
			$redirect = add_query_arg(
3076
				urlencode_deep(
3077
					array(
3078
						'contact-form-id'   => $id,
3079
						'contact-form-sent' => $post_id,
3080
						'contact-form-hash' => $this->hash,
3081
						'_wpnonce'          => wp_create_nonce( "contact-form-sent-{$post_id}" ), // wp_nonce_url HTMLencodes :( .
3082
					)
3083
				),
3084
				$redirect
3085
			);
3086
		}
3087
3088
		/**
3089
		 * Filter the URL where the reader is redirected after submitting a form.
3090
		 *
3091
		 * @module contact-form
3092
		 *
3093
		 * @since 1.9.0
3094
		 *
3095
		 * @param string $redirect Post submission URL.
3096
		 * @param int $id Contact Form ID.
3097
		 * @param int $post_id Post ID.
3098
		 */
3099
		$redirect = apply_filters( 'grunion_contact_form_redirect_url', $redirect, $id, $post_id );
0 ignored issues
show
Unused Code introduced by
The call to apply_filters() has too many arguments starting with $id.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
3100
3101
		// phpcs:ignore WordPress.Security.SafeRedirect.wp_redirect_wp_redirect -- We intentially allow external redirects here.
3102
		wp_redirect( $redirect );
3103
		exit;
3104
	}
3105
3106
	/**
3107
	 * Wrapper for wp_mail() that enables HTML messages with text alternatives
3108
	 *
3109
	 * @param string|array $to          Array or comma-separated list of email addresses to send message.
3110
	 * @param string       $subject     Email subject.
3111
	 * @param string       $message     Message contents.
3112
	 * @param string|array $headers     Optional. Additional headers.
3113
	 * @param string|array $attachments Optional. Files to attach.
3114
	 *
3115
	 * @return bool Whether the email contents were sent successfully.
3116
	 */
3117
	public static function wp_mail( $to, $subject, $message, $headers = '', $attachments = array() ) {
3118
		add_filter( 'wp_mail_content_type', __CLASS__ . '::get_mail_content_type' );
3119
		add_action( 'phpmailer_init', __CLASS__ . '::add_plain_text_alternative' );
3120
3121
		$result = wp_mail( $to, $subject, $message, $headers, $attachments );
3122
3123
		remove_filter( 'wp_mail_content_type', __CLASS__ . '::get_mail_content_type' );
3124
		remove_action( 'phpmailer_init', __CLASS__ . '::add_plain_text_alternative' );
3125
3126
		return $result;
3127
	}
3128
3129
	/**
3130
	 * Add a display name part to an email address
3131
	 *
3132
	 * SpamAssassin doesn't like addresses in HTML messages that are missing display names (e.g., `[email protected]`
3133
	 * instead of `"Foo Bar" <[email protected]>`.
3134
	 *
3135
	 * @param string $address
3136
	 *
3137
	 * @return string
3138
	 */
3139
	function add_name_to_address( $address ) {
3140
		// If it's just the address, without a display name
3141
		if ( is_email( $address ) ) {
3142
			$address_parts = explode( '@', $address );
3143
			$address       = sprintf( '"%s" <%s>', $address_parts[0], $address );
3144
		}
3145
3146
		return $address;
3147
	}
3148
3149
	/**
3150
	 * Get the content type that should be assigned to outbound emails
3151
	 *
3152
	 * @return string
3153
	 */
3154
	static function get_mail_content_type() {
3155
		return 'text/html';
3156
	}
3157
3158
	/**
3159
	 * Wrap a message body with the appropriate in HTML tags
3160
	 *
3161
	 * This helps to ensure correct parsing by clients, and also helps avoid triggering spam filtering rules
3162
	 *
3163
	 * @param string $body
3164
	 *
3165
	 * @return string
3166
	 */
3167
	static function wrap_message_in_html_tags( $body ) {
3168
		// Don't do anything if the message was already wrapped in HTML tags
3169
		// That could have be done by a plugin via filters
3170
		if ( false !== strpos( $body, '<html' ) ) {
3171
			return $body;
3172
		}
3173
3174
		$html_message = sprintf(
3175
			// The tabs are just here so that the raw code is correctly formatted for developers
3176
			// They're removed so that they don't affect the final message sent to users
3177
			str_replace(
3178
				"\t", '',
3179
				'<!doctype html>
3180
				<html xmlns="http://www.w3.org/1999/xhtml">
3181
				<body>
3182
3183
				%s
3184
3185
				</body>
3186
				</html>'
3187
			),
3188
			$body
3189
		);
3190
3191
		return $html_message;
3192
	}
3193
3194
	/**
3195
	 * Add a plain-text alternative part to an outbound email
3196
	 *
3197
	 * This makes the message more accessible to mail clients that aren't HTML-aware, and decreases the likelihood
3198
	 * that the message will be flagged as spam.
3199
	 *
3200
	 * @param PHPMailer $phpmailer
3201
	 */
3202
	static function add_plain_text_alternative( $phpmailer ) {
3203
		// Add an extra break so that the extra space above the <p> is preserved after the <p> is stripped out
3204
		$alt_body = str_replace( '<p>', '<p><br />', $phpmailer->Body );
3205
3206
		// Convert <br> to \n breaks, to preserve the space between lines that we want to keep
3207
		$alt_body = str_replace( array( '<br>', '<br />' ), "\n", $alt_body );
3208
3209
		// Convert <hr> to an plain-text equivalent, to preserve the integrity of the message
3210
		$alt_body = str_replace( array( '<hr>', '<hr />' ), "----\n", $alt_body );
3211
3212
		// Trim the plain text message to remove the \n breaks that were after <doctype>, <html>, and <body>
3213
		$phpmailer->AltBody = trim( strip_tags( $alt_body ) );
3214
	}
3215
3216
	function addslashes_deep( $value ) {
3217
		if ( is_array( $value ) ) {
3218
			return array_map( array( $this, 'addslashes_deep' ), $value );
3219
		} elseif ( is_object( $value ) ) {
3220
			$vars = get_object_vars( $value );
3221
			foreach ( $vars as $key => $data ) {
3222
				$value->{$key} = $this->addslashes_deep( $data );
3223
			}
3224
			return $value;
3225
		}
3226
3227
		return addslashes( $value );
3228
	}
3229
3230
} // end class Grunion_Contact_Form
3231
3232
/**
3233
 * Class for the contact-field shortcode.
3234
 * Parses shortcode to output the contact form field as HTML.
3235
 * Validates input.
3236
 */
3237
class Grunion_Contact_Form_Field extends Crunion_Contact_Form_Shortcode {
3238
	public $shortcode_name = 'contact-field';
3239
3240
	/**
3241
	 * @var Grunion_Contact_Form parent form
3242
	 */
3243
	public $form;
3244
3245
	/**
3246
	 * @var string default or POSTed value
3247
	 */
3248
	public $value;
3249
3250
	/**
3251
	 * @var bool Is the input invalid?
3252
	 */
3253
	public $error = false;
3254
3255
	/**
3256
	 * @param array                $attributes An associative array of shortcode attributes.  @see shortcode_atts()
3257
	 * @param null|string          $content Null for selfclosing shortcodes.  The inner content otherwise.
3258
	 * @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...
3259
	 */
3260
	function __construct( $attributes, $content = null, $form = null ) {
3261
		$attributes = shortcode_atts(
3262
			array(
3263
				'label'                  => null,
3264
				'type'                   => 'text',
3265
				'required'               => false,
3266
				'options'                => array(),
3267
				'id'                     => null,
3268
				'default'                => null,
3269
				'values'                 => null,
3270
				'placeholder'            => null,
3271
				'class'                  => null,
3272
				'width'                  => null,
3273
				'consenttype'            => null,
3274
				'implicitconsentmessage' => null,
3275
				'explicitconsentmessage' => null,
3276
			), $attributes, 'contact-field'
3277
		);
3278
3279
		// special default for subject field
3280
		if ( 'subject' == $attributes['type'] && is_null( $attributes['default'] ) && ! is_null( $form ) ) {
3281
			$attributes['default'] = $form->get_attribute( 'subject' );
3282
		}
3283
3284
		// allow required=1 or required=true
3285
		if ( '1' == $attributes['required'] || 'true' == strtolower( $attributes['required'] ) ) {
3286
			$attributes['required'] = true;
3287
		} else {
3288
			$attributes['required'] = false;
3289
		}
3290
3291
		// parse out comma-separated options list (for selects, radios, and checkbox-multiples)
3292
		if ( ! empty( $attributes['options'] ) && is_string( $attributes['options'] ) ) {
3293
			$attributes['options'] = array_map( 'trim', explode( ',', $attributes['options'] ) );
3294
3295 View Code Duplication
			if ( ! empty( $attributes['values'] ) && is_string( $attributes['values'] ) ) {
3296
				$attributes['values'] = array_map( 'trim', explode( ',', $attributes['values'] ) );
3297
			}
3298
		}
3299
3300
		if ( $form ) {
3301
			// make a unique field ID based on the label, with an incrementing number if needed to avoid clashes
3302
			$form_id = $form->get_attribute( 'id' );
3303
			$id      = isset( $attributes['id'] ) ? $attributes['id'] : false;
3304
3305
			$unescaped_label = $this->unesc_attr( $attributes['label'] );
3306
			$unescaped_label = str_replace( '%', '-', $unescaped_label ); // jQuery doesn't like % in IDs?
3307
			$unescaped_label = preg_replace( '/[^a-zA-Z0-9.-_:]/', '', $unescaped_label );
3308
3309
			if ( empty( $id ) ) {
3310
				$id        = sanitize_title_with_dashes( 'g' . $form_id . '-' . $unescaped_label );
3311
				$i         = 0;
3312
				$max_tries = 99;
3313
				while ( isset( $form->fields[ $id ] ) ) {
3314
					$i++;
3315
					$id = sanitize_title_with_dashes( 'g' . $form_id . '-' . $unescaped_label . '-' . $i );
3316
3317
					if ( $i > $max_tries ) {
3318
						break;
3319
					}
3320
				}
3321
			}
3322
3323
			$attributes['id'] = $id;
3324
		}
3325
3326
		parent::__construct( $attributes, $content );
3327
3328
		// Store parent form
3329
		$this->form = $form;
3330
	}
3331
3332
	/**
3333
	 * This field's input is invalid.  Flag as invalid and add an error to the parent form
3334
	 *
3335
	 * @param string $message The error message to display on the form.
3336
	 */
3337
	function add_error( $message ) {
3338
		$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...
3339
3340
		if ( ! is_wp_error( $this->form->errors ) ) {
3341
			$this->form->errors = new WP_Error;
3342
		}
3343
3344
		$this->form->errors->add( $this->get_attribute( 'id' ), $message );
0 ignored issues
show
Bug introduced by
The method add() does not seem to exist on object<WP_Error>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
3345
	}
3346
3347
	/**
3348
	 * Is the field input invalid?
3349
	 *
3350
	 * @see $error
3351
	 *
3352
	 * @return bool
3353
	 */
3354
	function is_error() {
3355
		return $this->error;
3356
	}
3357
3358
	/**
3359
	 * Validates the form input
3360
	 */
3361
	function validate() {
3362
		// If it's not required, there's nothing to validate
3363
		if ( ! $this->get_attribute( 'required' ) ) {
3364
			return;
3365
		}
3366
3367
		$field_id    = $this->get_attribute( 'id' );
3368
		$field_type  = $this->get_attribute( 'type' );
3369
		$field_label = $this->get_attribute( 'label' );
3370
3371
		if ( isset( $_POST[ $field_id ] ) ) {
3372
			if ( is_array( $_POST[ $field_id ] ) ) {
3373
				$field_value = array_map( 'stripslashes', $_POST[ $field_id ] );
3374
			} else {
3375
				$field_value = stripslashes( $_POST[ $field_id ] );
3376
			}
3377
		} else {
3378
			$field_value = '';
3379
		}
3380
3381
		switch ( $field_type ) {
3382 View Code Duplication
			case 'email':
3383
				// Make sure the email address is valid
3384
				if ( ! is_string( $field_value ) || ! is_email( $field_value ) ) {
3385
					/* translators: %s is the name of a form field */
3386
					$this->add_error( sprintf( __( '%s requires a valid email address', 'jetpack' ), $field_label ) );
3387
				}
3388
				break;
3389
			case 'checkbox-multiple':
3390
				// Check that there is at least one option selected
3391
				if ( empty( $field_value ) ) {
3392
					/* translators: %s is the name of a form field */
3393
					$this->add_error( sprintf( __( '%s requires at least one selection', 'jetpack' ), $field_label ) );
3394
				}
3395
				break;
3396 View Code Duplication
			default:
3397
				// Just check for presence of any text
3398
				if ( ! is_string( $field_value ) || ! strlen( trim( $field_value ) ) ) {
3399
					/* translators: %s is the name of a form field */
3400
					$this->add_error( sprintf( __( '%s is required', 'jetpack' ), $field_label ) );
3401
				}
3402
		}
3403
	}
3404
3405
3406
	/**
3407
	 * Check the default value for options field
3408
	 *
3409
	 * @param string value
3410
	 * @param int index
3411
	 * @param string default value
3412
	 *
3413
	 * @return string
3414
	 */
3415
	public function get_option_value( $value, $index, $options ) {
3416
		if ( empty( $value[ $index ] ) ) {
3417
			return $options;
3418
		}
3419
		return $value[ $index ];
3420
	}
3421
3422
	/**
3423
	 * Outputs the HTML for this form field
3424
	 *
3425
	 * @return string HTML
3426
	 */
3427
	function render() {
3428
		global $current_user, $user_identity;
3429
3430
		$field_id          = $this->get_attribute( 'id' );
3431
		$field_type        = $this->get_attribute( 'type' );
3432
		$field_label       = $this->get_attribute( 'label' );
3433
		$field_required    = $this->get_attribute( 'required' );
3434
		$field_placeholder = $this->get_attribute( 'placeholder' );
3435
		$field_width       = $this->get_attribute( 'width' );
3436
		$class             = 'date' === $field_type ? 'jp-contact-form-date' : $this->get_attribute( 'class' );
3437
3438
		if ( ! empty( $field_width ) ) {
3439
			$class .= ' grunion-field-width-' . $field_width;
3440
		}
3441
3442
		/**
3443
		 * Filters the "class" attribute of the contact form input
3444
		 *
3445
		 * @module contact-form
3446
		 *
3447
		 * @since 6.6.0
3448
		 *
3449
		 * @param string $class Additional CSS classes for input class attribute.
3450
		 */
3451
		$field_class = apply_filters( 'jetpack_contact_form_input_class', $class );
3452
3453
		if ( isset( $_POST[ $field_id ] ) ) {
3454
			if ( is_array( $_POST[ $field_id ] ) ) {
3455
				$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...
3456
			} else {
3457
				$this->value = stripslashes( (string) $_POST[ $field_id ] );
3458
			}
3459
		} elseif ( isset( $_GET[ $field_id ] ) ) {
3460
			$this->value = stripslashes( (string) $_GET[ $field_id ] );
3461
		} elseif (
3462
			is_user_logged_in() &&
3463
			( ( defined( 'IS_WPCOM' ) && IS_WPCOM ) ||
3464
			  /**
3465
			   * Allow third-party tools to prefill the contact form with the user's details when they're logged in.
3466
			   *
3467
			   * @module contact-form
3468
			   *
3469
			   * @since 3.2.0
3470
			   *
3471
			   * @param bool false Should the Contact Form be prefilled with your details when you're logged in. Default to false.
3472
			   */
3473
			  true === apply_filters( 'jetpack_auto_fill_logged_in_user', false )
3474
			)
3475
		) {
3476
			// Special defaults for logged-in users
3477
			switch ( $this->get_attribute( 'type' ) ) {
3478
				case 'email':
3479
					$this->value = $current_user->data->user_email;
3480
					break;
3481
				case 'name':
3482
					$this->value = $user_identity;
3483
					break;
3484
				case 'url':
3485
					$this->value = $current_user->data->user_url;
3486
					break;
3487
				default:
3488
					$this->value = $this->get_attribute( 'default' );
3489
			}
3490
		} else {
3491
			$this->value = $this->get_attribute( 'default' );
3492
		}
3493
3494
		$field_value = Grunion_Contact_Form_Plugin::strip_tags( $this->value );
3495
		$field_label = Grunion_Contact_Form_Plugin::strip_tags( $field_label );
3496
3497
		$rendered_field = $this->render_field( $field_type, $field_id, $field_label, $field_value, $field_class, $field_placeholder, $field_required );
3498
3499
		/**
3500
		 * Filter the HTML of the Contact Form.
3501
		 *
3502
		 * @module contact-form
3503
		 *
3504
		 * @since 2.6.0
3505
		 *
3506
		 * @param string $rendered_field Contact Form HTML output.
3507
		 * @param string $field_label Field label.
3508
		 * @param int|null $id Post ID.
3509
		 */
3510
		return apply_filters( 'grunion_contact_form_field_html', $rendered_field, $field_label, ( in_the_loop() ? get_the_ID() : null ) );
0 ignored issues
show
Unused Code introduced by
The call to apply_filters() has too many arguments starting with $field_label.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
3511
	}
3512
3513
	public function render_label( $type, $id, $label, $required, $required_field_text ) {
3514
3515
		$type_class = $type ? ' ' .$type : '';
3516
		return
3517
			"<label
3518
				for='" . esc_attr( $id ) . "'
3519
				class='grunion-field-label{$type_class}" . ( $this->is_error() ? ' form-error' : '' ) . "'
3520
				>"
3521
				. esc_html( $label )
3522
				. ( $required ? '<span>' . $required_field_text . '</span>' : '' )
3523
			. "</label>\n";
3524
3525
	}
3526
3527
	function render_input_field( $type, $id, $value, $class, $placeholder, $required ) {
3528
		return "<input
3529
					type='". esc_attr( $type ) ."'
3530
					name='" . esc_attr( $id ) . "'
3531
					id='" . esc_attr( $id ) . "'
3532
					value='" . esc_attr( $value ) . "'
3533
					" . $class . $placeholder . '
3534
					' . ( $required ? "required aria-required='true'" : '' ) . "
3535
				/>\n";
3536
	}
3537
3538
	function render_email_field( $id, $label, $value, $class, $required, $required_field_text, $placeholder ) {
3539
		$field = $this->render_label( 'email', $id, $label, $required, $required_field_text );
3540
		$field .= $this->render_input_field( 'email', $id, $value, $class, $placeholder, $required );
3541
		return $field;
3542
	}
3543
3544
	function render_telephone_field( $id, $label, $value, $class, $required, $required_field_text, $placeholder ) {
3545
		$field = $this->render_label( 'telephone', $id, $label, $required, $required_field_text );
3546
		$field .= $this->render_input_field( 'tel', $id, $value, $class, $placeholder, $required );
3547
		return $field;
3548
	}
3549
3550
	function render_url_field( $id, $label, $value, $class, $required, $required_field_text, $placeholder ) {
3551
		$field = $this->render_label( 'url', $id, $label, $required, $required_field_text );
3552
		$field .= $this->render_input_field( 'url', $id, $value, $class, $placeholder, $required );
3553
		return $field;
3554
	}
3555
3556
	function render_textarea_field( $id, $label, $value, $class, $required, $required_field_text, $placeholder ) {
3557
		$field = $this->render_label( 'textarea', 'contact-form-comment-' . $id, $label, $required, $required_field_text );
3558
		$field .= "<textarea
3559
		                name='" . esc_attr( $id ) . "'
3560
		                id='contact-form-comment-" . esc_attr( $id ) . "'
3561
		                rows='20' "
3562
		                . $class
3563
		                . $placeholder
3564
		                . ' ' . ( $required ? "required aria-required='true'" : '' ) .
3565
		                '>' . esc_textarea( $value )
3566
		          . "</textarea>\n";
3567
		return $field;
3568
	}
3569
3570
	function render_radio_field( $id, $label, $value, $class, $required, $required_field_text ) {
3571
		$field = $this->render_label( '', $id, $label, $required, $required_field_text );
3572
		foreach ( (array) $this->get_attribute( 'options' ) as $optionIndex => $option ) {
3573
			$option = Grunion_Contact_Form_Plugin::strip_tags( $option );
3574
			if ( $option ) {
3575
				$field .= "\t\t<label class='grunion-radio-label radio" . ( $this->is_error() ? ' form-error' : '' ) . "'>";
3576
				$field .= "<input
3577
									type='radio'
3578
									name='" . esc_attr( $id ) . "'
3579
									value='" . esc_attr( $this->get_option_value( $this->get_attribute( 'values' ), $optionIndex, $option ) ) . "' "
3580
				                    . $class
3581
				                    . checked( $option, $value, false ) . ' '
3582
				                    . ( $required ? "required aria-required='true'" : '' )
3583
				              . '/> ';
3584
				$field .= esc_html( $option ) . "</label>\n";
3585
				$field .= "\t\t<div class='clear-form'></div>\n";
3586
			}
3587
		}
3588
		return $field;
3589
	}
3590
3591
	function render_checkbox_field( $id, $label, $value, $class, $required, $required_field_text ) {
3592
		$field = "<label class='grunion-field-label checkbox" . ( $this->is_error() ? ' form-error' : '' ) . "'>";
3593
			$field .= "\t\t<input type='checkbox' name='" . esc_attr( $id ) . "' value='" . esc_attr__( 'Yes', 'jetpack' ) . "' " . $class . checked( (bool) $value, true, false ) . ' ' . ( $required ? "required aria-required='true'" : '' ) . "/> \n";
3594
			$field .= "\t\t" . esc_html( $label ) . ( $required ? '<span>' . $required_field_text . '</span>' : '' );
3595
		$field .=  "</label>\n";
3596
		$field .= "<div class='clear-form'></div>\n";
3597
		return $field;
3598
	}
3599
3600
	/**
3601
	 * Render the consent field.
3602
	 *
3603
	 * @param string $id field id.
3604
	 * @param string $class html classes (can be set by the admin).
3605
	 */
3606
	private function render_consent_field( $id, $class ) {
3607
		$consent_type    = 'explicit' === $this->get_attribute( 'consenttype' ) ? 'explicit' : 'implicit';
3608
		$consent_message = 'explicit' === $consent_type ? $this->get_attribute( 'explicitconsentmessage' ) : $this->get_attribute( 'implicitconsentmessage' );
3609
3610
		$field  = "<label class='grunion-field-label consent consent-" . $consent_type . "'>";
3611
3612
		if ( 'implicit' === $consent_type ) {
3613
			$field .= "\t\t<input aria-hidden='true' type='checkbox' checked name='" . esc_attr( $id ) . "' value='" . esc_attr__( 'Yes', 'jetpack' ) . "' style='display:none;' /> \n";
3614
		} else {
3615
			$field .= "\t\t<input type='checkbox' name='" . esc_attr( $id ) . "' value='" . esc_attr__( 'Yes', 'jetpack' ) . "' " . $class . "/> \n";
3616
		}
3617
		$field .= "\t\t" . esc_html( $consent_message );
3618
		$field .= "</label>\n";
3619
		$field .= "<div class='clear-form'></div>\n";
3620
		return $field;
3621
	}
3622
3623
	function render_checkbox_multiple_field( $id, $label, $value, $class, $required, $required_field_text  ) {
3624
		$field = $this->render_label( '', $id, $label, $required, $required_field_text );
3625
		foreach ( (array) $this->get_attribute( 'options' ) as $optionIndex => $option ) {
3626
			$option = Grunion_Contact_Form_Plugin::strip_tags( $option );
3627
			if ( $option  ) {
3628
				$field .= "\t\t<label class='grunion-checkbox-multiple-label checkbox-multiple" . ( $this->is_error() ? ' form-error' : '' ) . "'>";
3629
				$field .= "<input type='checkbox' name='" . esc_attr( $id ) . "[]' value='" . esc_attr( $this->get_option_value( $this->get_attribute( 'values' ), $optionIndex, $option ) ) . "' " . $class . checked( in_array( $option, (array) $value ), true, false ) . ' /> ';
3630
				$field .= esc_html( $option ) . "</label>\n";
3631
				$field .= "\t\t<div class='clear-form'></div>\n";
3632
			}
3633
		}
3634
3635
		return $field;
3636
	}
3637
3638
	function render_select_field( $id, $label, $value, $class, $required, $required_field_text ) {
3639
		$field = $this->render_label( 'select', $id, $label, $required, $required_field_text );
3640
		$field  .= "\t<select name='" . esc_attr( $id ) . "' id='" . esc_attr( $id ) . "' " . $class . ( $required ? "required aria-required='true'" : '' ) . ">\n";
3641
		foreach ( (array) $this->get_attribute( 'options' ) as $optionIndex => $option ) {
3642
			$option = Grunion_Contact_Form_Plugin::strip_tags( $option );
3643
			if ( $option ) {
3644
				$field .= "\t\t<option"
3645
				               . selected( $option, $value, false )
3646
				               . " value='" . esc_attr( $this->get_option_value( $this->get_attribute( 'values' ), $optionIndex, $option ) )
3647
				               . "'>" . esc_html( $option )
3648
				          . "</option>\n";
3649
			}
3650
		}
3651
		$field  .= "\t</select>\n";
3652
		return $field;
3653
	}
3654
3655
	function render_date_field( $id, $label, $value, $class, $required, $required_field_text, $placeholder ) {
3656
3657
		$field = $this->render_label( 'date', $id, $label, $required, $required_field_text );
3658
		$field .= $this->render_input_field( 'text', $id, $value, $class, $placeholder, $required );
3659
3660
		/* For AMP requests, use amp-date-picker element: https://amp.dev/documentation/components/amp-date-picker */
3661
		if ( class_exists( 'Jetpack_AMP_Support' ) && Jetpack_AMP_Support::is_amp_request() ) {
3662
			return sprintf(
3663
				'<%1$s mode="overlay" layout="container" type="single" input-selector="[name=%2$s]">%3$s</%1$s>',
3664
				'amp-date-picker',
3665
				esc_attr( $id ),
3666
				$field
3667
			);
3668
		}
3669
3670
		wp_enqueue_script(
3671
			'grunion-frontend',
3672
			Assets::get_file_url_for_environment(
3673
				'_inc/build/contact-form/js/grunion-frontend.min.js',
3674
				'modules/contact-form/js/grunion-frontend.js'
3675
			),
3676
			array( 'jquery', 'jquery-ui-datepicker' )
3677
		);
3678
		wp_enqueue_style( 'jp-jquery-ui-datepicker', plugins_url( 'css/jquery-ui-datepicker.css', __FILE__ ), array( 'dashicons' ), '1.0' );
3679
3680
		// Using Core's built-in datepicker localization routine
3681
		wp_localize_jquery_ui_datepicker();
3682
		return $field;
3683
	}
3684
3685
	function render_default_field( $id, $label, $value, $class, $required, $required_field_text, $placeholder, $type ) {
3686
		$field = $this->render_label( $type, $id, $label, $required, $required_field_text );
3687
		$field .= $this->render_input_field( 'text', $id, $value, $class, $placeholder, $required );
3688
		return $field;
3689
	}
3690
3691
	function render_field( $type, $id, $label, $value, $class, $placeholder, $required ) {
3692
3693
		$field_placeholder = ( ! empty( $placeholder ) ) ? "placeholder='" . esc_attr( $placeholder ) . "'" : '';
3694
		$field_class       = "class='" . trim( esc_attr( $type ) . ' ' . esc_attr( $class ) ) . "' ";
3695
		$wrap_classes = empty( $class ) ? '' : implode( '-wrap ', array_filter( explode( ' ', $class ) ) ) . '-wrap'; // this adds
3696
3697
		$shell_field_class = "class='grunion-field-wrap grunion-field-" . trim( esc_attr( $type ) . '-wrap ' . esc_attr( $wrap_classes ) ) . "' ";
3698
		/**
3699
		/**
3700
		 * Filter the Contact Form required field text
3701
		 *
3702
		 * @module contact-form
3703
		 *
3704
		 * @since 3.8.0
3705
		 *
3706
		 * @param string $var Required field text. Default is "(required)".
3707
		 */
3708
		$required_field_text = esc_html( apply_filters( 'jetpack_required_field_text', __( '(required)', 'jetpack' ) ) );
3709
3710
		$field = "\n<div {$shell_field_class} >\n"; // new in Jetpack 6.8.0
3711
		// If they are logged in, and this is their site, don't pre-populate fields
3712
		if ( current_user_can( 'manage_options' ) ) {
3713
			$value = '';
3714
		}
3715
		switch ( $type ) {
3716
			case 'email':
3717
				$field .= $this->render_email_field( $id, $label, $value, $field_class, $required, $required_field_text, $field_placeholder );
3718
				break;
3719
			case 'telephone':
3720
				$field .= $this->render_telephone_field( $id, $label, $value, $field_class, $required, $required_field_text, $field_placeholder );
3721
				break;
3722
			case 'url':
3723
				$field .= $this->render_url_field( $id, $label, $value, $field_class, $required, $required_field_text, $field_placeholder );
3724
				break;
3725
			case 'textarea':
3726
				$field .= $this->render_textarea_field( $id, $label, $value, $field_class, $required, $required_field_text, $field_placeholder );
3727
				break;
3728
			case 'radio':
3729
				$field .= $this->render_radio_field( $id, $label, $value, $field_class, $required, $required_field_text, $field_placeholder );
0 ignored issues
show
Unused Code introduced by
The call to Grunion_Contact_Form_Field::render_radio_field() has too many arguments starting with $field_placeholder.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
3730
				break;
3731
			case 'checkbox':
3732
				$field .= $this->render_checkbox_field( $id, $label, $value, $field_class, $required, $required_field_text );
3733
				break;
3734
			case 'checkbox-multiple':
3735
				$field .= $this->render_checkbox_multiple_field( $id, $label, $value, $field_class, $required, $required_field_text );
3736
				break;
3737
			case 'select':
3738
				$field .= $this->render_select_field( $id, $label, $value, $field_class, $required, $required_field_text );
3739
				break;
3740
			case 'date':
3741
				$field .= $this->render_date_field( $id, $label, $value, $field_class, $required, $required_field_text, $field_placeholder );
3742
				break;
3743
			case 'consent':
3744
				$field .= $this->render_consent_field( $id, $field_class );
3745
				break;
3746
			default: // text field
3747
				$field .= $this->render_default_field( $id, $label, $value, $field_class, $required, $required_field_text, $field_placeholder, $type );
3748
				break;
3749
		}
3750
		$field .= "\t</div>\n";
3751
		return $field;
3752
	}
3753
}
3754
3755
add_action( 'init', array( 'Grunion_Contact_Form_Plugin', 'init' ), 9 );
3756
3757
add_action( 'grunion_scheduled_delete', 'grunion_delete_old_spam' );
3758
3759
/**
3760
 * Deletes old spam feedbacks to keep the posts table size under control
3761
 */
3762
function grunion_delete_old_spam() {
3763
	global $wpdb;
3764
3765
	$grunion_delete_limit = 100;
3766
3767
	$now_gmt  = current_time( 'mysql', 1 );
3768
	$sql      = $wpdb->prepare(
3769
		"
3770
		SELECT `ID`
3771
		FROM $wpdb->posts
3772
		WHERE DATE_SUB( %s, INTERVAL 15 DAY ) > `post_date_gmt`
3773
			AND `post_type` = 'feedback'
3774
			AND `post_status` = 'spam'
3775
		LIMIT %d
3776
	", $now_gmt, $grunion_delete_limit
3777
	);
3778
	$post_ids = $wpdb->get_col( $sql );
3779
3780
	foreach ( (array) $post_ids as $post_id ) {
3781
		// force a full delete, skip the trash
3782
		wp_delete_post( $post_id, true );
3783
	}
3784
3785
	if (
3786
		/**
3787
		 * Filter if the module run OPTIMIZE TABLE on the core WP tables.
3788
		 *
3789
		 * @module contact-form
3790
		 *
3791
		 * @since 1.3.1
3792
		 * @since 6.4.0 Set to false by default.
3793
		 *
3794
		 * @param bool $filter Should Jetpack optimize the table, defaults to false.
3795
		 */
3796
		apply_filters( 'grunion_optimize_table', false )
3797
	) {
3798
		$wpdb->query( "OPTIMIZE TABLE $wpdb->posts" );
3799
	}
3800
3801
	// if we hit the max then schedule another run
3802
	if ( count( $post_ids ) >= $grunion_delete_limit ) {
3803
		wp_schedule_single_event( time() + 700, 'grunion_scheduled_delete' );
3804
	}
3805
}
3806
3807
/**
3808
 * Send an event to Tracks on form submission.
3809
 *
3810
 * @param int   $post_id - the post_id for the CPT that is created.
3811
 * @param array $all_values - fields from the default contact form.
3812
 * @param array $extra_values - extra fields added to from the contact form.
3813
 *
3814
 * @return null|void
3815
 */
3816
function jetpack_tracks_record_grunion_pre_message_sent( $post_id, $all_values, $extra_values ) {
3817
	// Do not do anything if the submission is not from a block.
3818
	if (
3819
		! isset( $extra_values['is_block'] )
3820
		|| ! $extra_values['is_block']
3821
	) {
3822
		return;
3823
	}
3824
3825
	/*
3826
	 * Event details.
3827
	 */
3828
	$event_user  = wp_get_current_user();
3829
	$event_name  = 'contact_form_block_message_sent';
3830
	$event_props = array(
3831
		'entry_permalink' => esc_url( $all_values['entry_permalink'] ),
3832
		'feedback_id'     => esc_attr( $all_values['feedback_id'] ),
3833
	);
3834
3835
	/*
3836
	 * Record event.
3837
	 * We use different libs on wpcom and Jetpack.
3838
	 */
3839
	if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
3840
		$event_name             = 'wpcom_' . $event_name;
3841
		$event_props['blog_id'] = get_current_blog_id();
3842
		// If the form was sent by a logged out visitor, record event with blog owner.
3843
		if ( empty( $event_user->ID ) ) {
3844
			$event_user_id = wpcom_get_blog_owner( $event_props['blog_id'] );
3845
			$event_user    = get_userdata( $event_user_id );
3846
		}
3847
3848
		jetpack_require_lib( 'tracks/client' );
3849
		tracks_record_event( $event_user, $event_name, $event_props );
3850
	} else {
3851
		// If the form was sent by a logged out visitor, record event with Jetpack master user.
3852
		if ( empty( $event_user->ID ) ) {
3853
			$master_user_id = Jetpack_Options::get_option( 'master_user' );
3854
			if ( ! empty( $master_user_id ) ) {
3855
				$event_user = get_userdata( $master_user_id );
3856
			}
3857
		}
3858
3859
		$tracking = new Automattic\Jetpack\Tracking();
3860
		$tracking->record_user_event( $event_name, $event_props, $event_user );
3861
	}
3862
}
3863
add_action( 'grunion_pre_message_sent', 'jetpack_tracks_record_grunion_pre_message_sent', 12, 3 );
3864