Completed
Push — update/feedback_menu_name ( 52d569...fe5981 )
by
unknown
38:34 queued 26:22
created

Grunion_Contact_Form_Plugin::admin_menu()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

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