Completed
Push — update/e2e-use-github-actions ( 91c2f8...ac39b2 )
by Yaroslav
271:44 queued 262:22
created

Grunion_Contact_Form::esc_shortcode_val()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
nc 1
nop 1
dl 0
loc 15
rs 9.7666
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 Jetpack
8
 */
9
10
use Automattic\Jetpack\Assets;
11
use Automattic\Jetpack\Blocks;
12
use Automattic\Jetpack\Sync\Settings;
13
14
define( 'GRUNION_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
15
define( 'GRUNION_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
16
17
if ( is_admin() ) {
18
	require_once GRUNION_PLUGIN_DIR . 'admin.php';
19
}
20
21
add_action( 'rest_api_init', 'grunion_contact_form_require_endpoint' );
22
function grunion_contact_form_require_endpoint() {
23
	require_once GRUNION_PLUGIN_DIR . 'class-grunion-contact-form-endpoint.php';
24
}
25
26
/**
27
 * Sets up various actions, filters, post types, post statuses, shortcodes.
28
 */
29
class Grunion_Contact_Form_Plugin {
30
31
	/**
32
	 * @var string The Widget ID of the widget currently being processed.  Used to build the unique contact-form ID for forms embedded in widgets.
33
	 */
34
	public $current_widget_id;
35
36
	static $using_contact_form_field = false;
37
38
	/**
39
	 * @var int The last Feedback Post ID Erased as part of the Personal Data Eraser.
40
	 * Helps with pagination.
41
	 */
42
	private $pde_last_post_id_erased = 0;
43
44
	/**
45
	 * @var string The email address for which we are deleting/exporting all feedbacks
46
	 * as part of a Personal Data Eraser or Personal Data Exporter request.
47
	 */
48
	private $pde_email_address = '';
49
50
	static function init() {
51
		static $instance = false;
52
53
		if ( ! $instance ) {
54
			$instance = new Grunion_Contact_Form_Plugin();
55
56
			// Schedule our daily cleanup
57
			add_action( 'wp_scheduled_delete', array( $instance, 'daily_akismet_meta_cleanup' ) );
58
		}
59
60
		return $instance;
61
	}
62
63
	/**
64
	 * Runs daily to clean up spam detection metadata after 15 days.  Keeps your DB squeaky clean.
65
	 */
66
	public function daily_akismet_meta_cleanup() {
67
		global $wpdb;
68
69
		$feedback_ids = $wpdb->get_col( "SELECT p.ID FROM {$wpdb->posts} as p INNER JOIN {$wpdb->postmeta} as m on m.post_id = p.ID WHERE p.post_type = 'feedback' AND m.meta_key = '_feedback_akismet_values' AND DATE_SUB(NOW(), INTERVAL 15 DAY) > p.post_date_gmt LIMIT 10000" );
70
71
		if ( empty( $feedback_ids ) ) {
72
			return;
73
		}
74
75
		/**
76
		 * Fires right before deleting the _feedback_akismet_values post meta on $feedback_ids
77
		 *
78
		 * @module contact-form
79
		 *
80
		 * @since 6.1.0
81
		 *
82
		 * @param array $feedback_ids list of feedback post ID
83
		 */
84
		do_action( 'jetpack_daily_akismet_meta_cleanup_before', $feedback_ids );
85
		foreach ( $feedback_ids as $feedback_id ) {
86
			delete_post_meta( $feedback_id, '_feedback_akismet_values' );
87
		}
88
89
		/**
90
		 * Fires right after deleting the _feedback_akismet_values post meta on $feedback_ids
91
		 *
92
		 * @module contact-form
93
		 *
94
		 * @since 6.1.0
95
		 *
96
		 * @param array $feedback_ids list of feedback post ID
97
		 */
98
		do_action( 'jetpack_daily_akismet_meta_cleanup_after', $feedback_ids );
99
	}
100
101
	/**
102
	 * Strips HTML tags from input.  Output is NOT HTML safe.
103
	 *
104
	 * @param mixed $data_with_tags
105
	 * @return mixed
106
	 */
107
	public static function strip_tags( $data_with_tags ) {
108
		if ( is_array( $data_with_tags ) ) {
109
			foreach ( $data_with_tags as $index => $value ) {
110
				$index = sanitize_text_field( (string) $index );
111
				$value = wp_kses( (string) $value, array() );
112
				$value = str_replace( '&amp;', '&', $value ); // undo damage done by wp_kses_normalize_entities()
113
114
				$data_without_tags[ $index ] = $value;
0 ignored issues
show
Coding Style Comprehensibility introduced by
$data_without_tags was never initialized. Although not strictly required by PHP, it is generally a good practice to add $data_without_tags = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
115
			}
116
		} else {
117
			$data_without_tags = wp_kses( $data_with_tags, array() );
118
			$data_without_tags = str_replace( '&amp;', '&', $data_without_tags ); // undo damage done by wp_kses_normalize_entities()
119
		}
120
121
		return $data_without_tags;
0 ignored issues
show
Bug introduced by
The variable $data_without_tags does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

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