Grunion_Contact_Form_Plugin   F
last analyzed

Complexity

Total Complexity 204

Size/Duplication

Total Lines 1697
Duplicated Lines 0 %

Coupling/Cohesion

Components 4
Dependencies 3

Importance

Changes 0
Metric Value
dl 0
loc 1697
rs 0.8
c 0
b 0
f 0
wmc 204
lcom 4
cbo 3

58 Methods

Rating   Name   Duplication   Size   Complexity  
A init() 0 12 2
A daily_akismet_meta_cleanup() 0 34 3
A strip_tags() 0 16 3
C __construct() 0 119 11
B register_contact_form_blocks() 0 94 1
A gutenblock_render_form() 0 14 3
A block_attributes_to_shortcode_attributes() 0 14 3
A gutenblock_render_field_text() 0 4 1
A gutenblock_render_field_name() 0 4 1
A gutenblock_render_field_email() 0 4 1
A gutenblock_render_field_url() 0 4 1
A gutenblock_render_field_date() 0 4 1
A gutenblock_render_field_telephone() 0 4 1
A gutenblock_render_field_textarea() 0 4 1
A gutenblock_render_field_checkbox() 0 4 1
A gutenblock_render_field_checkbox_multiple() 0 4 1
A gutenblock_render_field_radio() 0 4 1
A gutenblock_render_field_select() 0 4 1
A gutenblock_render_field_consent() 0 13 3
A admin_menu() 0 28 1
A allow_feedback_rest_api_type() 0 4 1
B unread_count() 0 21 10
F process_form_submission() 0 95 22
A ajax_request() 0 19 3
A insert_feedback_filter() 0 7 3
A add_shortcode() 0 4 1
A tokenize_label() 0 3 1
A sanitize_value() 0 3 1
A replace_tokens_with_input() 0 16 3
A track_current_widget() 0 3 1
A widget_atts() 0 5 1
A widget_shortcode_hack() 0 17 2
A is_spam_blocklist() 0 7 2
A is_in_disallowed_list() 0 20 3
B prepare_for_akismet() 0 26 8
C is_spam_akismet() 0 48 12
A akismet_submit() 0 19 4
B export_form() 0 48 5
A get_post_content_for_csv_export() 0 6 1
A get_post_meta_for_csv_export() 0 18 4
A get_parsed_field_contents_of_post() 0 3 1
A map_parsed_field_contents_of_post_to_field_names() 0 24 4
A register_personal_data_exporter() 0 8 1
A register_personal_data_eraser() 0 8 1
A personal_data_exporter() 0 3 1
B _internal_personal_data_exporter() 0 45 8
A personal_data_eraser() 0 3 1
B _internal_personal_data_eraser() 0 63 8
A personal_data_post_ids_by_email() 0 29 2
A personal_data_search_filter() 0 25 4
C get_export_data_for_posts() 0 106 11
C download_feedback_as_csv() 0 93 10
A esc_csv() 0 9 2
A get_feedbacks_as_options() 0 32 2
A get_field_names() 0 16 3
B parse_fields_from_content() 0 55 8
B make_csv_row_from_feedback() 0 30 6
A get_ip_address() 0 3 2

How to fix   Complexity   

Complex Class

Complex classes like Grunion_Contact_Form_Plugin often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Grunion_Contact_Form_Plugin, and based on these observations, apply Extract Interface, too.

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

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

Let’s take a look at an example:

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

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

    // do something with $myArray
}

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

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

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

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

Let’s take a look at an example:

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

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

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

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

Available Fixes

  1. Check for existence of the variable explicitly:

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

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

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