Completed
Push — branch-6.8-built ( ab1229...17d608 )
by Jeremy
15:49 queued 07:50
created

Grunion_Contact_Form::parse()   F

Complexity

Conditions 18
Paths 27

Size

Total Lines 126

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 18
nc 27
nop 2
dl 0
loc 126
rs 3.8933
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

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