Completed
Push — fix/7633-contact-form-radio-co... ( 3234f0 )
by
unknown
30:05 queued 21:30
created

Grunion_Contact_Form::esc_shortcode_val()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

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

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

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

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

Loading history...
2784
2785
		if ( ! $comment_author ) {
2786
			$comment_author = $comment_author_email;
2787
		}
2788
2789
		/**
2790
		 * Filter the email where a submitted feedback is sent.
2791
		 *
2792
		 * @module contact-form
2793
		 *
2794
		 * @since 1.3.1
2795
		 *
2796
		 * @param string|array $to Array of valid email addresses, or single email address.
2797
		 */
2798
		$to            = (array) apply_filters( 'contact_form_to', $to );
2799
		$reply_to_addr = $to[0]; // get just the address part before the name part is added
2800
2801
		foreach ( $to as $to_key => $to_value ) {
2802
			$to[ $to_key ] = Grunion_Contact_Form_Plugin::strip_tags( $to_value );
2803
			$to[ $to_key ] = self::add_name_to_address( $to_value );
2804
		}
2805
2806
		$blog_url        = wp_parse_url( site_url() );
2807
		$from_email_addr = 'wordpress@' . $blog_url['host'];
2808
2809
		if ( ! empty( $comment_author_email ) ) {
2810
			$reply_to_addr = $comment_author_email;
2811
		}
2812
2813
		$headers = 'From: "' . $comment_author . '" <' . $from_email_addr . ">\r\n" .
2814
		           'Reply-To: "' . $comment_author . '" <' . $reply_to_addr . ">\r\n";
2815
2816
		$all_values['email_marketing_consent'] = $email_marketing_consent;
2817
2818
		// Build feedback reference
2819
		$feedback_time  = current_time( 'mysql' );
2820
		$feedback_title = "{$comment_author} - {$feedback_time}";
2821
		$feedback_id    = md5( $feedback_title );
2822
2823
		$entry_values = array(
2824
			'entry_title'     => the_title_attribute( 'echo=0' ),
2825
			'entry_permalink' => esc_url( get_permalink( get_the_ID() ) ),
2826
			'feedback_id'     => $feedback_id,
2827
		);
2828
2829
		$all_values = array_merge( $all_values, $entry_values );
2830
2831
		/** This filter is already documented in modules/contact-form/admin.php */
2832
		$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...
2833
		$url     = $widget ? home_url( '/' ) : get_permalink( $post->ID );
2834
2835
		$date_time_format = _x( '%1$s \a\t %2$s', '{$date_format} \a\t {$time_format}', 'jetpack' );
2836
		$date_time_format = sprintf( $date_time_format, get_option( 'date_format' ), get_option( 'time_format' ) );
2837
		$time             = date_i18n( $date_time_format, current_time( 'timestamp' ) );
2838
2839
		// Keep a copy of the feedback as a custom post type.
2840
		if ( $in_comment_disallowed_list ) {
2841
			$feedback_status = 'trash';
2842
		} elseif ( $is_spam ) {
2843
			$feedback_status = 'spam';
2844
		} else {
2845
			$feedback_status = 'publish';
2846
		}
2847
2848
		foreach ( (array) $akismet_values as $av_key => $av_value ) {
2849
			$akismet_values[ $av_key ] = Grunion_Contact_Form_Plugin::strip_tags( $av_value );
2850
		}
2851
2852
		foreach ( (array) $all_values as $all_key => $all_value ) {
2853
			$all_values[ $all_key ] = Grunion_Contact_Form_Plugin::strip_tags( $all_value );
2854
		}
2855
2856
		foreach ( (array) $extra_values as $ev_key => $ev_value ) {
2857
			$extra_values[ $ev_key ] = Grunion_Contact_Form_Plugin::strip_tags( $ev_value );
2858
		}
2859
2860
		/*
2861
		 We need to make sure that the post author is always zero for contact
2862
		 * form submissions.  This prevents export/import from trying to create
2863
		 * new users based on form submissions from people who were logged in
2864
		 * at the time.
2865
		 *
2866
		 * Unfortunately wp_insert_post() tries very hard to make sure the post
2867
		 * author gets the currently logged in user id.  That is how we ended up
2868
		 * with this work around. */
2869
		add_filter( 'wp_insert_post_data', array( $plugin, 'insert_feedback_filter' ), 10, 2 );
2870
2871
		$post_id = wp_insert_post(
2872
			array(
2873
				'post_date'    => addslashes( $feedback_time ),
2874
				'post_type'    => 'feedback',
2875
				'post_status'  => addslashes( $feedback_status ),
2876
				'post_parent'  => (int) $post->ID,
2877
				'post_title'   => addslashes( wp_kses( $feedback_title, array() ) ),
2878
				'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
2879
				'post_name'    => $feedback_id,
2880
			)
2881
		);
2882
2883
		// once insert has finished we don't need this filter any more
2884
		remove_filter( 'wp_insert_post_data', array( $plugin, 'insert_feedback_filter' ), 10 );
2885
2886
		update_post_meta( $post_id, '_feedback_extra_fields', $this->addslashes_deep( $extra_values ) );
2887
2888
		if ( 'publish' == $feedback_status ) {
2889
			// Increase count of unread feedback.
2890
			$unread = get_option( 'feedback_unread_count', 0 ) + 1;
2891
			update_option( 'feedback_unread_count', $unread );
2892
		}
2893
2894
		if ( defined( 'AKISMET_VERSION' ) ) {
2895
			update_post_meta( $post_id, '_feedback_akismet_values', $this->addslashes_deep( $akismet_values ) );
2896
		}
2897
2898
		/**
2899
		 * Fires after the feedback post for the contact form submission has been inserted.
2900
		 *
2901
		 * @module contact-form
2902
		 *
2903
		 * @since 8.6.0
2904
		 *
2905
		 * @param integer $post_id The post id that contains the contact form data.
2906
		 * @param array   $this->fields An array containg the form's Grunion_Contact_Form_Field objects.
2907
		 * @param boolean $is_spam Whether the form submission has been identified as spam.
2908
		 * @param array   $entry_values The feedback entry values.
2909
		 */
2910
		do_action( 'grunion_after_feedback_post_inserted', $post_id, $this->fields, $is_spam, $entry_values );
2911
2912
		$message = self::get_compiled_form( $post_id, $this );
2913
2914
		array_push(
2915
			$message,
2916
			'<br />',
2917
			'<hr />',
2918
			__( 'Time:', 'jetpack' ) . ' ' . $time . '<br />',
2919
			__( 'IP Address:', 'jetpack' ) . ' ' . $comment_author_IP . '<br />',
2920
			__( 'Contact Form URL:', 'jetpack' ) . ' ' . $url . '<br />'
2921
		);
2922
2923
		if ( is_user_logged_in() ) {
2924
			array_push(
2925
				$message,
2926
				sprintf(
2927
					'<p>' . __( 'Sent by a verified %s user.', 'jetpack' ) . '</p>',
2928
					isset( $GLOBALS['current_site']->site_name ) && $GLOBALS['current_site']->site_name ?
2929
						$GLOBALS['current_site']->site_name : '"' . get_option( 'blogname' ) . '"'
2930
				)
2931
			);
2932
		} else {
2933
			array_push( $message, '<p>' . __( 'Sent by an unverified visitor to your site.', 'jetpack' ) . '</p>' );
2934
		}
2935
2936
		$message = join( '', $message );
2937
2938
		/**
2939
		 * Filters the message sent via email after a successful form submission.
2940
		 *
2941
		 * @module contact-form
2942
		 *
2943
		 * @since 1.3.1
2944
		 *
2945
		 * @param string $message Feedback email message.
2946
		 */
2947
		$message = apply_filters( 'contact_form_message', $message );
2948
2949
		// This is called after `contact_form_message`, in order to preserve back-compat
2950
		$message = self::wrap_message_in_html_tags( $message );
2951
2952
		update_post_meta( $post_id, '_feedback_email', $this->addslashes_deep( compact( 'to', 'message' ) ) );
2953
2954
		/**
2955
		 * Fires right before the contact form message is sent via email to
2956
		 * the recipient specified in the contact form.
2957
		 *
2958
		 * @module contact-form
2959
		 *
2960
		 * @since 1.3.1
2961
		 *
2962
		 * @param integer $post_id Post contact form lives on
2963
		 * @param array $all_values Contact form fields
2964
		 * @param array $extra_values Contact form fields not included in $all_values
2965
		 */
2966
		do_action( 'grunion_pre_message_sent', $post_id, $all_values, $extra_values );
2967
2968
		// schedule deletes of old spam feedbacks
2969
		if ( ! wp_next_scheduled( 'grunion_scheduled_delete' ) ) {
2970
			wp_schedule_event( time() + 250, 'daily', 'grunion_scheduled_delete' );
2971
		}
2972
2973
		if (
2974
			$is_spam !== true &&
2975
			/**
2976
			 * Filter to choose whether an email should be sent after each successful contact form submission.
2977
			 *
2978
			 * @module contact-form
2979
			 *
2980
			 * @since 2.6.0
2981
			 *
2982
			 * @param bool true Should an email be sent after a form submission. Default to true.
2983
			 * @param int $post_id Post ID.
2984
			 */
2985
			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...
2986
		) {
2987
			self::wp_mail( $to, "{$spam}{$subject}", $message, $headers );
2988
		} elseif (
2989
			true === $is_spam &&
2990
			/**
2991
			 * Choose whether an email should be sent for each spam contact form submission.
2992
			 *
2993
			 * @module contact-form
2994
			 *
2995
			 * @since 1.3.1
2996
			 *
2997
			 * @param bool false Should an email be sent after a spam form submission. Default to false.
2998
			 */
2999
			apply_filters( 'grunion_still_email_spam', false ) == true
3000
		) { // don't send spam by default.  Filterable.
3001
			self::wp_mail( $to, "{$spam}{$subject}", $message, $headers );
3002
		}
3003
3004
		/**
3005
		 * Fires an action hook right after the email(s) have been sent.
3006
		 *
3007
		 * @module contact-form
3008
		 *
3009
		 * @since 7.3.0
3010
		 *
3011
		 * @param int $post_id Post contact form lives on.
3012
		 * @param string|array $to Array of valid email addresses, or single email address.
3013
		 * @param string $subject Feedback email subject.
3014
		 * @param string $message Feedback email message.
3015
		 * @param string|array $headers Optional. Additional headers.
3016
		 * @param array $all_values Contact form fields.
3017
		 * @param array $extra_values Contact form fields not included in $all_values
3018
		 */
3019
		do_action( 'grunion_after_message_sent', $post_id, $to, $subject, $message, $headers, $all_values, $extra_values );
3020
3021
		if ( defined( 'DOING_AJAX' ) && DOING_AJAX ) {
3022
			return self::success_message( $post_id, $this );
3023
		}
3024
3025
		$redirect = '';
3026
		$custom_redirect = false;
3027
		if ( 'redirect' === $this->get_attribute( 'customThankyou' ) ) {
3028
			$custom_redirect = true;
3029
			$redirect        = esc_url( $this->get_attribute( 'customThankyouRedirect' ) );
3030
		}
3031
3032
		if ( ! $redirect ) {
3033
			$custom_redirect = false;
3034
			$redirect        = wp_get_referer();
3035
		}
3036
3037
		if ( ! $redirect ) { // wp_get_referer() returns false if the referer is the same as the current page.
3038
			$custom_redirect = false;
3039
			$redirect        = $_SERVER['REQUEST_URI'];
3040
		}
3041
3042
		if ( ! $custom_redirect ) {
3043
			$redirect = add_query_arg(
3044
				urlencode_deep(
3045
					array(
3046
						'contact-form-id'   => $id,
3047
						'contact-form-sent' => $post_id,
3048
						'contact-form-hash' => $this->hash,
3049
						'_wpnonce'          => wp_create_nonce( "contact-form-sent-{$post_id}" ), // wp_nonce_url HTMLencodes :( .
3050
					)
3051
				),
3052
				$redirect
3053
			);
3054
		}
3055
3056
		/**
3057
		 * Filter the URL where the reader is redirected after submitting a form.
3058
		 *
3059
		 * @module contact-form
3060
		 *
3061
		 * @since 1.9.0
3062
		 *
3063
		 * @param string $redirect Post submission URL.
3064
		 * @param int $id Contact Form ID.
3065
		 * @param int $post_id Post ID.
3066
		 */
3067
		$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...
3068
3069
		// phpcs:ignore WordPress.Security.SafeRedirect.wp_redirect_wp_redirect -- We intentially allow external redirects here.
3070
		wp_redirect( $redirect );
3071
		exit;
3072
	}
3073
3074
	/**
3075
	 * Wrapper for wp_mail() that enables HTML messages with text alternatives
3076
	 *
3077
	 * @param string|array $to          Array or comma-separated list of email addresses to send message.
3078
	 * @param string       $subject     Email subject.
3079
	 * @param string       $message     Message contents.
3080
	 * @param string|array $headers     Optional. Additional headers.
3081
	 * @param string|array $attachments Optional. Files to attach.
3082
	 *
3083
	 * @return bool Whether the email contents were sent successfully.
3084
	 */
3085
	public static function wp_mail( $to, $subject, $message, $headers = '', $attachments = array() ) {
3086
		add_filter( 'wp_mail_content_type', __CLASS__ . '::get_mail_content_type' );
3087
		add_action( 'phpmailer_init', __CLASS__ . '::add_plain_text_alternative' );
3088
3089
		$result = wp_mail( $to, $subject, $message, $headers, $attachments );
3090
3091
		remove_filter( 'wp_mail_content_type', __CLASS__ . '::get_mail_content_type' );
3092
		remove_action( 'phpmailer_init', __CLASS__ . '::add_plain_text_alternative' );
3093
3094
		return $result;
3095
	}
3096
3097
	/**
3098
	 * Add a display name part to an email address
3099
	 *
3100
	 * SpamAssassin doesn't like addresses in HTML messages that are missing display names (e.g., `[email protected]`
3101
	 * instead of `"Foo Bar" <[email protected]>`.
3102
	 *
3103
	 * @param string $address
3104
	 *
3105
	 * @return string
3106
	 */
3107
	function add_name_to_address( $address ) {
3108
		// If it's just the address, without a display name
3109
		if ( is_email( $address ) ) {
3110
			$address_parts = explode( '@', $address );
3111
			$address       = sprintf( '"%s" <%s>', $address_parts[0], $address );
3112
		}
3113
3114
		return $address;
3115
	}
3116
3117
	/**
3118
	 * Get the content type that should be assigned to outbound emails
3119
	 *
3120
	 * @return string
3121
	 */
3122
	static function get_mail_content_type() {
3123
		return 'text/html';
3124
	}
3125
3126
	/**
3127
	 * Wrap a message body with the appropriate in HTML tags
3128
	 *
3129
	 * This helps to ensure correct parsing by clients, and also helps avoid triggering spam filtering rules
3130
	 *
3131
	 * @param string $body
3132
	 *
3133
	 * @return string
3134
	 */
3135
	static function wrap_message_in_html_tags( $body ) {
3136
		// Don't do anything if the message was already wrapped in HTML tags
3137
		// That could have be done by a plugin via filters
3138
		if ( false !== strpos( $body, '<html' ) ) {
3139
			return $body;
3140
		}
3141
3142
		$html_message = sprintf(
3143
			// The tabs are just here so that the raw code is correctly formatted for developers
3144
			// They're removed so that they don't affect the final message sent to users
3145
			str_replace(
3146
				"\t", '',
3147
				'<!doctype html>
3148
				<html xmlns="http://www.w3.org/1999/xhtml">
3149
				<body>
3150
3151
				%s
3152
3153
				</body>
3154
				</html>'
3155
			),
3156
			$body
3157
		);
3158
3159
		return $html_message;
3160
	}
3161
3162
	/**
3163
	 * Add a plain-text alternative part to an outbound email
3164
	 *
3165
	 * This makes the message more accessible to mail clients that aren't HTML-aware, and decreases the likelihood
3166
	 * that the message will be flagged as spam.
3167
	 *
3168
	 * @param PHPMailer $phpmailer
3169
	 */
3170
	static function add_plain_text_alternative( $phpmailer ) {
3171
		// Add an extra break so that the extra space above the <p> is preserved after the <p> is stripped out
3172
		$alt_body = str_replace( '<p>', '<p><br />', $phpmailer->Body );
3173
3174
		// Convert <br> to \n breaks, to preserve the space between lines that we want to keep
3175
		$alt_body = str_replace( array( '<br>', '<br />' ), "\n", $alt_body );
3176
3177
		// Convert <hr> to an plain-text equivalent, to preserve the integrity of the message
3178
		$alt_body = str_replace( array( '<hr>', '<hr />' ), "----\n", $alt_body );
3179
3180
		// Trim the plain text message to remove the \n breaks that were after <doctype>, <html>, and <body>
3181
		$phpmailer->AltBody = trim( strip_tags( $alt_body ) );
3182
	}
3183
3184
	function addslashes_deep( $value ) {
3185
		if ( is_array( $value ) ) {
3186
			return array_map( array( $this, 'addslashes_deep' ), $value );
3187
		} elseif ( is_object( $value ) ) {
3188
			$vars = get_object_vars( $value );
3189
			foreach ( $vars as $key => $data ) {
3190
				$value->{$key} = $this->addslashes_deep( $data );
3191
			}
3192
			return $value;
3193
		}
3194
3195
		return addslashes( $value );
3196
	}
3197
3198
} // end class Grunion_Contact_Form
3199
3200
/**
3201
 * Class for the contact-field shortcode.
3202
 * Parses shortcode to output the contact form field as HTML.
3203
 * Validates input.
3204
 */
3205
class Grunion_Contact_Form_Field extends Crunion_Contact_Form_Shortcode {
3206
	public $shortcode_name = 'contact-field';
3207
3208
	/**
3209
	 * @var Grunion_Contact_Form parent form
3210
	 */
3211
	public $form;
3212
3213
	/**
3214
	 * @var string default or POSTed value
3215
	 */
3216
	public $value;
3217
3218
	/**
3219
	 * @var bool Is the input invalid?
3220
	 */
3221
	public $error = false;
3222
3223
	/**
3224
	 * @param array                $attributes An associative array of shortcode attributes.  @see shortcode_atts()
3225
	 * @param null|string          $content Null for selfclosing shortcodes.  The inner content otherwise.
3226
	 * @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...
3227
	 */
3228
	function __construct( $attributes, $content = null, $form = null ) {
3229
		$attributes = shortcode_atts(
3230
			array(
3231
				'label'                  => null,
3232
				'type'                   => 'text',
3233
				'required'               => false,
3234
				'options'                => array(),
3235
				'id'                     => null,
3236
				'default'                => null,
3237
				'values'                 => null,
3238
				'placeholder'            => null,
3239
				'class'                  => null,
3240
				'width'                  => null,
3241
				'consenttype'            => null,
3242
				'implicitconsentmessage' => null,
3243
				'explicitconsentmessage' => null,
3244
			), $attributes, 'contact-field'
3245
		);
3246
3247
		// special default for subject field
3248
		if ( 'subject' == $attributes['type'] && is_null( $attributes['default'] ) && ! is_null( $form ) ) {
3249
			$attributes['default'] = $form->get_attribute( 'subject' );
3250
		}
3251
3252
		// allow required=1 or required=true
3253
		if ( '1' == $attributes['required'] || 'true' == strtolower( $attributes['required'] ) ) {
3254
			$attributes['required'] = true;
3255
		} else {
3256
			$attributes['required'] = false;
3257
		}
3258
3259
		// parse out comma-separated options list (for selects, radios, and checkbox-multiples)
3260
		if ( ! empty( $attributes['options'] ) && is_string( $attributes['options'] ) ) {
3261
			$attributes['options'] = array_map( 'trim', explode( ',', $attributes['options'] ) );
3262
3263 View Code Duplication
			if ( ! empty( $attributes['values'] ) && is_string( $attributes['values'] ) ) {
3264
				$attributes['values'] = array_map( 'trim', explode( ',', $attributes['values'] ) );
3265
			}
3266
		}
3267
3268
		if ( $form ) {
3269
			// make a unique field ID based on the label, with an incrementing number if needed to avoid clashes
3270
			$form_id = $form->get_attribute( 'id' );
3271
			$id      = isset( $attributes['id'] ) ? $attributes['id'] : false;
3272
3273
			$unescaped_label = $this->unesc_attr( $attributes['label'] );
3274
			$unescaped_label = str_replace( '%', '-', $unescaped_label ); // jQuery doesn't like % in IDs?
3275
			$unescaped_label = preg_replace( '/[^a-zA-Z0-9.-_:]/', '', $unescaped_label );
3276
3277
			if ( empty( $id ) ) {
3278
				$id        = sanitize_title_with_dashes( 'g' . $form_id . '-' . $unescaped_label );
3279
				$i         = 0;
3280
				$max_tries = 99;
3281
				while ( isset( $form->fields[ $id ] ) ) {
3282
					$i++;
3283
					$id = sanitize_title_with_dashes( 'g' . $form_id . '-' . $unescaped_label . '-' . $i );
3284
3285
					if ( $i > $max_tries ) {
3286
						break;
3287
					}
3288
				}
3289
			}
3290
3291
			$attributes['id'] = $id;
3292
		}
3293
3294
		parent::__construct( $attributes, $content );
3295
3296
		// Store parent form
3297
		$this->form = $form;
3298
	}
3299
3300
	/**
3301
	 * This field's input is invalid.  Flag as invalid and add an error to the parent form
3302
	 *
3303
	 * @param string $message The error message to display on the form.
3304
	 */
3305
	function add_error( $message ) {
3306
		$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...
3307
3308
		if ( ! is_wp_error( $this->form->errors ) ) {
3309
			$this->form->errors = new WP_Error;
3310
		}
3311
3312
		$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...
3313
	}
3314
3315
	/**
3316
	 * Is the field input invalid?
3317
	 *
3318
	 * @see $error
3319
	 *
3320
	 * @return bool
3321
	 */
3322
	function is_error() {
3323
		return $this->error;
3324
	}
3325
3326
	/**
3327
	 * Validates the form input
3328
	 */
3329
	function validate() {
3330
		// If it's not required, there's nothing to validate
3331
		if ( ! $this->get_attribute( 'required' ) ) {
3332
			return;
3333
		}
3334
3335
		$field_id    = $this->get_attribute( 'id' );
3336
		$field_type  = $this->get_attribute( 'type' );
3337
		$field_label = $this->get_attribute( 'label' );
3338
3339
		if ( isset( $_POST[ $field_id ] ) ) {
3340
			if ( is_array( $_POST[ $field_id ] ) ) {
3341
				$field_value = array_map( 'stripslashes', $_POST[ $field_id ] );
3342
			} else {
3343
				$field_value = stripslashes( $_POST[ $field_id ] );
3344
			}
3345
		} else {
3346
			$field_value = '';
3347
		}
3348
3349
		switch ( $field_type ) {
3350 View Code Duplication
			case 'email':
3351
				// Make sure the email address is valid
3352
				if ( ! is_string( $field_value ) || ! is_email( $field_value ) ) {
3353
					/* translators: %s is the name of a form field */
3354
					$this->add_error( sprintf( __( '%s requires a valid email address', 'jetpack' ), $field_label ) );
3355
				}
3356
				break;
3357
			case 'checkbox-multiple':
3358
				// Check that there is at least one option selected
3359
				if ( empty( $field_value ) ) {
3360
					/* translators: %s is the name of a form field */
3361
					$this->add_error( sprintf( __( '%s requires at least one selection', 'jetpack' ), $field_label ) );
3362
				}
3363
				break;
3364 View Code Duplication
			default:
3365
				// Just check for presence of any text
3366
				if ( ! is_string( $field_value ) || ! strlen( trim( $field_value ) ) ) {
3367
					/* translators: %s is the name of a form field */
3368
					$this->add_error( sprintf( __( '%s is required', 'jetpack' ), $field_label ) );
3369
				}
3370
		}
3371
	}
3372
3373
3374
	/**
3375
	 * Check the default value for options field
3376
	 *
3377
	 * @param string value
3378
	 * @param int index
3379
	 * @param string default value
3380
	 *
3381
	 * @return string
3382
	 */
3383
	public function get_option_value( $value, $index, $options ) {
3384
		if ( empty( $value[ $index ] ) ) {
3385
			return $options;
3386
		}
3387
		return $value[ $index ];
3388
	}
3389
3390
	/**
3391
	 * Outputs the HTML for this form field
3392
	 *
3393
	 * @return string HTML
3394
	 */
3395
	function render() {
3396
		global $current_user, $user_identity;
3397
3398
		$field_id          = $this->get_attribute( 'id' );
3399
		$field_type        = $this->get_attribute( 'type' );
3400
		$field_label       = $this->get_attribute( 'label' );
3401
		$field_required    = $this->get_attribute( 'required' );
3402
		$field_placeholder = $this->get_attribute( 'placeholder' );
3403
		$field_width       = $this->get_attribute( 'width' );
3404
		$class             = 'date' === $field_type ? 'jp-contact-form-date' : $this->get_attribute( 'class' );
3405
3406
		if ( ! empty( $field_width ) ) {
3407
			$class .= ' grunion-field-width-' . $field_width;
3408
		}
3409
3410
		/**
3411
		 * Filters the "class" attribute of the contact form input
3412
		 *
3413
		 * @module contact-form
3414
		 *
3415
		 * @since 6.6.0
3416
		 *
3417
		 * @param string $class Additional CSS classes for input class attribute.
3418
		 */
3419
		$field_class = apply_filters( 'jetpack_contact_form_input_class', $class );
3420
3421
		if ( isset( $_POST[ $field_id ] ) ) {
3422
			if ( is_array( $_POST[ $field_id ] ) ) {
3423
				$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...
3424
			} else {
3425
				$this->value = stripslashes( (string) $_POST[ $field_id ] );
3426
			}
3427
		} elseif ( isset( $_GET[ $field_id ] ) ) {
3428
			$this->value = stripslashes( (string) $_GET[ $field_id ] );
3429
		} elseif (
3430
			is_user_logged_in() &&
3431
			( ( defined( 'IS_WPCOM' ) && IS_WPCOM ) ||
3432
			  /**
3433
			   * Allow third-party tools to prefill the contact form with the user's details when they're logged in.
3434
			   *
3435
			   * @module contact-form
3436
			   *
3437
			   * @since 3.2.0
3438
			   *
3439
			   * @param bool false Should the Contact Form be prefilled with your details when you're logged in. Default to false.
3440
			   */
3441
			  true === apply_filters( 'jetpack_auto_fill_logged_in_user', false )
3442
			)
3443
		) {
3444
			// Special defaults for logged-in users
3445
			switch ( $this->get_attribute( 'type' ) ) {
3446
				case 'email':
3447
					$this->value = $current_user->data->user_email;
3448
					break;
3449
				case 'name':
3450
					$this->value = $user_identity;
3451
					break;
3452
				case 'url':
3453
					$this->value = $current_user->data->user_url;
3454
					break;
3455
				default:
3456
					$this->value = $this->get_attribute( 'default' );
3457
			}
3458
		} else {
3459
			$this->value = $this->get_attribute( 'default' );
3460
		}
3461
3462
		$field_value = Grunion_Contact_Form_Plugin::strip_tags( $this->value );
3463
		$field_label = Grunion_Contact_Form_Plugin::strip_tags( $field_label );
3464
3465
		$rendered_field = $this->render_field( $field_type, $field_id, $field_label, $field_value, $field_class, $field_placeholder, $field_required );
3466
3467
		/**
3468
		 * Filter the HTML of the Contact Form.
3469
		 *
3470
		 * @module contact-form
3471
		 *
3472
		 * @since 2.6.0
3473
		 *
3474
		 * @param string $rendered_field Contact Form HTML output.
3475
		 * @param string $field_label Field label.
3476
		 * @param int|null $id Post ID.
3477
		 */
3478
		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...
3479
	}
3480
3481
	function render_label( $type = '', $id, $label, $required, $required_field_text ) {
3482
3483
		$type_class = $type ? ' ' .$type : '';
3484
		return
3485
			"<label
3486
				for='" . esc_attr( $id ) . "'
3487
				class='grunion-field-label{$type_class}" . ( $this->is_error() ? ' form-error' : '' ) . "'
3488
				>"
3489
				. esc_html( $label )
3490
				. ( $required ? '<span>' . $required_field_text . '</span>' : '' )
3491
			. "</label>\n";
3492
3493
	}
3494
3495
	function render_input_field( $type, $id, $value, $class, $placeholder, $required ) {
3496
		return "<input
3497
					type='". esc_attr( $type ) ."'
3498
					name='" . esc_attr( $id ) . "'
3499
					id='" . esc_attr( $id ) . "'
3500
					value='" . esc_attr( $value ) . "'
3501
					" . $class . $placeholder . '
3502
					' . ( $required ? "required aria-required='true'" : '' ) . "
3503
				/>\n";
3504
	}
3505
3506
	function render_email_field( $id, $label, $value, $class, $required, $required_field_text, $placeholder ) {
3507
		$field = $this->render_label( 'email', $id, $label, $required, $required_field_text );
3508
		$field .= $this->render_input_field( 'email', $id, $value, $class, $placeholder, $required );
3509
		return $field;
3510
	}
3511
3512
	function render_telephone_field( $id, $label, $value, $class, $required, $required_field_text, $placeholder ) {
3513
		$field = $this->render_label( 'telephone', $id, $label, $required, $required_field_text );
3514
		$field .= $this->render_input_field( 'tel', $id, $value, $class, $placeholder, $required );
3515
		return $field;
3516
	}
3517
3518
	function render_url_field( $id, $label, $value, $class, $required, $required_field_text, $placeholder ) {
3519
		$field = $this->render_label( 'url', $id, $label, $required, $required_field_text );
3520
		$field .= $this->render_input_field( 'url', $id, $value, $class, $placeholder, $required );
3521
		return $field;
3522
	}
3523
3524
	function render_textarea_field( $id, $label, $value, $class, $required, $required_field_text, $placeholder ) {
3525
		$field = $this->render_label( 'textarea', 'contact-form-comment-' . $id, $label, $required, $required_field_text );
3526
		$field .= "<textarea
3527
		                name='" . esc_attr( $id ) . "'
3528
		                id='contact-form-comment-" . esc_attr( $id ) . "'
3529
		                rows='20' "
3530
		                . $class
3531
		                . $placeholder
3532
		                . ' ' . ( $required ? "required aria-required='true'" : '' ) .
3533
		                '>' . esc_textarea( $value )
3534
		          . "</textarea>\n";
3535
		return $field;
3536
	}
3537
3538
	function render_radio_field( $id, $label, $value, $class, $required, $required_field_text ) {
3539
		$field = $this->render_label( '', $id, $label, $required, $required_field_text );
3540
		foreach ( (array) $this->get_attribute( 'options' ) as $optionIndex => $option ) {
3541
			$option = Grunion_Contact_Form_Plugin::strip_tags( $option );
3542
			if ( $option ) {
3543
				$field .= "\t\t<label class='grunion-radio-label radio" . ( $this->is_error() ? ' form-error' : '' ) . "'>";
3544
				$field .= "<input
3545
									type='radio'
3546
									name='" . esc_attr( $id ) . "'
3547
									value='" . esc_attr( $this->get_option_value( $this->get_attribute( 'values' ), $optionIndex, $option ) ) . "' "
3548
				                    . $class
3549
				                    . checked( $option, $value, false ) . ' '
3550
				                    . ( $required ? "required aria-required='true'" : '' )
3551
				              . '/> ';
3552
				$field .= esc_html( $option ) . "</label>\n";
3553
				$field .= "\t\t<div class='clear-form'></div>\n";
3554
			}
3555
		}
3556
		return $field;
3557
	}
3558
3559
	function render_checkbox_field( $id, $label, $value, $class, $required, $required_field_text ) {
3560
		$field = "<label class='grunion-field-label checkbox" . ( $this->is_error() ? ' form-error' : '' ) . "'>";
3561
			$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";
3562
			$field .= "\t\t" . esc_html( $label ) . ( $required ? '<span>' . $required_field_text . '</span>' : '' );
3563
		$field .=  "</label>\n";
3564
		$field .= "<div class='clear-form'></div>\n";
3565
		return $field;
3566
	}
3567
3568
	/**
3569
	 * Render the consent field.
3570
	 *
3571
	 * @param string $id field id.
3572
	 * @param string $class html classes (can be set by the admin).
3573
	 */
3574
	private function render_consent_field( $id, $class ) {
3575
		$consent_type    = 'explicit' === $this->get_attribute( 'consenttype' ) ? 'explicit' : 'implicit';
3576
		$consent_message = 'explicit' === $consent_type ? $this->get_attribute( 'explicitconsentmessage' ) : $this->get_attribute( 'implicitconsentmessage' );
3577
3578
		$field  = "<label class='grunion-field-label consent consent-" . $consent_type . "'>";
3579
3580
		if ( 'implicit' === $consent_type ) {
3581
			$field .= "\t\t<input aria-hidden='true' type='checkbox' checked name='" . esc_attr( $id ) . "' value='" . esc_attr__( 'Yes', 'jetpack' ) . "' style='display:none;' /> \n";
3582
		} else {
3583
			$field .= "\t\t<input type='checkbox' name='" . esc_attr( $id ) . "' value='" . esc_attr__( 'Yes', 'jetpack' ) . "' " . $class . "/> \n";
3584
		}
3585
		$field .= "\t\t" . esc_html( $consent_message );
3586
		$field .= "</label>\n";
3587
		$field .= "<div class='clear-form'></div>\n";
3588
		return $field;
3589
	}
3590
3591
	function render_checkbox_multiple_field( $id, $label, $value, $class, $required, $required_field_text  ) {
3592
		$field = $this->render_label( '', $id, $label, $required, $required_field_text );
3593
		foreach ( (array) $this->get_attribute( 'options' ) as $optionIndex => $option ) {
3594
			$option = Grunion_Contact_Form_Plugin::strip_tags( $option );
3595
			if ( $option  ) {
3596
				$field .= "\t\t<label class='grunion-checkbox-multiple-label checkbox-multiple" . ( $this->is_error() ? ' form-error' : '' ) . "'>";
3597
				$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 ) . ' /> ';
3598
				$field .= esc_html( $option ) . "</label>\n";
3599
				$field .= "\t\t<div class='clear-form'></div>\n";
3600
			}
3601
		}
3602
3603
		return $field;
3604
	}
3605
3606
	function render_select_field( $id, $label, $value, $class, $required, $required_field_text ) {
3607
		$field = $this->render_label( 'select', $id, $label, $required, $required_field_text );
3608
		$field  .= "\t<select name='" . esc_attr( $id ) . "' id='" . esc_attr( $id ) . "' " . $class . ( $required ? "required aria-required='true'" : '' ) . ">\n";
3609
		foreach ( (array) $this->get_attribute( 'options' ) as $optionIndex => $option ) {
3610
			$option = Grunion_Contact_Form_Plugin::strip_tags( $option );
3611
			if ( $option ) {
3612
				$field .= "\t\t<option"
3613
				               . selected( $option, $value, false )
3614
				               . " value='" . esc_attr( $this->get_option_value( $this->get_attribute( 'values' ), $optionIndex, $option ) )
3615
				               . "'>" . esc_html( $option )
3616
				          . "</option>\n";
3617
			}
3618
		}
3619
		$field  .= "\t</select>\n";
3620
		return $field;
3621
	}
3622
3623
	function render_date_field( $id, $label, $value, $class, $required, $required_field_text, $placeholder ) {
3624
3625
		$field = $this->render_label( 'date', $id, $label, $required, $required_field_text );
3626
		$field .= $this->render_input_field( 'text', $id, $value, $class, $placeholder, $required );
3627
3628
		/* For AMP requests, use amp-date-picker element: https://amp.dev/documentation/components/amp-date-picker */
3629
		if ( class_exists( 'Jetpack_AMP_Support' ) && Jetpack_AMP_Support::is_amp_request() ) {
3630
			return sprintf(
3631
				'<%1$s mode="overlay" layout="container" type="single" input-selector="[name=%2$s]">%3$s</%1$s>',
3632
				'amp-date-picker',
3633
				esc_attr( $id ),
3634
				$field
3635
			);
3636
		}
3637
3638
		wp_enqueue_script(
3639
			'grunion-frontend',
3640
			Assets::get_file_url_for_environment(
3641
				'_inc/build/contact-form/js/grunion-frontend.min.js',
3642
				'modules/contact-form/js/grunion-frontend.js'
3643
			),
3644
			array( 'jquery', 'jquery-ui-datepicker' )
3645
		);
3646
		wp_enqueue_style( 'jp-jquery-ui-datepicker', plugins_url( 'css/jquery-ui-datepicker.css', __FILE__ ), array( 'dashicons' ), '1.0' );
3647
3648
		// Using Core's built-in datepicker localization routine
3649
		wp_localize_jquery_ui_datepicker();
3650
		return $field;
3651
	}
3652
3653
	function render_default_field( $id, $label, $value, $class, $required, $required_field_text, $placeholder, $type ) {
3654
		$field = $this->render_label( $type, $id, $label, $required, $required_field_text );
3655
		$field .= $this->render_input_field( 'text', $id, $value, $class, $placeholder, $required );
3656
		return $field;
3657
	}
3658
3659
	function render_field( $type, $id, $label, $value, $class, $placeholder, $required ) {
3660
3661
		$field_placeholder = ( ! empty( $placeholder ) ) ? "placeholder='" . esc_attr( $placeholder ) . "'" : '';
3662
		$field_class       = "class='" . trim( esc_attr( $type ) . ' ' . esc_attr( $class ) ) . "' ";
3663
		$wrap_classes = empty( $class ) ? '' : implode( '-wrap ', array_filter( explode( ' ', $class ) ) ) . '-wrap'; // this adds
3664
3665
		$shell_field_class = "class='grunion-field-wrap grunion-field-" . trim( esc_attr( $type ) . '-wrap ' . esc_attr( $wrap_classes ) ) . "' ";
3666
		/**
3667
		/**
3668
		 * Filter the Contact Form required field text
3669
		 *
3670
		 * @module contact-form
3671
		 *
3672
		 * @since 3.8.0
3673
		 *
3674
		 * @param string $var Required field text. Default is "(required)".
3675
		 */
3676
		$required_field_text = esc_html( apply_filters( 'jetpack_required_field_text', __( '(required)', 'jetpack' ) ) );
3677
3678
		$field = "\n<div {$shell_field_class} >\n"; // new in Jetpack 6.8.0
3679
		// If they are logged in, and this is their site, don't pre-populate fields
3680
		if ( current_user_can( 'manage_options' ) ) {
3681
			$value = '';
3682
		}
3683
		switch ( $type ) {
3684
			case 'email':
3685
				$field .= $this->render_email_field( $id, $label, $value, $field_class, $required, $required_field_text, $field_placeholder );
3686
				break;
3687
			case 'telephone':
3688
				$field .= $this->render_telephone_field( $id, $label, $value, $field_class, $required, $required_field_text, $field_placeholder );
3689
				break;
3690
			case 'url':
3691
				$field .= $this->render_url_field( $id, $label, $value, $field_class, $required, $required_field_text, $field_placeholder );
3692
				break;
3693
			case 'textarea':
3694
				$field .= $this->render_textarea_field( $id, $label, $value, $field_class, $required, $required_field_text, $field_placeholder );
3695
				break;
3696
			case 'radio':
3697
				$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...
3698
				break;
3699
			case 'checkbox':
3700
				$field .= $this->render_checkbox_field( $id, $label, $value, $field_class, $required, $required_field_text );
3701
				break;
3702
			case 'checkbox-multiple':
3703
				$field .= $this->render_checkbox_multiple_field( $id, $label, $value, $field_class, $required, $required_field_text );
3704
				break;
3705
			case 'select':
3706
				$field .= $this->render_select_field( $id, $label, $value, $field_class, $required, $required_field_text );
3707
				break;
3708
			case 'date':
3709
				$field .= $this->render_date_field( $id, $label, $value, $field_class, $required, $required_field_text, $field_placeholder );
3710
				break;
3711
			case 'consent':
3712
				$field .= $this->render_consent_field( $id, $field_class );
3713
				break;
3714
			default: // text field
3715
				$field .= $this->render_default_field( $id, $label, $value, $field_class, $required, $required_field_text, $field_placeholder, $type );
3716
				break;
3717
		}
3718
		$field .= "\t</div>\n";
3719
		return $field;
3720
	}
3721
}
3722
3723
add_action( 'init', array( 'Grunion_Contact_Form_Plugin', 'init' ), 9 );
3724
3725
add_action( 'grunion_scheduled_delete', 'grunion_delete_old_spam' );
3726
3727
/**
3728
 * Deletes old spam feedbacks to keep the posts table size under control
3729
 */
3730
function grunion_delete_old_spam() {
3731
	global $wpdb;
3732
3733
	$grunion_delete_limit = 100;
3734
3735
	$now_gmt  = current_time( 'mysql', 1 );
3736
	$sql      = $wpdb->prepare(
3737
		"
3738
		SELECT `ID`
3739
		FROM $wpdb->posts
3740
		WHERE DATE_SUB( %s, INTERVAL 15 DAY ) > `post_date_gmt`
3741
			AND `post_type` = 'feedback'
3742
			AND `post_status` = 'spam'
3743
		LIMIT %d
3744
	", $now_gmt, $grunion_delete_limit
3745
	);
3746
	$post_ids = $wpdb->get_col( $sql );
3747
3748
	foreach ( (array) $post_ids as $post_id ) {
3749
		// force a full delete, skip the trash
3750
		wp_delete_post( $post_id, true );
3751
	}
3752
3753
	if (
3754
		/**
3755
		 * Filter if the module run OPTIMIZE TABLE on the core WP tables.
3756
		 *
3757
		 * @module contact-form
3758
		 *
3759
		 * @since 1.3.1
3760
		 * @since 6.4.0 Set to false by default.
3761
		 *
3762
		 * @param bool $filter Should Jetpack optimize the table, defaults to false.
3763
		 */
3764
		apply_filters( 'grunion_optimize_table', false )
3765
	) {
3766
		$wpdb->query( "OPTIMIZE TABLE $wpdb->posts" );
3767
	}
3768
3769
	// if we hit the max then schedule another run
3770
	if ( count( $post_ids ) >= $grunion_delete_limit ) {
3771
		wp_schedule_single_event( time() + 700, 'grunion_scheduled_delete' );
3772
	}
3773
}
3774
3775
/**
3776
 * Send an event to Tracks on form submission.
3777
 *
3778
 * @param int   $post_id - the post_id for the CPT that is created.
3779
 * @param array $all_values - fields from the default contact form.
3780
 * @param array $extra_values - extra fields added to from the contact form.
3781
 *
3782
 * @return null|void
3783
 */
3784
function jetpack_tracks_record_grunion_pre_message_sent( $post_id, $all_values, $extra_values ) {
3785
	// Do not do anything if the submission is not from a block.
3786
	if (
3787
		! isset( $extra_values['is_block'] )
3788
		|| ! $extra_values['is_block']
3789
	) {
3790
		return;
3791
	}
3792
3793
	/*
3794
	 * Event details.
3795
	 */
3796
	$event_user  = wp_get_current_user();
3797
	$event_name  = 'contact_form_block_message_sent';
3798
	$event_props = array(
3799
		'entry_permalink' => esc_url( $all_values['entry_permalink'] ),
3800
		'feedback_id'     => esc_attr( $all_values['feedback_id'] ),
3801
	);
3802
3803
	/*
3804
	 * Record event.
3805
	 * We use different libs on wpcom and Jetpack.
3806
	 */
3807
	if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
3808
		$event_name             = 'wpcom_' . $event_name;
3809
		$event_props['blog_id'] = get_current_blog_id();
3810
		// If the form was sent by a logged out visitor, record event with blog owner.
3811
		if ( empty( $event_user->ID ) ) {
3812
			$event_user_id = wpcom_get_blog_owner( $event_props['blog_id'] );
3813
			$event_user    = get_userdata( $event_user_id );
3814
		}
3815
3816
		jetpack_require_lib( 'tracks/client' );
3817
		tracks_record_event( $event_user, $event_name, $event_props );
3818
	} else {
3819
		// If the form was sent by a logged out visitor, record event with Jetpack master user.
3820
		if ( empty( $event_user->ID ) ) {
3821
			$master_user_id = Jetpack_Options::get_option( 'master_user' );
3822
			if ( ! empty( $master_user_id ) ) {
3823
				$event_user = get_userdata( $master_user_id );
3824
			}
3825
		}
3826
3827
		$tracking = new Automattic\Jetpack\Tracking();
3828
		$tracking->record_user_event( $event_name, $event_props, $event_user );
3829
	}
3830
}
3831
add_action( 'grunion_pre_message_sent', 'jetpack_tracks_record_grunion_pre_message_sent', 12, 3 );
3832