Completed
Push — update/use-wordpress-api-fetch... ( 2f6fe4...cd05e3 )
by
unknown
124:11 queued 116:32
created

Grunion_Contact_Form::parse_contact_field()   D

Complexity

Conditions 22
Paths 40

Size

Total Lines 62

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 22
nc 40
nop 2
dl 0
loc 62
rs 4.1666
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
				// Matrial Ballot icon
174
				'menu_icon'             => 'data:image/svg+xml;base64,' . base64_encode('<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="none" d="M13 7.5h5v2h-5zm0 7h5v2h-5zM19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H5V5h14v14zM11 6H6v5h5V6zm-1 4H7V7h3v3zm1 3H6v5h5v-5zm-1 4H7v-3h3v3z"/></svg>'),
175
				'show_ui'               => true,
176
				'show_in_admin_bar'     => false,
177
				'public'                => false,
178
				'rewrite'               => false,
179
				'query_var'             => false,
180
				'capability_type'       => 'page',
181
				'show_in_rest'          => true,
182
				'rest_controller_class' => 'Grunion_Contact_Form_Endpoint',
183
				'capabilities'          => array(
184
					'create_posts'        => false,
185
					'publish_posts'       => 'publish_pages',
186
					'edit_posts'          => 'edit_pages',
187
					'edit_others_posts'   => 'edit_others_pages',
188
					'delete_posts'        => 'delete_pages',
189
					'delete_others_posts' => 'delete_others_pages',
190
					'read_private_posts'  => 'read_private_pages',
191
					'edit_post'           => 'edit_page',
192
					'delete_post'         => 'delete_page',
193
					'read_post'           => 'read_page',
194
				),
195
				'map_meta_cap'          => true,
196
			)
197
		);
198
199
		// Add to REST API post type whitelist
200
		add_filter( 'rest_api_allowed_post_types', array( $this, 'allow_feedback_rest_api_type' ) );
201
202
		// Add "spam" as a post status
203
		register_post_status(
204
			'spam', array(
205
				'label'                  => 'Spam',
206
				'public'                 => false,
207
				'exclude_from_search'    => true,
208
				'show_in_admin_all_list' => false,
209
				'label_count'            => _n_noop( 'Spam <span class="count">(%s)</span>', 'Spam <span class="count">(%s)</span>', 'jetpack' ),
210
				'protected'              => true,
211
				'_builtin'               => false,
212
			)
213
		);
214
215
		// POST handler
216
		if (
217
			isset( $_SERVER['REQUEST_METHOD'] ) && 'POST' == strtoupper( $_SERVER['REQUEST_METHOD'] )
218
			&&
219
			isset( $_POST['action'] ) && 'grunion-contact-form' == $_POST['action']
220
			&&
221
			isset( $_POST['contact-form-id'] )
222
		) {
223
			add_action( 'template_redirect', array( $this, 'process_form_submission' ) );
224
		}
225
226
		/*
227
		 Can be dequeued by placing the following in wp-content/themes/yourtheme/functions.php
228
		 *
229
		 * 	function remove_grunion_style() {
230
		 *		wp_deregister_style('grunion.css');
231
		 *	}
232
		 *	add_action('wp_print_styles', 'remove_grunion_style');
233
		 */
234
		wp_register_style( 'grunion.css', GRUNION_PLUGIN_URL . 'css/grunion.css', array(), JETPACK__VERSION );
235
		wp_style_add_data( 'grunion.css', 'rtl', 'replace' );
236
237
		if ( Jetpack_Gutenberg::is_gutenberg_available() ) {
238
			self::register_contact_form_blocks();
239
		}
240
	}
241
242
	private static function register_contact_form_blocks() {
243
		jetpack_register_block( 'contact-form', array(
244
			'render_callback' => array( __CLASS__, 'gutenblock_render_form' ),
245
		) );
246
247
		// Field render methods.
248
		jetpack_register_block( 'field-text', array(
249
			'render_callback' => array( __CLASS__, 'gutenblock_render_field_text' ),
250
		) );
251
		jetpack_register_block( 'field-name', array(
252
			'render_callback' => array( __CLASS__, 'gutenblock_render_field_name' ),
253
		) );
254
		jetpack_register_block( 'field-email', array(
255
			'render_callback' => array( __CLASS__, 'gutenblock_render_field_email' ),
256
		) );
257
		jetpack_register_block( 'field-url', array(
258
			'render_callback' => array( __CLASS__, 'gutenblock_render_field_url' ),
259
		) );
260
		jetpack_register_block( 'field-date', array(
261
			'render_callback' => array( __CLASS__, 'gutenblock_render_field_date' ),
262
		) );
263
		jetpack_register_block( 'field-telephone', array(
264
			'render_callback' => array( __CLASS__, 'gutenblock_render_field_telephone' ),
265
		) );
266
		jetpack_register_block( 'field-textarea', array(
267
			'render_callback' => array( __CLASS__, 'gutenblock_render_field_textarea' ),
268
		) );
269
		jetpack_register_block( 'field-checkbox', array(
270
			'render_callback' => array( __CLASS__, 'gutenblock_render_field_checkbox' ),
271
		) );
272
		jetpack_register_block( 'field-checkbox-multiple', array(
273
			'render_callback' => array( __CLASS__, 'gutenblock_render_field_checkbox_multiple' ),
274
		) );
275
		jetpack_register_block( 'field-radio', array(
276
			'render_callback' => array( __CLASS__, 'gutenblock_render_field_radio' ),
277
		) );
278
		jetpack_register_block( 'field-select', array(
279
			'render_callback' => array( __CLASS__, 'gutenblock_render_field_select' ),
280
		) );
281
	}
282
283
	public static function gutenblock_render_form( $atts, $content ) {
284
		return Grunion_Contact_Form::parse( $atts, do_blocks( $content ) );
285
	}
286
287
	public static function block_attributes_to_shortcode_attributes( $atts, $type ) {
288
		$atts['type'] = $type;
289
		if ( isset( $atts['className'] ) ) {
290
			$atts['class'] = $atts['className'];
291
			unset( $atts['className'] );
292
		}
293
294
		if ( isset( $atts['defaultValue'] ) ) {
295
			$atts['default'] = $atts['defaultValue'];
296
			unset( $atts['defaultValue'] );
297
		}
298
299
		return $atts;
300
	}
301
302
	public static function gutenblock_render_field_text( $atts, $content ) {
303
		$atts = self::block_attributes_to_shortcode_attributes( $atts, 'text' );
304
		return Grunion_Contact_Form::parse_contact_field( $atts, $content );
305
	}
306
	public static function gutenblock_render_field_name( $atts, $content ) {
307
		$atts = self::block_attributes_to_shortcode_attributes( $atts, 'name' );
308
		return Grunion_Contact_Form::parse_contact_field( $atts, $content );
309
	}
310
	public static function gutenblock_render_field_email( $atts, $content ) {
311
		$atts = self::block_attributes_to_shortcode_attributes( $atts, 'email' );
312
		return Grunion_Contact_Form::parse_contact_field( $atts, $content );
313
	}
314
	public static function gutenblock_render_field_url( $atts, $content ) {
315
		$atts = self::block_attributes_to_shortcode_attributes( $atts, 'url' );
316
		return Grunion_Contact_Form::parse_contact_field( $atts, $content );
317
	}
318
	public static function gutenblock_render_field_date( $atts, $content ) {
319
		$atts = self::block_attributes_to_shortcode_attributes( $atts, 'date' );
320
		return Grunion_Contact_Form::parse_contact_field( $atts, $content );
321
	}
322
	public static function gutenblock_render_field_telephone( $atts, $content ) {
323
		$atts = self::block_attributes_to_shortcode_attributes( $atts, 'telephone' );
324
		return Grunion_Contact_Form::parse_contact_field( $atts, $content );
325
	}
326
	public static function gutenblock_render_field_textarea( $atts, $content ) {
327
		$atts = self::block_attributes_to_shortcode_attributes( $atts, 'textarea' );
328
		return Grunion_Contact_Form::parse_contact_field( $atts, $content );
329
	}
330
	public static function gutenblock_render_field_checkbox( $atts, $content ) {
331
		$atts = self::block_attributes_to_shortcode_attributes( $atts, 'checkbox' );
332
		return Grunion_Contact_Form::parse_contact_field( $atts, $content );
333
	}
334
	public static function gutenblock_render_field_checkbox_multiple( $atts, $content ) {
335
		$atts = self::block_attributes_to_shortcode_attributes( $atts, 'checkbox-multiple' );
336
		return Grunion_Contact_Form::parse_contact_field( $atts, $content );
337
	}
338
	public static function gutenblock_render_field_radio( $atts, $content ) {
339
		$atts = self::block_attributes_to_shortcode_attributes( $atts, 'radio' );
340
		return Grunion_Contact_Form::parse_contact_field( $atts, $content );
341
	}
342
	public static function gutenblock_render_field_select( $atts, $content ) {
343
		$atts = self::block_attributes_to_shortcode_attributes( $atts, 'select' );
344
		return Grunion_Contact_Form::parse_contact_field( $atts, $content );
345
	}
346
347
	/**
348
	 * Add the 'Export' menu item as a submenu of Feedback.
349
	 */
350
	public function admin_menu() {
351
		add_submenu_page(
352
			'edit.php?post_type=feedback',
353
			__( 'Export feedback as CSV', 'jetpack' ),
354
			__( 'Export CSV', 'jetpack' ),
355
			'export',
356
			'feedback-export',
357
			array( $this, 'export_form' )
358
		);
359
	}
360
361
	/**
362
	 * Add to REST API post type whitelist
363
	 */
364
	function allow_feedback_rest_api_type( $post_types ) {
365
		$post_types[] = 'feedback';
366
		return $post_types;
367
	}
368
369
	/**
370
	 * Display the count of new feedback entries received. It's reset when user visits the Feedback screen.
371
	 *
372
	 * @since 4.1.0
373
	 *
374
	 * @param object $screen Information about the current screen.
375
	 */
376
	function unread_count( $screen ) {
377
		if ( isset( $screen->post_type ) && 'feedback' == $screen->post_type ) {
378
			update_option( 'feedback_unread_count', 0 );
379
		} else {
380
			global $menu;
381
			if ( isset( $menu ) && is_array( $menu ) && ! empty( $menu ) ) {
382
				foreach ( $menu as $index => $menu_item ) {
383
					if ( 'edit.php?post_type=feedback' == $menu_item[2] ) {
384
						$unread = get_option( 'feedback_unread_count', 0 );
385
						if ( $unread > 0 ) {
386
							$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>' : '';
387
							$menu[ $index ][0] .= $unread_count;
388
						}
389
						break;
390
					}
391
				}
392
			}
393
		}
394
	}
395
396
	/**
397
	 * Handles all contact-form POST submissions
398
	 *
399
	 * Conditionally attached to `template_redirect`
400
	 */
401
	function process_form_submission() {
402
		// Add a filter to replace tokens in the subject field with sanitized field values
403
		add_filter( 'contact_form_subject', array( $this, 'replace_tokens_with_input' ), 10, 2 );
404
405
		$id   = stripslashes( $_POST['contact-form-id'] );
406
		$hash = isset( $_POST['contact-form-hash'] ) ? $_POST['contact-form-hash'] : null;
407
		$hash = preg_replace( '/[^\da-f]/i', '', $hash );
408
409
		if ( is_user_logged_in() ) {
410
			check_admin_referer( "contact-form_{$id}" );
411
		}
412
413
		$is_widget = 0 === strpos( $id, 'widget-' );
414
415
		$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...
416
417
		if ( $is_widget ) {
418
			// It's a form embedded in a text widget
419
			$this->current_widget_id = substr( $id, 7 ); // remove "widget-"
420
			$widget_type             = implode( '-', array_slice( explode( '-', $this->current_widget_id ), 0, -1 ) ); // Remove trailing -#
421
422
			// Is the widget active?
423
			$sidebar = is_active_widget( false, $this->current_widget_id, $widget_type );
424
425
			// This is lame - no core API for getting a widget by ID
426
			$widget = isset( $GLOBALS['wp_registered_widgets'][ $this->current_widget_id ] ) ? $GLOBALS['wp_registered_widgets'][ $this->current_widget_id ] : false;
427
428
			if ( $sidebar && $widget && isset( $widget['callback'] ) ) {
429
				// prevent PHP notices by populating widget args
430
				$widget_args = array(
431
					'before_widget' => '',
432
					'after_widget'  => '',
433
					'before_title'  => '',
434
					'after_title'   => '',
435
				);
436
				// This is lamer - no API for outputting a given widget by ID
437
				ob_start();
438
				// Process the widget to populate Grunion_Contact_Form::$last
439
				call_user_func( $widget['callback'], $widget_args, $widget['params'][0] );
440
				ob_end_clean();
441
			}
442
		} else {
443
			// It's a form embedded in a post
444
			$post = get_post( $id );
445
446
			// Process the content to populate Grunion_Contact_Form::$last
447
			/** This filter is already documented in core. wp-includes/post-template.php */
448
			apply_filters( 'the_content', $post->post_content );
449
		}
450
451
		$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...
452
453
		// No form may mean user is using do_shortcode, grab the form using the stored post meta
454
		if ( ! $form ) {
455
456
			// Get shortcode from post meta
457
			$shortcode = get_post_meta( $_POST['contact-form-id'], "_g_feedback_shortcode_{$hash}", true );
458
459
			// Format it
460
			if ( $shortcode != '' ) {
461
462
				// Get attributes from post meta.
463
				$parameters = '';
464
				$attributes = get_post_meta( $_POST['contact-form-id'], "_g_feedback_shortcode_atts_{$hash}", true );
465
				if ( ! empty( $attributes ) && is_array( $attributes ) ) {
466
					foreach ( array_filter( $attributes ) as $param => $value ) {
467
						$parameters .= " $param=\"$value\"";
468
					}
469
				}
470
471
				$shortcode = '[contact-form' . $parameters . ']' . $shortcode . '[/contact-form]';
472
				do_shortcode( $shortcode );
473
474
				// Recreate form
475
				$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...
476
			}
477
478
			if ( ! $form ) {
479
				return false;
480
			}
481
		}
482
483
		if ( is_wp_error( $form->errors ) && $form->errors->get_error_codes() ) {
484
			return $form->errors;
485
		}
486
487
		// Process the form
488
		return $form->process_submission();
489
	}
490
491
	function ajax_request() {
492
		$submission_result = self::process_form_submission();
493
494
		if ( ! $submission_result ) {
495
			header( 'HTTP/1.1 500 Server Error', 500, true );
496
			echo '<div class="form-error"><ul class="form-errors"><li class="form-error-message">';
497
			esc_html_e( 'An error occurred. Please try again later.', 'jetpack' );
498
			echo '</li></ul></div>';
499
		} elseif ( is_wp_error( $submission_result ) ) {
500
			header( 'HTTP/1.1 400 Bad Request', 403, true );
501
			echo '<div class="form-error"><ul class="form-errors"><li class="form-error-message">';
502
			echo esc_html( $submission_result->get_error_message() );
503
			echo '</li></ul></div>';
504
		} else {
505
			echo '<h3>' . esc_html__( 'Message Sent', 'jetpack' ) . '</h3>' . $submission_result;
506
		}
507
508
		die;
509
	}
510
511
	/**
512
	 * Ensure the post author is always zero for contact-form feedbacks
513
	 * Attached to `wp_insert_post_data`
514
	 *
515
	 * @see Grunion_Contact_Form::process_submission()
516
	 *
517
	 * @param array $data the data to insert
518
	 * @param array $postarr the data sent to wp_insert_post()
519
	 * @return array The filtered $data to insert
520
	 */
521
	function insert_feedback_filter( $data, $postarr ) {
522
		if ( $data['post_type'] == 'feedback' && $postarr['post_type'] == 'feedback' ) {
523
			$data['post_author'] = 0;
524
		}
525
526
		return $data;
527
	}
528
	/*
529
	 * Adds our contact-form shortcode
530
	 * The "child" contact-field shortcode is enabled as needed by the contact-form shortcode handler
531
	 */
532
	function add_shortcode() {
533
		add_shortcode( 'contact-form', array( 'Grunion_Contact_Form', 'parse' ) );
534
		add_shortcode( 'contact-field', array( 'Grunion_Contact_Form', 'parse_contact_field' ) );
535
	}
536
537
	static function tokenize_label( $label ) {
538
		return '{' . trim( preg_replace( '#^\d+_#', '', $label ) ) . '}';
539
	}
540
541
	static function sanitize_value( $value ) {
542
		return preg_replace( '=((<CR>|<LF>|0x0A/%0A|0x0D/%0D|\\n|\\r)\S).*=i', null, $value );
543
	}
544
545
	/**
546
	 * Replaces tokens like {city} or {City} (case insensitive) with the value
547
	 * of an input field of that name
548
	 *
549
	 * @param string $subject
550
	 * @param array  $field_values Array with field label => field value associations
551
	 *
552
	 * @return string The filtered $subject with the tokens replaced
553
	 */
554
	function replace_tokens_with_input( $subject, $field_values ) {
555
		// Wrap labels into tokens (inside {})
556
		$wrapped_labels = array_map( array( 'Grunion_Contact_Form_Plugin', 'tokenize_label' ), array_keys( $field_values ) );
557
		// Sanitize all values
558
		$sanitized_values = array_map( array( 'Grunion_Contact_Form_Plugin', 'sanitize_value' ), array_values( $field_values ) );
559
560
		foreach ( $sanitized_values as $k => $sanitized_value ) {
561
			if ( is_array( $sanitized_value ) ) {
562
				$sanitized_values[ $k ] = implode( ', ', $sanitized_value );
563
			}
564
		}
565
566
		// Search for all valid tokens (based on existing fields) and replace with the field's value
567
		$subject = str_ireplace( $wrapped_labels, $sanitized_values, $subject );
568
		return $subject;
569
	}
570
571
	/**
572
	 * Tracks the widget currently being processed.
573
	 * Attached to `dynamic_sidebar`
574
	 *
575
	 * @see $current_widget_id
576
	 *
577
	 * @param array $widget The widget data
578
	 */
579
	function track_current_widget( $widget ) {
580
		$this->current_widget_id = $widget['id'];
581
	}
582
583
	/**
584
	 * Adds a "widget" attribute to every contact-form embedded in a text widget.
585
	 * Used to tell the difference between post-embedded contact-forms and widget-embedded contact-forms
586
	 * Attached to `widget_text`
587
	 *
588
	 * @param string $text The widget text
589
	 * @return string The filtered widget text
590
	 */
591
	function widget_atts( $text ) {
592
		Grunion_Contact_Form::style( true );
593
594
		return preg_replace( '/\[contact-form([^a-zA-Z_-])/', '[contact-form widget="' . $this->current_widget_id . '"\\1', $text );
595
	}
596
597
	/**
598
	 * For sites where text widgets are not processed for shortcodes, we add this hack to process just our shortcode
599
	 * Attached to `widget_text`
600
	 *
601
	 * @param string $text The widget text
602
	 * @return string The contact-form filtered widget text
603
	 */
604
	function widget_shortcode_hack( $text ) {
605
		if ( ! preg_match( '/\[contact-form([^a-zA-Z_-])/', $text ) ) {
606
			return $text;
607
		}
608
609
		$old = $GLOBALS['shortcode_tags'];
610
		remove_all_shortcodes();
611
		Grunion_Contact_Form_Plugin::$using_contact_form_field = true;
612
		$this->add_shortcode();
613
614
		$text = do_shortcode( $text );
615
616
		Grunion_Contact_Form_Plugin::$using_contact_form_field = false;
617
		$GLOBALS['shortcode_tags']                             = $old;
618
619
		return $text;
620
	}
621
622
	/**
623
	 * Populate an array with all values necessary to submit a NEW contact-form feedback to Akismet.
624
	 * Note that this includes the current user_ip etc, so this should only be called when accepting a new item via $_POST
625
	 *
626
	 * @param array $form Contact form feedback array
627
	 * @return array feedback array with additional data ready for submission to Akismet
628
	 */
629
	function prepare_for_akismet( $form ) {
630
		$form['comment_type'] = 'contact_form';
631
		$form['user_ip']      = $_SERVER['REMOTE_ADDR'];
632
		$form['user_agent']   = $_SERVER['HTTP_USER_AGENT'];
633
		$form['referrer']     = $_SERVER['HTTP_REFERER'];
634
		$form['blog']         = get_option( 'home' );
635
636
		foreach ( $_SERVER as $key => $value ) {
637
			if ( ! is_string( $value ) ) {
638
				continue;
639
			}
640
			if ( in_array( $key, array( 'HTTP_COOKIE', 'HTTP_COOKIE2', 'HTTP_USER_AGENT', 'HTTP_REFERER' ) ) ) {
641
				// We don't care about cookies, and the UA and Referrer were caught above.
642
				continue;
643
			} elseif ( in_array( $key, array( 'REMOTE_ADDR', 'REQUEST_URI', 'DOCUMENT_URI' ) ) ) {
644
				// All three of these are relevant indicators and should be passed along.
645
				$form[ $key ] = $value;
646
			} elseif ( wp_startswith( $key, 'HTTP_' ) ) {
647
				// Any other HTTP header indicators.
648
				// `wp_startswith()` is a wpcom helper function and is included in Jetpack via `functions.compat.php`
649
				$form[ $key ] = $value;
650
			}
651
		}
652
653
		return $form;
654
	}
655
656
	/**
657
	 * Submit contact-form data to Akismet to check for spam.
658
	 * If you're accepting a new item via $_POST, run it Grunion_Contact_Form_Plugin::prepare_for_akismet() first
659
	 * Attached to `jetpack_contact_form_is_spam`
660
	 *
661
	 * @param bool  $is_spam
662
	 * @param array $form
663
	 * @return bool|WP_Error TRUE => spam, FALSE => not spam, WP_Error => stop processing entirely
664
	 */
665
	function is_spam_akismet( $is_spam, $form = array() ) {
666
		global $akismet_api_host, $akismet_api_port;
667
668
		// The signature of this function changed from accepting just $form.
669
		// If something only sends an array, assume it's still using the old
670
		// signature and work around it.
671
		if ( empty( $form ) && is_array( $is_spam ) ) {
672
			$form    = $is_spam;
673
			$is_spam = false;
674
		}
675
676
		// If a previous filter has alrady marked this as spam, trust that and move on.
677
		if ( $is_spam ) {
678
			return $is_spam;
679
		}
680
681
		if ( ! function_exists( 'akismet_http_post' ) && ! defined( 'AKISMET_VERSION' ) ) {
682
			return false;
683
		}
684
685
		$query_string = http_build_query( $form );
686
687
		if ( method_exists( 'Akismet', 'http_post' ) ) {
688
			$response = Akismet::http_post( $query_string, 'comment-check' );
689
		} else {
690
			$response = akismet_http_post( $query_string, $akismet_api_host, '/1.1/comment-check', $akismet_api_port );
691
		}
692
693
		$result = false;
694
695
		if ( isset( $response[0]['x-akismet-pro-tip'] ) && 'discard' === trim( $response[0]['x-akismet-pro-tip'] ) && get_option( 'akismet_strictness' ) === '1' ) {
696
			$result = new WP_Error( 'feedback-discarded', __( 'Feedback discarded.', 'jetpack' ) );
697
		} elseif ( isset( $response[1] ) && 'true' == trim( $response[1] ) ) { // 'true' is spam
698
			$result = true;
699
		}
700
701
		/**
702
		 * Filter the results returned by Akismet for each submitted contact form.
703
		 *
704
		 * @module contact-form
705
		 *
706
		 * @since 1.3.1
707
		 *
708
		 * @param WP_Error|bool $result Is the submitted feedback spam.
709
		 * @param array|bool $form Submitted feedback.
710
		 */
711
		return apply_filters( 'contact_form_is_spam_akismet', $result, $form );
712
	}
713
714
	/**
715
	 * Submit a feedback as either spam or ham
716
	 *
717
	 * @param string $as Either 'spam' or 'ham'.
718
	 * @param array  $form the contact-form data
719
	 */
720
	function akismet_submit( $as, $form ) {
721
		global $akismet_api_host, $akismet_api_port;
722
723
		if ( ! in_array( $as, array( 'ham', 'spam' ) ) ) {
724
			return false;
725
		}
726
727
		$query_string = '';
728
		if ( is_array( $form ) ) {
729
			$query_string = http_build_query( $form );
730
		}
731
		if ( method_exists( 'Akismet', 'http_post' ) ) {
732
			$response = Akismet::http_post( $query_string, "submit-{$as}" );
733
		} else {
734
			$response = akismet_http_post( $query_string, $akismet_api_host, "/1.1/submit-{$as}", $akismet_api_port );
735
		}
736
737
		return trim( $response[1] );
738
	}
739
740
	/**
741
	 * Prints the menu
742
	 */
743
	function export_form() {
744
		$current_screen = get_current_screen();
745
		if ( ! in_array( $current_screen->id, array( 'edit-feedback', 'feedback_page_feedback-export' ) ) ) {
746
			return;
747
		}
748
749
		if ( ! current_user_can( 'export' ) ) {
750
			return;
751
		}
752
753
		// if there aren't any feedbacks, bail out
754
		if ( ! (int) wp_count_posts( 'feedback' )->publish ) {
755
			return;
756
		}
757
		?>
758
759
		<div id="feedback-export" style="display:none">
760
			<h2><?php _e( 'Export feedback as CSV', 'jetpack' ); ?></h2>
761
			<div class="clear"></div>
762
			<form action="<?php echo admin_url( 'admin-post.php' ); ?>" method="post" class="form">
763
				<?php wp_nonce_field( 'feedback_export', 'feedback_export_nonce' ); ?>
764
765
				<input name="action" value="feedback_export" type="hidden">
766
				<label for="post"><?php _e( 'Select feedback to download', 'jetpack' ); ?></label>
767
				<select name="post">
768
					<option value="all"><?php esc_html_e( 'All posts', 'jetpack' ); ?></option>
769
					<?php echo $this->get_feedbacks_as_options(); ?>
770
				</select>
771
772
				<br><br>
773
				<input type="submit" name="submit" id="submit" class="button button-primary" value="<?php esc_html_e( 'Download', 'jetpack' ); ?>">
774
			</form>
775
		</div>
776
777
		<?php
778
		// There aren't any usable actions in core to output the "export feedback" form in the correct place,
779
		// so this inline JS moves it from the top of the page to the bottom.
780
		?>
781
		<script type='text/javascript'>
782
		    var menu = document.getElementById( 'feedback-export' ),
783
                wrapper = document.getElementsByClassName( 'wrap' )[0];
784
            <?php if ( 'edit-feedback' === $current_screen->id ) : ?>
785
            wrapper.appendChild(menu);
786
            <?php endif; ?>
787
            menu.style.display = 'block';
788
		</script>
789
		<?php
790
	}
791
792
	/**
793
	 * Fetch post content for a post and extract just the comment.
794
	 *
795
	 * @param int $post_id The post id to fetch the content for.
796
	 *
797
	 * @return string Trimmed post comment.
798
	 *
799
	 * @codeCoverageIgnore
800
	 */
801
	public function get_post_content_for_csv_export( $post_id ) {
802
		$post_content = get_post_field( 'post_content', $post_id );
803
		$content      = explode( '<!--more-->', $post_content );
804
805
		return trim( $content[0] );
806
	}
807
808
	/**
809
	 * Get `_feedback_extra_fields` field from post meta data.
810
	 *
811
	 * @param int $post_id Id of the post to fetch meta data for.
812
	 *
813
	 * @return mixed
814
	 *
815
	 * @codeCoverageIgnore - No need to be covered.
816
	 */
817
	public function get_post_meta_for_csv_export( $post_id ) {
818
		return get_post_meta( $post_id, '_feedback_extra_fields', true );
819
	}
820
821
	/**
822
	 * Get parsed feedback post fields.
823
	 *
824
	 * @param int $post_id Id of the post to fetch parsed contents for.
825
	 *
826
	 * @return array
827
	 *
828
	 * @codeCoverageIgnore - No need to be covered.
829
	 */
830
	public function get_parsed_field_contents_of_post( $post_id ) {
831
		return self::parse_fields_from_content( $post_id );
832
	}
833
834
	/**
835
	 * Properly maps fields that are missing from the post meta data
836
	 * to names, that are similar to those of the post meta.
837
	 *
838
	 * @param array $parsed_post_content Parsed post content
839
	 *
840
	 * @see parse_fields_from_content for how the input data is generated.
841
	 *
842
	 * @return array Mapped fields.
843
	 */
844
	public function map_parsed_field_contents_of_post_to_field_names( $parsed_post_content ) {
845
846
		$mapped_fields = array();
847
848
		$field_mapping = array(
849
			'_feedback_subject'      => __( 'Contact Form', 'jetpack' ),
850
			'_feedback_author'       => '1_Name',
851
			'_feedback_author_email' => '2_Email',
852
			'_feedback_author_url'   => '3_Website',
853
			'_feedback_main_comment' => '4_Comment',
854
		);
855
856
		foreach ( $field_mapping as $parsed_field_name => $field_name ) {
857
			if (
858
				isset( $parsed_post_content[ $parsed_field_name ] )
859
				&& ! empty( $parsed_post_content[ $parsed_field_name ] )
860
			) {
861
				$mapped_fields[ $field_name ] = $parsed_post_content[ $parsed_field_name ];
862
			}
863
		}
864
865
		return $mapped_fields;
866
	}
867
868
	/**
869
	 * Registers the personal data exporter.
870
	 *
871
	 * @since 6.1.1
872
	 *
873
	 * @param  array $exporters An array of personal data exporters.
874
	 *
875
	 * @return array $exporters An array of personal data exporters.
876
	 */
877
	public function register_personal_data_exporter( $exporters ) {
878
		$exporters['jetpack-feedback'] = array(
879
			'exporter_friendly_name' => __( 'Feedback', 'jetpack' ),
880
			'callback'               => array( $this, 'personal_data_exporter' ),
881
		);
882
883
		return $exporters;
884
	}
885
886
	/**
887
	 * Registers the personal data eraser.
888
	 *
889
	 * @since 6.1.1
890
	 *
891
	 * @param  array $erasers An array of personal data erasers.
892
	 *
893
	 * @return array $erasers An array of personal data erasers.
894
	 */
895
	public function register_personal_data_eraser( $erasers ) {
896
		$erasers['jetpack-feedback'] = array(
897
			'eraser_friendly_name' => __( 'Feedback', 'jetpack' ),
898
			'callback'             => array( $this, 'personal_data_eraser' ),
899
		);
900
901
		return $erasers;
902
	}
903
904
	/**
905
	 * Exports personal data.
906
	 *
907
	 * @since 6.1.1
908
	 *
909
	 * @param  string $email  Email address.
910
	 * @param  int    $page   Page to export.
911
	 *
912
	 * @return array  $return Associative array with keys expected by core.
913
	 */
914
	public function personal_data_exporter( $email, $page = 1 ) {
915
		return $this->_internal_personal_data_exporter( $email, $page );
916
	}
917
918
	/**
919
	 * Internal method for exporting personal data.
920
	 *
921
	 * Allows us to have a different signature than core expects
922
	 * while protecting against future core API changes.
923
	 *
924
	 * @internal
925
	 * @since 6.5
926
	 *
927
	 * @param  string $email    Email address.
928
	 * @param  int    $page     Page to export.
929
	 * @param  int    $per_page Number of feedbacks to process per page. Internal use only (testing)
930
	 *
931
	 * @return array            Associative array with keys expected by core.
932
	 */
933
	public function _internal_personal_data_exporter( $email, $page = 1, $per_page = 250 ) {
934
		$export_data = array();
935
		$post_ids    = $this->personal_data_post_ids_by_email( $email, $per_page, $page );
936
937
		foreach ( $post_ids as $post_id ) {
938
			$post_fields = $this->get_parsed_field_contents_of_post( $post_id );
939
940
			if ( ! is_array( $post_fields ) || empty( $post_fields['_feedback_subject'] ) ) {
941
				continue; // Corrupt data.
942
			}
943
944
			$post_fields['_feedback_main_comment'] = $this->get_post_content_for_csv_export( $post_id );
945
			$post_fields                           = $this->map_parsed_field_contents_of_post_to_field_names( $post_fields );
946
947
			if ( ! is_array( $post_fields ) || empty( $post_fields ) ) {
948
				continue; // No fields to export.
949
			}
950
951
			$post_meta = $this->get_post_meta_for_csv_export( $post_id );
952
			$post_meta = is_array( $post_meta ) ? $post_meta : array();
953
954
			$post_export_data = array();
955
			$post_data        = array_merge( $post_fields, $post_meta );
956
			ksort( $post_data );
957
958
			foreach ( $post_data as $post_data_key => $post_data_value ) {
959
				$post_export_data[] = array(
960
					'name'  => preg_replace( '/^[0-9]+_/', '', $post_data_key ),
961
					'value' => $post_data_value,
962
				);
963
			}
964
965
			$export_data[] = array(
966
				'group_id'    => 'feedback',
967
				'group_label' => __( 'Feedback', 'jetpack' ),
968
				'item_id'     => 'feedback-' . $post_id,
969
				'data'        => $post_export_data,
970
			);
971
		}
972
973
		return array(
974
			'data' => $export_data,
975
			'done' => count( $post_ids ) < $per_page,
976
		);
977
	}
978
979
	/**
980
	 * Erases personal data.
981
	 *
982
	 * @since 6.1.1
983
	 *
984
	 * @param  string $email Email address.
985
	 * @param  int    $page  Page to erase.
986
	 *
987
	 * @return array         Associative array with keys expected by core.
988
	 */
989
	public function personal_data_eraser( $email, $page = 1 ) {
990
		return $this->_internal_personal_data_eraser( $email, $page );
991
	}
992
993
	/**
994
	 * Internal method for erasing personal data.
995
	 *
996
	 * Allows us to have a different signature than core expects
997
	 * while protecting against future core API changes.
998
	 *
999
	 * @internal
1000
	 * @since 6.5
1001
	 *
1002
	 * @param  string $email    Email address.
1003
	 * @param  int    $page     Page to erase.
1004
	 * @param  int    $per_page Number of feedbacks to process per page. Internal use only (testing)
1005
	 *
1006
	 * @return array            Associative array with keys expected by core.
1007
	 */
1008
	public function _internal_personal_data_eraser( $email, $page = 1, $per_page = 250 ) {
1009
		$removed      = false;
1010
		$retained     = false;
1011
		$messages     = array();
1012
		$option_name  = sprintf( '_jetpack_pde_feedback_%s', md5( $email ) );
1013
		$last_post_id = 1 === $page ? 0 : get_option( $option_name, 0 );
1014
		$post_ids     = $this->personal_data_post_ids_by_email( $email, $per_page, $page, $last_post_id );
1015
1016
		foreach ( $post_ids as $post_id ) {
1017
			/**
1018
			 * Filters whether to erase a particular Feedback post.
1019
			 *
1020
			 * @since 6.3.0
1021
			 *
1022
			 * @param bool|string $prevention_message Whether to apply erase the Feedback post (bool).
1023
			 *                                        Custom prevention message (string). Default true.
1024
			 * @param int         $post_id            Feedback post ID.
1025
			 */
1026
			$prevention_message = apply_filters( 'grunion_contact_form_delete_feedback_post', true, $post_id );
1027
1028
			if ( true !== $prevention_message ) {
1029
				if ( $prevention_message && is_string( $prevention_message ) ) {
1030
					$messages[] = esc_html( $prevention_message );
1031
				} else {
1032
					$messages[] = sprintf(
1033
					// translators: %d: Post ID.
1034
						__( 'Feedback ID %d could not be removed at this time.', 'jetpack' ),
1035
						$post_id
1036
					);
1037
				}
1038
1039
				$retained = true;
1040
1041
				continue;
1042
			}
1043
1044
			if ( wp_delete_post( $post_id, true ) ) {
1045
				$removed = true;
1046
			} else {
1047
				$retained   = true;
1048
				$messages[] = sprintf(
1049
				// translators: %d: Post ID.
1050
					__( 'Feedback ID %d could not be removed at this time.', 'jetpack' ),
1051
					$post_id
1052
				);
1053
			}
1054
		}
1055
1056
		$done = count( $post_ids ) < $per_page;
1057
1058
		if ( $done ) {
1059
			delete_option( $option_name );
1060
		} else {
1061
			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 1016. 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...
1062
		}
1063
1064
		return array(
1065
			'items_removed'  => $removed,
1066
			'items_retained' => $retained,
1067
			'messages'       => $messages,
1068
			'done'           => $done,
1069
		);
1070
	}
1071
1072
	/**
1073
	 * Queries personal data by email address.
1074
	 *
1075
	 * @since 6.1.1
1076
	 *
1077
	 * @param  string $email        Email address.
1078
	 * @param  int    $per_page     Post IDs per page. Default is `250`.
1079
	 * @param  int    $page         Page to query. Default is `1`.
1080
	 * @param  int    $last_post_id Page to query. Default is `0`. If non-zero, used instead of $page.
1081
	 *
1082
	 * @return array An array of post IDs.
1083
	 */
1084
	public function personal_data_post_ids_by_email( $email, $per_page = 250, $page = 1, $last_post_id = 0 ) {
1085
		add_filter( 'posts_search', array( $this, 'personal_data_search_filter' ) );
1086
1087
		$this->pde_last_post_id_erased = $last_post_id;
1088
		$this->pde_email_address       = $email;
1089
1090
		$post_ids = get_posts(
1091
			array(
1092
				'post_type'        => 'feedback',
1093
				'post_status'      => 'publish',
1094
				// This search parameter gets overwritten in ->personal_data_search_filter()
1095
				's'                => '..PDE..AUTHOR EMAIL:..PDE..',
1096
				'sentence'         => true,
1097
				'order'            => 'ASC',
1098
				'orderby'          => 'ID',
1099
				'fields'           => 'ids',
1100
				'posts_per_page'   => $per_page,
1101
				'paged'            => $last_post_id ? 1 : $page,
1102
				'suppress_filters' => false,
1103
			)
1104
		);
1105
1106
		$this->pde_last_post_id_erased = 0;
1107
		$this->pde_email_address       = '';
1108
1109
		remove_filter( 'posts_search', array( $this, 'personal_data_search_filter' ) );
1110
1111
		return $post_ids;
1112
	}
1113
1114
	/**
1115
	 * Filters searches by email address.
1116
	 *
1117
	 * @since 6.1.1
1118
	 *
1119
	 * @param  string $search SQL where clause.
1120
	 *
1121
	 * @return array          Filtered SQL where clause.
1122
	 */
1123
	public function personal_data_search_filter( $search ) {
1124
		global $wpdb;
1125
1126
		/*
1127
		 * Limits search to `post_content` only, and we only match the
1128
		 * author's email address whenever it's on a line by itself.
1129
		 */
1130
		if ( $this->pde_email_address && false !== strpos( $search, '..PDE..AUTHOR EMAIL:..PDE..' ) ) {
1131
			$search = $wpdb->prepare(
1132
				" AND (
1133
					{$wpdb->posts}.post_content LIKE %s
1134
					OR {$wpdb->posts}.post_content LIKE %s
1135
				)",
1136
				// `chr( 10 )` = `\n`, `chr( 13 )` = `\r`
1137
				'%' . $wpdb->esc_like( chr( 10 ) . 'AUTHOR EMAIL: ' . $this->pde_email_address . chr( 10 ) ) . '%',
1138
				'%' . $wpdb->esc_like( chr( 13 ) . 'AUTHOR EMAIL: ' . $this->pde_email_address . chr( 13 ) ) . '%'
1139
			);
1140
1141
			if ( $this->pde_last_post_id_erased ) {
1142
				$search .= $wpdb->prepare( " AND {$wpdb->posts}.ID > %d", $this->pde_last_post_id_erased );
1143
			}
1144
		}
1145
1146
		return $search;
1147
	}
1148
1149
	/**
1150
	 * Prepares feedback post data for CSV export.
1151
	 *
1152
	 * @param array $post_ids Post IDs to fetch the data for. These need to be Feedback posts.
1153
	 *
1154
	 * @return array
1155
	 */
1156
	public function get_export_data_for_posts( $post_ids ) {
1157
1158
		$posts_data  = array();
1159
		$field_names = array();
1160
		$result      = array();
1161
1162
		/**
1163
		 * Fetch posts and get the possible field names for later use
1164
		 */
1165
		foreach ( $post_ids as $post_id ) {
1166
1167
			/**
1168
			 * Fetch post main data, because we need the subject and author data for the feedback form.
1169
			 */
1170
			$post_real_data = $this->get_parsed_field_contents_of_post( $post_id );
1171
1172
			/**
1173
			 * If `$post_real_data` is not an array or there is no `_feedback_subject` set,
1174
			 * then something must be wrong with the feedback post. Skip it.
1175
			 */
1176
			if ( ! is_array( $post_real_data ) || ! isset( $post_real_data['_feedback_subject'] ) ) {
1177
				continue;
1178
			}
1179
1180
			/**
1181
			 * Fetch main post comment. This is from the default textarea fields.
1182
			 * If it is non-empty, then we add it to data, otherwise skip it.
1183
			 */
1184
			$post_comment_content = $this->get_post_content_for_csv_export( $post_id );
1185
			if ( ! empty( $post_comment_content ) ) {
1186
				$post_real_data['_feedback_main_comment'] = $post_comment_content;
1187
			}
1188
1189
			/**
1190
			 * Map parsed fields to proper field names
1191
			 */
1192
			$mapped_fields = $this->map_parsed_field_contents_of_post_to_field_names( $post_real_data );
1193
1194
			/**
1195
			 * Fetch post meta data.
1196
			 */
1197
			$post_meta_data = $this->get_post_meta_for_csv_export( $post_id );
1198
1199
			/**
1200
			 * If `$post_meta_data` is not an array or if it is empty, then there is no
1201
			 * extra feedback to work with. Create an empty array.
1202
			 */
1203
			if ( ! is_array( $post_meta_data ) || empty( $post_meta_data ) ) {
1204
				$post_meta_data = array();
1205
			}
1206
1207
			/**
1208
			 * Prepend the feedback subject to the list of fields.
1209
			 */
1210
			$post_meta_data = array_merge(
1211
				$mapped_fields,
1212
				$post_meta_data
1213
			);
1214
1215
			/**
1216
			 * Save post metadata for later usage.
1217
			 */
1218
			$posts_data[ $post_id ] = $post_meta_data;
1219
1220
			/**
1221
			 * Save field names, so we can use them as header fields later in the CSV.
1222
			 */
1223
			$field_names = array_merge( $field_names, array_keys( $post_meta_data ) );
1224
		}
1225
1226
		/**
1227
		 * Make sure the field names are unique, because we don't want duplicate data.
1228
		 */
1229
		$field_names = array_unique( $field_names );
1230
1231
		/**
1232
		 * Sort the field names by the field id number
1233
		 */
1234
		sort( $field_names, SORT_NUMERIC );
1235
1236
		/**
1237
		 * Loop through every post, which is essentially CSV row.
1238
		 */
1239
		foreach ( $posts_data as $post_id => $single_post_data ) {
1240
1241
			/**
1242
			 * Go through all the possible fields and check if the field is available
1243
			 * in the current post.
1244
			 *
1245
			 * If it is - add the data as a value.
1246
			 * If it is not - add an empty string, which is just a placeholder in the CSV.
1247
			 */
1248
			foreach ( $field_names as $single_field_name ) {
1249
				if (
1250
					isset( $single_post_data[ $single_field_name ] )
1251
					&& ! empty( $single_post_data[ $single_field_name ] )
1252
				) {
1253
					$result[ $single_field_name ][] = trim( $single_post_data[ $single_field_name ] );
1254
				} else {
1255
					$result[ $single_field_name ][] = '';
1256
				}
1257
			}
1258
		}
1259
1260
		return $result;
1261
	}
1262
1263
	/**
1264
	 * download as a csv a contact form or all of them in a csv file
1265
	 */
1266
	function download_feedback_as_csv() {
1267
		if ( empty( $_POST['feedback_export_nonce'] ) ) {
1268
			return;
1269
		}
1270
1271
		check_admin_referer( 'feedback_export', 'feedback_export_nonce' );
1272
1273
		if ( ! current_user_can( 'export' ) ) {
1274
			return;
1275
		}
1276
1277
		$args = array(
1278
			'posts_per_page'   => -1,
1279
			'post_type'        => 'feedback',
1280
			'post_status'      => 'publish',
1281
			'order'            => 'ASC',
1282
			'fields'           => 'ids',
1283
			'suppress_filters' => false,
1284
		);
1285
1286
		$filename = date( 'Y-m-d' ) . '-feedback-export.csv';
1287
1288
		// Check if we want to download all the feedbacks or just a certain contact form
1289
		if ( ! empty( $_POST['post'] ) && $_POST['post'] !== 'all' ) {
1290
			$args['post_parent'] = (int) $_POST['post'];
1291
			$filename            = date( 'Y-m-d' ) . '-' . str_replace( '&nbsp;', '-', get_the_title( (int) $_POST['post'] ) ) . '.csv';
1292
		}
1293
1294
		$feedbacks = get_posts( $args );
1295
1296
		if ( empty( $feedbacks ) ) {
1297
			return;
1298
		}
1299
1300
		$filename = sanitize_file_name( $filename );
1301
1302
		/**
1303
		 * Prepare data for export.
1304
		 */
1305
		$data = $this->get_export_data_for_posts( $feedbacks );
1306
1307
		/**
1308
		 * If `$data` is empty, there's nothing we can do below.
1309
		 */
1310
		if ( ! is_array( $data ) || empty( $data ) ) {
1311
			return;
1312
		}
1313
1314
		/**
1315
		 * Extract field names from `$data` for later use.
1316
		 */
1317
		$fields = array_keys( $data );
1318
1319
		/**
1320
		 * Count how many rows will be exported.
1321
		 */
1322
		$row_count = count( reset( $data ) );
1323
1324
		// Forces the download of the CSV instead of echoing
1325
		header( 'Content-Disposition: attachment; filename=' . $filename );
1326
		header( 'Pragma: no-cache' );
1327
		header( 'Expires: 0' );
1328
		header( 'Content-Type: text/csv; charset=utf-8' );
1329
1330
		$output = fopen( 'php://output', 'w' );
1331
1332
		/**
1333
		 * Print CSV headers
1334
		 */
1335
		fputcsv( $output, $fields );
1336
1337
		/**
1338
		 * Print rows to the output.
1339
		 */
1340
		for ( $i = 0; $i < $row_count; $i ++ ) {
1341
1342
			$current_row = array();
1343
1344
			/**
1345
			 * Put all the fields in `$current_row` array.
1346
			 */
1347
			foreach ( $fields as $single_field_name ) {
1348
				$current_row[] = $this->esc_csv( $data[ $single_field_name ][ $i ] );
1349
			}
1350
1351
			/**
1352
			 * Output the complete CSV row
1353
			 */
1354
			fputcsv( $output, $current_row );
1355
		}
1356
1357
		fclose( $output );
1358
	}
1359
1360
	/**
1361
	 * Escape a string to be used in a CSV context
1362
	 *
1363
	 * Malicious input can inject formulas into CSV files, opening up the possibility for phishing attacks and
1364
	 * disclosure of sensitive information.
1365
	 *
1366
	 * Additionally, Excel exposes the ability to launch arbitrary commands through the DDE protocol.
1367
	 *
1368
	 * @see http://www.contextis.com/resources/blog/comma-separated-vulnerabilities/
1369
	 *
1370
	 * @param string $field
1371
	 *
1372
	 * @return string
1373
	 */
1374
	public function esc_csv( $field ) {
1375
		$active_content_triggers = array( '=', '+', '-', '@' );
1376
1377
		if ( in_array( mb_substr( $field, 0, 1 ), $active_content_triggers, true ) ) {
1378
			$field = "'" . $field;
1379
		}
1380
1381
		return $field;
1382
	}
1383
1384
	/**
1385
	 * Returns a string of HTML <option> items from an array of posts
1386
	 *
1387
	 * @return string a string of HTML <option> items
1388
	 */
1389
	protected function get_feedbacks_as_options() {
1390
		$options = '';
1391
1392
		// Get the feedbacks' parents' post IDs
1393
		$feedbacks = get_posts(
1394
			array(
1395
				'fields'           => 'id=>parent',
1396
				'posts_per_page'   => 100000,
1397
				'post_type'        => 'feedback',
1398
				'post_status'      => 'publish',
1399
				'suppress_filters' => false,
1400
			)
1401
		);
1402
		$parents   = array_unique( array_values( $feedbacks ) );
1403
1404
		$posts = get_posts(
1405
			array(
1406
				'orderby'          => 'ID',
1407
				'posts_per_page'   => 1000,
1408
				'post_type'        => 'any',
1409
				'post__in'         => array_values( $parents ),
1410
				'suppress_filters' => false,
1411
			)
1412
		);
1413
1414
		// creates the string of <option> elements
1415
		foreach ( $posts as $post ) {
1416
			$options .= sprintf( '<option value="%s">%s</option>', esc_attr( $post->ID ), esc_html( $post->post_title ) );
1417
		}
1418
1419
		return $options;
1420
	}
1421
1422
	/**
1423
	 * Get the names of all the form's fields
1424
	 *
1425
	 * @param  array|int $posts the post we want the fields of
1426
	 *
1427
	 * @return array     the array of fields
1428
	 *
1429
	 * @deprecated As this is no longer necessary as of the CSV export rewrite. - 2015-12-29
1430
	 */
1431
	protected function get_field_names( $posts ) {
1432
		$posts      = (array) $posts;
1433
		$all_fields = array();
1434
1435
		foreach ( $posts as $post ) {
1436
			$fields = self::parse_fields_from_content( $post );
1437
1438
			if ( isset( $fields['_feedback_all_fields'] ) ) {
1439
				$extra_fields = array_keys( $fields['_feedback_all_fields'] );
1440
				$all_fields   = array_merge( $all_fields, $extra_fields );
1441
			}
1442
		}
1443
1444
		$all_fields = array_unique( $all_fields );
1445
		return $all_fields;
1446
	}
1447
1448
	public static function parse_fields_from_content( $post_id ) {
1449
		static $post_fields;
1450
1451
		if ( ! is_array( $post_fields ) ) {
1452
			$post_fields = array();
1453
		}
1454
1455
		if ( isset( $post_fields[ $post_id ] ) ) {
1456
			return $post_fields[ $post_id ];
1457
		}
1458
1459
		$all_values   = array();
1460
		$post_content = get_post_field( 'post_content', $post_id );
1461
		$content      = explode( '<!--more-->', $post_content );
1462
		$lines        = array();
1463
1464
		if ( count( $content ) > 1 ) {
1465
			$content  = str_ireplace( array( '<br />', ')</p>' ), '', $content[1] );
1466
			$one_line = preg_replace( '/\s+/', ' ', $content );
1467
			$one_line = preg_replace( '/.*Array \( (.*)\)/', '$1', $one_line );
1468
1469
			preg_match_all( '/\[([^\]]+)\] =\&gt\; ([^\[]+)/', $one_line, $matches );
1470
1471
			if ( count( $matches ) > 1 ) {
1472
				$all_values = array_combine( array_map( 'trim', $matches[1] ), array_map( 'trim', $matches[2] ) );
1473
			}
1474
1475
			$lines = array_filter( explode( "\n", $content ) );
1476
		}
1477
1478
		$var_map = array(
1479
			'AUTHOR'       => '_feedback_author',
1480
			'AUTHOR EMAIL' => '_feedback_author_email',
1481
			'AUTHOR URL'   => '_feedback_author_url',
1482
			'SUBJECT'      => '_feedback_subject',
1483
			'IP'           => '_feedback_ip',
1484
		);
1485
1486
		$fields = array();
1487
1488
		foreach ( $lines as $line ) {
1489
			$vars = explode( ': ', $line, 2 );
1490
			if ( ! empty( $vars ) ) {
1491
				if ( isset( $var_map[ $vars[0] ] ) ) {
1492
					$fields[ $var_map[ $vars[0] ] ] = self::strip_tags( trim( $vars[1] ) );
1493
				}
1494
			}
1495
		}
1496
1497
		$fields['_feedback_all_fields'] = $all_values;
1498
1499
		$post_fields[ $post_id ] = $fields;
1500
1501
		return $fields;
1502
	}
1503
1504
	/**
1505
	 * Creates a valid csv row from a post id
1506
	 *
1507
	 * @param  int   $post_id The id of the post
1508
	 * @param  array $fields  An array containing the names of all the fields of the csv
1509
	 * @return String The csv row
1510
	 *
1511
	 * @deprecated This is no longer needed, as of the CSV export rewrite.
1512
	 */
1513
	protected static function make_csv_row_from_feedback( $post_id, $fields ) {
1514
		$content_fields = self::parse_fields_from_content( $post_id );
1515
		$all_fields     = array();
1516
1517
		if ( isset( $content_fields['_feedback_all_fields'] ) ) {
1518
			$all_fields = $content_fields['_feedback_all_fields'];
1519
		}
1520
1521
		// Overwrite the parsed content with the content we stored in post_meta in a better format.
1522
		$extra_fields = get_post_meta( $post_id, '_feedback_extra_fields', true );
1523
		foreach ( $extra_fields as $extra_field => $extra_value ) {
1524
			$all_fields[ $extra_field ] = $extra_value;
1525
		}
1526
1527
		// The first element in all of the exports will be the subject
1528
		$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...
1529
1530
		// Loop the fields array in order to fill the $row_items array correctly
1531
		foreach ( $fields as $field ) {
1532
			if ( $field === __( 'Contact Form', 'jetpack' ) ) { // the first field will ever be the contact form, so we can continue
1533
				continue;
1534
			} elseif ( array_key_exists( $field, $all_fields ) ) {
1535
				$row_items[] = $all_fields[ $field ];
1536
			} else {
1537
				$row_items[] = '';
1538
			}
1539
		}
1540
1541
		return $row_items;
1542
	}
1543
1544
	public static function get_ip_address() {
1545
		return isset( $_SERVER['REMOTE_ADDR'] ) ? $_SERVER['REMOTE_ADDR'] : null;
1546
	}
1547
}
1548
1549
/**
1550
 * Generic shortcode class.
1551
 * Does nothing other than store structured data and output the shortcode as a string
1552
 *
1553
 * Not very general - specific to Grunion.
1554
 */
1555
class Crunion_Contact_Form_Shortcode {
1556
	/**
1557
	 * @var string the name of the shortcode: [$shortcode_name /]
1558
	 */
1559
	public $shortcode_name;
1560
1561
	/**
1562
	 * @var array key => value pairs for the shortcode's attributes: [$shortcode_name key="value" ... /]
1563
	 */
1564
	public $attributes;
1565
1566
	/**
1567
	 * @var array key => value pair for attribute defaults
1568
	 */
1569
	public $defaults = array();
1570
1571
	/**
1572
	 * @var null|string Null for selfclosing shortcodes.  Hhe inner content of otherwise: [$shortcode_name]$content[/$shortcode_name]
1573
	 */
1574
	public $content;
1575
1576
	/**
1577
	 * @var array Associative array of inner "child" shortcodes equivalent to the $content: [$shortcode_name][child 1/][child 2/][/$shortcode_name]
1578
	 */
1579
	public $fields;
1580
1581
	/**
1582
	 * @var null|string The HTML of the parsed inner "child" shortcodes".  Null for selfclosing shortcodes.
1583
	 */
1584
	public $body;
1585
1586
	/**
1587
	 * @param array       $attributes An associative array of shortcode attributes.  @see shortcode_atts()
1588
	 * @param null|string $content Null for selfclosing shortcodes.  The inner content otherwise.
1589
	 */
1590
	function __construct( $attributes, $content = null ) {
1591
		$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...
1592
		if ( is_array( $content ) ) {
1593
			$string_content = '';
1594
			foreach ( $content as $field ) {
1595
				$string_content .= (string) $field;
1596
			}
1597
1598
			$this->content = $string_content;
1599
		} else {
1600
			$this->content = $content;
1601
		}
1602
1603
		$this->parse_content( $this->content );
1604
	}
1605
1606
	/**
1607
	 * Processes the shortcode's inner content for "child" shortcodes
1608
	 *
1609
	 * @param string $content The shortcode's inner content: [shortcode]$content[/shortcode]
1610
	 */
1611
	function parse_content( $content ) {
1612
		if ( is_null( $content ) ) {
1613
			$this->body = null;
1614
		}
1615
1616
		$this->body = do_shortcode( $content );
1617
	}
1618
1619
	/**
1620
	 * Returns the value of the requested attribute.
1621
	 *
1622
	 * @param string $key The attribute to retrieve
1623
	 * @return mixed
1624
	 */
1625
	function get_attribute( $key ) {
1626
		return isset( $this->attributes[ $key ] ) ? $this->attributes[ $key ] : null;
1627
	}
1628
1629
	function esc_attr( $value ) {
1630
		if ( is_array( $value ) ) {
1631
			return array_map( array( $this, 'esc_attr' ), $value );
1632
		}
1633
1634
		$value = Grunion_Contact_Form_Plugin::strip_tags( $value );
1635
		$value = _wp_specialchars( $value, ENT_QUOTES, false, true );
1636
1637
		// Shortcode attributes can't contain "]"
1638
		$value = str_replace( ']', '', $value );
1639
		$value = str_replace( ',', '&#x002c;', $value ); // store commas encoded
1640
		$value = strtr(
1641
			$value, array(
1642
				'%' => '%25',
1643
				'&' => '%26',
1644
			)
1645
		);
1646
1647
		// shortcode_parse_atts() does stripcslashes()
1648
		$value = addslashes( $value );
1649
		return $value;
1650
	}
1651
1652
	function unesc_attr( $value ) {
1653
		if ( is_array( $value ) ) {
1654
			return array_map( array( $this, 'unesc_attr' ), $value );
1655
		}
1656
1657
		// For back-compat with old Grunion encoding
1658
		// Also, unencode commas
1659
		$value = strtr(
1660
			$value, array(
1661
				'%26' => '&',
1662
				'%25' => '%',
1663
			)
1664
		);
1665
		$value = preg_replace( array( '/&#x0*22;/i', '/&#x0*27;/i', '/&#x0*26;/i', '/&#x0*2c;/i' ), array( '"', "'", '&', ',' ), $value );
1666
		$value = htmlspecialchars_decode( $value, ENT_QUOTES );
1667
		$value = Grunion_Contact_Form_Plugin::strip_tags( $value );
1668
1669
		return $value;
1670
	}
1671
1672
	/**
1673
	 * Generates the shortcode
1674
	 */
1675
	function __toString() {
1676
		$r = "[{$this->shortcode_name} ";
1677
1678
		foreach ( $this->attributes as $key => $value ) {
1679
			if ( ! $value ) {
1680
				continue;
1681
			}
1682
1683
			if ( isset( $this->defaults[ $key ] ) && $this->defaults[ $key ] == $value ) {
1684
				continue;
1685
			}
1686
1687
			if ( 'id' == $key ) {
1688
				continue;
1689
			}
1690
1691
			$value = $this->esc_attr( $value );
1692
1693
			if ( is_array( $value ) ) {
1694
				$value = join( ',', $value );
1695
			}
1696
1697
			if ( false === strpos( $value, "'" ) ) {
1698
				$value = "'$value'";
1699
			} elseif ( false === strpos( $value, '"' ) ) {
1700
				$value = '"' . $value . '"';
1701
			} else {
1702
				// Shortcodes can't contain both '"' and "'".  Strip one.
1703
				$value = str_replace( "'", '', $value );
1704
				$value = "'$value'";
1705
			}
1706
1707
			$r .= "{$key}={$value} ";
1708
		}
1709
1710
		$r = rtrim( $r );
1711
1712
		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...
1713
			$r .= ']';
1714
1715
			foreach ( $this->fields as $field ) {
1716
				$r .= (string) $field;
1717
			}
1718
1719
			$r .= "[/{$this->shortcode_name}]";
1720
		} else {
1721
			$r .= '/]';
1722
		}
1723
1724
		return $r;
1725
	}
1726
}
1727
1728
/**
1729
 * Class for the contact-form shortcode.
1730
 * Parses shortcode to output the contact form as HTML
1731
 * Sends email and stores the contact form response (a.k.a. "feedback")
1732
 */
1733
class Grunion_Contact_Form extends Crunion_Contact_Form_Shortcode {
1734
	public $shortcode_name = 'contact-form';
1735
1736
	/**
1737
	 * @var WP_Error stores form submission errors
1738
	 */
1739
	public $errors;
1740
1741
	/**
1742
	 * @var string The SHA1 hash of the attributes that comprise the form.
1743
	 */
1744
	public $hash;
1745
1746
	/**
1747
	 * @var Grunion_Contact_Form The most recent (inclusive) contact-form shortcode processed
1748
	 */
1749
	static $last;
1750
1751
	/**
1752
	 * @var Whatever form we are currently looking at. If processed, will become $last
1753
	 */
1754
	static $current_form;
1755
1756
	/**
1757
	 * @var array All found forms, indexed by hash.
1758
	 */
1759
	static $forms = array();
1760
1761
	/**
1762
	 * @var bool Whether to print the grunion.css style when processing the contact-form shortcode
1763
	 */
1764
	static $style = false;
1765
1766
	function __construct( $attributes, $content = null ) {
1767
		global $post;
1768
1769
		$this->hash                 = sha1( json_encode( $attributes ) . $content );
1770
		self::$forms[ $this->hash ] = $this;
1771
1772
		// Set up the default subject and recipient for this form
1773
		$default_to      = '';
1774
		$default_subject = '[' . get_option( 'blogname' ) . ']';
1775
1776
		if ( ! isset( $attributes ) || ! is_array( $attributes ) ) {
1777
			$attributes = array();
1778
		}
1779
1780
		if ( ! empty( $attributes['widget'] ) && $attributes['widget'] ) {
1781
			$default_to      .= get_option( 'admin_email' );
1782
			$attributes['id'] = 'widget-' . $attributes['widget'];
1783
			$default_subject  = sprintf( _x( '%1$s Sidebar', '%1$s = blog name', 'jetpack' ), $default_subject );
1784
		} elseif ( $post ) {
1785
			$attributes['id'] = $post->ID;
1786
			$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 ) );
1787
			$post_author      = get_userdata( $post->post_author );
1788
			$default_to      .= $post_author->user_email;
1789
		}
1790
1791
		// Keep reference to $this for parsing form fields
1792
		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...
1793
1794
		$this->defaults = array(
1795
			'to'                 => $default_to,
1796
			'subject'            => $default_subject,
1797
			'show_subject'       => 'no', // only used in back-compat mode
1798
			'widget'             => 0,    // Not exposed to the user. Works with Grunion_Contact_Form_Plugin::widget_atts()
1799
			'id'                 => null, // Not exposed to the user. Set above.
1800
			'submit_button_text' => __( 'Submit', 'jetpack' ),
1801
		);
1802
1803
		$attributes = shortcode_atts( $this->defaults, $attributes, 'contact-form' );
1804
1805
		// We only enable the contact-field shortcode temporarily while processing the contact-form shortcode
1806
		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...
1807
1808
		parent::__construct( $attributes, $content );
1809
1810
		// There were no fields in the contact form. The form was probably just [contact-form /]. Build a default form.
1811
		if ( empty( $this->fields ) ) {
1812
			// same as the original Grunion v1 form
1813
			$default_form = '
1814
				[contact-field label="' . __( 'Name', 'jetpack' ) . '" type="name"  required="true" /]
1815
				[contact-field label="' . __( 'Email', 'jetpack' ) . '" type="email" required="true" /]
1816
				[contact-field label="' . __( 'Website', 'jetpack' ) . '" type="url" /]';
1817
1818
			if ( 'yes' == strtolower( $this->get_attribute( 'show_subject' ) ) ) {
1819
				$default_form .= '
1820
					[contact-field label="' . __( 'Subject', 'jetpack' ) . '" type="subject" /]';
1821
			}
1822
1823
			$default_form .= '
1824
				[contact-field label="' . __( 'Message', 'jetpack' ) . '" type="textarea" /]';
1825
1826
			$this->parse_content( $default_form );
1827
1828
			// Store the shortcode
1829
			$this->store_shortcode( $default_form, $attributes, $this->hash );
1830
		} else {
1831
			// Store the shortcode
1832
			$this->store_shortcode( $content, $attributes, $this->hash );
1833
		}
1834
1835
		// $this->body and $this->fields have been setup.  We no longer need the contact-field shortcode.
1836
		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...
1837
	}
1838
1839
	/**
1840
	 * Store shortcode content for recall later
1841
	 *  - used to receate shortcode when user uses do_shortcode
1842
	 *
1843
	 * @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...
1844
	 * @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...
1845
	 * @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...
1846
	 */
1847
	static function store_shortcode( $content = null, $attributes = null, $hash = null ) {
1848
1849
		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...
1850
1851
			if ( empty( $hash ) ) {
1852
				$hash = sha1( json_encode( $attributes ) . $content );
1853
			}
1854
1855
			$shortcode_meta = get_post_meta( $attributes['id'], "_g_feedback_shortcode_{$hash}", true );
1856
1857
			if ( $shortcode_meta != '' or $shortcode_meta != $content ) {
1858
				update_post_meta( $attributes['id'], "_g_feedback_shortcode_{$hash}", $content );
1859
1860
				// Save attributes to post_meta for later use. They're not available later in do_shortcode situations.
1861
				update_post_meta( $attributes['id'], "_g_feedback_shortcode_atts_{$hash}", $attributes );
1862
			}
1863
		}
1864
	}
1865
1866
	/**
1867
	 * Toggle for printing the grunion.css stylesheet
1868
	 *
1869
	 * @param bool $style
1870
	 */
1871
	static function style( $style ) {
1872
		$previous_style = self::$style;
1873
		self::$style    = (bool) $style;
1874
		return $previous_style;
1875
	}
1876
1877
	/**
1878
	 * Turn on printing of grunion.css stylesheet
1879
	 *
1880
	 * @see ::style()
1881
	 * @internal
1882
	 * @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...
1883
	 */
1884
	static function _style_on() {
1885
		return self::style( true );
1886
	}
1887
1888
	/**
1889
	 * The contact-form shortcode processor
1890
	 *
1891
	 * @param array       $attributes Key => Value pairs as parsed by shortcode_parse_atts()
1892
	 * @param string|null $content The shortcode's inner content: [contact-form]$content[/contact-form]
1893
	 * @return string HTML for the concat form.
1894
	 */
1895
	static function parse( $attributes, $content ) {
1896
		require_once JETPACK__PLUGIN_DIR . '/sync/class.jetpack-sync-settings.php';
1897
		if ( Jetpack_Sync_Settings::is_syncing() ) {
1898
			return '';
1899
		}
1900
		// Create a new Grunion_Contact_Form object (this class)
1901
		$form = new Grunion_Contact_Form( $attributes, $content );
1902
1903
		$id = $form->get_attribute( 'id' );
1904
1905
		if ( ! $id ) { // something terrible has happened
1906
			return '[contact-form]';
1907
		}
1908
1909
		if ( is_feed() ) {
1910
			return '[contact-form]';
1911
		}
1912
1913
		self::$last = $form;
1914
1915
		// Enqueue the grunion.css stylesheet if self::$style allows it
1916
		if ( self::$style && ( empty( $_REQUEST['action'] ) || $_REQUEST['action'] != 'grunion_shortcode_to_json' ) ) {
1917
			// Enqueue the style here instead of printing it, because if some other plugin has run the_post()+rewind_posts(),
1918
			// (like VideoPress does), the style tag gets "printed" the first time and discarded, leaving the contact form unstyled.
1919
			// when WordPress does the real loop.
1920
			wp_enqueue_style( 'grunion.css' );
1921
		}
1922
1923
		$r  = '';
1924
		$r .= "<div id='contact-form-$id'>\n";
1925
1926
		if ( is_wp_error( $form->errors ) && $form->errors->get_error_codes() ) {
1927
			// There are errors.  Display them
1928
			$r .= "<div class='form-error'>\n<h3>" . __( 'Error!', 'jetpack' ) . "</h3>\n<ul class='form-errors'>\n";
1929
			foreach ( $form->errors->get_error_messages() as $message ) {
1930
				$r .= "\t<li class='form-error-message'>" . esc_html( $message ) . "</li>\n";
1931
			}
1932
			$r .= "</ul>\n</div>\n\n";
1933
		}
1934
1935
		if ( isset( $_GET['contact-form-id'] )
1936
		     && $_GET['contact-form-id'] == self::$last->get_attribute( 'id' )
1937
		     && isset( $_GET['contact-form-sent'], $_GET['contact-form-hash'] )
1938
		     && hash_equals( $form->hash, $_GET['contact-form-hash'] ) ) {
1939
			// The contact form was submitted.  Show the success message/results
1940
			$feedback_id = (int) $_GET['contact-form-sent'];
1941
1942
			$back_url = remove_query_arg( array( 'contact-form-id', 'contact-form-sent', '_wpnonce' ) );
1943
1944
			$r_success_message =
1945
				'<h3>' . __( 'Message Sent', 'jetpack' ) .
1946
				' (<a href="' . esc_url( $back_url ) . '">' . esc_html__( 'go back', 'jetpack' ) . '</a>)' .
1947
				"</h3>\n\n";
1948
1949
			// Don't show the feedback details unless the nonce matches
1950
			if ( $feedback_id && wp_verify_nonce( stripslashes( $_GET['_wpnonce'] ), "contact-form-sent-{$feedback_id}" ) ) {
1951
				$r_success_message .= self::success_message( $feedback_id, $form );
1952
			}
1953
1954
			/**
1955
			 * Filter the message returned after a successful contact form submission.
1956
			 *
1957
			 * @module contact-form
1958
			 *
1959
			 * @since 1.3.1
1960
			 *
1961
			 * @param string $r_success_message Success message.
1962
			 */
1963
			$r .= apply_filters( 'grunion_contact_form_success_message', $r_success_message );
1964
		} else {
1965
			// Nothing special - show the normal contact form
1966
			if ( $form->get_attribute( 'widget' ) ) {
1967
				// Submit form to the current URL
1968
				$url = remove_query_arg( array( 'contact-form-id', 'contact-form-sent', 'action', '_wpnonce' ) );
1969
			} else {
1970
				// Submit form to the post permalink
1971
				$url = get_permalink();
1972
			}
1973
1974
			// For SSL/TLS page. See RFC 3986 Section 4.2
1975
			$url = set_url_scheme( $url );
1976
1977
			// May eventually want to send this to admin-post.php...
1978
			/**
1979
			 * Filter the contact form action URL.
1980
			 *
1981
			 * @module contact-form
1982
			 *
1983
			 * @since 1.3.1
1984
			 *
1985
			 * @param string $contact_form_id Contact form post URL.
1986
			 * @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...
1987
			 * @param int $id Contact Form ID.
1988
			 */
1989
			$url = apply_filters( 'grunion_contact_form_form_action', "{$url}#contact-form-{$id}", $GLOBALS['post'], $id );
1990
1991
			$r .= "<form action='" . esc_url( $url ) . "' method='post' class='contact-form commentsblock'>\n";
1992
			$r .= $form->body;
1993
			$r .= "\t<p class='contact-submit'>\n";
1994
1995
			/**
1996
			 * Filter the contact form submit button class attribute.
1997
			 *
1998
			 * @module contact-form
1999
			 *
2000
			 * @since 6.6.0
2001
			 *
2002
			 * @param string $class Additional CSS classes for button attribute.
2003
			 */
2004
			$submit_button_class = apply_filters( 'jetpack_contact_form_submit_button_class', 'pushbutton-wide' );
2005
2006
			$r .= "\t\t<input type='submit' value='" . esc_attr( $form->get_attribute( 'submit_button_text' ) ) . "' class='" . $submit_button_class . "'/>\n";
2007
			if ( is_user_logged_in() ) {
2008
				$r .= "\t\t" . wp_nonce_field( 'contact-form_' . $id, '_wpnonce', true, false ) . "\n"; // nonce and referer
2009
			}
2010
			$r .= "\t\t<input type='hidden' name='contact-form-id' value='$id' />\n";
2011
			$r .= "\t\t<input type='hidden' name='action' value='grunion-contact-form' />\n";
2012
			$r .= "\t\t<input type='hidden' name='contact-form-hash' value='" . esc_attr( $form->hash ) . "' />\n";
2013
			$r .= "\t</p>\n";
2014
			$r .= "</form>\n";
2015
		}
2016
2017
		$r .= '</div>';
2018
2019
		return $r;
2020
	}
2021
2022
	/**
2023
	 * Returns a success message to be returned if the form is sent via AJAX.
2024
	 *
2025
	 * @param int                         $feedback_id
2026
	 * @param object Grunion_Contact_Form $form
2027
	 *
2028
	 * @return string $message
2029
	 */
2030
	static function success_message( $feedback_id, $form ) {
2031
		return wp_kses(
2032
			'<blockquote class="contact-form-submission">'
2033
			. '<p>' . join( self::get_compiled_form( $feedback_id, $form ), '</p><p>' ) . '</p>'
2034
			. '</blockquote>',
2035
			array(
2036
				'br'         => array(),
2037
				'blockquote' => array( 'class' => array() ),
2038
				'p'          => array(),
2039
			)
2040
		);
2041
	}
2042
2043
	/**
2044
	 * Returns a compiled form with labels and values in a form of  an array
2045
	 * of lines.
2046
	 *
2047
	 * @param int                         $feedback_id
2048
	 * @param object Grunion_Contact_Form $form
2049
	 *
2050
	 * @return array $lines
2051
	 */
2052
	static function get_compiled_form( $feedback_id, $form ) {
2053
		$feedback       = get_post( $feedback_id );
2054
		$field_ids      = $form->get_field_ids();
2055
		$content_fields = Grunion_Contact_Form_Plugin::parse_fields_from_content( $feedback_id );
2056
2057
		// Maps field_ids to post_meta keys
2058
		$field_value_map = array(
2059
			'name'     => 'author',
2060
			'email'    => 'author_email',
2061
			'url'      => 'author_url',
2062
			'subject'  => 'subject',
2063
			'textarea' => false, // not a post_meta key.  This is stored in post_content
2064
		);
2065
2066
		$compiled_form = array();
2067
2068
		// "Standard" field whitelist
2069
		foreach ( $field_value_map as $type => $meta_key ) {
2070
			if ( isset( $field_ids[ $type ] ) ) {
2071
				$field = $form->fields[ $field_ids[ $type ] ];
2072
2073
				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...
2074
					if ( isset( $content_fields[ "_feedback_{$meta_key}" ] ) ) {
2075
						$value = $content_fields[ "_feedback_{$meta_key}" ];
2076
					}
2077
				} else {
2078
					// The feedback content is stored as the first "half" of post_content
2079
					$value         = $feedback->post_content;
2080
					list( $value ) = explode( '<!--more-->', $value );
2081
					$value         = trim( $value );
2082
				}
2083
2084
				$field_index                   = array_search( $field_ids[ $type ], $field_ids['all'] );
2085
				$compiled_form[ $field_index ] = sprintf(
2086
					'<b>%1$s:</b> %2$s<br /><br />',
2087
					wp_kses( $field->get_attribute( 'label' ), array() ),
2088
					self::escape_and_sanitize_field_value( $value )
0 ignored issues
show
Bug introduced by
The variable $value does not seem to be defined for all execution paths leading up to this point.

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

Let’s take a look at an example:

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

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

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

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

Available Fixes

  1. Check for existence of the variable explicitly:

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

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

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
2089
				);
2090
			}
2091
		}
2092
2093
		// "Non-standard" fields
2094
		if ( $field_ids['extra'] ) {
2095
			// array indexed by field label (not field id)
2096
			$extra_fields = get_post_meta( $feedback_id, '_feedback_extra_fields', true );
2097
2098
			/**
2099
			 * Only get data for the compiled form if `$extra_fields` is a valid and non-empty array.
2100
			 */
2101
			if ( is_array( $extra_fields ) && ! empty( $extra_fields ) ) {
2102
2103
				$extra_field_keys = array_keys( $extra_fields );
2104
2105
				$i = 0;
2106
				foreach ( $field_ids['extra'] as $field_id ) {
2107
					$field       = $form->fields[ $field_id ];
2108
					$field_index = array_search( $field_id, $field_ids['all'] );
2109
2110
					$label = $field->get_attribute( 'label' );
2111
2112
					$compiled_form[ $field_index ] = sprintf(
2113
						'<b>%1$s:</b> %2$s<br /><br />',
2114
						wp_kses( $label, array() ),
2115
						self::escape_and_sanitize_field_value( $extra_fields[ $extra_field_keys[ $i ] ] )
2116
					);
2117
2118
					$i++;
2119
				}
2120
			}
2121
		}
2122
2123
		// Sorting lines by the field index
2124
		ksort( $compiled_form );
2125
2126
		return $compiled_form;
2127
	}
2128
2129
	static function escape_and_sanitize_field_value( $value ) {
2130
        $value = str_replace( array( '[' , ']' ) ,  array( '&#91;' , '&#93;' ) , $value );
2131
        return nl2br( wp_kses( $value, array() ) );
2132
    }
2133
2134
	/**
2135
	 * Only strip out empty string values and keep all the other values as they are.
2136
     *
2137
	 * @param $single_value
2138
	 *
2139
	 * @return bool
2140
	 */
2141
	static function remove_empty( $single_value ) {
2142
		return ( $single_value !== '' );
2143
	}
2144
2145
	/**
2146
	 * The contact-field shortcode processor
2147
	 * 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.
2148
	 *
2149
	 * @param array       $attributes Key => Value pairs as parsed by shortcode_parse_atts()
2150
	 * @param string|null $content The shortcode's inner content: [contact-field]$content[/contact-field]
2151
	 * @return HTML for the contact form field
2152
	 */
2153
	static function parse_contact_field( $attributes, $content ) {
2154
		// Don't try to parse contact form fields if not inside a contact form
2155
		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...
2156
			$att_strs = array();
2157
			if ( ! isset( $attributes['label'] )  ) {
2158
				$type = isset( $attributes['type'] ) ? $attributes['type'] : null;
2159
				$attributes['label'] = self::get_default_label_from_type( $type );
2160
			}
2161
			foreach ( $attributes as $att => $val ) {
2162
				if ( is_numeric( $att ) ) { // Is a valueless attribute
2163
					$att_strs[] = esc_html( $val );
2164
				} elseif ( isset( $val ) ) { // A regular attr - value pair
2165
					if ( ( $att === 'options' || $att === 'values' ) && is_string( $val ) ) { // remove any empty strings
2166
						$val = explode( ',', $val );
2167
					}
2168
 					if ( is_array( $val ) ) {
2169
						$val =  array_filter( $val, array( __CLASS__, 'remove_empty' ) ); // removes any empty strings
2170
						$att_strs[] = esc_html( $att ) . '="' . implode( ',', array_map( 'esc_html', $val ) ) . '"';
2171
					} elseif ( is_bool( $val ) ) {
2172
						$att_strs[] = esc_html( $att ) . '="' . esc_html( $val ? '1' : '' ) . '"';
2173
					} else {
2174
						$att_strs[] = esc_html( $att ) . '="' . esc_html( $val ) . '"';
2175
					}
2176
				}
2177
			}
2178
2179
			$html = '[contact-field ' . implode( ' ', $att_strs );
2180
2181
			if ( isset( $content ) && ! empty( $content ) ) { // If there is content, let's add a closing tag
2182
				$html .= ']' . esc_html( $content ) . '[/contact-field]';
2183
			} else { // Otherwise let's add a closing slash in the first tag
2184
				$html .= '/]';
2185
			}
2186
2187
			return $html;
2188
		}
2189
2190
		$form = Grunion_Contact_Form::$current_form;
2191
2192
		$field = new Grunion_Contact_Form_Field( $attributes, $content, $form );
2193
2194
		$field_id = $field->get_attribute( 'id' );
2195
		if ( $field_id ) {
2196
			$form->fields[ $field_id ] = $field;
2197
		} else {
2198
			$form->fields[] = $field;
2199
		}
2200
2201
		if (
2202
			isset( $_POST['action'] ) && 'grunion-contact-form' === $_POST['action']
2203
			&&
2204
			isset( $_POST['contact-form-id'] ) && $form->get_attribute( 'id' ) == $_POST['contact-form-id']
2205
			&&
2206
			isset( $_POST['contact-form-hash'] ) && hash_equals( $form->hash, $_POST['contact-form-hash'] )
2207
		) {
2208
			// If we're processing a POST submission for this contact form, validate the field value so we can show errors as necessary.
2209
			$field->validate();
2210
		}
2211
2212
		// Output HTML
2213
		return $field->render();
2214
	}
2215
2216
	static function get_default_label_from_type( $type ) {
2217
		switch ( $type ) {
2218
			case 'text':
2219
				return __( 'Text', 'jetpack' );
2220
			case 'name':
2221
				return __( 'Name', 'jetpack' );
2222
			case 'email':
2223
				return __( 'Email', 'jetpack' );
2224
			case 'url':
2225
				return __( 'Website', 'jetpack' );
2226
			case 'date':
2227
				return __( 'Date', 'jetpack' );
2228
			case 'telephone':
2229
				return __( 'Phone', 'jetpack' );
2230
			case 'textarea':
2231
				return __( 'Message', 'jetpack' );
2232
			case 'checkbox':
2233
				return __( 'Checkbox', 'jetpack' );
2234
			case 'checkbox-multiple':
2235
				return __( 'Choose several', 'jetpack' );
2236
			case 'radio':
2237
				return __( 'Choose one', 'jetpack' );
2238
			case 'select':
2239
				return __( 'Select one', 'jetpack' );
2240
			default:
2241
				return null;
2242
		}
2243
	}
2244
2245
	/**
2246
	 * Loops through $this->fields to generate a (structured) list of field IDs.
2247
	 *
2248
	 * Important: Currently the whitelisted fields are defined as follows:
2249
	 *  `name`, `email`, `url`, `subject`, `textarea`
2250
	 *
2251
	 * If you need to add new fields to the Contact Form, please don't add them
2252
	 * to the whitelisted fields and leave them as extra fields.
2253
	 *
2254
	 * The reasoning behind this is that both the admin Feedback view and the CSV
2255
	 * export will not include any fields that are added to the list of
2256
	 * whitelisted fields without taking proper care to add them to all the
2257
	 * other places where they accessed/used/saved.
2258
	 *
2259
	 * The safest way to add new fields is to add them to the dropdown and the
2260
	 * HTML list ( @see Grunion_Contact_Form_Field::render ) and don't add them
2261
	 * to the list of whitelisted fields. This way they will become a part of the
2262
	 * `extra fields` which are saved in the post meta and will be properly
2263
	 * handled by the admin Feedback view and the CSV Export without any extra
2264
	 * work.
2265
	 *
2266
	 * If there is need to add a field to the whitelisted fields, then please
2267
	 * take proper care to add logic to handle the field in the following places:
2268
	 *
2269
	 *  - Below in the switch statement - so the field is recognized as whitelisted.
2270
	 *
2271
	 *  - Grunion_Contact_Form::process_submission - validation and logic.
2272
	 *
2273
	 *  - Grunion_Contact_Form::process_submission - add the field as an additional
2274
	 *      field in the `post_content` when saving the feedback content.
2275
	 *
2276
	 *  - Grunion_Contact_Form_Plugin::parse_fields_from_content - add mapping
2277
	 *      for the field, defined in the above method.
2278
	 *
2279
	 *  - Grunion_Contact_Form_Plugin::map_parsed_field_contents_of_post_to_field_names -
2280
	 *      add mapping of the field for the CSV Export. Otherwise it will be missing
2281
	 *      from the exported data.
2282
	 *
2283
	 *  - admin.php / grunion_manage_post_columns - add the field to the render logic.
2284
	 *      Otherwise it will be missing from the admin Feedback view.
2285
	 *
2286
	 * @return array
2287
	 */
2288
	function get_field_ids() {
2289
		$field_ids = array(
2290
			'all'   => array(), // array of all field_ids
2291
			'extra' => array(), // array of all non-whitelisted field IDs
2292
2293
			// Whitelisted "standard" field IDs:
2294
			// 'email'    => field_id,
2295
			// 'name'     => field_id,
2296
			// 'url'      => field_id,
2297
			// 'subject'  => field_id,
2298
			// 'textarea' => field_id,
2299
		);
2300
2301
		foreach ( $this->fields as $id => $field ) {
2302
			$field_ids['all'][] = $id;
2303
2304
			$type = $field->get_attribute( 'type' );
2305
			if ( isset( $field_ids[ $type ] ) ) {
2306
				// This type of field is already present in our whitelist of "standard" fields for this form
2307
				// Put it in extra
2308
				$field_ids['extra'][] = $id;
2309
				continue;
2310
			}
2311
2312
			/**
2313
			 * See method description before modifying the switch cases.
2314
			 */
2315
			switch ( $type ) {
2316
				case 'email':
2317
				case 'name':
2318
				case 'url':
2319
				case 'subject':
2320
				case 'textarea':
2321
					$field_ids[ $type ] = $id;
2322
					break;
2323
				default:
2324
					// Put everything else in extra
2325
					$field_ids['extra'][] = $id;
2326
			}
2327
		}
2328
2329
		return $field_ids;
2330
	}
2331
2332
	/**
2333
	 * Process the contact form's POST submission
2334
	 * Stores feedback.  Sends email.
2335
	 */
2336
	function process_submission() {
2337
		global $post;
2338
2339
		$plugin = Grunion_Contact_Form_Plugin::init();
2340
2341
		$id     = $this->get_attribute( 'id' );
2342
		$to     = $this->get_attribute( 'to' );
2343
		$widget = $this->get_attribute( 'widget' );
2344
2345
		$contact_form_subject = $this->get_attribute( 'subject' );
2346
2347
		$to     = str_replace( ' ', '', $to );
2348
		$emails = explode( ',', $to );
2349
2350
		$valid_emails = array();
2351
2352
		foreach ( (array) $emails as $email ) {
2353
			if ( ! is_email( $email ) ) {
2354
				continue;
2355
			}
2356
2357
			if ( function_exists( 'is_email_address_unsafe' ) && is_email_address_unsafe( $email ) ) {
2358
				continue;
2359
			}
2360
2361
			$valid_emails[] = $email;
2362
		}
2363
2364
		// No one to send it to, which means none of the "to" attributes are valid emails.
2365
		// Use default email instead.
2366
		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...
2367
			$valid_emails = $this->defaults['to'];
2368
		}
2369
2370
		$to = $valid_emails;
2371
2372
		// Last ditch effort to set a recipient if somehow none have been set.
2373
		if ( empty( $to ) ) {
2374
			$to = get_option( 'admin_email' );
2375
		}
2376
2377
		// Make sure we're processing the form we think we're processing... probably a redundant check.
2378
		if ( $widget ) {
2379
			if ( 'widget-' . $widget != $_POST['contact-form-id'] ) {
2380
				return false;
2381
			}
2382
		} else {
2383
			if ( $post->ID != $_POST['contact-form-id'] ) {
2384
				return false;
2385
			}
2386
		}
2387
2388
		$field_ids = $this->get_field_ids();
2389
2390
		// Initialize all these "standard" fields to null
2391
		$comment_author_email = $comment_author_email_label = // v
2392
		$comment_author       = $comment_author_label       = // v
2393
		$comment_author_url   = $comment_author_url_label   = // v
2394
		$comment_content      = $comment_content_label = null;
2395
2396
		// For each of the "standard" fields, grab their field label and value.
2397 View Code Duplication
		if ( isset( $field_ids['name'] ) ) {
2398
			$field          = $this->fields[ $field_ids['name'] ];
2399
			$comment_author = Grunion_Contact_Form_Plugin::strip_tags(
2400
				stripslashes(
2401
					/** This filter is already documented in core/wp-includes/comment-functions.php */
2402
					apply_filters( 'pre_comment_author_name', addslashes( $field->value ) )
2403
				)
2404
			);
2405
			$comment_author_label = Grunion_Contact_Form_Plugin::strip_tags( $field->get_attribute( 'label' ) );
2406
		}
2407
2408 View Code Duplication
		if ( isset( $field_ids['email'] ) ) {
2409
			$field                = $this->fields[ $field_ids['email'] ];
2410
			$comment_author_email = Grunion_Contact_Form_Plugin::strip_tags(
2411
				stripslashes(
2412
					/** This filter is already documented in core/wp-includes/comment-functions.php */
2413
					apply_filters( 'pre_comment_author_email', addslashes( $field->value ) )
2414
				)
2415
			);
2416
			$comment_author_email_label = Grunion_Contact_Form_Plugin::strip_tags( $field->get_attribute( 'label' ) );
2417
		}
2418
2419
		if ( isset( $field_ids['url'] ) ) {
2420
			$field              = $this->fields[ $field_ids['url'] ];
2421
			$comment_author_url = Grunion_Contact_Form_Plugin::strip_tags(
2422
				stripslashes(
2423
					/** This filter is already documented in core/wp-includes/comment-functions.php */
2424
					apply_filters( 'pre_comment_author_url', addslashes( $field->value ) )
2425
				)
2426
			);
2427
			if ( 'http://' == $comment_author_url ) {
2428
				$comment_author_url = '';
2429
			}
2430
			$comment_author_url_label = Grunion_Contact_Form_Plugin::strip_tags( $field->get_attribute( 'label' ) );
2431
		}
2432
2433
		if ( isset( $field_ids['textarea'] ) ) {
2434
			$field                 = $this->fields[ $field_ids['textarea'] ];
2435
			$comment_content       = trim( Grunion_Contact_Form_Plugin::strip_tags( $field->value ) );
2436
			$comment_content_label = Grunion_Contact_Form_Plugin::strip_tags( $field->get_attribute( 'label' ) );
2437
		}
2438
2439
		if ( isset( $field_ids['subject'] ) ) {
2440
			$field = $this->fields[ $field_ids['subject'] ];
2441
			if ( $field->value ) {
2442
				$contact_form_subject = Grunion_Contact_Form_Plugin::strip_tags( $field->value );
2443
			}
2444
		}
2445
2446
		$all_values = $extra_values = array();
2447
		$i          = 1; // Prefix counter for stored metadata
2448
2449
		// For all fields, grab label and value
2450
		foreach ( $field_ids['all'] as $field_id ) {
2451
			$field = $this->fields[ $field_id ];
2452
			$label = $i . '_' . $field->get_attribute( 'label' );
2453
			$value = $field->value;
2454
2455
			$all_values[ $label ] = $value;
2456
			$i++; // Increment prefix counter for the next field
2457
		}
2458
2459
		// For the "non-standard" fields, grab label and value
2460
		// Extra fields have their prefix starting from count( $all_values ) + 1
2461
		foreach ( $field_ids['extra'] as $field_id ) {
2462
			$field = $this->fields[ $field_id ];
2463
			$label = $i . '_' . $field->get_attribute( 'label' );
2464
			$value = $field->value;
2465
2466
			if ( is_array( $value ) ) {
2467
				$value = implode( ', ', $value );
2468
			}
2469
2470
			$extra_values[ $label ] = $value;
2471
			$i++; // Increment prefix counter for the next extra field
2472
		}
2473
2474
		$contact_form_subject = trim( $contact_form_subject );
2475
2476
		$comment_author_IP = Grunion_Contact_Form_Plugin::get_ip_address();
2477
2478
		$vars = array( 'comment_author', 'comment_author_email', 'comment_author_url', 'contact_form_subject', 'comment_author_IP' );
2479
		foreach ( $vars as $var ) {
2480
			$$var = str_replace( array( "\n", "\r" ), '', $$var );
2481
		}
2482
2483
		// Ensure that Akismet gets all of the relevant information from the contact form,
2484
		// not just the textarea field and predetermined subject.
2485
		$akismet_vars                    = compact( $vars );
2486
		$akismet_vars['comment_content'] = $comment_content;
2487
2488
		foreach ( array_merge( $field_ids['all'], $field_ids['extra'] ) as $field_id ) {
2489
			$field = $this->fields[ $field_id ];
2490
2491
			// Skip any fields that are just a choice from a pre-defined list. They wouldn't have any value
2492
			// from a spam-filtering point of view.
2493
			if ( in_array( $field->get_attribute( 'type' ), array( 'select', 'checkbox', 'checkbox-multiple', 'radio' ) ) ) {
2494
				continue;
2495
			}
2496
2497
			// Normalize the label into a slug.
2498
			$field_slug = trim( // Strip all leading/trailing dashes.
2499
				preg_replace(   // Normalize everything to a-z0-9_-
2500
					'/[^a-z0-9_]+/',
2501
					'-',
2502
					strtolower( $field->get_attribute( 'label' ) ) // Lowercase
2503
				),
2504
				'-'
2505
			);
2506
2507
			$field_value = ( is_array( $field->value ) ) ? trim( implode( ', ', $field->value ) ) : trim( $field->value );
2508
2509
			// Skip any values that are already in the array we're sending.
2510
			if ( $field_value && in_array( $field_value, $akismet_vars ) ) {
2511
				continue;
2512
			}
2513
2514
			$akismet_vars[ 'contact_form_field_' . $field_slug ] = $field_value;
2515
		}
2516
2517
		$spam           = '';
2518
		$akismet_values = $plugin->prepare_for_akismet( $akismet_vars );
2519
2520
		// Is it spam?
2521
		/** This filter is already documented in modules/contact-form/admin.php */
2522
		$is_spam = apply_filters( 'jetpack_contact_form_is_spam', false, $akismet_values );
2523
		if ( is_wp_error( $is_spam ) ) { // WP_Error to abort
2524
			return $is_spam; // abort
2525
		} elseif ( $is_spam === true ) {  // TRUE to flag a spam
2526
			$spam = '***SPAM*** ';
2527
		}
2528
2529
		if ( ! $comment_author ) {
2530
			$comment_author = $comment_author_email;
2531
		}
2532
2533
		/**
2534
		 * Filter the email where a submitted feedback is sent.
2535
		 *
2536
		 * @module contact-form
2537
		 *
2538
		 * @since 1.3.1
2539
		 *
2540
		 * @param string|array $to Array of valid email addresses, or single email address.
2541
		 */
2542
		$to            = (array) apply_filters( 'contact_form_to', $to );
2543
		$reply_to_addr = $to[0]; // get just the address part before the name part is added
2544
2545
		foreach ( $to as $to_key => $to_value ) {
2546
			$to[ $to_key ] = Grunion_Contact_Form_Plugin::strip_tags( $to_value );
2547
			$to[ $to_key ] = self::add_name_to_address( $to_value );
2548
		}
2549
2550
		$blog_url        = parse_url( site_url() );
2551
		$from_email_addr = 'wordpress@' . $blog_url['host'];
2552
2553
		if ( ! empty( $comment_author_email ) ) {
2554
			$reply_to_addr = $comment_author_email;
2555
		}
2556
2557
		$headers = 'From: "' . $comment_author . '" <' . $from_email_addr . ">\r\n" .
2558
		           'Reply-To: "' . $comment_author . '" <' . $reply_to_addr . ">\r\n";
2559
2560
		// Build feedback reference
2561
		$feedback_time  = current_time( 'mysql' );
2562
		$feedback_title = "{$comment_author} - {$feedback_time}";
2563
		$feedback_id    = md5( $feedback_title );
2564
2565
		$all_values = array_merge(
2566
			$all_values, array(
2567
				'entry_title'     => the_title_attribute( 'echo=0' ),
2568
				'entry_permalink' => esc_url( get_permalink( get_the_ID() ) ),
2569
				'feedback_id'     => $feedback_id,
2570
			)
2571
		);
2572
2573
		/** This filter is already documented in modules/contact-form/admin.php */
2574
		$subject = apply_filters( 'contact_form_subject', $contact_form_subject, $all_values );
2575
		$url     = $widget ? home_url( '/' ) : get_permalink( $post->ID );
2576
2577
		$date_time_format = _x( '%1$s \a\t %2$s', '{$date_format} \a\t {$time_format}', 'jetpack' );
2578
		$date_time_format = sprintf( $date_time_format, get_option( 'date_format' ), get_option( 'time_format' ) );
2579
		$time             = date_i18n( $date_time_format, current_time( 'timestamp' ) );
2580
2581
		// keep a copy of the feedback as a custom post type
2582
		$feedback_status = $is_spam === true ? 'spam' : 'publish';
2583
2584
		foreach ( (array) $akismet_values as $av_key => $av_value ) {
2585
			$akismet_values[ $av_key ] = Grunion_Contact_Form_Plugin::strip_tags( $av_value );
2586
		}
2587
2588
		foreach ( (array) $all_values as $all_key => $all_value ) {
2589
			$all_values[ $all_key ] = Grunion_Contact_Form_Plugin::strip_tags( $all_value );
2590
		}
2591
2592
		foreach ( (array) $extra_values as $ev_key => $ev_value ) {
2593
			$extra_values[ $ev_key ] = Grunion_Contact_Form_Plugin::strip_tags( $ev_value );
2594
		}
2595
2596
		/*
2597
		 We need to make sure that the post author is always zero for contact
2598
		 * form submissions.  This prevents export/import from trying to create
2599
		 * new users based on form submissions from people who were logged in
2600
		 * at the time.
2601
		 *
2602
		 * Unfortunately wp_insert_post() tries very hard to make sure the post
2603
		 * author gets the currently logged in user id.  That is how we ended up
2604
		 * with this work around. */
2605
		add_filter( 'wp_insert_post_data', array( $plugin, 'insert_feedback_filter' ), 10, 2 );
2606
2607
		$post_id = wp_insert_post(
2608
			array(
2609
				'post_date'    => addslashes( $feedback_time ),
2610
				'post_type'    => 'feedback',
2611
				'post_status'  => addslashes( $feedback_status ),
2612
				'post_parent'  => (int) $post->ID,
2613
				'post_title'   => addslashes( wp_kses( $feedback_title, array() ) ),
2614
				'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
2615
				'post_name'    => $feedback_id,
2616
			)
2617
		);
2618
2619
		// once insert has finished we don't need this filter any more
2620
		remove_filter( 'wp_insert_post_data', array( $plugin, 'insert_feedback_filter' ), 10 );
2621
2622
		update_post_meta( $post_id, '_feedback_extra_fields', $this->addslashes_deep( $extra_values ) );
2623
2624
		if ( 'publish' == $feedback_status ) {
2625
			// Increase count of unread feedback.
2626
			$unread = get_option( 'feedback_unread_count', 0 ) + 1;
2627
			update_option( 'feedback_unread_count', $unread );
2628
		}
2629
2630
		if ( defined( 'AKISMET_VERSION' ) ) {
2631
			update_post_meta( $post_id, '_feedback_akismet_values', $this->addslashes_deep( $akismet_values ) );
2632
		}
2633
2634
		$message = self::get_compiled_form( $post_id, $this );
2635
2636
		array_push(
2637
			$message,
2638
			'<br />',
2639
			'<hr />',
2640
			__( 'Time:', 'jetpack' ) . ' ' . $time . '<br />',
2641
			__( 'IP Address:', 'jetpack' ) . ' ' . $comment_author_IP . '<br />',
2642
			__( 'Contact Form URL:', 'jetpack' ) . ' ' . $url . '<br />'
2643
		);
2644
2645
		if ( is_user_logged_in() ) {
2646
			array_push(
2647
				$message,
2648
				sprintf(
2649
					'<p>' . __( 'Sent by a verified %s user.', 'jetpack' ) . '</p>',
2650
					isset( $GLOBALS['current_site']->site_name ) && $GLOBALS['current_site']->site_name ?
2651
						$GLOBALS['current_site']->site_name : '"' . get_option( 'blogname' ) . '"'
2652
				)
2653
			);
2654
		} else {
2655
			array_push( $message, '<p>' . __( 'Sent by an unverified visitor to your site.', 'jetpack' ) . '</p>' );
2656
		}
2657
2658
		$message = join( $message, '' );
2659
2660
		/**
2661
		 * Filters the message sent via email after a successful form submission.
2662
		 *
2663
		 * @module contact-form
2664
		 *
2665
		 * @since 1.3.1
2666
		 *
2667
		 * @param string $message Feedback email message.
2668
		 */
2669
		$message = apply_filters( 'contact_form_message', $message );
2670
2671
		// This is called after `contact_form_message`, in order to preserve back-compat
2672
		$message = self::wrap_message_in_html_tags( $message );
2673
2674
		update_post_meta( $post_id, '_feedback_email', $this->addslashes_deep( compact( 'to', 'message' ) ) );
2675
2676
		/**
2677
		 * Fires right before the contact form message is sent via email to
2678
		 * the recipient specified in the contact form.
2679
		 *
2680
		 * @module contact-form
2681
		 *
2682
		 * @since 1.3.1
2683
		 *
2684
		 * @param integer $post_id Post contact form lives on
2685
		 * @param array $all_values Contact form fields
2686
		 * @param array $extra_values Contact form fields not included in $all_values
2687
		 */
2688
		do_action( 'grunion_pre_message_sent', $post_id, $all_values, $extra_values );
2689
2690
		// schedule deletes of old spam feedbacks
2691
		if ( ! wp_next_scheduled( 'grunion_scheduled_delete' ) ) {
2692
			wp_schedule_event( time() + 250, 'daily', 'grunion_scheduled_delete' );
2693
		}
2694
2695
		if (
2696
			$is_spam !== true &&
2697
			/**
2698
			 * Filter to choose whether an email should be sent after each successful contact form submission.
2699
			 *
2700
			 * @module contact-form
2701
			 *
2702
			 * @since 2.6.0
2703
			 *
2704
			 * @param bool true Should an email be sent after a form submission. Default to true.
2705
			 * @param int $post_id Post ID.
2706
			 */
2707
			true === apply_filters( 'grunion_should_send_email', true, $post_id )
2708
		) {
2709
			self::wp_mail( $to, "{$spam}{$subject}", $message, $headers );
2710
		} elseif (
2711
			true === $is_spam &&
2712
			/**
2713
			 * Choose whether an email should be sent for each spam contact form submission.
2714
			 *
2715
			 * @module contact-form
2716
			 *
2717
			 * @since 1.3.1
2718
			 *
2719
			 * @param bool false Should an email be sent after a spam form submission. Default to false.
2720
			 */
2721
			apply_filters( 'grunion_still_email_spam', false ) == true
2722
		) { // don't send spam by default.  Filterable.
2723
			self::wp_mail( $to, "{$spam}{$subject}", $message, $headers );
2724
		}
2725
2726
		if ( defined( 'DOING_AJAX' ) && DOING_AJAX ) {
2727
			return self::success_message( $post_id, $this );
2728
		}
2729
2730
		$redirect = wp_get_referer();
2731
		if ( ! $redirect ) { // wp_get_referer() returns false if the referer is the same as the current page
2732
			$redirect = $_SERVER['REQUEST_URI'];
2733
		}
2734
2735
		$redirect = add_query_arg(
2736
			urlencode_deep(
2737
				array(
2738
					'contact-form-id'   => $id,
2739
					'contact-form-sent' => $post_id,
2740
					'contact-form-hash' => $this->hash,
2741
					'_wpnonce'          => wp_create_nonce( "contact-form-sent-{$post_id}" ), // wp_nonce_url HTMLencodes :(
2742
				)
2743
			), $redirect
2744
		);
2745
2746
		/**
2747
		 * Filter the URL where the reader is redirected after submitting a form.
2748
		 *
2749
		 * @module contact-form
2750
		 *
2751
		 * @since 1.9.0
2752
		 *
2753
		 * @param string $redirect Post submission URL.
2754
		 * @param int $id Contact Form ID.
2755
		 * @param int $post_id Post ID.
2756
		 */
2757
		$redirect = apply_filters( 'grunion_contact_form_redirect_url', $redirect, $id, $post_id );
2758
2759
		wp_safe_redirect( $redirect );
2760
		exit;
2761
	}
2762
2763
	/**
2764
	 * Wrapper for wp_mail() that enables HTML messages with text alternatives
2765
	 *
2766
	 * @param string|array $to          Array or comma-separated list of email addresses to send message.
2767
	 * @param string       $subject     Email subject.
2768
	 * @param string       $message     Message contents.
2769
	 * @param string|array $headers     Optional. Additional headers.
2770
	 * @param string|array $attachments Optional. Files to attach.
2771
	 *
2772
	 * @return bool Whether the email contents were sent successfully.
2773
	 */
2774
	public static function wp_mail( $to, $subject, $message, $headers = '', $attachments = array() ) {
2775
		add_filter( 'wp_mail_content_type', __CLASS__ . '::get_mail_content_type' );
2776
		add_action( 'phpmailer_init', __CLASS__ . '::add_plain_text_alternative' );
2777
2778
		$result = wp_mail( $to, $subject, $message, $headers, $attachments );
2779
2780
		remove_filter( 'wp_mail_content_type', __CLASS__ . '::get_mail_content_type' );
2781
		remove_action( 'phpmailer_init', __CLASS__ . '::add_plain_text_alternative' );
2782
2783
		return $result;
2784
	}
2785
2786
	/**
2787
	 * Add a display name part to an email address
2788
	 *
2789
	 * SpamAssassin doesn't like addresses in HTML messages that are missing display names (e.g., `[email protected]`
2790
	 * instead of `"Foo Bar" <[email protected]>`.
2791
	 *
2792
	 * @param string $address
2793
	 *
2794
	 * @return string
2795
	 */
2796
	function add_name_to_address( $address ) {
2797
		// If it's just the address, without a display name
2798
		if ( is_email( $address ) ) {
2799
			$address_parts = explode( '@', $address );
2800
			$address       = sprintf( '"%s" <%s>', $address_parts[0], $address );
2801
		}
2802
2803
		return $address;
2804
	}
2805
2806
	/**
2807
	 * Get the content type that should be assigned to outbound emails
2808
	 *
2809
	 * @return string
2810
	 */
2811
	static function get_mail_content_type() {
2812
		return 'text/html';
2813
	}
2814
2815
	/**
2816
	 * Wrap a message body with the appropriate in HTML tags
2817
	 *
2818
	 * This helps to ensure correct parsing by clients, and also helps avoid triggering spam filtering rules
2819
	 *
2820
	 * @param string $body
2821
	 *
2822
	 * @return string
2823
	 */
2824
	static function wrap_message_in_html_tags( $body ) {
2825
		// Don't do anything if the message was already wrapped in HTML tags
2826
		// That could have be done by a plugin via filters
2827
		if ( false !== strpos( $body, '<html' ) ) {
2828
			return $body;
2829
		}
2830
2831
		$html_message = sprintf(
2832
			// The tabs are just here so that the raw code is correctly formatted for developers
2833
			// They're removed so that they don't affect the final message sent to users
2834
			str_replace(
2835
				"\t", '',
2836
				'<!doctype html>
2837
				<html xmlns="http://www.w3.org/1999/xhtml">
2838
				<body>
2839
2840
				%s
2841
2842
				</body>
2843
				</html>'
2844
			),
2845
			$body
2846
		);
2847
2848
		return $html_message;
2849
	}
2850
2851
	/**
2852
	 * Add a plain-text alternative part to an outbound email
2853
	 *
2854
	 * This makes the message more accessible to mail clients that aren't HTML-aware, and decreases the likelihood
2855
	 * that the message will be flagged as spam.
2856
	 *
2857
	 * @param PHPMailer $phpmailer
2858
	 */
2859
	static function add_plain_text_alternative( $phpmailer ) {
2860
		// Add an extra break so that the extra space above the <p> is preserved after the <p> is stripped out
2861
		$alt_body = str_replace( '<p>', '<p><br />', $phpmailer->Body );
2862
2863
		// Convert <br> to \n breaks, to preserve the space between lines that we want to keep
2864
		$alt_body = str_replace( array( '<br>', '<br />' ), "\n", $alt_body );
2865
2866
		// Convert <hr> to an plain-text equivalent, to preserve the integrity of the message
2867
		$alt_body = str_replace( array( '<hr>', '<hr />' ), "----\n", $alt_body );
2868
2869
		// Trim the plain text message to remove the \n breaks that were after <doctype>, <html>, and <body>
2870
		$phpmailer->AltBody = trim( strip_tags( $alt_body ) );
2871
	}
2872
2873
	function addslashes_deep( $value ) {
2874
		if ( is_array( $value ) ) {
2875
			return array_map( array( $this, 'addslashes_deep' ), $value );
2876
		} elseif ( is_object( $value ) ) {
2877
			$vars = get_object_vars( $value );
2878
			foreach ( $vars as $key => $data ) {
2879
				$value->{$key} = $this->addslashes_deep( $data );
2880
			}
2881
			return $value;
2882
		}
2883
2884
		return addslashes( $value );
2885
	}
2886
}
2887
2888
/**
2889
 * Class for the contact-field shortcode.
2890
 * Parses shortcode to output the contact form field as HTML.
2891
 * Validates input.
2892
 */
2893
class Grunion_Contact_Form_Field extends Crunion_Contact_Form_Shortcode {
2894
	public $shortcode_name = 'contact-field';
2895
2896
	/**
2897
	 * @var Grunion_Contact_Form parent form
2898
	 */
2899
	public $form;
2900
2901
	/**
2902
	 * @var string default or POSTed value
2903
	 */
2904
	public $value;
2905
2906
	/**
2907
	 * @var bool Is the input invalid?
2908
	 */
2909
	public $error = false;
2910
2911
	/**
2912
	 * @param array                $attributes An associative array of shortcode attributes.  @see shortcode_atts()
2913
	 * @param null|string          $content Null for selfclosing shortcodes.  The inner content otherwise.
2914
	 * @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...
2915
	 */
2916
	function __construct( $attributes, $content = null, $form = null ) {
2917
		$attributes = shortcode_atts(
2918
			array(
2919
				'label'       => null,
2920
				'type'        => 'text',
2921
				'required'    => false,
2922
				'options'     => array(),
2923
				'id'          => null,
2924
				'default'     => null,
2925
				'values'      => null,
2926
				'placeholder' => null,
2927
				'class'       => null,
2928
			), $attributes, 'contact-field'
2929
		);
2930
2931
		// special default for subject field
2932
		if ( 'subject' == $attributes['type'] && is_null( $attributes['default'] ) && ! is_null( $form ) ) {
2933
			$attributes['default'] = $form->get_attribute( 'subject' );
2934
		}
2935
2936
		// allow required=1 or required=true
2937
		if ( '1' == $attributes['required'] || 'true' == strtolower( $attributes['required'] ) ) {
2938
			$attributes['required'] = true;
2939
		} else {
2940
			$attributes['required'] = false;
2941
		}
2942
2943
		// parse out comma-separated options list (for selects, radios, and checkbox-multiples)
2944
		if ( ! empty( $attributes['options'] ) && is_string( $attributes['options'] ) ) {
2945
			$attributes['options'] = array_map( 'trim', explode( ',', $attributes['options'] ) );
2946
2947 View Code Duplication
			if ( ! empty( $attributes['values'] ) && is_string( $attributes['values'] ) ) {
2948
				$attributes['values'] = array_map( 'trim', explode( ',', $attributes['values'] ) );
2949
			}
2950
		}
2951
2952
		if ( $form ) {
2953
			// make a unique field ID based on the label, with an incrementing number if needed to avoid clashes
2954
			$form_id = $form->get_attribute( 'id' );
2955
			$id      = isset( $attributes['id'] ) ? $attributes['id'] : false;
2956
2957
			$unescaped_label = $this->unesc_attr( $attributes['label'] );
2958
			$unescaped_label = str_replace( '%', '-', $unescaped_label ); // jQuery doesn't like % in IDs?
2959
			$unescaped_label = preg_replace( '/[^a-zA-Z0-9.-_:]/', '', $unescaped_label );
2960
2961
			if ( empty( $id ) ) {
2962
				$id        = sanitize_title_with_dashes( 'g' . $form_id . '-' . $unescaped_label );
2963
				$i         = 0;
2964
				$max_tries = 99;
2965
				while ( isset( $form->fields[ $id ] ) ) {
2966
					$i++;
2967
					$id = sanitize_title_with_dashes( 'g' . $form_id . '-' . $unescaped_label . '-' . $i );
2968
2969
					if ( $i > $max_tries ) {
2970
						break;
2971
					}
2972
				}
2973
			}
2974
2975
			$attributes['id'] = $id;
2976
		}
2977
2978
		parent::__construct( $attributes, $content );
2979
2980
		// Store parent form
2981
		$this->form = $form;
2982
	}
2983
2984
	/**
2985
	 * This field's input is invalid.  Flag as invalid and add an error to the parent form
2986
	 *
2987
	 * @param string $message The error message to display on the form.
2988
	 */
2989
	function add_error( $message ) {
2990
		$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...
2991
2992
		if ( ! is_wp_error( $this->form->errors ) ) {
2993
			$this->form->errors = new WP_Error;
2994
		}
2995
2996
		$this->form->errors->add( $this->get_attribute( 'id' ), $message );
2997
	}
2998
2999
	/**
3000
	 * Is the field input invalid?
3001
	 *
3002
	 * @see $error
3003
	 *
3004
	 * @return bool
3005
	 */
3006
	function is_error() {
3007
		return $this->error;
3008
	}
3009
3010
	/**
3011
	 * Validates the form input
3012
	 */
3013
	function validate() {
3014
		// If it's not required, there's nothing to validate
3015
		if ( ! $this->get_attribute( 'required' ) ) {
3016
			return;
3017
		}
3018
3019
		$field_id    = $this->get_attribute( 'id' );
3020
		$field_type  = $this->get_attribute( 'type' );
3021
		$field_label = $this->get_attribute( 'label' );
3022
3023
		if ( isset( $_POST[ $field_id ] ) ) {
3024
			if ( is_array( $_POST[ $field_id ] ) ) {
3025
				$field_value = array_map( 'stripslashes', $_POST[ $field_id ] );
3026
			} else {
3027
				$field_value = stripslashes( $_POST[ $field_id ] );
3028
			}
3029
		} else {
3030
			$field_value = '';
3031
		}
3032
3033
		switch ( $field_type ) {
3034
			case 'email':
3035
				// Make sure the email address is valid
3036
				if ( ! is_email( $field_value ) ) {
3037
					/* translators: %s is the name of a form field */
3038
					$this->add_error( sprintf( __( '%s requires a valid email address', 'jetpack' ), $field_label ) );
3039
				}
3040
				break;
3041
			case 'checkbox-multiple':
3042
				// Check that there is at least one option selected
3043
				if ( empty( $field_value ) ) {
3044
					/* translators: %s is the name of a form field */
3045
					$this->add_error( sprintf( __( '%s requires at least one selection', 'jetpack' ), $field_label ) );
3046
				}
3047
				break;
3048
			default:
3049
				// Just check for presence of any text
3050
				if ( ! strlen( trim( $field_value ) ) ) {
3051
					/* translators: %s is the name of a form field */
3052
					$this->add_error( sprintf( __( '%s is required', 'jetpack' ), $field_label ) );
3053
				}
3054
		}
3055
	}
3056
3057
3058
	/**
3059
	 * Check the default value for options field
3060
	 *
3061
	 * @param string value
3062
	 * @param int index
3063
	 * @param string default value
3064
	 *
3065
	 * @return string
3066
	 */
3067
	public function get_option_value( $value, $index, $options ) {
3068
		if ( empty( $value[ $index ] ) ) {
3069
			return $options;
3070
		}
3071
		return $value[ $index ];
3072
	}
3073
3074
	/**
3075
	 * Outputs the HTML for this form field
3076
	 *
3077
	 * @return string HTML
3078
	 */
3079
	function render() {
3080
		global $current_user, $user_identity;
3081
3082
		$field_id          = $this->get_attribute( 'id' );
3083
		$field_type        = $this->get_attribute( 'type' );
3084
		$field_label       = $this->get_attribute( 'label' );
3085
		$field_required    = $this->get_attribute( 'required' );
3086
		$field_placeholder = $this->get_attribute( 'placeholder' );
3087
		$class             = 'date' === $field_type ? 'jp-contact-form-date' : $this->get_attribute( 'class' );
3088
3089
		/**
3090
		 * Filters the "class" attribute of the contact form input
3091
		 *
3092
		 * @module contact-form
3093
		 *
3094
		 * @since 6.6.0
3095
		 *
3096
		 * @param string $class Additional CSS classes for input class attribute.
3097
		 */
3098
		$field_class = apply_filters( 'jetpack_contact_form_input_class', $class );
3099
3100
		if ( isset( $_POST[ $field_id ] ) ) {
3101
			if ( is_array( $_POST[ $field_id ] ) ) {
3102
				$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...
3103
			} else {
3104
				$this->value = stripslashes( (string) $_POST[ $field_id ] );
3105
			}
3106
		} elseif ( isset( $_GET[ $field_id ] ) ) {
3107
			$this->value = stripslashes( (string) $_GET[ $field_id ] );
3108
		} elseif (
3109
			is_user_logged_in() &&
3110
			( ( defined( 'IS_WPCOM' ) && IS_WPCOM ) ||
3111
			  /**
3112
			   * Allow third-party tools to prefill the contact form with the user's details when they're logged in.
3113
			   *
3114
			   * @module contact-form
3115
			   *
3116
			   * @since 3.2.0
3117
			   *
3118
			   * @param bool false Should the Contact Form be prefilled with your details when you're logged in. Default to false.
3119
			   */
3120
			  true === apply_filters( 'jetpack_auto_fill_logged_in_user', false )
3121
			)
3122
		) {
3123
			// Special defaults for logged-in users
3124
			switch ( $this->get_attribute( 'type' ) ) {
3125
				case 'email':
3126
					$this->value = $current_user->data->user_email;
3127
					break;
3128
				case 'name':
3129
					$this->value = $user_identity;
3130
					break;
3131
				case 'url':
3132
					$this->value = $current_user->data->user_url;
3133
					break;
3134
				default:
3135
					$this->value = $this->get_attribute( 'default' );
3136
			}
3137
		} else {
3138
			$this->value = $this->get_attribute( 'default' );
3139
		}
3140
3141
		$field_value = Grunion_Contact_Form_Plugin::strip_tags( $this->value );
3142
		$field_label = Grunion_Contact_Form_Plugin::strip_tags( $field_label );
3143
3144
		$rendered_field = $this->render_field( $field_type, $field_id, $field_label, $field_value, $field_class, $field_placeholder, $field_required );
3145
3146
		/**
3147
		 * Filter the HTML of the Contact Form.
3148
		 *
3149
		 * @module contact-form
3150
		 *
3151
		 * @since 2.6.0
3152
		 *
3153
		 * @param string $rendered_field Contact Form HTML output.
3154
		 * @param string $field_label Field label.
3155
		 * @param int|null $id Post ID.
3156
		 */
3157
		return apply_filters( 'grunion_contact_form_field_html', $rendered_field, $field_label, ( in_the_loop() ? get_the_ID() : null ) );
3158
	}
3159
3160
	function render_label( $type = '', $id, $label, $required, $required_field_text ) {
3161
3162
		$type_class = $type ? ' ' .$type : '';
3163
		return
3164
			"<label 
3165
				for='" . esc_attr( $id ) . "' 
3166
				class='grunion-field-label{$type_class}" . ( $this->is_error() ? ' form-error' : '' ) . "'
3167
				>"
3168
				. esc_html( $label )
3169
				. ( $required ? '<span>' . $required_field_text . '</span>' : '' )
3170
			. "</label>\n";
3171
3172
	}
3173
3174
	function render_input_field( $type, $id, $value, $class, $placeholder, $required ) {
3175
		return "<input 
3176
					type='". esc_attr( $type ) ."' 
3177
					name='" . esc_attr( $id ) . "' 
3178
					id='" . esc_attr( $id ) . "' 
3179
					value='" . esc_attr( $value ) . "' 
3180
					" . $class . $placeholder . ' 
3181
					' . ( $required ? "required aria-required='true'" : '' ) . " 
3182
				/>\n";
3183
	}
3184
3185
	function render_email_field( $id, $label, $value, $class, $required, $required_field_text, $placeholder ) {
3186
		$field = $this->render_label( 'email', $id, $label, $required, $required_field_text );
3187
		$field .= $this->render_input_field( 'email', $id, $value, $class, $placeholder, $required );
3188
		return $field;
3189
	}
3190
3191
	function render_telephone_field( $id, $label, $value, $class, $required, $required_field_text, $placeholder ) {
3192
		$field = $this->render_label( 'telephone', $id, $label, $required, $required_field_text );
3193
		$field .= $this->render_input_field( 'tel', $id, $value, $class, $placeholder, $required );
3194
		return $field;
3195
	}
3196
3197
	function render_url_field( $id, $label, $value, $class, $required, $required_field_text, $placeholder ) {
3198
		$field = $this->render_label( 'url', $id, $label, $required, $required_field_text );
3199
		$field .= $this->render_input_field( 'url', $id, $value, $class, $placeholder, $required );
3200
		return $field;
3201
	}
3202
3203
	function render_textarea_field( $id, $label, $value, $class, $required, $required_field_text, $placeholder ) {
3204
		$field = $this->render_label( 'textarea', 'contact-form-comment-' . $id, $label, $required, $required_field_text );
3205
		$field .= "<textarea
3206
		                name='" . esc_attr( $id ) . "' 
3207
		                id='contact-form-comment-" . esc_attr( $id ) . "' 
3208
		                rows='20' "
3209
		                . $class
3210
		                . $placeholder
3211
		                . ' ' . ( $required ? "required aria-required='true'" : '' ) .
3212
		                '>' . esc_textarea( $value )
3213
		          . "</textarea>\n";
3214
		return $field;
3215
	}
3216
3217
	function render_radio_field( $id, $label, $value, $class, $required, $required_field_text ) {
3218
		$field = $this->render_label( '', $id, $label, $required, $required_field_text );
3219
		foreach ( (array) $this->get_attribute( 'options' ) as $optionIndex => $option ) {
3220
			$option = Grunion_Contact_Form_Plugin::strip_tags( $option );
3221
			if ( $option ) {
3222
				$field .= "\t\t<label class='grunion-radio-label radio" . ( $this->is_error() ? ' form-error' : '' ) . "'>";
3223
				$field .= "<input 
3224
									type='radio' 
3225
									name='" . esc_attr( $id ) . "' 
3226
									value='" . esc_attr( $this->get_option_value( $this->get_attribute( 'values' ), $optionIndex, $option ) ) . "' "
3227
				                    . $class
3228
				                    . checked( $option, $value, false ) . ' '
3229
				                    . ( $required ? "required aria-required='true'" : '' )
3230
				              . '/> ';
3231
				$field .= esc_html( $option ) . "</label>\n";
3232
				$field .= "\t\t<div class='clear-form'></div>\n";
3233
			}
3234
		}
3235
		return $field;
3236
	}
3237
3238
	function render_checkbox_field( $id, $label, $value, $class, $required, $required_field_text ) {
3239
		$field = "<label class='grunion-field-label checkbox" . ( $this->is_error() ? ' form-error' : '' ) . "'>";
3240
			$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";
3241
			$field .= "\t\t" . esc_html( $label ) . ( $required ? '<span>' . $required_field_text . '</span>' : '' );
3242
		$field .=  "</label>\n";
3243
		$field .= "<div class='clear-form'></div>\n";
3244
		return $field;
3245
	}
3246
3247
	function render_checkbox_multiple_field( $id, $label, $value, $class, $required, $required_field_text  ) {
3248
		$field = $this->render_label( '', $id, $label, $required, $required_field_text );
3249
		foreach ( (array) $this->get_attribute( 'options' ) as $optionIndex => $option ) {
3250
			$option = Grunion_Contact_Form_Plugin::strip_tags( $option );
3251
			if ( $option  ) {
3252
				$field .= "\t\t<label class='grunion-checkbox-multiple-label checkbox-multiple" . ( $this->is_error() ? ' form-error' : '' ) . "'>";
3253
				$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 ) . ' /> ';
3254
				$field .= esc_html( $option ) . "</label>\n";
3255
				$field .= "\t\t<div class='clear-form'></div>\n";
3256
			}
3257
		}
3258
3259
		return $field;
3260
	}
3261
3262
	function render_select_field( $id, $label, $value, $class, $required, $required_field_text ) {
3263
		$field = $this->render_label( 'select', $id, $label, $required, $required_field_text );
3264
		$field  .= "\t<select name='" . esc_attr( $id ) . "' id='" . esc_attr( $id ) . "' " . $class . ( $required ? "required aria-required='true'" : '' ) . ">\n";
3265
		foreach ( (array) $this->get_attribute( 'options' ) as $optionIndex => $option ) {
3266
			$option = Grunion_Contact_Form_Plugin::strip_tags( $option );
3267
			if ( $option ) {
3268
				$field .= "\t\t<option"
3269
				               . selected( $option, $value, false )
3270
				               . " value='" . esc_attr( $this->get_option_value( $this->get_attribute( 'values' ), $optionIndex, $option ) )
3271
				               . "'>" . esc_html( $option )
3272
				          . "</option>\n";
3273
			}
3274
		}
3275
		$field  .= "\t</select>\n";
3276
		return $field;
3277
	}
3278
3279
	function render_date_field( $id, $label, $value, $class, $required, $required_field_text, $placeholder ) {
3280
		$field = $this->render_label( 'date', $id, $label, $required, $required_field_text );
3281
		$field .= $this->render_input_field( 'text', $id, $value, $class, $placeholder, $required );
3282
3283
		wp_enqueue_script(
3284
			'grunion-frontend',
3285
			Jetpack::get_file_url_for_environment(
3286
				'_inc/build/contact-form/js/grunion-frontend.min.js',
3287
				'modules/contact-form/js/grunion-frontend.js'
3288
			),
3289
			array( 'jquery', 'jquery-ui-datepicker' )
3290
		);
3291
		wp_enqueue_style( 'jp-jquery-ui-datepicker', plugins_url( 'css/jquery-ui-datepicker.css', __FILE__ ), array( 'dashicons' ), '1.0' );
3292
3293
		// Using Core's built-in datepicker localization routine
3294
		wp_localize_jquery_ui_datepicker();
3295
		return $field;
3296
	}
3297
3298
	function render_default_field( $id, $label, $value, $class, $required, $required_field_text, $placeholder, $type ) {
3299
		$field = $this->render_label( $type, $id, $label, $required, $required_field_text );
3300
		$field .= $this->render_input_field( 'text', $id, $value, $class, $placeholder, $required );
3301
		return $field;
3302
	}
3303
3304
	function render_field( $type, $id, $label, $value, $class, $placeholder, $required ) {
3305
		$field_placeholder = ( ! empty( $placeholder ) ) ? "placeholder='" . esc_attr( $placeholder ) . "'" : '';
3306
		$field_class       = "class='" . trim( esc_attr( $type ) . ' ' . esc_attr( $class ) ) . "' ";
3307
		$wrap_classes = empty( $class ) ? '' : implode( '-wrap ', array_filter( explode( ' ', $class ) ) ) . '-wrap'; // this adds
3308
3309
		$shell_field_class = "class='grunion-field-wrap grunion-field-" . trim( esc_attr( $type ) . '-wrap ' . esc_attr( $wrap_classes ) ) . "' ";
3310
		/**
3311
		/**
3312
		 * Filter the Contact Form required field text
3313
		 *
3314
		 * @module contact-form
3315
		 *
3316
		 * @since 3.8.0
3317
		 *
3318
		 * @param string $var Required field text. Default is "(required)".
3319
		 */
3320
		$required_field_text = esc_html( apply_filters( 'jetpack_required_field_text', __( '(required)', 'jetpack' ) ) );
3321
3322
		$field = "\n<div {$shell_field_class} >\n"; // new in Jetpack 6.8.0
3323
		switch ( $type ) {
3324
			case 'email':
3325
				$field .= $this->render_email_field( $id, $label, $value, $field_class, $required, $required_field_text, $field_placeholder );
3326
				break;
3327
			case 'telephone':
3328
				$field .= $this->render_telephone_field( $id, $label, $value, $field_class, $required, $required_field_text, $field_placeholder );
3329
				break;
3330
			case 'url':
3331
				$field .= $this->render_url_field( $id, $label, $value, $field_class, $required, $required_field_text, $field_placeholder );
3332
				break;
3333
			case 'textarea':
3334
				$field .= $this->render_textarea_field( $id, $label, $value, $field_class, $required, $required_field_text, $field_placeholder );
3335
				break;
3336
			case 'radio':
3337
				$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...
3338
				break;
3339
			case 'checkbox':
3340
				$field .= $this->render_checkbox_field( $id, $label, $value, $field_class, $required, $required_field_text );
3341
				break;
3342
			case 'checkbox-multiple':
3343
				$field .= $this->render_checkbox_multiple_field( $id, $label, $value, $field_class, $required, $required_field_text );
3344
				break;
3345
			case 'select':
3346
				$field .= $this->render_select_field( $id, $label, $value, $field_class, $required, $required_field_text );
3347
				break;
3348
			case 'date':
3349
				$field .= $this->render_date_field( $id, $label, $value, $field_class, $required, $required_field_text, $field_placeholder );
3350
				break;
3351
			default: // text field
3352
				$field .= $this->render_default_field( $id, $label, $value, $field_class, $required, $required_field_text, $field_placeholder, $type );
3353
				break;
3354
		}
3355
		$field .= "\t</div>\n";
3356
		return $field;
3357
	}
3358
}
3359
3360
add_action( 'init', array( 'Grunion_Contact_Form_Plugin', 'init' ), 9 );
3361
3362
add_action( 'grunion_scheduled_delete', 'grunion_delete_old_spam' );
3363
3364
/**
3365
 * Deletes old spam feedbacks to keep the posts table size under control
3366
 */
3367
function grunion_delete_old_spam() {
3368
	global $wpdb;
3369
3370
	$grunion_delete_limit = 100;
3371
3372
	$now_gmt  = current_time( 'mysql', 1 );
3373
	$sql      = $wpdb->prepare(
3374
		"
3375
		SELECT `ID`
3376
		FROM $wpdb->posts
3377
		WHERE DATE_SUB( %s, INTERVAL 15 DAY ) > `post_date_gmt`
3378
			AND `post_type` = 'feedback'
3379
			AND `post_status` = 'spam'
3380
		LIMIT %d
3381
	", $now_gmt, $grunion_delete_limit
3382
	);
3383
	$post_ids = $wpdb->get_col( $sql );
3384
3385
	foreach ( (array) $post_ids as $post_id ) {
3386
		// force a full delete, skip the trash
3387
		wp_delete_post( $post_id, true );
3388
	}
3389
3390
	if (
3391
		/**
3392
		 * Filter if the module run OPTIMIZE TABLE on the core WP tables.
3393
		 *
3394
		 * @module contact-form
3395
		 *
3396
		 * @since 1.3.1
3397
		 * @since 6.4.0 Set to false by default.
3398
		 *
3399
		 * @param bool $filter Should Jetpack optimize the table, defaults to false.
3400
		 */
3401
		apply_filters( 'grunion_optimize_table', false )
3402
	) {
3403
		$wpdb->query( "OPTIMIZE TABLE $wpdb->posts" );
3404
	}
3405
3406
	// if we hit the max then schedule another run
3407
	if ( count( $post_ids ) >= $grunion_delete_limit ) {
3408
		wp_schedule_single_event( time() + 700, 'grunion_scheduled_delete' );
3409
	}
3410
}
3411