Completed
Push — revert-9951-update/sharing-lin... ( fe5edd...595b5f )
by
unknown
14:50 queued 04:58
created

personal_data_eraser()   B

Complexity

Conditions 6
Paths 5

Size

Total Lines 54

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
nc 5
nop 2
dl 0
loc 54
rs 8.3814
c 0
b 0
f 0

How to fix   Long Method   

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;
0 ignored issues
show
Coding Style introduced by
The visibility should be declared for property $using_contact_form_field.

The PSR-2 coding standard requires that all properties in a class have their visibility explicitly declared. If you declare a property using

class A {
    var $property;
}

the property is implicitly global.

To learn more about the PSR-2, please see the PHP-FIG site on the PSR-2.

Loading history...
36
37
	static function init() {
38
		static $instance = false;
39
40
		if ( ! $instance ) {
41
			$instance = new Grunion_Contact_Form_Plugin;
42
43
			// Schedule our daily cleanup
44
			add_action( 'wp_scheduled_delete', array( $instance, 'daily_akismet_meta_cleanup' ) );
45
		}
46
47
		return $instance;
48
	}
49
50
	/**
51
	 * Runs daily to clean up spam detection metadata after 15 days.  Keeps your DB squeaky clean.
52
	 */
53
	public function daily_akismet_meta_cleanup() {
54
		global $wpdb;
55
56
		$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" );
57
58
		if ( empty( $feedback_ids ) ) {
59
			return;
60
		}
61
62
		/**
63
		 * Fires right before deleting the _feedback_akismet_values post meta on $feedback_ids
64
		 *
65
		 * @module contact-form
66
		 *
67
		 * @since 6.1.0
68
		 *
69
		 * @param array $feedback_ids list of feedback post ID
70
		 */
71
		do_action( 'jetpack_daily_akismet_meta_cleanup_before', $feedback_ids );
72
		foreach ( $feedback_ids as $feedback_id ) {
73
			delete_post_meta( $feedback_id, '_feedback_akismet_values' );
74
		}
75
76
		/**
77
		 * Fires right after deleting the _feedback_akismet_values post meta on $feedback_ids
78
		 *
79
		 * @module contact-form
80
		 *
81
		 * @since 6.1.0
82
		 *
83
		 * @param array $feedback_ids list of feedback post ID
84
		 */
85
		do_action( 'jetpack_daily_akismet_meta_cleanup_after', $feedback_ids );
86
	}
87
88
	/**
89
	 * Strips HTML tags from input.  Output is NOT HTML safe.
90
	 *
91
	 * @param mixed $data_with_tags
92
	 * @return mixed
93
	 */
94
	public static function strip_tags( $data_with_tags ) {
95
		if ( is_array( $data_with_tags ) ) {
96
			foreach ( $data_with_tags as $index => $value ) {
97
				$index = sanitize_text_field( strval( $index ) );
98
				$value = wp_kses( strval( $value ), array() );
99
				$value = str_replace( '&amp;', '&', $value ); // undo damage done by wp_kses_normalize_entities()
100
101
				$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...
102
			}
103
		} else {
104
			$data_without_tags = wp_kses( $data_with_tags, array() );
105
			$data_without_tags = str_replace( '&amp;', '&', $data_without_tags ); // undo damage done by wp_kses_normalize_entities()
106
		}
107
108
		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...
109
	}
110
111
	function __construct() {
112
		$this->add_shortcode();
113
114
		// While generating the output of a text widget with a contact-form shortcode, we need to know its widget ID.
115
		add_action( 'dynamic_sidebar', array( $this, 'track_current_widget' ) );
116
117
		// Add a "widget" shortcode attribute to all contact-form shortcodes embedded in widgets
118
		add_filter( 'widget_text', array( $this, 'widget_atts' ), 0 );
119
120
		// If Text Widgets don't get shortcode processed, hack ours into place.
121
		if (
122
			version_compare( get_bloginfo( 'version' ), '4.9-z', '<=' )
123
			&& ! has_filter( 'widget_text', 'do_shortcode' )
124
		) {
125
			add_filter( 'widget_text', array( $this, 'widget_shortcode_hack' ), 5 );
126
		}
127
128
		// Akismet to the rescue
129
		if ( defined( 'AKISMET_VERSION' ) || function_exists( 'akismet_http_post' ) ) {
130
			add_filter( 'jetpack_contact_form_is_spam', array( $this, 'is_spam_akismet' ), 10, 2 );
131
			add_action( 'contact_form_akismet', array( $this, 'akismet_submit' ), 10, 2 );
132
		}
133
134
		add_action( 'loop_start', array( 'Grunion_Contact_Form', '_style_on' ) );
135
136
		add_action( 'wp_ajax_grunion-contact-form', array( $this, 'ajax_request' ) );
137
		add_action( 'wp_ajax_nopriv_grunion-contact-form', array( $this, 'ajax_request' ) );
138
139
		// GDPR: personal data exporter & eraser.
140
		add_filter( 'wp_privacy_personal_data_exporters', array( $this, 'register_personal_data_exporter' ) );
141
		add_filter( 'wp_privacy_personal_data_erasers', array( $this, 'register_personal_data_eraser' ) );
142
143
		// Export to CSV feature
144
		if ( is_admin() ) {
145
			add_action( 'admin_init',            array( $this, 'download_feedback_as_csv' ) );
146
			add_action( 'admin_footer-edit.php', array( $this, 'export_form' ) );
147
			add_action( 'admin_menu',            array( $this, 'admin_menu' ) );
148
			add_action( 'current_screen',        array( $this, 'unread_count' ) );
149
		}
150
151
		// custom post type we'll use to keep copies of the feedback items
152
		register_post_type( 'feedback', array(
153
			'labels'            => array(
154
				'name'               => __( 'Feedback', 'jetpack' ),
155
				'singular_name'      => __( 'Feedback', 'jetpack' ),
156
				'search_items'       => __( 'Search Feedback', 'jetpack' ),
157
				'not_found'          => __( 'No feedback found', 'jetpack' ),
158
				'not_found_in_trash' => __( 'No feedback found', 'jetpack' ),
159
			),
160
			'menu_icon'         	=> 'dashicons-feedback',
161
			'show_ui'           	=> TRUE,
162
			'show_in_admin_bar' 	=> FALSE,
163
			'public'            	=> FALSE,
164
			'rewrite'           	=> FALSE,
165
			'query_var'         	=> FALSE,
166
			'capability_type'   	=> 'page',
167
			'show_in_rest'      	=> true,
168
			'rest_controller_class' => 'Grunion_Contact_Form_Endpoint',
169
			'capabilities'			=> array(
170
				'create_posts'        => false,
171
				'publish_posts'       => 'publish_pages',
172
				'edit_posts'          => 'edit_pages',
173
				'edit_others_posts'   => 'edit_others_pages',
174
				'delete_posts'        => 'delete_pages',
175
				'delete_others_posts' => 'delete_others_pages',
176
				'read_private_posts'  => 'read_private_pages',
177
				'edit_post'           => 'edit_page',
178
				'delete_post'         => 'delete_page',
179
				'read_post'           => 'read_page',
180
			),
181
			'map_meta_cap'			=> true,
182
		) );
183
184
		// Add to REST API post type whitelist
185
		add_filter( 'rest_api_allowed_post_types', array( $this, 'allow_feedback_rest_api_type' ) );
186
187
		// Add "spam" as a post status
188
		register_post_status( 'spam', array(
189
			'label'                  => 'Spam',
190
			'public'                 => false,
191
			'exclude_from_search'    => true,
192
			'show_in_admin_all_list' => false,
193
			'label_count'            => _n_noop( 'Spam <span class="count">(%s)</span>', 'Spam <span class="count">(%s)</span>', 'jetpack' ),
194
			'protected'              => true,
195
			'_builtin'               => false,
196
		) );
197
198
		// POST handler
199
		if (
200
			isset( $_SERVER['REQUEST_METHOD'] ) && 'POST' == strtoupper( $_SERVER['REQUEST_METHOD'] )
201
		&&
202
			isset( $_POST['action'] ) && 'grunion-contact-form' == $_POST['action']
203
		&&
204
			isset( $_POST['contact-form-id'] )
205
		) {
206
			add_action( 'template_redirect', array( $this, 'process_form_submission' ) );
207
		}
208
209
		/*
210
		 Can be dequeued by placing the following in wp-content/themes/yourtheme/functions.php
211
		 *
212
		 * 	function remove_grunion_style() {
213
		 *		wp_deregister_style('grunion.css');
214
		 *	}
215
		 *	add_action('wp_print_styles', 'remove_grunion_style');
216
		 */
217
		wp_register_style( 'grunion.css', GRUNION_PLUGIN_URL . 'css/grunion.css', array(), JETPACK__VERSION );
218
		wp_style_add_data( 'grunion.css', 'rtl', 'replace' );
219
	}
220
221
	/**
222
	 * Add the 'Export' menu item as a submenu of Feedback.
223
	 */
224
	public function admin_menu() {
225
		add_submenu_page(
226
			'edit.php?post_type=feedback',
227
			__( 'Export feedback as CSV', 'jetpack' ),
228
			__( 'Export CSV', 'jetpack' ),
229
			'export',
230
			'feedback-export',
231
			array( $this, 'export_form' )
232
		);
233
	}
234
235
	/**
236
	 * Add to REST API post type whitelist
237
	 */
238
	function allow_feedback_rest_api_type( $post_types ) {
239
		$post_types[] = 'feedback';
240
		return $post_types;
241
	}
242
243
	/**
244
	 * Display the count of new feedback entries received. It's reset when user visits the Feedback screen.
245
	 *
246
	 * @since 4.1.0
247
	 *
248
	 * @param object $screen Information about the current screen.
249
	 */
250
	function unread_count( $screen ) {
251
		if ( isset( $screen->post_type ) && 'feedback' == $screen->post_type ) {
252
			update_option( 'feedback_unread_count', 0 );
253
		} else {
254
			global $menu;
255
			if ( isset( $menu ) && is_array( $menu ) && ! empty( $menu ) ) {
256
				foreach ( $menu as $index => $menu_item ) {
257
					if ( 'edit.php?post_type=feedback' == $menu_item[2] ) {
258
						$unread = get_option( 'feedback_unread_count', 0 );
259
						if ( $unread > 0 ) {
260
							$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>' : '';
261
							$menu[ $index ][0] .= $unread_count;
262
						}
263
						break;
264
					}
265
				}
266
			}
267
		}
268
	}
269
270
	/**
271
	 * Handles all contact-form POST submissions
272
	 *
273
	 * Conditionally attached to `template_redirect`
274
	 */
275
	function process_form_submission() {
276
		// Add a filter to replace tokens in the subject field with sanitized field values
277
		add_filter( 'contact_form_subject', array( $this, 'replace_tokens_with_input' ), 10, 2 );
278
279
		$id = stripslashes( $_POST['contact-form-id'] );
280
		$hash = isset( $_POST['contact-form-hash'] ) ? $_POST['contact-form-hash'] : null;
281
		$hash = preg_replace( '/[^\da-f]/i', '', $hash );
282
283
		if ( is_user_logged_in() ) {
284
			check_admin_referer( "contact-form_{$id}" );
285
		}
286
287
		$is_widget = 0 === strpos( $id, 'widget-' );
288
289
		$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...
290
291
		if ( $is_widget ) {
292
			// It's a form embedded in a text widget
293
			$this->current_widget_id = substr( $id, 7 ); // remove "widget-"
294
			$widget_type = implode( '-', array_slice( explode( '-', $this->current_widget_id ), 0, -1 ) ); // Remove trailing -#
295
296
			// Is the widget active?
297
			$sidebar = is_active_widget( false, $this->current_widget_id, $widget_type );
298
299
			// This is lame - no core API for getting a widget by ID
300
			$widget = isset( $GLOBALS['wp_registered_widgets'][ $this->current_widget_id ] ) ? $GLOBALS['wp_registered_widgets'][ $this->current_widget_id ] : false;
301
302
			if ( $sidebar && $widget && isset( $widget['callback'] ) ) {
303
				// prevent PHP notices by populating widget args
304
				$widget_args = array(
305
					'before_widget' => '',
306
					'after_widget' => '',
307
					'before_title' => '',
308
					'after_title' => '',
309
				);
310
				// This is lamer - no API for outputting a given widget by ID
311
				ob_start();
312
				// Process the widget to populate Grunion_Contact_Form::$last
313
				call_user_func( $widget['callback'], $widget_args, $widget['params'][0] );
314
				ob_end_clean();
315
			}
316
		} else {
317
			// It's a form embedded in a post
318
			$post = get_post( $id );
319
320
			// Process the content to populate Grunion_Contact_Form::$last
321
			/** This filter is already documented in core. wp-includes/post-template.php */
322
			apply_filters( 'the_content', $post->post_content );
323
		}
324
325
		$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...
326
327
		// No form may mean user is using do_shortcode, grab the form using the stored post meta
328
		if ( ! $form ) {
329
330
			// Get shortcode from post meta
331
			$shortcode = get_post_meta( $_POST['contact-form-id'], "_g_feedback_shortcode_{$hash}", true );
332
333
			// Format it
334
			if ( $shortcode != '' ) {
335
336
				// Get attributes from post meta.
337
				$parameters = '';
338
				$attributes = get_post_meta( $_POST['contact-form-id'], "_g_feedback_shortcode_atts_{$hash}", true );
339
				if ( ! empty( $attributes ) && is_array( $attributes ) ) {
340
					foreach( array_filter( $attributes ) as $param => $value  ) {
341
						$parameters .= " $param=\"$value\"";
342
					}
343
				}
344
345
				$shortcode = '[contact-form' . $parameters . ']' . $shortcode . '[/contact-form]';
346
				do_shortcode( $shortcode );
347
348
				// Recreate form
349
				$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...
350
			}
351
352
			if ( ! $form ) {
353
				return false;
354
			}
355
		}
356
357
		if ( is_wp_error( $form->errors ) && $form->errors->get_error_codes() ) {
358
			return $form->errors;
359
		}
360
361
		// Process the form
362
		return $form->process_submission();
363
	}
364
365
	function ajax_request() {
366
		$submission_result = self::process_form_submission();
367
368
		if ( ! $submission_result ) {
369
			header( 'HTTP/1.1 500 Server Error', 500, true );
370
			echo '<div class="form-error"><ul class="form-errors"><li class="form-error-message">';
371
			esc_html_e( 'An error occurred. Please try again later.', 'jetpack' );
372
			echo '</li></ul></div>';
373
		} elseif ( is_wp_error( $submission_result ) ) {
374
			header( 'HTTP/1.1 400 Bad Request', 403, true );
375
			echo '<div class="form-error"><ul class="form-errors"><li class="form-error-message">';
376
			echo esc_html( $submission_result->get_error_message() );
377
			echo '</li></ul></div>';
378
		} else {
379
			echo '<h3>' . esc_html__( 'Message Sent', 'jetpack' ) . '</h3>' . $submission_result;
380
		}
381
382
		die;
0 ignored issues
show
Coding Style Compatibility introduced by
The method ajax_request() contains an exit expression.

An exit expression should only be used in rare cases. For example, if you write a short command line script.

In most cases however, using an exit expression makes the code untestable and often causes incompatibilities with other libraries. Thus, unless you are absolutely sure it is required here, we recommend to refactor your code to avoid its usage.

Loading history...
383
	}
384
385
	/**
386
	 * Ensure the post author is always zero for contact-form feedbacks
387
	 * Attached to `wp_insert_post_data`
388
	 *
389
	 * @see Grunion_Contact_Form::process_submission()
390
	 *
391
	 * @param array $data the data to insert
392
	 * @param array $postarr the data sent to wp_insert_post()
393
	 * @return array The filtered $data to insert
394
	 */
395
	function insert_feedback_filter( $data, $postarr ) {
396
		if ( $data['post_type'] == 'feedback' && $postarr['post_type'] == 'feedback' ) {
397
			$data['post_author'] = 0;
398
		}
399
400
		return $data;
401
	}
402
	/*
403
	 * Adds our contact-form shortcode
404
	 * The "child" contact-field shortcode is enabled as needed by the contact-form shortcode handler
405
	 */
406
	function add_shortcode() {
407
		add_shortcode( 'contact-form',         array( 'Grunion_Contact_Form', 'parse' ) );
408
		add_shortcode( 'contact-field',        array( 'Grunion_Contact_Form', 'parse_contact_field' ) );
409
	}
410
411
	static function tokenize_label( $label ) {
412
		return '{' . trim( preg_replace( '#^\d+_#', '', $label ) ) . '}';
413
	}
414
415
	static function sanitize_value( $value ) {
416
		return preg_replace( '=((<CR>|<LF>|0x0A/%0A|0x0D/%0D|\\n|\\r)\S).*=i', null, $value );
417
	}
418
419
	/**
420
	 * Replaces tokens like {city} or {City} (case insensitive) with the value
421
	 * of an input field of that name
422
	 *
423
	 * @param string $subject
424
	 * @param array  $field_values Array with field label => field value associations
425
	 *
426
	 * @return string The filtered $subject with the tokens replaced
427
	 */
428
	function replace_tokens_with_input( $subject, $field_values ) {
429
		// Wrap labels into tokens (inside {})
430
		$wrapped_labels = array_map( array( 'Grunion_Contact_Form_Plugin', 'tokenize_label' ), array_keys( $field_values ) );
431
		// Sanitize all values
432
		$sanitized_values = array_map( array( 'Grunion_Contact_Form_Plugin', 'sanitize_value' ), array_values( $field_values ) );
433
434
		foreach ( $sanitized_values as $k => $sanitized_value ) {
435
			if ( is_array( $sanitized_value ) ) {
436
				$sanitized_values[ $k ] = implode( ', ', $sanitized_value );
437
			}
438
		}
439
440
		// Search for all valid tokens (based on existing fields) and replace with the field's value
441
		$subject = str_ireplace( $wrapped_labels, $sanitized_values, $subject );
442
		return $subject;
443
	}
444
445
	/**
446
	 * Tracks the widget currently being processed.
447
	 * Attached to `dynamic_sidebar`
448
	 *
449
	 * @see $current_widget_id
450
	 *
451
	 * @param array $widget The widget data
452
	 */
453
	function track_current_widget( $widget ) {
454
		$this->current_widget_id = $widget['id'];
455
	}
456
457
	/**
458
	 * Adds a "widget" attribute to every contact-form embedded in a text widget.
459
	 * Used to tell the difference between post-embedded contact-forms and widget-embedded contact-forms
460
	 * Attached to `widget_text`
461
	 *
462
	 * @param string $text The widget text
463
	 * @return string The filtered widget text
464
	 */
465
	function widget_atts( $text ) {
466
		Grunion_Contact_Form::style( true );
467
468
		return preg_replace( '/\[contact-form([^a-zA-Z_-])/', '[contact-form widget="' . $this->current_widget_id . '"\\1', $text );
469
	}
470
471
	/**
472
	 * For sites where text widgets are not processed for shortcodes, we add this hack to process just our shortcode
473
	 * Attached to `widget_text`
474
	 *
475
	 * @param string $text The widget text
476
	 * @return string The contact-form filtered widget text
477
	 */
478
	function widget_shortcode_hack( $text ) {
479
		if ( ! preg_match( '/\[contact-form([^a-zA-Z_-])/', $text ) ) {
480
			return $text;
481
		}
482
483
		$old = $GLOBALS['shortcode_tags'];
484
		remove_all_shortcodes();
485
		Grunion_Contact_Form_Plugin::$using_contact_form_field = true;
486
		$this->add_shortcode();
487
488
		$text = do_shortcode( $text );
489
490
		Grunion_Contact_Form_Plugin::$using_contact_form_field = false;
491
		$GLOBALS['shortcode_tags'] = $old;
492
493
		return $text;
494
	}
495
496
	/**
497
	 * Populate an array with all values necessary to submit a NEW contact-form feedback to Akismet.
498
	 * Note that this includes the current user_ip etc, so this should only be called when accepting a new item via $_POST
499
	 *
500
	 * @param array $form Contact form feedback array
501
	 * @return array feedback array with additional data ready for submission to Akismet
502
	 */
503
	function prepare_for_akismet( $form ) {
504
		$form['comment_type'] = 'contact_form';
505
		$form['user_ip']      = $_SERVER['REMOTE_ADDR'];
506
		$form['user_agent']   = $_SERVER['HTTP_USER_AGENT'];
507
		$form['referrer']     = $_SERVER['HTTP_REFERER'];
508
		$form['blog']         = get_option( 'home' );
509
510
		foreach ( $_SERVER as $key => $value ) {
511
			if ( ! is_string( $value ) ) {
512
				continue;
513
			}
514
			if ( in_array( $key, array( 'HTTP_COOKIE', 'HTTP_COOKIE2', 'HTTP_USER_AGENT', 'HTTP_REFERER' ) ) ) {
515
				// We don't care about cookies, and the UA and Referrer were caught above.
516
				continue;
517
			} elseif ( in_array( $key, array( 'REMOTE_ADDR', 'REQUEST_URI', 'DOCUMENT_URI' ) ) ) {
518
				// All three of these are relevant indicators and should be passed along.
519
				$form[ $key ] = $value;
520
			} elseif ( wp_startswith( $key, 'HTTP_' ) ) {
521
				// Any other HTTP header indicators.
522
				// `wp_startswith()` is a wpcom helper function and is included in Jetpack via `functions.compat.php`
523
				$form[ $key ] = $value;
524
			}
525
		}
526
527
		return $form;
528
	}
529
530
	/**
531
	 * Submit contact-form data to Akismet to check for spam.
532
	 * If you're accepting a new item via $_POST, run it Grunion_Contact_Form_Plugin::prepare_for_akismet() first
533
	 * Attached to `jetpack_contact_form_is_spam`
534
	 *
535
	 * @param bool  $is_spam
536
	 * @param array $form
537
	 * @return bool|WP_Error TRUE => spam, FALSE => not spam, WP_Error => stop processing entirely
538
	 */
539
	function is_spam_akismet( $is_spam, $form = array() ) {
540
		global $akismet_api_host, $akismet_api_port;
541
542
		// The signature of this function changed from accepting just $form.
543
		// If something only sends an array, assume it's still using the old
544
		// signature and work around it.
545
		if ( empty( $form ) && is_array( $is_spam ) ) {
546
			$form = $is_spam;
547
			$is_spam = false;
548
		}
549
550
		// If a previous filter has alrady marked this as spam, trust that and move on.
551
		if ( $is_spam ) {
552
			return $is_spam;
553
		}
554
555
		if ( ! function_exists( 'akismet_http_post' ) && ! defined( 'AKISMET_VERSION' ) ) {
556
			return false;
557
		}
558
559
		$query_string = http_build_query( $form );
560
561
		if ( method_exists( 'Akismet', 'http_post' ) ) {
562
			$response = Akismet::http_post( $query_string, 'comment-check' );
563
		} else {
564
			$response = akismet_http_post( $query_string, $akismet_api_host, '/1.1/comment-check', $akismet_api_port );
565
		}
566
567
		$result = false;
568
569
		if ( isset( $response[0]['x-akismet-pro-tip'] ) && 'discard' === trim( $response[0]['x-akismet-pro-tip'] ) && get_option( 'akismet_strictness' ) === '1' ) {
570
			$result = new WP_Error( 'feedback-discarded', __( 'Feedback discarded.', 'jetpack' ) );
571
		} elseif ( isset( $response[1] ) && 'true' == trim( $response[1] ) ) { // 'true' is spam
572
			$result = true;
573
		}
574
575
		/**
576
		 * Filter the results returned by Akismet for each submitted contact form.
577
		 *
578
		 * @module contact-form
579
		 *
580
		 * @since 1.3.1
581
		 *
582
		 * @param WP_Error|bool $result Is the submitted feedback spam.
583
		 * @param array|bool $form Submitted feedback.
584
		 */
585
		return apply_filters( 'contact_form_is_spam_akismet', $result, $form );
586
	}
587
588
	/**
589
	 * Submit a feedback as either spam or ham
590
	 *
591
	 * @param string $as Either 'spam' or 'ham'.
592
	 * @param array  $form the contact-form data
593
	 */
594
	function akismet_submit( $as, $form ) {
595
		global $akismet_api_host, $akismet_api_port;
596
597
		if ( ! in_array( $as, array( 'ham', 'spam' ) ) ) {
598
			return false;
599
		}
600
601
		$query_string = '';
602
		if ( is_array( $form ) ) {
603
			$query_string = http_build_query( $form );
604
		}
605
		if ( method_exists( 'Akismet', 'http_post' ) ) {
606
			$response = Akismet::http_post( $query_string, "submit-{$as}" );
607
		} else {
608
			$response = akismet_http_post( $query_string, $akismet_api_host, "/1.1/submit-{$as}", $akismet_api_port );
609
		}
610
611
		return trim( $response[1] );
612
	}
613
614
	/**
615
	 * Prints the menu
616
	 */
617
	function export_form() {
618
		$current_screen = get_current_screen();
619
		if ( ! in_array( $current_screen->id, array( 'edit-feedback', 'feedback_page_feedback-export' ) ) ) {
620
			return;
621
		}
622
623
		if ( ! current_user_can( 'export' ) ) {
624
			return;
625
		}
626
627
		// if there aren't any feedbacks, bail out
628
		if ( ! (int) wp_count_posts( 'feedback' )->publish ) {
629
			return;
630
		}
631
		?>
632
633
		<div id="feedback-export" style="display:none">
634
			<h2><?php _e( 'Export feedback as CSV', 'jetpack' ) ?></h2>
635
			<div class="clear"></div>
636
			<form action="<?php echo admin_url( 'admin-post.php' ); ?>" method="post" class="form">
637
				<?php wp_nonce_field( 'feedback_export','feedback_export_nonce' ); ?>
638
639
				<input name="action" value="feedback_export" type="hidden">
640
				<label for="post"><?php _e( 'Select feedback to download', 'jetpack' ) ?></label>
641
				<select name="post">
642
					<option value="all"><?php esc_html_e( 'All posts', 'jetpack' ) ?></option>
643
					<?php echo $this->get_feedbacks_as_options() ?>
644
				</select>
645
646
				<br><br>
647
				<input type="submit" name="submit" id="submit" class="button button-primary" value="<?php esc_html_e( 'Download', 'jetpack' ); ?>">
648
			</form>
649
		</div>
650
651
		<?php
652
		// There aren't any usable actions in core to output the "export feedback" form in the correct place,
653
		// so this inline JS moves it from the top of the page to the bottom.
654
		?>
655
		<script type='text/javascript'>
656
		var menu = document.getElementById( 'feedback-export' ),
657
		wrapper = document.getElementsByClassName( 'wrap' )[0];
658
		<?php if ( 'edit-feedback' === $current_screen->id ) : ?>
659
		wrapper.appendChild(menu);
660
		<?php endif; ?>
661
		menu.style.display = 'block';
662
		</script>
663
		<?php
664
	}
665
666
	/**
667
	 * Fetch post content for a post and extract just the comment.
668
	 *
669
	 * @param int $post_id The post id to fetch the content for.
670
	 *
671
	 * @return string Trimmed post comment.
672
	 *
673
	 * @codeCoverageIgnore
674
	 */
675
	public function get_post_content_for_csv_export( $post_id ) {
676
		$post_content = get_post_field( 'post_content', $post_id );
677
		$content      = explode( '<!--more-->', $post_content );
678
679
		return trim( $content[0] );
680
	}
681
682
	/**
683
	 * Get `_feedback_extra_fields` field from post meta data.
684
	 *
685
	 * @param int $post_id Id of the post to fetch meta data for.
686
	 *
687
	 * @return mixed
688
	 *
689
	 * @codeCoverageIgnore - No need to be covered.
690
	 */
691
	public function get_post_meta_for_csv_export( $post_id ) {
692
		return get_post_meta( $post_id, '_feedback_extra_fields', true );
693
	}
694
695
	/**
696
	 * Get parsed feedback post fields.
697
	 *
698
	 * @param int $post_id Id of the post to fetch parsed contents for.
699
	 *
700
	 * @return array
701
	 *
702
	 * @codeCoverageIgnore - No need to be covered.
703
	 */
704
	public function get_parsed_field_contents_of_post( $post_id ) {
705
		return self::parse_fields_from_content( $post_id );
706
	}
707
708
	/**
709
	 * Properly maps fields that are missing from the post meta data
710
	 * to names, that are similar to those of the post meta.
711
	 *
712
	 * @param array $parsed_post_content Parsed post content
713
	 *
714
	 * @see parse_fields_from_content for how the input data is generated.
715
	 *
716
	 * @return array Mapped fields.
717
	 */
718
	public function map_parsed_field_contents_of_post_to_field_names( $parsed_post_content ) {
719
720
		$mapped_fields = array();
721
722
		$field_mapping = array(
723
			'_feedback_subject'      => __( 'Contact Form', 'jetpack' ),
724
			'_feedback_author'       => '1_Name',
725
			'_feedback_author_email' => '2_Email',
726
			'_feedback_author_url'   => '3_Website',
727
			'_feedback_main_comment' => '4_Comment',
728
		);
729
730
		foreach ( $field_mapping as $parsed_field_name => $field_name ) {
731
			if (
732
				isset( $parsed_post_content[ $parsed_field_name ] )
733
				&& ! empty( $parsed_post_content[ $parsed_field_name ] )
734
			) {
735
				$mapped_fields[ $field_name ] = $parsed_post_content[ $parsed_field_name ];
736
			}
737
		}
738
739
		return $mapped_fields;
740
	}
741
742
	/**
743
	 * Registers the personal data exporter.
744
	 *
745
	 * @since 6.1.1
746
	 *
747
	 * @param  array $exporters An array of personal data exporters.
748
	 *
749
	 * @return array $exporters An array of personal data exporters.
750
	 */
751
	public function register_personal_data_exporter( $exporters ) {
752
		$exporters['jetpack-feedback'] = array(
753
			'exporter_friendly_name' => __( 'Feedback', 'jetpack' ),
754
			'callback'               => array( $this, 'personal_data_exporter' ),
755
		);
756
757
		return $exporters;
758
	}
759
760
	/**
761
	 * Registers the personal data eraser.
762
	 *
763
	 * @since 6.1.1
764
	 *
765
	 * @param  array $erasers An array of personal data erasers.
766
	 *
767
	 * @return array $erasers An array of personal data erasers.
768
	 */
769
	public function register_personal_data_eraser( $erasers ) {
770
		$erasers['jetpack-feedback'] = array(
771
			'eraser_friendly_name' => __( 'Feedback', 'jetpack' ),
772
			'callback'             => array( $this, 'personal_data_eraser' ),
773
		);
774
775
		return $erasers;
776
	}
777
778
	/**
779
	 * Exports personal data.
780
	 *
781
	 * @since 6.1.1
782
	 *
783
	 * @param  string $email  Email address.
784
	 * @param  int    $page   Page to export.
785
	 *
786
	 * @return array  $return Associative array with keys expected by core.
787
	 */
788
	public function personal_data_exporter( $email, $page = 1 ) {
789
		$per_page    = 250;
790
		$export_data = array();
791
		$post_ids    = $this->personal_data_post_ids_by_email( $email, $per_page, $page );
792
793
		foreach ( $post_ids as $post_id ) {
794
			$post_fields = $this->get_parsed_field_contents_of_post( $post_id );
795
796
			if ( ! is_array( $post_fields ) || empty( $post_fields['_feedback_subject'] ) ) {
797
				continue; // Corrupt data.
798
			}
799
800
			$post_fields['_feedback_main_comment'] = $this->get_post_content_for_csv_export( $post_id );
801
			$post_fields                           = $this->map_parsed_field_contents_of_post_to_field_names( $post_fields );
802
803
			if ( ! is_array( $post_fields ) || empty( $post_fields ) ) {
804
				continue; // No fields to export.
805
			}
806
807
			$post_meta   = $this->get_post_meta_for_csv_export( $post_id );
808
			$post_meta   = is_array( $post_meta ) ? $post_meta : array();
809
810
			$post_export_data = array();
811
			$post_data        = array_merge( $post_fields, $post_meta );
812
			ksort( $post_data );
813
814
			foreach ( $post_data as $post_data_key => $post_data_value ) {
815
				$post_export_data[] = array(
816
					'name'  => preg_replace( '/^[0-9]+_/', '', $post_data_key ),
817
					'value' => $post_data_value,
818
				);
819
			}
820
821
			$export_data[] = array(
822
				'group_id'    => 'feedback',
823
				'group_label' => __( 'Feedback', 'jetpack' ),
824
				'item_id'     => 'feedback-' . $post_id,
825
				'data'        => $post_export_data,
826
			);
827
		}
828
829
		return array(
830
			'data' => $export_data,
831
			'done' => count( $post_ids ) < $per_page,
832
		);
833
	}
834
835
	/**
836
	 * Erases personal data.
837
	 *
838
	 * @since 6.1.1
839
	 *
840
	 * @param  string $email Email address.
841
	 * @param  int    $page  Page to erase.
842
	 *
843
	 * @return array         Associative array with keys expected by core.
844
	 */
845
	public function personal_data_eraser( $email, $page = 1 ) {
846
		$per_page = 250;
847
		$removed  = false;
848
		$retained = false;
849
		$messages = array();
850
		$post_ids = $this->personal_data_post_ids_by_email( $email, $per_page, $page );
851
852
		foreach ( $post_ids as $post_id ) {
853
			/**
854
			 * Filters whether to erase a particular Feedback post.
855
			 *
856
			 * @since 6.3.0
857
			 *
858
			 * @param bool|string $prevention_message Whether to apply erase the Feedback post (bool).
859
			 *                                        Custom prevention message (string). Default true.
860
			 * @param int         $post_id            Feedback post ID.
861
			 */
862
			$prevention_message = apply_filters( 'grunion_contact_form_delete_feedback_post', true, $post_id );
863
864
			if ( true !== $prevention_message ) {
865
				if ( $prevention_message && is_string( $prevention_message ) ) {
866
					$messages[] = esc_html( $prevention_message );
867
				} else {
868
					$messages[] = sprintf(
869
						// translators: %d: Post ID.
870
						__( 'Feedback ID %d could not be removed at this time.', 'jetpack' ),
871
						$post_id
872
					);
873
				}
874
875
				$retained = true;
876
877
				continue;
878
			}
879
880
			if ( wp_delete_post( $post_id, true ) ) {
881
				$removed = true;
882
			} else {
883
				$retained = true;
884
				$messages[] = sprintf(
885
					// translators: %d: Post ID.
886
					__( 'Feedback ID %d could not be removed at this time.', 'jetpack' ),
887
					$post_id
888
				);
889
			}
890
		}
891
892
		return array(
893
			'items_removed'  => $removed,
894
			'items_retained' => $retained,
895
			'messages'       => $messages,
896
			'done'           => count( $post_ids ) < $per_page,
897
		);
898
	}
899
900
	/**
901
	 * Queries personal data by email address.
902
	 *
903
	 * @since 6.1.1
904
	 *
905
	 * @param  string $email    Email address.
906
	 * @param  int    $per_page Post IDs per page. Default is `250`.
907
	 * @param  int    $page     Page to query. Default is `1`.
908
	 *
909
	 * @return array            An array of post IDs.
910
	 */
911
	public function personal_data_post_ids_by_email( $email, $per_page = 250, $page = 1 ) {
912
		add_filter( 'posts_search', array( $this, 'personal_data_search_filter' ) );
913
914
		$post_ids = get_posts( array(
915
			'post_type'        => 'feedback',
916
			'post_status'      => 'publish',
917
			's'                => 'AUTHOR EMAIL: ' . $email,
918
			'sentence'         => true,
919
			'order'            => 'ASC',
920
			'fields'           => 'ids',
921
			'posts_per_page'   => $per_page,
922
			'paged'            => $page,
923
			'suppress_filters' => false,
924
		) );
925
926
		remove_filter( 'posts_search', array( $this, 'personal_data_search_filter' ) );
927
928
		return $post_ids;
929
	}
930
931
	/**
932
	 * Filters searches by email address.
933
	 *
934
	 * @since 6.1.1
935
	 *
936
	 * @param  string $search SQL where clause.
937
	 *
938
	 * @return array          Filtered SQL where clause.
939
	 */
940
	public function personal_data_search_filter( $search ) {
941
		global $wpdb;
942
943
		/*
944
		 * Limits search to `post_content` only, and we only match the
945
		 * author's email address whenever it's on a line by itself.
946
		 * `CHAR(13)` = `\r`, `CHAR(10)` = `\n`
947
		 */
948
		if ( preg_match( '/AUTHOR EMAIL\: ([^{\s]+)/', $search, $m ) ) {
949
			$esc_like_email = esc_sql( $wpdb->esc_like( 'AUTHOR EMAIL: ' . $m[1] ) );
950
			$search         = " AND (
951
				{$wpdb->posts}.post_content LIKE CONCAT('%', CHAR(13), '{$esc_like_email}', CHAR(13), '%')
952
				OR {$wpdb->posts}.post_content LIKE CONCAT('%', CHAR(10), '{$esc_like_email}', CHAR(10), '%')
953
			)";
954
		}
955
956
		return $search;
957
	}
958
959
	/**
960
	 * Prepares feedback post data for CSV export.
961
	 *
962
	 * @param array $post_ids Post IDs to fetch the data for. These need to be Feedback posts.
963
	 *
964
	 * @return array
965
	 */
966
	public function get_export_data_for_posts( $post_ids ) {
967
968
		$posts_data  = array();
969
		$field_names = array();
970
		$result      = array();
971
972
		/**
973
		 * Fetch posts and get the possible field names for later use
974
		 */
975
		foreach ( $post_ids as $post_id ) {
976
977
			/**
978
			 * Fetch post main data, because we need the subject and author data for the feedback form.
979
			 */
980
			$post_real_data = $this->get_parsed_field_contents_of_post( $post_id );
981
982
			/**
983
			 * If `$post_real_data` is not an array or there is no `_feedback_subject` set,
984
			 * then something must be wrong with the feedback post. Skip it.
985
			 */
986
			if ( ! is_array( $post_real_data ) || ! isset( $post_real_data['_feedback_subject'] ) ) {
987
				continue;
988
			}
989
990
			/**
991
			 * Fetch main post comment. This is from the default textarea fields.
992
			 * If it is non-empty, then we add it to data, otherwise skip it.
993
			 */
994
			$post_comment_content = $this->get_post_content_for_csv_export( $post_id );
995
			if ( ! empty( $post_comment_content ) ) {
996
				$post_real_data['_feedback_main_comment'] = $post_comment_content;
997
			}
998
999
			/**
1000
			 * Map parsed fields to proper field names
1001
			 */
1002
			$mapped_fields = $this->map_parsed_field_contents_of_post_to_field_names( $post_real_data );
1003
1004
			/**
1005
			 * Fetch post meta data.
1006
			 */
1007
			$post_meta_data = $this->get_post_meta_for_csv_export( $post_id );
1008
1009
			/**
1010
			 * If `$post_meta_data` is not an array or if it is empty, then there is no
1011
			 * extra feedback to work with. Create an empty array.
1012
			 */
1013
			if ( ! is_array( $post_meta_data ) || empty( $post_meta_data ) ) {
1014
				$post_meta_data = array();
1015
			}
1016
1017
			/**
1018
			 * Prepend the feedback subject to the list of fields.
1019
			 */
1020
			$post_meta_data = array_merge(
1021
				$mapped_fields,
1022
				$post_meta_data
1023
			);
1024
1025
			/**
1026
			 * Save post metadata for later usage.
1027
			 */
1028
			$posts_data[ $post_id ] = $post_meta_data;
1029
1030
			/**
1031
			 * Save field names, so we can use them as header fields later in the CSV.
1032
			 */
1033
			$field_names = array_merge( $field_names, array_keys( $post_meta_data ) );
1034
		}
1035
1036
		/**
1037
		 * Make sure the field names are unique, because we don't want duplicate data.
1038
		 */
1039
		$field_names = array_unique( $field_names );
1040
1041
		/**
1042
		 * Sort the field names by the field id number
1043
		 */
1044
		sort( $field_names, SORT_NUMERIC );
1045
1046
		/**
1047
		 * Loop through every post, which is essentially CSV row.
1048
		 */
1049
		foreach ( $posts_data as $post_id => $single_post_data ) {
1050
1051
			/**
1052
			 * Go through all the possible fields and check if the field is available
1053
			 * in the current post.
1054
			 *
1055
			 * If it is - add the data as a value.
1056
			 * If it is not - add an empty string, which is just a placeholder in the CSV.
1057
			 */
1058
			foreach ( $field_names as $single_field_name ) {
1059
				if (
1060
					isset( $single_post_data[ $single_field_name ] )
1061
					&& ! empty( $single_post_data[ $single_field_name ] )
1062
				) {
1063
					$result[ $single_field_name ][] = trim( $single_post_data[ $single_field_name ] );
1064
				} else {
1065
					$result[ $single_field_name ][] = '';
1066
				}
1067
			}
1068
		}
1069
1070
		return $result;
1071
	}
1072
1073
	/**
1074
	 * download as a csv a contact form or all of them in a csv file
1075
	 */
1076
	function download_feedback_as_csv() {
1077
		if ( empty( $_POST['feedback_export_nonce'] ) ) {
1078
			return;
1079
		}
1080
1081
		check_admin_referer( 'feedback_export', 'feedback_export_nonce' );
1082
1083
		if ( ! current_user_can( 'export' ) ) {
1084
			return;
1085
		}
1086
1087
		$args = array(
1088
			'posts_per_page'   => -1,
1089
			'post_type'        => 'feedback',
1090
			'post_status'      => 'publish',
1091
			'order'            => 'ASC',
1092
			'fields'           => 'ids',
1093
			'suppress_filters' => false,
1094
		);
1095
1096
		$filename = date( 'Y-m-d' ) . '-feedback-export.csv';
1097
1098
		// Check if we want to download all the feedbacks or just a certain contact form
1099
		if ( ! empty( $_POST['post'] ) && $_POST['post'] !== 'all' ) {
1100
			$args['post_parent'] = (int) $_POST['post'];
1101
			$filename            = date( 'Y-m-d' ) . '-' . str_replace( '&nbsp;', '-', get_the_title( (int) $_POST['post'] ) ) . '.csv';
1102
		}
1103
1104
		$feedbacks = get_posts( $args );
1105
1106
		if ( empty( $feedbacks ) ) {
1107
			return;
1108
		}
1109
1110
		$filename  = sanitize_file_name( $filename );
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned correctly; expected 1 space but found 2 spaces

This check looks for improperly formatted assignments.

Every assignment must have exactly one space before and one space after the equals operator.

To illustrate:

$a = "a";
$ab = "ab";
$abc = "abc";

will have no issues, while

$a   = "a";
$ab  = "ab";
$abc = "abc";

will report issues in lines 1 and 2.

Loading history...
1111
1112
		/**
1113
		 * Prepare data for export.
1114
		 */
1115
		$data = $this->get_export_data_for_posts( $feedbacks );
1116
1117
		/**
1118
		 * If `$data` is empty, there's nothing we can do below.
1119
		 */
1120
		if ( ! is_array( $data ) || empty( $data ) ) {
1121
			return;
1122
		}
1123
1124
		/**
1125
		 * Extract field names from `$data` for later use.
1126
		 */
1127
		$fields = array_keys( $data );
1128
1129
		/**
1130
		 * Count how many rows will be exported.
1131
		 */
1132
		$row_count = count( reset( $data ) );
1133
1134
		// Forces the download of the CSV instead of echoing
1135
		header( 'Content-Disposition: attachment; filename=' . $filename );
1136
		header( 'Pragma: no-cache' );
1137
		header( 'Expires: 0' );
1138
		header( 'Content-Type: text/csv; charset=utf-8' );
1139
1140
		$output = fopen( 'php://output', 'w' );
1141
1142
		/**
1143
		 * Print CSV headers
1144
		 */
1145
		fputcsv( $output, $fields );
1146
1147
		/**
1148
		 * Print rows to the output.
1149
		 */
1150
		for ( $i = 0; $i < $row_count; $i ++ ) {
1151
1152
			$current_row = array();
1153
1154
			/**
1155
			 * Put all the fields in `$current_row` array.
1156
			 */
1157
			foreach ( $fields as $single_field_name ) {
1158
				$current_row[] = $this->esc_csv( $data[ $single_field_name ][ $i ] );
1159
			}
1160
1161
			/**
1162
			 * Output the complete CSV row
1163
			 */
1164
			fputcsv( $output, $current_row );
1165
		}
1166
1167
		fclose( $output );
1168
	}
1169
1170
	/**
1171
	 * Escape a string to be used in a CSV context
1172
	 *
1173
	 * Malicious input can inject formulas into CSV files, opening up the possibility for phishing attacks and
1174
	 * disclosure of sensitive information.
1175
	 *
1176
	 * Additionally, Excel exposes the ability to launch arbitrary commands through the DDE protocol.
1177
	 *
1178
	 * @see http://www.contextis.com/resources/blog/comma-separated-vulnerabilities/
1179
	 *
1180
	 * @param string $field
1181
	 *
1182
	 * @return string
1183
	 */
1184
	public function esc_csv( $field ) {
1185
		$active_content_triggers = array( '=', '+', '-', '@' );
1186
1187
		if ( in_array( mb_substr( $field, 0, 1 ), $active_content_triggers, true ) ) {
1188
			$field = "'" . $field;
1189
		}
1190
1191
		return $field;
1192
	}
1193
1194
	/**
1195
	 * Returns a string of HTML <option> items from an array of posts
1196
	 *
1197
	 * @return string a string of HTML <option> items
1198
	 */
1199
	protected function get_feedbacks_as_options() {
1200
		$options = '';
1201
1202
		// Get the feedbacks' parents' post IDs
1203
		$feedbacks = get_posts( array(
1204
			'fields'           => 'id=>parent',
1205
			'posts_per_page'   => 100000,
1206
			'post_type'        => 'feedback',
1207
			'post_status'      => 'publish',
1208
			'suppress_filters' => false,
1209
		) );
1210
		$parents = array_unique( array_values( $feedbacks ) );
1211
1212
		$posts = get_posts( array(
1213
			'orderby'          => 'ID',
1214
			'posts_per_page'   => 1000,
1215
			'post_type'        => 'any',
1216
			'post__in'         => array_values( $parents ),
1217
			'suppress_filters' => false,
1218
		) );
1219
1220
		// creates the string of <option> elements
1221
		foreach ( $posts as $post ) {
1222
			$options .= sprintf( '<option value="%s">%s</option>', esc_attr( $post->ID ), esc_html( $post->post_title ) );
1223
		}
1224
1225
		return $options;
1226
	}
1227
1228
	/**
1229
	 * Get the names of all the form's fields
1230
	 *
1231
	 * @param  array|int $posts the post we want the fields of
1232
	 *
1233
	 * @return array     the array of fields
1234
	 *
1235
	 * @deprecated As this is no longer necessary as of the CSV export rewrite. - 2015-12-29
1236
	 */
1237
	protected function get_field_names( $posts ) {
1238
		$posts = (array) $posts;
1239
		$all_fields = array();
1240
1241
		foreach ( $posts as $post ) {
1242
			$fields = self::parse_fields_from_content( $post );
1243
1244
			if ( isset( $fields['_feedback_all_fields'] ) ) {
1245
				$extra_fields = array_keys( $fields['_feedback_all_fields'] );
1246
				$all_fields = array_merge( $all_fields, $extra_fields );
1247
			}
1248
		}
1249
1250
		$all_fields = array_unique( $all_fields );
1251
		return $all_fields;
1252
	}
1253
1254
	public static function parse_fields_from_content( $post_id ) {
1255
		static $post_fields;
1256
1257
		if ( ! is_array( $post_fields ) ) {
1258
			$post_fields = array();
1259
		}
1260
1261
		if ( isset( $post_fields[ $post_id ] ) ) {
1262
			return $post_fields[ $post_id ];
1263
		}
1264
1265
		$all_values   = array();
1266
		$post_content = get_post_field( 'post_content', $post_id );
1267
		$content      = explode( '<!--more-->', $post_content );
1268
		$lines        = array();
1269
1270
		if ( count( $content ) > 1 ) {
1271
			$content  = str_ireplace( array( '<br />', ')</p>' ), '', $content[1] );
1272
			$one_line = preg_replace( '/\s+/', ' ', $content );
1273
			$one_line = preg_replace( '/.*Array \( (.*)\)/', '$1', $one_line );
1274
1275
			preg_match_all( '/\[([^\]]+)\] =\&gt\; ([^\[]+)/', $one_line, $matches );
1276
1277
			if ( count( $matches ) > 1 ) {
1278
				$all_values = array_combine( array_map( 'trim', $matches[1] ), array_map( 'trim', $matches[2] ) );
1279
			}
1280
1281
			$lines = array_filter( explode( "\n", $content ) );
1282
		}
1283
1284
		$var_map = array(
1285
			'AUTHOR'       => '_feedback_author',
1286
			'AUTHOR EMAIL' => '_feedback_author_email',
1287
			'AUTHOR URL'   => '_feedback_author_url',
1288
			'SUBJECT'      => '_feedback_subject',
1289
			'IP'           => '_feedback_ip',
1290
		);
1291
1292
		$fields = array();
1293
1294
		foreach ( $lines as $line ) {
1295
			$vars = explode( ': ', $line, 2 );
1296
			if ( ! empty( $vars ) ) {
1297
				if ( isset( $var_map[ $vars[0] ] ) ) {
1298
					$fields[ $var_map[ $vars[0] ] ] = self::strip_tags( trim( $vars[1] ) );
1299
				}
1300
			}
1301
		}
1302
1303
		$fields['_feedback_all_fields'] = $all_values;
1304
1305
		$post_fields[ $post_id ] = $fields;
1306
1307
		return $fields;
1308
	}
1309
1310
	/**
1311
	 * Creates a valid csv row from a post id
1312
	 *
1313
	 * @param  int   $post_id The id of the post
1314
	 * @param  array $fields  An array containing the names of all the fields of the csv
1315
	 * @return String The csv row
1316
	 *
1317
	 * @deprecated This is no longer needed, as of the CSV export rewrite.
1318
	 */
1319
	protected static function make_csv_row_from_feedback( $post_id, $fields ) {
1320
		$content_fields = self::parse_fields_from_content( $post_id );
1321
		$all_fields     = array();
1322
1323
		if ( isset( $content_fields['_feedback_all_fields'] ) ) {
1324
			$all_fields = $content_fields['_feedback_all_fields'];
1325
		}
1326
1327
		// Overwrite the parsed content with the content we stored in post_meta in a better format.
1328
		$extra_fields   = get_post_meta( $post_id, '_feedback_extra_fields', true );
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned correctly; expected 1 space but found 3 spaces

This check looks for improperly formatted assignments.

Every assignment must have exactly one space before and one space after the equals operator.

To illustrate:

$a = "a";
$ab = "ab";
$abc = "abc";

will have no issues, while

$a   = "a";
$ab  = "ab";
$abc = "abc";

will report issues in lines 1 and 2.

Loading history...
1329
		foreach ( $extra_fields as $extra_field => $extra_value ) {
1330
			$all_fields[ $extra_field ] = $extra_value;
1331
		}
1332
1333
		// The first element in all of the exports will be the subject
1334
		$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...
1335
1336
		// Loop the fields array in order to fill the $row_items array correctly
1337
		foreach ( $fields as $field ) {
1338
			if ( $field === __( 'Contact Form', 'jetpack' ) ) { // the first field will ever be the contact form, so we can continue
1339
				continue;
1340
			} elseif ( array_key_exists( $field, $all_fields ) ) {
1341
				$row_items[] = $all_fields[ $field ];
1342
			} else { $row_items[] = '';
1343
			}
1344
		}
1345
1346
		return $row_items;
1347
	}
1348
1349
	public static function get_ip_address() {
1350
		return isset( $_SERVER['REMOTE_ADDR'] ) ? $_SERVER['REMOTE_ADDR'] : null;
1351
	}
1352
}
1353
1354
/**
1355
 * Generic shortcode class.
1356
 * Does nothing other than store structured data and output the shortcode as a string
1357
 *
1358
 * Not very general - specific to Grunion.
1359
 */
1360
class Crunion_Contact_Form_Shortcode {
0 ignored issues
show
Coding Style Compatibility introduced by
PSR1 recommends that each class should be in its own file to aid autoloaders.

Having each class in a dedicated file usually plays nice with PSR autoloaders and is therefore a well established practice. If you use other autoloaders, you might not want to follow this rule.

Loading history...
1361
	/**
1362
	 * @var string the name of the shortcode: [$shortcode_name /]
1363
	 */
1364
	public $shortcode_name;
1365
1366
	/**
1367
	 * @var array key => value pairs for the shortcode's attributes: [$shortcode_name key="value" ... /]
1368
	 */
1369
	public $attributes;
1370
1371
	/**
1372
	 * @var array key => value pair for attribute defaults
1373
	 */
1374
	public $defaults = array();
1375
1376
	/**
1377
	 * @var null|string Null for selfclosing shortcodes.  Hhe inner content of otherwise: [$shortcode_name]$content[/$shortcode_name]
1378
	 */
1379
	public $content;
1380
1381
	/**
1382
	 * @var array Associative array of inner "child" shortcodes equivalent to the $content: [$shortcode_name][child 1/][child 2/][/$shortcode_name]
1383
	 */
1384
	public $fields;
1385
1386
	/**
1387
	 * @var null|string The HTML of the parsed inner "child" shortcodes".  Null for selfclosing shortcodes.
1388
	 */
1389
	public $body;
1390
1391
	/**
1392
	 * @param array       $attributes An associative array of shortcode attributes.  @see shortcode_atts()
1393
	 * @param null|string $content Null for selfclosing shortcodes.  The inner content otherwise.
1394
	 */
1395
	function __construct( $attributes, $content = null ) {
1396
		$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...
1397
		if ( is_array( $content ) ) {
1398
			$string_content = '';
1399
			foreach ( $content as $field ) {
1400
				$string_content .= (string) $field;
1401
			}
1402
1403
			$this->content = $string_content;
1404
		} else {
1405
			$this->content = $content;
1406
		}
1407
1408
		$this->parse_content( $this->content );
1409
	}
1410
1411
	/**
1412
	 * Processes the shortcode's inner content for "child" shortcodes
1413
	 *
1414
	 * @param string $content The shortcode's inner content: [shortcode]$content[/shortcode]
1415
	 */
1416
	function parse_content( $content ) {
1417
		if ( is_null( $content ) ) {
1418
			$this->body = null;
1419
		}
1420
1421
		$this->body = do_shortcode( $content );
1422
	}
1423
1424
	/**
1425
	 * Returns the value of the requested attribute.
1426
	 *
1427
	 * @param string $key The attribute to retrieve
1428
	 * @return mixed
1429
	 */
1430
	function get_attribute( $key ) {
1431
		return isset( $this->attributes[ $key ] ) ? $this->attributes[ $key ] : null;
1432
	}
1433
1434
	function esc_attr( $value ) {
1435
		if ( is_array( $value ) ) {
1436
			return array_map( array( $this, 'esc_attr' ), $value );
1437
		}
1438
1439
		$value = Grunion_Contact_Form_Plugin::strip_tags( $value );
1440
		$value = _wp_specialchars( $value, ENT_QUOTES, false, true );
1441
1442
		// Shortcode attributes can't contain "]"
1443
		$value = str_replace( ']', '', $value );
1444
		$value = str_replace( ',', '&#x002c;', $value ); // store commas encoded
1445
		$value = strtr( $value, array( '%' => '%25', '&' => '%26' ) );
1446
1447
		// shortcode_parse_atts() does stripcslashes()
1448
		$value = addslashes( $value );
1449
		return $value;
1450
	}
1451
1452
	function unesc_attr( $value ) {
1453
		if ( is_array( $value ) ) {
1454
			return array_map( array( $this, 'unesc_attr' ), $value );
1455
		}
1456
1457
		// For back-compat with old Grunion encoding
1458
		// Also, unencode commas
1459
		$value = strtr( $value, array( '%26' => '&', '%25' => '%' ) );
1460
		$value = preg_replace( array( '/&#x0*22;/i', '/&#x0*27;/i', '/&#x0*26;/i', '/&#x0*2c;/i' ), array( '"', "'", '&', ',' ), $value );
1461
		$value = htmlspecialchars_decode( $value, ENT_QUOTES );
1462
		$value = Grunion_Contact_Form_Plugin::strip_tags( $value );
1463
1464
		return $value;
1465
	}
1466
1467
	/**
1468
	 * Generates the shortcode
1469
	 */
1470
	function __toString() {
1471
		$r = "[{$this->shortcode_name} ";
1472
1473
		foreach ( $this->attributes as $key => $value ) {
1474
			if ( ! $value ) {
1475
				continue;
1476
			}
1477
1478
			if ( isset( $this->defaults[ $key ] ) && $this->defaults[ $key ] == $value ) {
1479
				continue;
1480
			}
1481
1482
			if ( 'id' == $key ) {
1483
				continue;
1484
			}
1485
1486
			$value = $this->esc_attr( $value );
1487
1488
			if ( is_array( $value ) ) {
1489
				$value = join( ',', $value );
1490
			}
1491
1492
			if ( false === strpos( $value, "'" ) ) {
1493
				$value = "'$value'";
1494
			} elseif ( false === strpos( $value, '"' ) ) {
1495
				$value = '"' . $value . '"';
1496
			} else {
1497
				// Shortcodes can't contain both '"' and "'".  Strip one.
1498
				$value = str_replace( "'", '', $value );
1499
				$value = "'$value'";
1500
			}
1501
1502
			$r .= "{$key}={$value} ";
1503
		}
1504
1505
		$r = rtrim( $r );
1506
1507
		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...
1508
			$r .= ']';
1509
1510
			foreach ( $this->fields as $field ) {
1511
				$r .= (string) $field;
1512
			}
1513
1514
			$r .= "[/{$this->shortcode_name}]";
1515
		} else {
1516
			$r .= '/]';
1517
		}
1518
1519
		return $r;
1520
	}
1521
}
1522
1523
/**
1524
 * Class for the contact-form shortcode.
1525
 * Parses shortcode to output the contact form as HTML
1526
 * Sends email and stores the contact form response (a.k.a. "feedback")
1527
 */
1528
class Grunion_Contact_Form extends Crunion_Contact_Form_Shortcode {
0 ignored issues
show
Coding Style Compatibility introduced by
PSR1 recommends that each class should be in its own file to aid autoloaders.

Having each class in a dedicated file usually plays nice with PSR autoloaders and is therefore a well established practice. If you use other autoloaders, you might not want to follow this rule.

Loading history...
1529
	public $shortcode_name = 'contact-form';
1530
1531
	/**
1532
	 * @var WP_Error stores form submission errors
1533
	 */
1534
	public $errors;
1535
1536
	/**
1537
	 * @var string The SHA1 hash of the attributes that comprise the form.
1538
	 */
1539
	public $hash;
1540
1541
	/**
1542
	 * @var Grunion_Contact_Form The most recent (inclusive) contact-form shortcode processed
1543
	 */
1544
	static $last;
0 ignored issues
show
Coding Style introduced by
The visibility should be declared for property $last.

The PSR-2 coding standard requires that all properties in a class have their visibility explicitly declared. If you declare a property using

class A {
    var $property;
}

the property is implicitly global.

To learn more about the PSR-2, please see the PHP-FIG site on the PSR-2.

Loading history...
1545
1546
	/**
1547
	 * @var Whatever form we are currently looking at. If processed, will become $last
1548
	 */
1549
	static $current_form;
0 ignored issues
show
Coding Style introduced by
The visibility should be declared for property $current_form.

The PSR-2 coding standard requires that all properties in a class have their visibility explicitly declared. If you declare a property using

class A {
    var $property;
}

the property is implicitly global.

To learn more about the PSR-2, please see the PHP-FIG site on the PSR-2.

Loading history...
1550
1551
	/**
1552
	 * @var array All found forms, indexed by hash.
1553
	 */
1554
	static $forms = array();
0 ignored issues
show
Coding Style introduced by
The visibility should be declared for property $forms.

The PSR-2 coding standard requires that all properties in a class have their visibility explicitly declared. If you declare a property using

class A {
    var $property;
}

the property is implicitly global.

To learn more about the PSR-2, please see the PHP-FIG site on the PSR-2.

Loading history...
1555
1556
	/**
1557
	 * @var bool Whether to print the grunion.css style when processing the contact-form shortcode
1558
	 */
1559
	static $style = false;
0 ignored issues
show
Coding Style introduced by
The visibility should be declared for property $style.

The PSR-2 coding standard requires that all properties in a class have their visibility explicitly declared. If you declare a property using

class A {
    var $property;
}

the property is implicitly global.

To learn more about the PSR-2, please see the PHP-FIG site on the PSR-2.

Loading history...
1560
1561
	function __construct( $attributes, $content = null ) {
1562
		global $post;
1563
1564
		$this->hash = sha1( json_encode( $attributes ) . $content );
1565
		self::$forms[ $this->hash ] = $this;
1566
1567
		// Set up the default subject and recipient for this form
1568
		$default_to = '';
1569
		$default_subject = '[' . get_option( 'blogname' ) . ']';
1570
1571
		if ( ! isset( $attributes ) || ! is_array( $attributes ) ) {
1572
			$attributes = array();
1573
		}
1574
1575
		if ( ! empty( $attributes['widget'] ) && $attributes['widget'] ) {
1576
			$default_to .= get_option( 'admin_email' );
1577
			$attributes['id'] = 'widget-' . $attributes['widget'];
1578
			$default_subject = sprintf( _x( '%1$s Sidebar', '%1$s = blog name', 'jetpack' ), $default_subject );
1579
		} elseif ( $post ) {
1580
			$attributes['id'] = $post->ID;
1581
			$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 ) );
1582
			$post_author = get_userdata( $post->post_author );
1583
			$default_to .= $post_author->user_email;
1584
		}
1585
1586
		// Keep reference to $this for parsing form fields
1587
		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...
1588
1589
		$this->defaults = array(
1590
			'to'                 => $default_to,
1591
			'subject'            => $default_subject,
1592
			'show_subject'       => 'no', // only used in back-compat mode
1593
			'widget'             => 0,    // Not exposed to the user. Works with Grunion_Contact_Form_Plugin::widget_atts()
1594
			'id'                 => null, // Not exposed to the user. Set above.
1595
			'submit_button_text' => __( 'Submit &#187;', 'jetpack' ),
1596
		);
1597
1598
		$attributes = shortcode_atts( $this->defaults, $attributes, 'contact-form' );
1599
1600
		// We only enable the contact-field shortcode temporarily while processing the contact-form shortcode
1601
		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...
1602
1603
		parent::__construct( $attributes, $content );
1604
1605
		// There were no fields in the contact form. The form was probably just [contact-form /]. Build a default form.
1606
		if ( empty( $this->fields ) ) {
1607
			// same as the original Grunion v1 form
1608
			$default_form = '
1609
				[contact-field label="' . __( 'Name', 'jetpack' ) . '" type="name"  required="true" /]
1610
				[contact-field label="' . __( 'Email', 'jetpack' ) . '" type="email" required="true" /]
1611
				[contact-field label="' . __( 'Website', 'jetpack' ) . '" type="url" /]';
1612
1613
			if ( 'yes' == strtolower( $this->get_attribute( 'show_subject' ) ) ) {
1614
				$default_form .= '
1615
					[contact-field label="' . __( 'Subject', 'jetpack' ) . '" type="subject" /]';
1616
			}
1617
1618
			$default_form .= '
1619
				[contact-field label="' . __( 'Message', 'jetpack' ) . '" type="textarea" /]';
1620
1621
			$this->parse_content( $default_form );
1622
1623
			// Store the shortcode
1624
			$this->store_shortcode( $default_form, $attributes, $this->hash );
1625
		} else {
1626
			// Store the shortcode
1627
			$this->store_shortcode( $content, $attributes, $this->hash );
1628
		}
1629
1630
		// $this->body and $this->fields have been setup.  We no longer need the contact-field shortcode.
1631
		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...
1632
	}
1633
1634
	/**
1635
	 * Store shortcode content for recall later
1636
	 *	- used to receate shortcode when user uses do_shortcode
1637
	 *
1638
	 * @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...
1639
	 * @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...
1640
	 * @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...
1641
	 */
1642
	static function store_shortcode( $content = null, $attributes = null, $hash = null ) {
1643
1644
		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...
Comprehensibility Best Practice introduced by
Using logical operators such as and instead of && is generally not recommended.

PHP has two types of connecting operators (logical operators, and boolean operators):

  Logical Operators Boolean Operator
AND - meaning and &&
OR - meaning or ||

The difference between these is the order in which they are executed. In most cases, you would want to use a boolean operator like &&, or ||.

Let’s take a look at a few examples:

// Logical operators have lower precedence:
$f = false or true;

// is executed like this:
($f = false) or true;


// Boolean operators have higher precedence:
$f = false || true;

// is executed like this:
$f = (false || true);

Logical Operators are used for Control-Flow

One case where you explicitly want to use logical operators is for control-flow such as this:

$x === 5
    or die('$x must be 5.');

// Instead of
if ($x !== 5) {
    die('$x must be 5.');
}

Since die introduces problems of its own, f.e. it makes our code hardly testable, and prevents any kind of more sophisticated error handling; you probably do not want to use this in real-world code. Unfortunately, logical operators cannot be combined with throw at this point:

// The following is currently a parse error.
$x === 5
    or throw new RuntimeException('$x must be 5.');

These limitations lead to logical operators rarely being of use in current PHP code.

Loading history...
1645
1646
			if ( empty( $hash ) ) {
1647
				$hash = sha1( json_encode( $attributes ) . $content );
1648
			}
1649
1650
			$shortcode_meta = get_post_meta( $attributes['id'], "_g_feedback_shortcode_{$hash}", true );
1651
1652
			if ( $shortcode_meta != '' or $shortcode_meta != $content ) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
Using logical operators such as or instead of || is generally not recommended.

PHP has two types of connecting operators (logical operators, and boolean operators):

  Logical Operators Boolean Operator
AND - meaning and &&
OR - meaning or ||

The difference between these is the order in which they are executed. In most cases, you would want to use a boolean operator like &&, or ||.

Let’s take a look at a few examples:

// Logical operators have lower precedence:
$f = false or true;

// is executed like this:
($f = false) or true;


// Boolean operators have higher precedence:
$f = false || true;

// is executed like this:
$f = (false || true);

Logical Operators are used for Control-Flow

One case where you explicitly want to use logical operators is for control-flow such as this:

$x === 5
    or die('$x must be 5.');

// Instead of
if ($x !== 5) {
    die('$x must be 5.');
}

Since die introduces problems of its own, f.e. it makes our code hardly testable, and prevents any kind of more sophisticated error handling; you probably do not want to use this in real-world code. Unfortunately, logical operators cannot be combined with throw at this point:

// The following is currently a parse error.
$x === 5
    or throw new RuntimeException('$x must be 5.');

These limitations lead to logical operators rarely being of use in current PHP code.

Loading history...
1653
				update_post_meta( $attributes['id'], "_g_feedback_shortcode_{$hash}", $content );
1654
1655
				// Save attributes to post_meta for later use. They're not available later in do_shortcode situations.
1656
				update_post_meta( $attributes['id'], "_g_feedback_shortcode_atts_{$hash}", $attributes );
1657
			}
1658
		}
1659
	}
1660
1661
	/**
1662
	 * Toggle for printing the grunion.css stylesheet
1663
	 *
1664
	 * @param bool $style
1665
	 */
1666
	static function style( $style ) {
1667
		$previous_style = self::$style;
1668
		self::$style = (bool) $style;
1669
		return $previous_style;
1670
	}
1671
1672
	/**
1673
	 * Turn on printing of grunion.css stylesheet
1674
	 *
1675
	 * @see ::style()
1676
	 * @internal
1677
	 * @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...
1678
	 */
1679
	static function _style_on() {
1680
		return self::style( true );
1681
	}
1682
1683
	/**
1684
	 * The contact-form shortcode processor
1685
	 *
1686
	 * @param array       $attributes Key => Value pairs as parsed by shortcode_parse_atts()
1687
	 * @param string|null $content The shortcode's inner content: [contact-form]$content[/contact-form]
1688
	 * @return string HTML for the concat form.
1689
	 */
1690
	static function parse( $attributes, $content ) {
1691
		require_once JETPACK__PLUGIN_DIR . '/sync/class.jetpack-sync-settings.php';
1692
		if ( Jetpack_Sync_Settings::is_syncing() ) {
1693
			return '';
1694
		}
1695
		// Create a new Grunion_Contact_Form object (this class)
1696
		$form = new Grunion_Contact_Form( $attributes, $content );
1697
1698
		$id = $form->get_attribute( 'id' );
1699
1700
		if ( ! $id ) { // something terrible has happened
1701
			return '[contact-form]';
1702
		}
1703
1704
		if ( is_feed() ) {
1705
			return '[contact-form]';
1706
		}
1707
1708
		self::$last = $form;
1709
1710
		// Enqueue the grunion.css stylesheet if self::$style allows it
1711
		if ( self::$style && ( empty( $_REQUEST['action'] ) || $_REQUEST['action'] != 'grunion_shortcode_to_json' ) ) {
1712
			// Enqueue the style here instead of printing it, because if some other plugin has run the_post()+rewind_posts(),
1713
			// (like VideoPress does), the style tag gets "printed" the first time and discarded, leaving the contact form unstyled.
1714
			// when WordPress does the real loop.
1715
			wp_enqueue_style( 'grunion.css' );
1716
		}
1717
1718
		$r = '';
1719
		$r .= "<div id='contact-form-$id'>\n";
1720
1721
		if ( is_wp_error( $form->errors ) && $form->errors->get_error_codes() ) {
1722
			// There are errors.  Display them
1723
			$r .= "<div class='form-error'>\n<h3>" . __( 'Error!', 'jetpack' ) . "</h3>\n<ul class='form-errors'>\n";
1724
			foreach ( $form->errors->get_error_messages() as $message ) {
1725
				$r .= "\t<li class='form-error-message'>" . esc_html( $message ) . "</li>\n";
1726
			}
1727
			$r .= "</ul>\n</div>\n\n";
1728
		}
1729
1730
		if ( isset( $_GET['contact-form-id'] )
1731
			&& $_GET['contact-form-id'] == self::$last->get_attribute( 'id' )
1732
			&& isset( $_GET['contact-form-sent'], $_GET['contact-form-hash'] )
1733
			&& hash_equals( $form->hash, $_GET['contact-form-hash'] ) ) { // phpcs:ignore PHPCompatibility -- skipping since `hash_equals` is part of WP core
1734
			// The contact form was submitted.  Show the success message/results
1735
			$feedback_id = (int) $_GET['contact-form-sent'];
1736
1737
			$back_url = remove_query_arg( array( 'contact-form-id', 'contact-form-sent', '_wpnonce' ) );
1738
1739
			$r_success_message =
1740
				'<h3>' . __( 'Message Sent', 'jetpack' ) .
1741
				' (<a href="' . esc_url( $back_url ) . '">' . esc_html__( 'go back', 'jetpack' ) . '</a>)' .
1742
				"</h3>\n\n";
1743
1744
			// Don't show the feedback details unless the nonce matches
1745
			if ( $feedback_id && wp_verify_nonce( stripslashes( $_GET['_wpnonce'] ), "contact-form-sent-{$feedback_id}" ) ) {
1746
				$r_success_message .= self::success_message( $feedback_id, $form );
1747
			}
1748
1749
			/**
1750
			 * Filter the message returned after a successful contact form submission.
1751
			 *
1752
			 * @module contact-form
1753
			 *
1754
			 * @since 1.3.1
1755
			 *
1756
			 * @param string $r_success_message Success message.
1757
			 */
1758
			$r .= apply_filters( 'grunion_contact_form_success_message', $r_success_message );
1759
		} else {
1760
			// Nothing special - show the normal contact form
1761
			if ( $form->get_attribute( 'widget' ) ) {
1762
				// Submit form to the current URL
1763
				$url = remove_query_arg( array( 'contact-form-id', 'contact-form-sent', 'action', '_wpnonce' ) );
1764
			} else {
1765
				// Submit form to the post permalink
1766
				$url = get_permalink();
1767
			}
1768
1769
			// For SSL/TLS page. See RFC 3986 Section 4.2
1770
			$url = set_url_scheme( $url );
1771
1772
			// May eventually want to send this to admin-post.php...
1773
			/**
1774
			 * Filter the contact form action URL.
1775
			 *
1776
			 * @module contact-form
1777
			 *
1778
			 * @since 1.3.1
1779
			 *
1780
			 * @param string $contact_form_id Contact form post URL.
1781
			 * @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...
1782
			 * @param int $id Contact Form ID.
1783
			 */
1784
			$url = apply_filters( 'grunion_contact_form_form_action', "{$url}#contact-form-{$id}", $GLOBALS['post'], $id );
1785
1786
			$r .= "<form action='" . esc_url( $url ) . "' method='post' class='contact-form commentsblock'>\n";
1787
			$r .= $form->body;
1788
			$r .= "\t<p class='contact-submit'>\n";
1789
			$r .= "\t\t<input type='submit' value='" . esc_attr( $form->get_attribute( 'submit_button_text' ) ) . "' class='pushbutton-wide'/>\n";
1790
			if ( is_user_logged_in() ) {
1791
				$r .= "\t\t" . wp_nonce_field( 'contact-form_' . $id, '_wpnonce', true, false ) . "\n"; // nonce and referer
1792
			}
1793
			$r .= "\t\t<input type='hidden' name='contact-form-id' value='$id' />\n";
1794
			$r .= "\t\t<input type='hidden' name='action' value='grunion-contact-form' />\n";
1795
			$r .= "\t\t<input type='hidden' name='contact-form-hash' value='" . esc_attr( $form->hash ) . "' />\n";
1796
			$r .= "\t</p>\n";
1797
			$r .= "</form>\n";
1798
		}
1799
1800
		$r .= '</div>';
1801
1802
		return $r;
1803
	}
1804
1805
	/**
1806
	 * Returns a success message to be returned if the form is sent via AJAX.
1807
	 *
1808
	 * @param int                         $feedback_id
1809
	 * @param object Grunion_Contact_Form $form
1810
	 *
1811
	 * @return string $message
1812
	 */
1813
	static function success_message( $feedback_id, $form ) {
1814
		return wp_kses(
1815
			'<blockquote class="contact-form-submission">'
1816
			. '<p>' . join( self::get_compiled_form( $feedback_id, $form ), '</p><p>' ) . '</p>'
1817
			. '</blockquote>',
1818
			array( 'br' => array(), 'blockquote' => array( 'class' => array() ), 'p' => array() )
1819
		);
1820
	}
1821
1822
	/**
1823
	 * Returns a compiled form with labels and values in a form of  an array
1824
	 * of lines.
1825
	 *
1826
	 * @param int                         $feedback_id
1827
	 * @param object Grunion_Contact_Form $form
1828
	 *
1829
	 * @return array $lines
1830
	 */
1831
	static function get_compiled_form( $feedback_id, $form ) {
1832
		$feedback       = get_post( $feedback_id );
1833
		$field_ids      = $form->get_field_ids();
1834
		$content_fields = Grunion_Contact_Form_Plugin::parse_fields_from_content( $feedback_id );
1835
1836
		// Maps field_ids to post_meta keys
1837
		$field_value_map = array(
1838
			'name'     => 'author',
1839
			'email'    => 'author_email',
1840
			'url'      => 'author_url',
1841
			'subject'  => 'subject',
1842
			'textarea' => false, // not a post_meta key.  This is stored in post_content
1843
		);
1844
1845
		$compiled_form = array();
1846
1847
		// "Standard" field whitelist
1848
		foreach ( $field_value_map as $type => $meta_key ) {
1849
			if ( isset( $field_ids[ $type ] ) ) {
1850
				$field = $form->fields[ $field_ids[ $type ] ];
1851
1852
				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...
1853
					if ( isset( $content_fields[ "_feedback_{$meta_key}" ] ) ) {
1854
						$value = $content_fields[ "_feedback_{$meta_key}" ];
1855
					}
1856
				} else {
1857
					// The feedback content is stored as the first "half" of post_content
1858
					$value = $feedback->post_content;
1859
					list( $value ) = explode( '<!--more-->', $value );
1860
					$value = trim( $value );
1861
				}
1862
1863
				$field_index = array_search( $field_ids[ $type ], $field_ids['all'] );
1864
				$compiled_form[ $field_index ] = sprintf(
1865
					'<b>%1$s:</b> %2$s<br /><br />',
1866
					wp_kses( $field->get_attribute( 'label' ), array() ),
1867
					nl2br( wp_kses( $value, array() ) )
0 ignored issues
show
Bug introduced by
The variable $value does not seem to be defined for all execution paths leading up to this point.

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

Let’s take a look at an example:

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

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

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

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

Available Fixes

  1. Check for existence of the variable explicitly:

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

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

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
1868
				);
1869
			}
1870
		}
1871
1872
		// "Non-standard" fields
1873
		if ( $field_ids['extra'] ) {
1874
			// array indexed by field label (not field id)
1875
			$extra_fields = get_post_meta( $feedback_id, '_feedback_extra_fields', true );
1876
1877
			/**
1878
			 * Only get data for the compiled form if `$extra_fields` is a valid and non-empty array.
1879
			 */
1880
			if ( is_array( $extra_fields ) && ! empty( $extra_fields ) ) {
1881
1882
				$extra_field_keys = array_keys( $extra_fields );
1883
1884
				$i = 0;
1885
				foreach ( $field_ids['extra'] as $field_id ) {
1886
					$field       = $form->fields[ $field_id ];
1887
					$field_index = array_search( $field_id, $field_ids['all'] );
1888
1889
					$label = $field->get_attribute( 'label' );
1890
1891
					$compiled_form[ $field_index ] = sprintf(
1892
						'<b>%1$s:</b> %2$s<br /><br />',
1893
						wp_kses( $label, array() ),
1894
						nl2br( wp_kses( $extra_fields[ $extra_field_keys[ $i ] ], array() ) )
1895
					);
1896
1897
					$i++;
1898
				}
1899
			}
1900
		}
1901
1902
		// Sorting lines by the field index
1903
		ksort( $compiled_form );
1904
1905
		return $compiled_form;
1906
	}
1907
1908
	/**
1909
	 * The contact-field shortcode processor
1910
	 * 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.
1911
	 *
1912
	 * @param array       $attributes Key => Value pairs as parsed by shortcode_parse_atts()
1913
	 * @param string|null $content The shortcode's inner content: [contact-field]$content[/contact-field]
1914
	 * @return HTML for the contact form field
1915
	 */
1916
	static function parse_contact_field( $attributes, $content ) {
1917
		// Don't try to parse contact form fields if not inside a contact form
1918
		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...
1919
			$att_strs = array();
1920
			foreach ( $attributes as $att => $val ) {
1921
				if ( is_numeric( $att ) ) { // Is a valueless attribute
1922
					$att_strs[] = esc_html( $val );
1923
				} elseif ( isset( $val ) ) { // A regular attr - value pair
1924
					$att_strs[] = esc_html( $att ) . '=\'' . esc_html( $val ) . '\'';
1925
				}
1926
			}
1927
1928
			$html = '[contact-field ' . implode( ' ', $att_strs );
1929
1930
			if ( isset( $content ) && ! empty( $content ) ) { // If there is content, let's add a closing tag
1931
				$html .= ']' . esc_html( $content ) . '[/contact-field]';
1932
			} else { // Otherwise let's add a closing slash in the first tag
1933
				$html .= '/]';
1934
			}
1935
1936
			return $html;
1937
		}
1938
1939
		$form = Grunion_Contact_Form::$current_form;
1940
1941
		$field = new Grunion_Contact_Form_Field( $attributes, $content, $form );
1942
1943
		$field_id = $field->get_attribute( 'id' );
1944
		if ( $field_id ) {
1945
			$form->fields[ $field_id ] = $field;
1946
		} else {
1947
			$form->fields[] = $field;
1948
		}
1949
1950
		if (
1951
			isset( $_POST['action'] ) && 'grunion-contact-form' === $_POST['action']
1952
		&&
1953
			isset( $_POST['contact-form-id'] ) && $form->get_attribute( 'id' ) == $_POST['contact-form-id']
1954
		&&
1955
			isset( $_POST['contact-form-hash'] ) && hash_equals( $form->hash, $_POST['contact-form-hash'] ) // phpcs:ignore PHPCompatibility -- skipping since `hash_equals` is part of WP core
1956
		) {
1957
			// If we're processing a POST submission for this contact form, validate the field value so we can show errors as necessary.
1958
			$field->validate();
1959
		}
1960
1961
		// Output HTML
1962
		return $field->render();
1963
	}
1964
1965
	/**
1966
	 * Loops through $this->fields to generate a (structured) list of field IDs.
1967
	 *
1968
	 * Important: Currently the whitelisted fields are defined as follows:
1969
	 *  `name`, `email`, `url`, `subject`, `textarea`
1970
	 *
1971
	 * If you need to add new fields to the Contact Form, please don't add them
1972
	 * to the whitelisted fields and leave them as extra fields.
1973
	 *
1974
	 * The reasoning behind this is that both the admin Feedback view and the CSV
1975
	 * export will not include any fields that are added to the list of
1976
	 * whitelisted fields without taking proper care to add them to all the
1977
	 * other places where they accessed/used/saved.
1978
	 *
1979
	 * The safest way to add new fields is to add them to the dropdown and the
1980
	 * HTML list ( @see Grunion_Contact_Form_Field::render ) and don't add them
1981
	 * to the list of whitelisted fields. This way they will become a part of the
1982
	 * `extra fields` which are saved in the post meta and will be properly
1983
	 * handled by the admin Feedback view and the CSV Export without any extra
1984
	 * work.
1985
	 *
1986
	 * If there is need to add a field to the whitelisted fields, then please
1987
	 * take proper care to add logic to handle the field in the following places:
1988
	 *
1989
	 *  - Below in the switch statement - so the field is recognized as whitelisted.
1990
	 *
1991
	 *  - Grunion_Contact_Form::process_submission - validation and logic.
1992
	 *
1993
	 *  - Grunion_Contact_Form::process_submission - add the field as an additional
1994
	 *      field in the `post_content` when saving the feedback content.
1995
	 *
1996
	 *  - Grunion_Contact_Form_Plugin::parse_fields_from_content - add mapping
1997
	 *      for the field, defined in the above method.
1998
	 *
1999
	 *  - Grunion_Contact_Form_Plugin::map_parsed_field_contents_of_post_to_field_names -
2000
	 *      add mapping of the field for the CSV Export. Otherwise it will be missing
2001
	 *      from the exported data.
2002
	 *
2003
	 *  - admin.php / grunion_manage_post_columns - add the field to the render logic.
2004
	 *      Otherwise it will be missing from the admin Feedback view.
2005
	 *
2006
	 * @return array
2007
	 */
2008
	function get_field_ids() {
2009
		$field_ids = array(
2010
			'all'   => array(), // array of all field_ids
2011
			'extra' => array(), // array of all non-whitelisted field IDs
2012
2013
			// Whitelisted "standard" field IDs:
2014
			// 'email'    => field_id,
2015
			// 'name'     => field_id,
2016
			// 'url'      => field_id,
2017
			// 'subject'  => field_id,
2018
			// 'textarea' => field_id,
2019
		);
2020
2021
		foreach ( $this->fields as $id => $field ) {
2022
			$field_ids['all'][] = $id;
2023
2024
			$type = $field->get_attribute( 'type' );
2025
			if ( isset( $field_ids[ $type ] ) ) {
2026
				// This type of field is already present in our whitelist of "standard" fields for this form
2027
				// Put it in extra
2028
				$field_ids['extra'][] = $id;
2029
				continue;
2030
			}
2031
2032
			/**
2033
			 * See method description before modifying the switch cases.
2034
			 */
2035
			switch ( $type ) {
2036
				case 'email' :
2037
				case 'name' :
2038
				case 'url' :
2039
				case 'subject' :
2040
				case 'textarea' :
2041
					$field_ids[ $type ] = $id;
2042
					break;
2043
				default :
0 ignored issues
show
Coding Style introduced by
There must be no space before the colon in a DEFAULT statement

As per the PSR-2 coding standard, there must not be a space in front of the colon in the default statement.

switch ($expr) {
    default : //wrong
        doSomething();
        break;
}

switch ($expr) {
    default: //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
2044
					// Put everything else in extra
2045
					$field_ids['extra'][] = $id;
2046
			}
2047
		}
2048
2049
		return $field_ids;
2050
	}
2051
2052
	/**
2053
	 * Process the contact form's POST submission
2054
	 * Stores feedback.  Sends email.
2055
	 */
2056
	function process_submission() {
2057
		global $post;
2058
2059
		$plugin = Grunion_Contact_Form_Plugin::init();
2060
2061
		$id     = $this->get_attribute( 'id' );
2062
		$to     = $this->get_attribute( 'to' );
2063
		$widget = $this->get_attribute( 'widget' );
2064
2065
		$contact_form_subject = $this->get_attribute( 'subject' );
2066
2067
		$to = str_replace( ' ', '', $to );
2068
		$emails = explode( ',', $to );
2069
2070
		$valid_emails = array();
2071
2072
		foreach ( (array) $emails as $email ) {
2073
			if ( ! is_email( $email ) ) {
2074
				continue;
2075
			}
2076
2077
			if ( function_exists( 'is_email_address_unsafe' ) && is_email_address_unsafe( $email ) ) {
2078
				continue;
2079
			}
2080
2081
			$valid_emails[] = $email;
2082
		}
2083
2084
		// No one to send it to, which means none of the "to" attributes are valid emails.
2085
		// Use default email instead.
2086
		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...
2087
			$valid_emails = $this->defaults['to'];
2088
		}
2089
2090
		$to = $valid_emails;
2091
2092
		// Last ditch effort to set a recipient if somehow none have been set.
2093
		if ( empty( $to ) ) {
2094
			$to = get_option( 'admin_email' );
2095
		}
2096
2097
		// Make sure we're processing the form we think we're processing... probably a redundant check.
2098
		if ( $widget ) {
2099
			if ( 'widget-' . $widget != $_POST['contact-form-id'] ) {
2100
				return false;
2101
			}
2102
		} else {
2103
			if ( $post->ID != $_POST['contact-form-id'] ) {
2104
				return false;
2105
			}
2106
		}
2107
2108
		$field_ids = $this->get_field_ids();
2109
2110
		// Initialize all these "standard" fields to null
2111
		$comment_author_email = $comment_author_email_label = // v
2112
		$comment_author       = $comment_author_label       = // v
2113
		$comment_author_url   = $comment_author_url_label   = // v
2114
		$comment_content      = $comment_content_label      = null;
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned correctly; expected 1 space but found 6 spaces

This check looks for improperly formatted assignments.

Every assignment must have exactly one space before and one space after the equals operator.

To illustrate:

$a = "a";
$ab = "ab";
$abc = "abc";

will have no issues, while

$a   = "a";
$ab  = "ab";
$abc = "abc";

will report issues in lines 1 and 2.

Loading history...
2115
2116
		// For each of the "standard" fields, grab their field label and value.
2117 View Code Duplication
		if ( isset( $field_ids['name'] ) ) {
2118
			$field = $this->fields[ $field_ids['name'] ];
2119
			$comment_author = Grunion_Contact_Form_Plugin::strip_tags(
2120
				stripslashes(
2121
					/** This filter is already documented in core/wp-includes/comment-functions.php */
2122
					apply_filters( 'pre_comment_author_name', addslashes( $field->value ) )
2123
				)
2124
			);
2125
			$comment_author_label = Grunion_Contact_Form_Plugin::strip_tags( $field->get_attribute( 'label' ) );
2126
		}
2127
2128 View Code Duplication
		if ( isset( $field_ids['email'] ) ) {
2129
			$field = $this->fields[ $field_ids['email'] ];
2130
			$comment_author_email = Grunion_Contact_Form_Plugin::strip_tags(
2131
				stripslashes(
2132
					/** This filter is already documented in core/wp-includes/comment-functions.php */
2133
					apply_filters( 'pre_comment_author_email', addslashes( $field->value ) )
2134
				)
2135
			);
2136
			$comment_author_email_label = Grunion_Contact_Form_Plugin::strip_tags( $field->get_attribute( 'label' ) );
2137
		}
2138
2139
		if ( isset( $field_ids['url'] ) ) {
2140
			$field = $this->fields[ $field_ids['url'] ];
2141
			$comment_author_url = Grunion_Contact_Form_Plugin::strip_tags(
2142
				stripslashes(
2143
					/** This filter is already documented in core/wp-includes/comment-functions.php */
2144
					apply_filters( 'pre_comment_author_url', addslashes( $field->value ) )
2145
				)
2146
			);
2147
			if ( 'http://' == $comment_author_url ) {
2148
				$comment_author_url = '';
2149
			}
2150
			$comment_author_url_label = Grunion_Contact_Form_Plugin::strip_tags( $field->get_attribute( 'label' ) );
2151
		}
2152
2153
		if ( isset( $field_ids['textarea'] ) ) {
2154
			$field = $this->fields[ $field_ids['textarea'] ];
2155
			$comment_content = trim( Grunion_Contact_Form_Plugin::strip_tags( $field->value ) );
2156
			$comment_content_label = Grunion_Contact_Form_Plugin::strip_tags( $field->get_attribute( 'label' ) );
2157
		}
2158
2159
		if ( isset( $field_ids['subject'] ) ) {
2160
			$field = $this->fields[ $field_ids['subject'] ];
2161
			if ( $field->value ) {
2162
				$contact_form_subject = Grunion_Contact_Form_Plugin::strip_tags( $field->value );
2163
			}
2164
		}
2165
2166
		$all_values = $extra_values = array();
2167
		$i = 1; // Prefix counter for stored metadata
2168
2169
		// For all fields, grab label and value
2170
		foreach ( $field_ids['all'] as $field_id ) {
2171
			$field = $this->fields[ $field_id ];
2172
			$label = $i . '_' . $field->get_attribute( 'label' );
2173
			$value = $field->value;
2174
2175
			$all_values[ $label ] = $value;
2176
			$i++; // Increment prefix counter for the next field
2177
		}
2178
2179
		// For the "non-standard" fields, grab label and value
2180
		// Extra fields have their prefix starting from count( $all_values ) + 1
2181
		foreach ( $field_ids['extra'] as $field_id ) {
2182
			$field = $this->fields[ $field_id ];
2183
			$label = $i . '_' . $field->get_attribute( 'label' );
2184
			$value = $field->value;
2185
2186
			if ( is_array( $value ) ) {
2187
				$value = implode( ', ', $value );
2188
			}
2189
2190
			$extra_values[ $label ] = $value;
2191
			$i++; // Increment prefix counter for the next extra field
2192
		}
2193
2194
		$contact_form_subject = trim( $contact_form_subject );
2195
2196
		$comment_author_IP = Grunion_Contact_Form_Plugin::get_ip_address();
2197
2198
		$vars = array( 'comment_author', 'comment_author_email', 'comment_author_url', 'contact_form_subject', 'comment_author_IP' );
2199
		foreach ( $vars as $var ) {
2200
			$$var = str_replace( array( "\n", "\r" ), '', $$var );
2201
		}
2202
2203
		// Ensure that Akismet gets all of the relevant information from the contact form,
2204
		// not just the textarea field and predetermined subject.
2205
		$akismet_vars = compact( $vars );
2206
		$akismet_vars['comment_content'] = $comment_content;
2207
2208
		foreach ( array_merge( $field_ids['all'], $field_ids['extra'] ) as $field_id ) {
2209
			$field = $this->fields[ $field_id ];
2210
2211
			// Skip any fields that are just a choice from a pre-defined list. They wouldn't have any value
2212
			// from a spam-filtering point of view.
2213
			if ( in_array( $field->get_attribute( 'type' ), array( 'select', 'checkbox', 'checkbox-multiple', 'radio' ) ) ) {
2214
				continue;
2215
			}
2216
2217
			// Normalize the label into a slug.
2218
			$field_slug = trim( // Strip all leading/trailing dashes.
2219
				preg_replace(   // Normalize everything to a-z0-9_-
2220
					'/[^a-z0-9_]+/',
2221
					'-',
2222
					strtolower( $field->get_attribute( 'label' ) ) // Lowercase
2223
				),
2224
				'-'
2225
			);
2226
2227
			$field_value = ( is_array( $field->value ) ) ? trim( implode( ', ', $field->value ) ) : trim( $field->value );
2228
2229
			// Skip any values that are already in the array we're sending.
2230
			if ( $field_value && in_array( $field_value, $akismet_vars ) ) {
2231
				continue;
2232
			}
2233
2234
			$akismet_vars[ 'contact_form_field_' . $field_slug ] = $field_value;
2235
		}
2236
2237
		$spam = '';
2238
		$akismet_values = $plugin->prepare_for_akismet( $akismet_vars );
2239
2240
		// Is it spam?
2241
		/** This filter is already documented in modules/contact-form/admin.php */
2242
		$is_spam = apply_filters( 'jetpack_contact_form_is_spam', false, $akismet_values );
2243
		if ( is_wp_error( $is_spam ) ) { // WP_Error to abort
2244
			return $is_spam; // abort
2245
		} elseif ( $is_spam === true ) {  // TRUE to flag a spam
2246
			$spam = '***SPAM*** ';
2247
		}
2248
2249
		if ( ! $comment_author ) {
2250
			$comment_author = $comment_author_email;
2251
		}
2252
2253
		/**
2254
		 * Filter the email where a submitted feedback is sent.
2255
		 *
2256
		 * @module contact-form
2257
		 *
2258
		 * @since 1.3.1
2259
		 *
2260
		 * @param string|array $to Array of valid email addresses, or single email address.
2261
		 */
2262
		$to = (array) apply_filters( 'contact_form_to', $to );
2263
		$reply_to_addr = $to[0]; // get just the address part before the name part is added
2264
2265
		foreach ( $to as $to_key => $to_value ) {
2266
			$to[ $to_key ] = Grunion_Contact_Form_Plugin::strip_tags( $to_value );
2267
			$to[ $to_key ] = self::add_name_to_address( $to_value );
2268
		}
2269
2270
		$blog_url = parse_url( site_url() );
2271
		$from_email_addr = 'wordpress@' . $blog_url['host'];
2272
2273
		if ( ! empty( $comment_author_email ) ) {
2274
			$reply_to_addr = $comment_author_email;
2275
		}
2276
2277
		$headers = 'From: "' . $comment_author . '" <' . $from_email_addr . ">\r\n" .
2278
					'Reply-To: "' . $comment_author . '" <' . $reply_to_addr . ">\r\n";
2279
2280
		// Build feedback reference
2281
		$feedback_time  = current_time( 'mysql' );
2282
		$feedback_title = "{$comment_author} - {$feedback_time}";
2283
		$feedback_id    = md5( $feedback_title );
2284
2285
		$all_values = array_merge( $all_values, array(
2286
			'entry_title'     => the_title_attribute( 'echo=0' ),
2287
			'entry_permalink' => esc_url( get_permalink( get_the_ID() ) ),
2288
			'feedback_id'     => $feedback_id,
2289
		) );
2290
2291
		/** This filter is already documented in modules/contact-form/admin.php */
2292
		$subject = apply_filters( 'contact_form_subject', $contact_form_subject, $all_values );
2293
		$url     = $widget ? home_url( '/' ) : get_permalink( $post->ID );
2294
2295
		$date_time_format = _x( '%1$s \a\t %2$s', '{$date_format} \a\t {$time_format}', 'jetpack' );
2296
		$date_time_format = sprintf( $date_time_format, get_option( 'date_format' ), get_option( 'time_format' ) );
2297
		$time = date_i18n( $date_time_format, current_time( 'timestamp' ) );
2298
2299
		// keep a copy of the feedback as a custom post type
2300
		$feedback_status = $is_spam === true ? 'spam' : 'publish';
2301
2302
		foreach ( (array) $akismet_values as $av_key => $av_value ) {
2303
			$akismet_values[ $av_key ] = Grunion_Contact_Form_Plugin::strip_tags( $av_value );
2304
		}
2305
2306
		foreach ( (array) $all_values as $all_key => $all_value ) {
2307
			$all_values[ $all_key ] = Grunion_Contact_Form_Plugin::strip_tags( $all_value );
2308
		}
2309
2310
		foreach ( (array) $extra_values as $ev_key => $ev_value ) {
2311
			$extra_values[ $ev_key ] = Grunion_Contact_Form_Plugin::strip_tags( $ev_value );
2312
		}
2313
2314
		/*
2315
		 We need to make sure that the post author is always zero for contact
2316
		 * form submissions.  This prevents export/import from trying to create
2317
		 * new users based on form submissions from people who were logged in
2318
		 * at the time.
2319
		 *
2320
		 * Unfortunately wp_insert_post() tries very hard to make sure the post
2321
		 * author gets the currently logged in user id.  That is how we ended up
2322
		 * with this work around. */
2323
		add_filter( 'wp_insert_post_data', array( $plugin, 'insert_feedback_filter' ), 10, 2 );
2324
2325
		$post_id = wp_insert_post( array(
2326
			'post_date'    => addslashes( $feedback_time ),
2327
			'post_type'    => 'feedback',
2328
			'post_status'  => addslashes( $feedback_status ),
2329
			'post_parent'  => (int) $post->ID,
2330
			'post_title'   => addslashes( wp_kses( $feedback_title, array() ) ),
2331
			'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
2332
			'post_name'    => $feedback_id,
2333
		) );
2334
2335
		// once insert has finished we don't need this filter any more
2336
		remove_filter( 'wp_insert_post_data', array( $plugin, 'insert_feedback_filter' ), 10 );
2337
2338
		update_post_meta( $post_id, '_feedback_extra_fields', $this->addslashes_deep( $extra_values ) );
2339
2340
		if ( 'publish' == $feedback_status ) {
2341
			// Increase count of unread feedback.
2342
			$unread = get_option( 'feedback_unread_count', 0 ) + 1;
2343
			update_option( 'feedback_unread_count', $unread );
2344
		}
2345
2346
		if ( defined( 'AKISMET_VERSION' ) ) {
2347
			update_post_meta( $post_id, '_feedback_akismet_values', $this->addslashes_deep( $akismet_values ) );
2348
		}
2349
2350
		$message = self::get_compiled_form( $post_id, $this );
2351
2352
		array_push(
2353
			$message,
2354
			"<br />",
2355
			'<hr />',
2356
			__( 'Time:', 'jetpack' ) . ' ' . $time . '<br />',
2357
			__( 'IP Address:', 'jetpack' ) . ' ' . $comment_author_IP . '<br />',
2358
			__( 'Contact Form URL:', 'jetpack' ) . ' ' . $url . '<br />'
2359
		);
2360
2361
		if ( is_user_logged_in() ) {
2362
			array_push(
2363
				$message,
2364
				sprintf(
2365
					'<p>' . __( 'Sent by a verified %s user.', 'jetpack' ) . '</p>',
2366
					isset( $GLOBALS['current_site']->site_name ) && $GLOBALS['current_site']->site_name ?
2367
						$GLOBALS['current_site']->site_name : '"' . get_option( 'blogname' ) . '"'
2368
				)
2369
			);
2370
		} else {
2371
			array_push( $message, '<p>' . __( 'Sent by an unverified visitor to your site.', 'jetpack' ) . '</p>' );
2372
		}
2373
2374
		$message = join( $message, '' );
2375
2376
		/**
2377
		 * Filters the message sent via email after a successful form submission.
2378
		 *
2379
		 * @module contact-form
2380
		 *
2381
		 * @since 1.3.1
2382
		 *
2383
		 * @param string $message Feedback email message.
2384
		 */
2385
		$message = apply_filters( 'contact_form_message', $message );
2386
2387
		// This is called after `contact_form_message`, in order to preserve back-compat
2388
		$message = self::wrap_message_in_html_tags( $message );
2389
2390
		update_post_meta( $post_id, '_feedback_email', $this->addslashes_deep( compact( 'to', 'message' ) ) );
2391
2392
		/**
2393
		 * Fires right before the contact form message is sent via email to
2394
		 * the recipient specified in the contact form.
2395
		 *
2396
		 * @module contact-form
2397
		 *
2398
		 * @since 1.3.1
2399
		 *
2400
		 * @param integer $post_id Post contact form lives on
2401
		 * @param array $all_values Contact form fields
2402
		 * @param array $extra_values Contact form fields not included in $all_values
2403
		 */
2404
		do_action( 'grunion_pre_message_sent', $post_id, $all_values, $extra_values );
2405
2406
		// schedule deletes of old spam feedbacks
2407
		if ( ! wp_next_scheduled( 'grunion_scheduled_delete' ) ) {
2408
			wp_schedule_event( time() + 250, 'daily', 'grunion_scheduled_delete' );
2409
		}
2410
2411
		if (
2412
			$is_spam !== true &&
2413
			/**
2414
			 * Filter to choose whether an email should be sent after each successful contact form submission.
2415
			 *
2416
			 * @module contact-form
2417
			 *
2418
			 * @since 2.6.0
2419
			 *
2420
			 * @param bool true Should an email be sent after a form submission. Default to true.
2421
			 * @param int $post_id Post ID.
2422
			 */
2423
			true === apply_filters( 'grunion_should_send_email', true, $post_id )
2424
		) {
2425
			self::wp_mail( $to, "{$spam}{$subject}", $message, $headers );
2426
		} elseif (
2427
			true === $is_spam &&
2428
			/**
2429
			 * Choose whether an email should be sent for each spam contact form submission.
2430
			 *
2431
			 * @module contact-form
2432
			 *
2433
			 * @since 1.3.1
2434
			 *
2435
			 * @param bool false Should an email be sent after a spam form submission. Default to false.
2436
			 */
2437
			apply_filters( 'grunion_still_email_spam', false ) == true
2438
		) { // don't send spam by default.  Filterable.
2439
			self::wp_mail( $to, "{$spam}{$subject}", $message, $headers );
2440
		}
2441
2442
		if ( defined( 'DOING_AJAX' ) && DOING_AJAX ) {
2443
			return self::success_message( $post_id, $this );
2444
		}
2445
2446
		$redirect = wp_get_referer();
2447
		if ( ! $redirect ) { // wp_get_referer() returns false if the referer is the same as the current page
2448
			$redirect = $_SERVER['REQUEST_URI'];
2449
		}
2450
2451
		$redirect = add_query_arg( urlencode_deep( array(
2452
			'contact-form-id'   => $id,
2453
			'contact-form-sent' => $post_id,
2454
			'contact-form-hash' => $this->hash,
2455
			'_wpnonce'          => wp_create_nonce( "contact-form-sent-{$post_id}" ), // wp_nonce_url HTMLencodes :(
2456
		) ), $redirect );
2457
2458
		/**
2459
		 * Filter the URL where the reader is redirected after submitting a form.
2460
		 *
2461
		 * @module contact-form
2462
		 *
2463
		 * @since 1.9.0
2464
		 *
2465
		 * @param string $redirect Post submission URL.
2466
		 * @param int $id Contact Form ID.
2467
		 * @param int $post_id Post ID.
2468
		 */
2469
		$redirect = apply_filters( 'grunion_contact_form_redirect_url', $redirect, $id, $post_id );
2470
2471
		wp_safe_redirect( $redirect );
2472
		exit;
0 ignored issues
show
Coding Style Compatibility introduced by
The method process_submission() contains an exit expression.

An exit expression should only be used in rare cases. For example, if you write a short command line script.

In most cases however, using an exit expression makes the code untestable and often causes incompatibilities with other libraries. Thus, unless you are absolutely sure it is required here, we recommend to refactor your code to avoid its usage.

Loading history...
2473
	}
2474
2475
	/**
2476
	 * Wrapper for wp_mail() that enables HTML messages with text alternatives
2477
	 *
2478
	 * @param string|array $to          Array or comma-separated list of email addresses to send message.
2479
	 * @param string       $subject     Email subject.
2480
	 * @param string       $message     Message contents.
2481
	 * @param string|array $headers     Optional. Additional headers.
2482
	 * @param string|array $attachments Optional. Files to attach.
2483
	 *
2484
	 * @return bool Whether the email contents were sent successfully.
2485
	 */
2486
	public static function wp_mail( $to, $subject, $message, $headers = '', $attachments = array() ) {
2487
		add_filter( 'wp_mail_content_type', __CLASS__ . '::get_mail_content_type' );
2488
		add_action( 'phpmailer_init',       __CLASS__ . '::add_plain_text_alternative' );
2489
2490
		$result = wp_mail( $to, $subject, $message, $headers, $attachments );
2491
2492
		remove_filter( 'wp_mail_content_type', __CLASS__ . '::get_mail_content_type' );
2493
		remove_action( 'phpmailer_init',       __CLASS__ . '::add_plain_text_alternative' );
2494
2495
		return $result;
2496
	}
2497
2498
	/**
2499
	 * Add a display name part to an email address
2500
	 *
2501
	 * SpamAssassin doesn't like addresses in HTML messages that are missing display names (e.g., `[email protected]`
2502
	 * instead of `"Foo Bar" <[email protected]>`.
2503
	 *
2504
	 * @param string $address
2505
	 *
2506
	 * @return string
2507
	 */
2508
	function add_name_to_address( $address ) {
2509
		// If it's just the address, without a display name
2510
		if ( is_email( $address ) ) {
2511
			$address_parts = explode( '@', $address );
2512
			$address = sprintf( '"%s" <%s>', $address_parts[0], $address );
2513
		}
2514
2515
		return $address;
2516
	}
2517
2518
	/**
2519
	 * Get the content type that should be assigned to outbound emails
2520
	 *
2521
	 * @return string
2522
	 */
2523
	static function get_mail_content_type() {
2524
		return 'text/html';
2525
	}
2526
2527
	/**
2528
	 * Wrap a message body with the appropriate in HTML tags
2529
	 *
2530
	 * This helps to ensure correct parsing by clients, and also helps avoid triggering spam filtering rules
2531
	 *
2532
	 * @param string $body
2533
	 *
2534
	 * @return string
2535
	 */
2536
	static function wrap_message_in_html_tags( $body ) {
2537
		// Don't do anything if the message was already wrapped in HTML tags
2538
		// That could have be done by a plugin via filters
2539
		if ( false !== strpos( $body, '<html' ) ) {
2540
			return $body;
2541
		}
2542
2543
		$html_message = sprintf(
2544
			// The tabs are just here so that the raw code is correctly formatted for developers
2545
			// They're removed so that they don't affect the final message sent to users
2546
			str_replace( "\t", '',
2547
				"<!doctype html>
2548
				<html xmlns=\"http://www.w3.org/1999/xhtml\">
2549
				<body>
2550
2551
				%s
2552
2553
				</body>
2554
				</html>"
2555
			),
2556
			$body
2557
		);
2558
2559
		return $html_message;
2560
	}
2561
2562
	/**
2563
	 * Add a plain-text alternative part to an outbound email
2564
	 *
2565
	 * This makes the message more accessible to mail clients that aren't HTML-aware, and decreases the likelihood
2566
	 * that the message will be flagged as spam.
2567
	 *
2568
	 * @param PHPMailer $phpmailer
2569
	 */
2570
	static function add_plain_text_alternative( $phpmailer ) {
2571
		// Add an extra break so that the extra space above the <p> is preserved after the <p> is stripped out
2572
		$alt_body = str_replace( '<p>', '<p><br />', $phpmailer->Body );
2573
2574
		// Convert <br> to \n breaks, to preserve the space between lines that we want to keep
2575
		$alt_body = str_replace( array( '<br>', '<br />' ), "\n", $alt_body );
2576
2577
		// Convert <hr> to an plain-text equivalent, to preserve the integrity of the message
2578
		$alt_body = str_replace( array( "<hr>", "<hr />" ), "----\n", $alt_body );
2579
2580
		// Trim the plain text message to remove the \n breaks that were after <doctype>, <html>, and <body>
2581
		$phpmailer->AltBody = trim( strip_tags( $alt_body ) );
2582
	}
2583
2584
	function addslashes_deep( $value ) {
2585
		if ( is_array( $value ) ) {
2586
			return array_map( array( $this, 'addslashes_deep' ), $value );
2587
		} elseif ( is_object( $value ) ) {
2588
			$vars = get_object_vars( $value );
2589
			foreach ( $vars as $key => $data ) {
2590
				$value->{$key} = $this->addslashes_deep( $data );
2591
			}
2592
			return $value;
2593
		}
2594
2595
		return addslashes( $value );
2596
	}
2597
}
2598
2599
/**
2600
 * Class for the contact-field shortcode.
2601
 * Parses shortcode to output the contact form field as HTML.
2602
 * Validates input.
2603
 */
2604
class Grunion_Contact_Form_Field extends Crunion_Contact_Form_Shortcode {
0 ignored issues
show
Coding Style Compatibility introduced by
PSR1 recommends that each class should be in its own file to aid autoloaders.

Having each class in a dedicated file usually plays nice with PSR autoloaders and is therefore a well established practice. If you use other autoloaders, you might not want to follow this rule.

Loading history...
2605
	public $shortcode_name = 'contact-field';
2606
2607
	/**
2608
	 * @var Grunion_Contact_Form parent form
2609
	 */
2610
	public $form;
2611
2612
	/**
2613
	 * @var string default or POSTed value
2614
	 */
2615
	public $value;
2616
2617
	/**
2618
	 * @var bool Is the input invalid?
2619
	 */
2620
	public $error = false;
2621
2622
	/**
2623
	 * @param array                $attributes An associative array of shortcode attributes.  @see shortcode_atts()
2624
	 * @param null|string          $content Null for selfclosing shortcodes.  The inner content otherwise.
2625
	 * @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...
2626
	 */
2627
	function __construct( $attributes, $content = null, $form = null ) {
2628
		$attributes = shortcode_atts( array(
2629
					'label'       => null,
2630
					'type'        => 'text',
2631
					'required'    => false,
2632
					'options'     => array(),
2633
					'id'          => null,
2634
					'default'     => null,
2635
					'values'      => null,
2636
					'placeholder' => null,
2637
					'class'       => null,
2638
		), $attributes, 'contact-field' );
2639
2640
		// special default for subject field
2641
		if ( 'subject' == $attributes['type'] && is_null( $attributes['default'] ) && ! is_null( $form ) ) {
2642
			$attributes['default'] = $form->get_attribute( 'subject' );
2643
		}
2644
2645
		// allow required=1 or required=true
2646
		if ( '1' == $attributes['required'] || 'true' == strtolower( $attributes['required'] ) ) {
2647
			$attributes['required'] = true;
2648
		} else { $attributes['required'] = false;
2649
		}
2650
2651
		// parse out comma-separated options list (for selects, radios, and checkbox-multiples)
2652
		if ( ! empty( $attributes['options'] ) && is_string( $attributes['options'] ) ) {
2653
			$attributes['options'] = array_map( 'trim', explode( ',', $attributes['options'] ) );
2654
2655 View Code Duplication
			if ( ! empty( $attributes['values'] ) && is_string( $attributes['values'] ) ) {
2656
				$attributes['values'] = array_map( 'trim', explode( ',', $attributes['values'] ) );
2657
			}
2658
		}
2659
2660
		if ( $form ) {
2661
			// make a unique field ID based on the label, with an incrementing number if needed to avoid clashes
2662
			$form_id = $form->get_attribute( 'id' );
2663
			$id = isset( $attributes['id'] ) ? $attributes['id'] : false;
2664
2665
			$unescaped_label = $this->unesc_attr( $attributes['label'] );
2666
			$unescaped_label = str_replace( '%', '-', $unescaped_label ); // jQuery doesn't like % in IDs?
2667
			$unescaped_label = preg_replace( '/[^a-zA-Z0-9.-_:]/', '', $unescaped_label );
2668
2669
			if ( empty( $id ) ) {
2670
				$id = sanitize_title_with_dashes( 'g' . $form_id . '-' . $unescaped_label );
2671
				$i = 0;
2672
				$max_tries = 99;
2673
				while ( isset( $form->fields[ $id ] ) ) {
2674
					$i++;
2675
					$id = sanitize_title_with_dashes( 'g' . $form_id . '-' . $unescaped_label . '-' . $i );
2676
2677
					if ( $i > $max_tries ) {
2678
						break;
2679
					}
2680
				}
2681
			}
2682
2683
			$attributes['id'] = $id;
2684
		}
2685
2686
		parent::__construct( $attributes, $content );
2687
2688
		// Store parent form
2689
		$this->form = $form;
2690
	}
2691
2692
	/**
2693
	 * This field's input is invalid.  Flag as invalid and add an error to the parent form
2694
	 *
2695
	 * @param string $message The error message to display on the form.
2696
	 */
2697
	function add_error( $message ) {
2698
		$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...
2699
2700
		if ( ! is_wp_error( $this->form->errors ) ) {
2701
			$this->form->errors = new WP_Error;
2702
		}
2703
2704
		$this->form->errors->add( $this->get_attribute( 'id' ), $message );
2705
	}
2706
2707
	/**
2708
	 * Is the field input invalid?
2709
	 *
2710
	 * @see $error
2711
	 *
2712
	 * @return bool
2713
	 */
2714
	function is_error() {
2715
		return $this->error;
2716
	}
2717
2718
	/**
2719
	 * Validates the form input
2720
	 */
2721
	function validate() {
2722
		// If it's not required, there's nothing to validate
2723
		if ( ! $this->get_attribute( 'required' ) ) {
2724
			return;
2725
		}
2726
2727
		$field_id    = $this->get_attribute( 'id' );
2728
		$field_type  = $this->get_attribute( 'type' );
2729
		$field_label = $this->get_attribute( 'label' );
2730
2731
		if ( isset( $_POST[ $field_id ] ) ) {
2732
			if ( is_array( $_POST[ $field_id ] ) ) {
2733
				$field_value = array_map( 'stripslashes', $_POST[ $field_id ] );
2734
			} else {
2735
				$field_value = stripslashes( $_POST[ $field_id ] );
2736
			}
2737
		} else {
2738
			$field_value = '';
2739
		}
2740
2741
		switch ( $field_type ) {
2742
			case 'email' :
2743
				// Make sure the email address is valid
2744
				if ( ! is_email( $field_value ) ) {
2745
					/* translators: %s is the name of a form field */
2746
					$this->add_error( sprintf( __( '%s requires a valid email address', 'jetpack' ), $field_label ) );
2747
				}
2748
			break;
2749
			case 'checkbox-multiple' :
2750
				// Check that there is at least one option selected
2751
				if ( empty( $field_value ) ) {
2752
					/* translators: %s is the name of a form field */
2753
					$this->add_error( sprintf( __( '%s requires at least one selection', 'jetpack' ), $field_label ) );
2754
				}
2755
			break;
2756
			default :
0 ignored issues
show
Coding Style introduced by
There must be no space before the colon in a DEFAULT statement

As per the PSR-2 coding standard, there must not be a space in front of the colon in the default statement.

switch ($expr) {
    default : //wrong
        doSomething();
        break;
}

switch ($expr) {
    default: //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
2757
				// Just check for presence of any text
2758
				if ( ! strlen( trim( $field_value ) ) ) {
2759
					/* translators: %s is the name of a form field */
2760
					$this->add_error( sprintf( __( '%s is required', 'jetpack' ), $field_label ) );
2761
				}
2762
		}
2763
	}
2764
2765
2766
	/**
2767
	 * Check the default value for options field
2768
	 *
2769
	 * @param string value
2770
	 * @param int index
2771
	 * @param string default value
2772
	 *
2773
	 * @return string
2774
	 */
2775
	public function get_option_value( $value, $index, $options ) {
2776
		if ( empty( $value[ $index ] ) ) {
2777
			return $options;
2778
		}
2779
		return $value[ $index ];
2780
	}
2781
2782
	/**
2783
	 * Outputs the HTML for this form field
2784
	 *
2785
	 * @return string HTML
2786
	 */
2787
	function render() {
2788
		global $current_user, $user_identity;
2789
2790
		$r = '';
2791
2792
		$field_id          = $this->get_attribute( 'id' );
2793
		$field_type        = $this->get_attribute( 'type' );
2794
		$field_label       = $this->get_attribute( 'label' );
2795
		$field_required    = $this->get_attribute( 'required' );
2796
		$placeholder       = $this->get_attribute( 'placeholder' );
2797
		$class             = 'date' === $field_type ? 'jp-contact-form-date' : $this->get_attribute( 'class' );
2798
		$field_placeholder = ( ! empty( $placeholder ) ) ? "placeholder='" . esc_attr( $placeholder ) . "'" : '';
2799
		$field_class       = "class='" . trim( esc_attr( $field_type ) . ' ' . esc_attr( $class ) ) . "' ";
2800
2801
		if ( isset( $_POST[ $field_id ] ) ) {
2802
			if ( is_array( $_POST[ $field_id ] ) ) {
2803
				$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...
2804
			} else {
2805
				$this->value = stripslashes( (string) $_POST[ $field_id ] );
2806
			}
2807
		} elseif ( isset( $_GET[ $field_id ] ) ) {
2808
			$this->value = stripslashes( (string) $_GET[ $field_id ] );
2809
		} elseif (
2810
			is_user_logged_in() &&
2811
			( ( defined( 'IS_WPCOM' ) && IS_WPCOM ) ||
2812
			/**
2813
			 * Allow third-party tools to prefill the contact form with the user's details when they're logged in.
2814
			 *
2815
			 * @module contact-form
2816
			 *
2817
			 * @since 3.2.0
2818
			 *
2819
			 * @param bool false Should the Contact Form be prefilled with your details when you're logged in. Default to false.
2820
			 */
2821
			true === apply_filters( 'jetpack_auto_fill_logged_in_user', false )
2822
			)
2823
		) {
2824
			// Special defaults for logged-in users
2825
			switch ( $this->get_attribute( 'type' ) ) {
2826
				case 'email' :
2827
					$this->value = $current_user->data->user_email;
2828
				break;
2829
				case 'name' :
2830
					$this->value = $user_identity;
2831
				break;
2832
				case 'url' :
2833
					$this->value = $current_user->data->user_url;
2834
				break;
2835
				default :
0 ignored issues
show
Coding Style introduced by
There must be no space before the colon in a DEFAULT statement

As per the PSR-2 coding standard, there must not be a space in front of the colon in the default statement.

switch ($expr) {
    default : //wrong
        doSomething();
        break;
}

switch ($expr) {
    default: //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
2836
					$this->value = $this->get_attribute( 'default' );
2837
			}
2838
		} else {
2839
			$this->value = $this->get_attribute( 'default' );
2840
		}
2841
2842
		$field_value = Grunion_Contact_Form_Plugin::strip_tags( $this->value );
2843
		$field_label = Grunion_Contact_Form_Plugin::strip_tags( $field_label );
2844
2845
		/**
2846
		 * Filter the Contact Form required field text
2847
		 *
2848
		 * @module contact-form
2849
		 *
2850
		 * @since 3.8.0
2851
		 *
2852
		 * @param string $var Required field text. Default is "(required)".
2853
		 */
2854
		$required_field_text = esc_html( apply_filters( 'jetpack_required_field_text', __( '(required)', 'jetpack' ) ) );
2855
2856
		switch ( $field_type ) {
2857 View Code Duplication
			case 'email' :
2858
				$r .= "\n<div>\n";
2859
				$r .= "\t\t<label for='" . esc_attr( $field_id ) . "' class='grunion-field-label email" . ( $this->is_error() ? ' form-error' : '' ) . "'>" . esc_html( $field_label ) . ( $field_required ? '<span>' . $required_field_text . '</span>' : '' ) . "</label>\n";
2860
				$r .= "\t\t<input type='email' name='" . esc_attr( $field_id ) . "' id='" . esc_attr( $field_id ) . "' value='" . esc_attr( $field_value ) . "' " . $field_class . $field_placeholder . ' ' . ( $field_required ? "required aria-required='true'" : '' ) . "/>\n";
2861
				$r .= "\t</div>\n";
2862
			break;
2863
			case 'telephone' :
2864
				$r .= "\n<div>\n";
2865
				$r .= "\t\t<label for='" . esc_attr( $field_id ) . "' class='grunion-field-label telephone" . ( $this->is_error() ? ' form-error' : '' ) . "'>" . esc_html( $field_label ) . ( $field_required ? '<span>' . $required_field_text . '</span>' : '' ) . "</label>\n";
2866
				$r .= "\t\t<input type='tel' name='" . esc_attr( $field_id ) . "' id='" . esc_attr( $field_id ) . "' value='" . esc_attr( $field_value ) . "' " . $field_class . $field_placeholder . "/>\n";
2867
			break;
2868
			case 'url' :
2869
				$r .= "\n<div>\n";
2870
				$r .= "\t\t<label for='" . esc_attr( $field_id ) . "' class='grunion-field-label url" . ( $this->is_error() ? ' form-error' : '' ) . "'>" . esc_html( $field_label ) . ( $field_required ? '<span>' . $required_field_text . '</span>' : '' ) . "</label>\n";
2871
				$r .= "\t\t<input type='url' name='" . esc_attr( $field_id ) . "' id='" . esc_attr( $field_id ) . "' value='" . esc_attr( $field_value ) . "' " . $field_class . $field_placeholder . ' ' . ( $field_required ? "required aria-required='true'" : '' ) . "/>\n";
2872
				$r .= "\t</div>\n";
2873
			break;
2874 View Code Duplication
			case 'textarea' :
2875
				$r .= "\n<div>\n";
2876
				$r .= "\t\t<label for='contact-form-comment-" . esc_attr( $field_id ) . "' class='grunion-field-label textarea" . ( $this->is_error() ? ' form-error' : '' ) . "'>" . esc_html( $field_label ) . ( $field_required ? '<span>' . $required_field_text . '</span>' : '' ) . "</label>\n";
2877
				$r .= "\t\t<textarea name='" . esc_attr( $field_id ) . "' id='contact-form-comment-" . esc_attr( $field_id ) . "' rows='20' " . $field_class . $field_placeholder . ' ' . ( $field_required ? "required aria-required='true'" : '' ) . '>' . esc_textarea( $field_value ) . "</textarea>\n";
2878
				$r .= "\t</div>\n";
2879
			break;
2880
			case 'radio' :
2881
				$r .= "\t<div><label class='grunion-field-label" . ( $this->is_error() ? ' form-error' : '' ) . "'>" . esc_html( $field_label ) . ( $field_required ? '<span>' . $required_field_text . '</span>' : '' ) . "</label>\n";
2882
				foreach ( $this->get_attribute( 'options' ) as $optionIndex => $option ) {
2883
					$option = Grunion_Contact_Form_Plugin::strip_tags( $option );
2884
					$r .= "\t\t<label class='grunion-radio-label radio" . ( $this->is_error() ? ' form-error' : '' ) . "'>";
2885
					$r .= "<input type='radio' name='" . esc_attr( $field_id ) . "' value='" . esc_attr( $this->get_option_value( $this->get_attribute( 'values' ), $optionIndex, $option ) ) . "' " . $field_class . checked( $option, $field_value, false ) . ' ' . ( $field_required ? "required aria-required='true'" : '' ) . '/> ';
2886
					$r .= esc_html( $option ) . "</label>\n";
2887
					$r .= "\t\t<div class='clear-form'></div>\n";
2888
				}
2889
				$r .= "\t\t</div>\n";
2890
			break;
2891
			case 'checkbox' :
2892
				$r .= "\t<div>\n";
2893
				$r .= "\t\t<label class='grunion-field-label checkbox" . ( $this->is_error() ? ' form-error' : '' ) . "'>\n";
2894
				$r .= "\t\t<input type='checkbox' name='" . esc_attr( $field_id ) . "' value='" . esc_attr__( 'Yes', 'jetpack' ) . "' " . $field_class . checked( (bool) $field_value, true, false ) . ' ' . ( $field_required ? "required aria-required='true'" : '' ) . "/> \n";
2895
				$r .= "\t\t" . esc_html( $field_label ) . ( $field_required ? '<span>' . $required_field_text . '</span>' : '' ) . "</label>\n";
2896
				$r .= "\t\t<div class='clear-form'></div>\n";
2897
				$r .= "\t</div>\n";
2898
			break;
2899
			case 'checkbox-multiple' :
2900
				$r .= "\t<div><label class='grunion-field-label" . ( $this->is_error() ? ' form-error' : '' ) . "'>" . esc_html( $field_label ) . ( $field_required ? '<span>' . $required_field_text . '</span>' : '' ) . "</label>\n";
2901
				foreach ( $this->get_attribute( 'options' ) as $optionIndex => $option ) {
2902
					$option = Grunion_Contact_Form_Plugin::strip_tags( $option );
2903
					$r .= "\t\t<label class='grunion-checkbox-multiple-label checkbox-multiple" . ( $this->is_error() ? ' form-error' : '' ) . "'>";
2904
					$r .= "<input type='checkbox' name='" . esc_attr( $field_id ) . "[]' value='" . esc_attr( $this->get_option_value( $this->get_attribute( 'values' ), $optionIndex, $option ) ) . "' " . $field_class . checked( in_array( $option, (array) $field_value ), true, false ) . ' /> ';
2905
					$r .= esc_html( $option ) . "</label>\n";
2906
					$r .= "\t\t<div class='clear-form'></div>\n";
2907
				}
2908
				$r .= "\t\t</div>\n";
2909
			break;
2910
			case 'select' :
2911
				$r .= "\n<div>\n";
2912
				$r .= "\t\t<label for='" . esc_attr( $field_id ) . "' class='grunion-field-label select" . ( $this->is_error() ? ' form-error' : '' ) . "'>" . esc_html( $field_label ) . ( $field_required ? '<span>' . $required_field_text . '</span>' : '' ) . "</label>\n";
2913
				$r .= "\t<select name='" . esc_attr( $field_id ) . "' id='" . esc_attr( $field_id ) . "' " . $field_class . ( $field_required ? "required aria-required='true'" : '' ) . ">\n";
2914
				foreach ( $this->get_attribute( 'options' ) as $optionIndex => $option ) {
2915
					$option = Grunion_Contact_Form_Plugin::strip_tags( $option );
2916
					$r .= "\t\t<option" . selected( $option, $field_value, false ) . " value='" . esc_attr( $this->get_option_value( $this->get_attribute( 'values' ), $optionIndex, $option ) ) . "'>" . esc_html( $option ) . "</option>\n";
2917
				}
2918
				$r .= "\t</select>\n";
2919
				$r .= "\t</div>\n";
2920
			break;
2921
			case 'date' :
2922
				$r .= "\n<div>\n";
2923
				$r .= "\t\t<label for='" . esc_attr( $field_id ) . "' class='grunion-field-label " . esc_attr( $field_type ) . ( $this->is_error() ? ' form-error' : '' ) . "'>" . esc_html( $field_label ) . ( $field_required ? '<span>' . $required_field_text . '</span>' : '' ) . "</label>\n";
2924
				$r .= "\t\t<input type='text' name='" . esc_attr( $field_id ) . "' id='" . esc_attr( $field_id ) . "' value='" . esc_attr( $field_value ) . "' " . $field_class . ( $field_required ? "required aria-required='true'" : '' ) . "/>\n";
2925
				$r .= "\t</div>\n";
2926
2927
				wp_enqueue_script(
2928
					'grunion-frontend',
2929
					Jetpack::get_file_url_for_environment(
2930
						'_inc/build/contact-form/js/grunion-frontend.min.js',
2931
						'modules/contact-form/js/grunion-frontend.js'
2932
					),
2933
					array( 'jquery', 'jquery-ui-datepicker' )
2934
				);
2935
				wp_enqueue_style( 'jp-jquery-ui-datepicker', plugins_url( 'css/jquery-ui-datepicker.css', __FILE__ ), array( 'dashicons' ), '1.0' );
2936
2937
				// Using Core's built-in datepicker localization routine
2938
				wp_localize_jquery_ui_datepicker();
2939
			break;
2940 View Code Duplication
			default : // text field
0 ignored issues
show
Coding Style introduced by
There must be no space before the colon in a DEFAULT statement

As per the PSR-2 coding standard, there must not be a space in front of the colon in the default statement.

switch ($expr) {
    default : //wrong
        doSomething();
        break;
}

switch ($expr) {
    default: //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
2941
				// note that any unknown types will produce a text input, so we can use arbitrary type names to handle
2942
				// input fields like name, email, url that require special validation or handling at POST
2943
				$r .= "\n<div>\n";
2944
				$r .= "\t\t<label for='" . esc_attr( $field_id ) . "' class='grunion-field-label " . esc_attr( $field_type ) . ( $this->is_error() ? ' form-error' : '' ) . "'>" . esc_html( $field_label ) . ( $field_required ? '<span>' . $required_field_text . '</span>' : '' ) . "</label>\n";
2945
				$r .= "\t\t<input type='text' name='" . esc_attr( $field_id ) . "' id='" . esc_attr( $field_id ) . "' value='" . esc_attr( $field_value ) . "' " . $field_class . $field_placeholder . ' ' . ( $field_required ? "required aria-required='true'" : '' ) . "/>\n";
2946
				$r .= "\t</div>\n";
2947
		}
2948
2949
		/**
2950
		 * Filter the HTML of the Contact Form.
2951
		 *
2952
		 * @module contact-form
2953
		 *
2954
		 * @since 2.6.0
2955
		 *
2956
		 * @param string $r Contact Form HTML output.
2957
		 * @param string $field_label Field label.
2958
		 * @param int|null $id Post ID.
2959
		 */
2960
		return apply_filters( 'grunion_contact_form_field_html', $r, $field_label, ( in_the_loop() ? get_the_ID() : null ) );
2961
	}
2962
}
2963
2964
add_action( 'init', array( 'Grunion_Contact_Form_Plugin', 'init' ) );
2965
2966
add_action( 'grunion_scheduled_delete', 'grunion_delete_old_spam' );
2967
2968
/**
2969
 * Deletes old spam feedbacks to keep the posts table size under control
2970
 */
2971
function grunion_delete_old_spam() {
2972
	global $wpdb;
2973
2974
	$grunion_delete_limit = 100;
2975
2976
	$now_gmt = current_time( 'mysql', 1 );
2977
	$sql = $wpdb->prepare( "
2978
		SELECT `ID`
2979
		FROM $wpdb->posts
2980
		WHERE DATE_SUB( %s, INTERVAL 15 DAY ) > `post_date_gmt`
2981
			AND `post_type` = 'feedback'
2982
			AND `post_status` = 'spam'
2983
		LIMIT %d
2984
	", $now_gmt, $grunion_delete_limit );
2985
	$post_ids = $wpdb->get_col( $sql );
2986
2987
	foreach ( (array) $post_ids as $post_id ) {
2988
		// force a full delete, skip the trash
2989
		wp_delete_post( $post_id, true );
2990
	}
2991
2992
	if (
2993
		/**
2994
		 * Filter if the module run OPTIMIZE TABLE on the core WP tables.
2995
		 *
2996
		 * @module contact-form
2997
		 *
2998
		 * @since 1.3.1
2999
		 * @since 6.4.0 Set to false by default.
3000
		 *
3001
		 * @param bool $filter Should Jetpack optimize the table, defaults to false.
3002
		 */
3003
		apply_filters( 'grunion_optimize_table', false )
3004
	) {
3005
		$wpdb->query( "OPTIMIZE TABLE $wpdb->posts" );
3006
	}
3007
3008
	// if we hit the max then schedule another run
3009
	if ( count( $post_ids ) >= $grunion_delete_limit ) {
3010
		wp_schedule_single_event( time() + 700, 'grunion_scheduled_delete' );
3011
	}
3012
}
3013