Completed
Push — try/contact-form-attachment ( 0d324c )
by
unknown
52:39 queued 20:06
created

Grunion_Contact_Form_Field::render()   C

Complexity

Conditions 13
Paths 16

Size

Total Lines 80

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 13
nc 16
nop 0
dl 0
loc 80
rs 5.7295
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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

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

Let’s take a look at an example:

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

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

    // do something with $myArray
}

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

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

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

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

Let’s take a look at an example:

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

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

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

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

Available Fixes

  1. Check for existence of the variable explicitly:

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

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

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

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...
3510
					break;
3511
			default: // text field
3512
				$field .= $this->render_default_field( $id, $label, $value, $field_class, $required, $required_field_text, $field_placeholder, $type );
3513
				break;
3514
		}
3515
		$field .= "\t</div>\n";
3516
		return $field;
3517
	}
3518
}
3519
3520
add_action( 'init', array( 'Grunion_Contact_Form_Plugin', 'init' ), 9 );
3521
3522
add_action( 'grunion_scheduled_delete', 'grunion_delete_old_spam' );
3523
3524
/**
3525
 * Deletes old spam feedbacks to keep the posts table size under control
3526
 */
3527
function grunion_delete_old_spam() {
3528
	global $wpdb;
3529
3530
	$grunion_delete_limit = 100;
3531
3532
	$now_gmt  = current_time( 'mysql', 1 );
3533
	$sql      = $wpdb->prepare(
3534
		"
3535
		SELECT `ID`
3536
		FROM $wpdb->posts
3537
		WHERE DATE_SUB( %s, INTERVAL 15 DAY ) > `post_date_gmt`
3538
			AND `post_type` = 'feedback'
3539
			AND `post_status` = 'spam'
3540
		LIMIT %d
3541
	", $now_gmt, $grunion_delete_limit
3542
	);
3543
	$post_ids = $wpdb->get_col( $sql );
3544
3545
	foreach ( (array) $post_ids as $post_id ) {
3546
		// force a full delete, skip the trash
3547
		wp_delete_post( $post_id, true );
3548
	}
3549
3550
	if (
3551
		/**
3552
		 * Filter if the module run OPTIMIZE TABLE on the core WP tables.
3553
		 *
3554
		 * @module contact-form
3555
		 *
3556
		 * @since 1.3.1
3557
		 * @since 6.4.0 Set to false by default.
3558
		 *
3559
		 * @param bool $filter Should Jetpack optimize the table, defaults to false.
3560
		 */
3561
		apply_filters( 'grunion_optimize_table', false )
3562
	) {
3563
		$wpdb->query( "OPTIMIZE TABLE $wpdb->posts" );
3564
	}
3565
3566
	// if we hit the max then schedule another run
3567
	if ( count( $post_ids ) >= $grunion_delete_limit ) {
3568
		wp_schedule_single_event( time() + 700, 'grunion_scheduled_delete' );
3569
	}
3570
}
3571
3572
/**
3573
 * Send an event to Tracks on form submission.
3574
 *
3575
 * @param int   $post_id - the post_id for the CPT that is created.
3576
 * @param array $all_values - fields from the default contact form.
3577
 * @param array $extra_values - extra fields added to from the contact form.
3578
 *
3579
 * @return null|void
3580
 */
3581
function jetpack_tracks_record_grunion_pre_message_sent( $post_id, $all_values, $extra_values ) {
3582
	// Do not do anything if the submission is not from a block.
3583
	if (
3584
		! isset( $extra_values['is_block'] )
3585
		|| ! $extra_values['is_block']
3586
	) {
3587
		return;
3588
	}
3589
3590
	/*
3591
	 * Event details.
3592
	 */
3593
	$event_user  = wp_get_current_user();
3594
	$event_name  = 'contact_form_block_message_sent';
3595
	$event_props = array(
3596
		'entry_permalink' => esc_url( $all_values['entry_permalink'] ),
3597
		'feedback_id'     => esc_attr( $all_values['feedback_id'] ),
3598
	);
3599
3600
	/*
3601
	 * Record event.
3602
	 * We use different libs on wpcom and Jetpack.
3603
	 */
3604
	if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
3605
		$event_name             = 'wpcom_' . $event_name;
3606
		$event_props['blog_id'] = get_current_blog_id();
3607
		// If the form was sent by a logged out visitor, record event with blog owner.
3608
		if ( empty( $event_user->ID ) ) {
3609
			$event_user_id = wpcom_get_blog_owner( $event_props['blog_id'] );
3610
			$event_user    = get_userdata( $event_user_id );
3611
		}
3612
3613
		require_lib( 'tracks/client' );
3614
		tracks_record_event( $event_user, $event_name, $event_props );
3615
	} else {
3616
		// If the form was sent by a logged out visitor, record event with Jetpack master user.
3617
		if ( empty( $event_user->ID ) ) {
3618
			$master_user_id = Jetpack_Options::get_option( 'master_user' );
3619
			if ( ! empty( $master_user_id ) ) {
3620
				$event_user = get_userdata( $master_user_id );
3621
			}
3622
		}
3623
3624
		$tracking = new Automattic\Jetpack\Tracking();
3625
		$tracking->record_user_event( $event_name, $event_props, $event_user );
3626
	}
3627
}
3628
add_action( 'grunion_pre_message_sent', 'jetpack_tracks_record_grunion_pre_message_sent', 12, 3 );
3629