Completed
Push — try/contact_form_provide_field... ( 5dc7ed )
by
unknown
06:32
created

Grunion_Contact_Form::get_all_field_data()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

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

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

Let’s take a look at an example:

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

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

    // do something with $myArray
}

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

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

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

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

Let’s take a look at an example:

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

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

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

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

Available Fixes

  1. Check for existence of the variable explicitly:

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

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

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