Completed
Push — try/jetpack-connection-no-user ( 368350...f1606c )
by
unknown
52:14 queued 43:52
created

Grunion_Contact_Form_Field::render_consent_field()   A

Complexity

Conditions 4
Paths 8

Size

Total Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

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

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

Let’s take a look at an example:

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

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

    // do something with $myArray
}

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

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

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

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

Let’s take a look at an example:

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

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

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

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

Available Fixes

  1. Check for existence of the variable explicitly:

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

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

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