Completed
Push — fix/nosara ( 09438e...93ba64 )
by
unknown
11:29
created

Grunion_Contact_Form_Plugin::__construct()   D

Complexity

Conditions 10
Paths 16

Size

Total Lines 102
Code Lines 63

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 10
eloc 63
nc 16
nop 0
dl 0
loc 102
rs 4.8196
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

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