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