Completed
Push — master-stable ( 2e1c2b...fd6891 )
by
unknown
07:39
created

Grunion_Contact_Form_Field   C

Complexity

Total Complexity 76

Size/Duplication

Total Lines 317
Duplicated Lines 13.88 %

Coupling/Cohesion

Components 1
Dependencies 3

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 44
loc 317
rs 5.488
wmc 76
lcom 1
cbo 3

5 Methods

Rating   Name   Duplication   Size   Complexity  
C __construct() 3 59 13
A add_error() 0 9 2
A is_error() 0 3 1
D validate() 0 40 9
F render() 41 158 51

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