Completed
Push — update/remove-disconnect-link ( 4b6a2c )
by
unknown
73:18 queued 63:47
created

Grunion_Contact_Form   D

Complexity

Total Complexity 119

Size/Duplication

Total Lines 941
Duplicated Lines 2.13 %

Coupling/Cohesion

Components 2
Dependencies 4

Importance

Changes 0
Metric Value
dl 20
loc 941
rs 4.4444
c 0
b 0
f 0
wmc 119
lcom 2
cbo 4

11 Methods

Rating   Name   Duplication   Size   Complexity  
C __construct() 0 69 8
B store_shortcode() 0 14 5
A style() 0 5 1
A _style_on() 0 3 1
D parse() 0 120 21
A success_message() 0 8 1
C get_compiled_form() 0 76 9
C parse_contact_field() 0 46 12
C get_field_ids() 0 43 8
F process_submission() 20 413 49
A addslashes_deep() 0 13 4

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like Grunion_Contact_Form often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Grunion_Contact_Form, and based on these observations, apply Extract Interface, too.

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