Completed
Push — try/upgrade-react-and-componen... ( 980eb7...de776d )
by
unknown
08:57
created

Grunion_Contact_Form_Plugin   D

Complexity

Total Complexity 142

Size/Duplication

Total Lines 1084
Duplicated Lines 0 %

Coupling/Cohesion

Components 3
Dependencies 1

Importance

Changes 0
Metric Value
dl 0
loc 1084
rs 4.4153
c 0
b 0
f 0
wmc 142
lcom 3
cbo 1

33 Methods

Rating   Name   Duplication   Size   Complexity  
A init() 0 12 2
A daily_akismet_meta_cleanup() 0 13 3
A strip_tags() 0 16 3
A admin_menu() 0 10 1
A allow_feedback_rest_api_type() 0 4 1
B unread_count() 0 19 10
A ajax_request() 0 19 3
A insert_feedback_filter() 0 7 3
A add_shortcode() 0 4 1
A tokenize_label() 0 3 1
A sanitize_value() 0 3 1
A replace_tokens_with_input() 0 16 3
A track_current_widget() 0 3 1
A widget_atts() 0 5 1
A widget_shortcode_hack() 0 17 2
B prepare_for_akismet() 0 26 6
C is_spam_akismet() 0 48 12
A akismet_submit() 0 19 4
B export_form() 0 48 5
A get_post_content_for_csv_export() 0 6 1
A get_post_meta_for_csv_export() 0 3 1
A get_parsed_field_contents_of_post() 0 3 1
B map_parsed_field_contents_of_post_to_field_names() 0 23 4
C get_export_data_for_posts() 0 106 11
C download_feedback_as_csv() 0 93 10
A esc_csv() 0 9 2
B get_feedbacks_as_options() 0 28 2
A get_field_names() 0 16 3
B parse_fields_from_content() 0 55 8
B make_csv_row_from_feedback() 0 29 6
A get_ip_address() 0 3 2
F process_form_submission() 0 89 17
C __construct() 0 105 11

How to fix   Complexity   

Complex Class

Complex classes like Grunion_Contact_Form_Plugin 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_Plugin, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/*
4
Plugin Name: Grunion Contact Form
5
Description: Add a contact form to any post, page or text widget.  Emails will be sent to the post's author by default, or any email address you choose.  As seen on WordPress.com.
6
Plugin URI: http://automattic.com/#
7
AUthor: Automattic, Inc.
8
Author URI: http://automattic.com/
9
Version: 2.4
10
License: GPLv2 or later
11
*/
12
13
define( 'GRUNION_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
14
define( 'GRUNION_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
15
16
if ( is_admin() ) {
17
	require_once GRUNION_PLUGIN_DIR . 'admin.php';
18
}
19
20
add_action( 'rest_api_init', 'grunion_contact_form_require_endpoint' );
21
function grunion_contact_form_require_endpoint() {
22
	require_once GRUNION_PLUGIN_DIR . 'class-grunion-contact-form-endpoint.php';
23
}
24
25
/**
26
 * Sets up various actions, filters, post types, post statuses, shortcodes.
27
 */
28
class Grunion_Contact_Form_Plugin {
29
30
	/**
31
	 * @var string The Widget ID of the widget currently being processed.  Used to build the unique contact-form ID for forms embedded in widgets.
32
	 */
33
	public $current_widget_id;
34
35
	static $using_contact_form_field = false;
0 ignored issues
show
Coding Style introduced by
The visibility should be declared for property $using_contact_form_field.

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

class A {
    var $property;
}

the property is implicitly global.

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

Loading history...
36
37
	static function init() {
38
		static $instance = false;
39
40
		if ( ! $instance ) {
41
			$instance = new Grunion_Contact_Form_Plugin;
42
43
			// Schedule our daily cleanup
44
			add_action( 'wp_scheduled_delete', array( $instance, 'daily_akismet_meta_cleanup' ) );
45
		}
46
47
		return $instance;
48
	}
49
50
	/**
51
	 * Runs daily to clean up spam detection metadata after 15 days.  Keeps your DB squeaky clean.
52
	 */
53
	public function daily_akismet_meta_cleanup() {
54
		global $wpdb;
55
56
		$feedback_ids = $wpdb->get_col( "SELECT p.ID FROM {$wpdb->posts} as p INNER JOIN {$wpdb->postmeta} as m on m.post_id = p.ID WHERE p.post_type = 'feedback' AND m.meta_key = '_feedback_akismet_values' AND DATE_SUB(NOW(), INTERVAL 15 DAY) > p.post_date_gmt LIMIT 10000" );
57
58
		if ( empty( $feedback_ids ) ) {
59
			return;
60
		}
61
62
		foreach ( $feedback_ids as $feedback_id ) {
63
			delete_post_meta( $feedback_id, '_feedback_akismet_values' );
64
		}
65
	}
66
67
	/**
68
	 * Strips HTML tags from input.  Output is NOT HTML safe.
69
	 *
70
	 * @param mixed $data_with_tags
71
	 * @return mixed
72
	 */
73
	public static function strip_tags( $data_with_tags ) {
74
		if ( is_array( $data_with_tags ) ) {
75
			foreach ( $data_with_tags as $index => $value ) {
76
				$index = sanitize_text_field( strval( $index ) );
77
				$value = wp_kses( strval( $value ), array() );
78
				$value = str_replace( '&amp;', '&', $value ); // undo damage done by wp_kses_normalize_entities()
79
80
				$data_without_tags[ $index ] = $value;
0 ignored issues
show
Coding Style Comprehensibility introduced by
$data_without_tags was never initialized. Although not strictly required by PHP, it is generally a good practice to add $data_without_tags = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
81
			}
82
		} else {
83
			$data_without_tags = wp_kses( $data_with_tags, array() );
84
			$data_without_tags = str_replace( '&amp;', '&', $data_without_tags ); // undo damage done by wp_kses_normalize_entities()
85
		}
86
87
		return $data_without_tags;
0 ignored issues
show
Bug introduced by
The variable $data_without_tags does not seem to be defined for all execution paths leading up to this point.

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

Let’s take a look at an example:

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

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

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

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

Available Fixes

  1. Check for existence of the variable explicitly:

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

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

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
88
	}
89
90
	function __construct() {
91
		$this->add_shortcode();
92
93
		// While generating the output of a text widget with a contact-form shortcode, we need to know its widget ID.
94
		add_action( 'dynamic_sidebar', array( $this, 'track_current_widget' ) );
95
96
		// Add a "widget" shortcode attribute to all contact-form shortcodes embedded in widgets
97
		add_filter( 'widget_text', array( $this, 'widget_atts' ), 0 );
98
99
		// If Text Widgets don't get shortcode processed, hack ours into place.
100
		if (
101
			version_compare( get_bloginfo( 'version' ), '4.9-z', '<=' )
102
			&& ! has_filter( 'widget_text', 'do_shortcode' )
103
		) {
104
			add_filter( 'widget_text', array( $this, 'widget_shortcode_hack' ), 5 );
105
		}
106
107
		// Akismet to the rescue
108
		if ( defined( 'AKISMET_VERSION' ) || function_exists( 'akismet_http_post' ) ) {
109
			add_filter( 'jetpack_contact_form_is_spam', array( $this, 'is_spam_akismet' ), 10, 2 );
110
			add_action( 'contact_form_akismet', array( $this, 'akismet_submit' ), 10, 2 );
111
		}
112
113
		add_action( 'loop_start', array( 'Grunion_Contact_Form', '_style_on' ) );
114
115
		add_action( 'wp_ajax_grunion-contact-form', array( $this, 'ajax_request' ) );
116
		add_action( 'wp_ajax_nopriv_grunion-contact-form', array( $this, 'ajax_request' ) );
117
118
		// Export to CSV feature
119
		if ( is_admin() ) {
120
			add_action( 'admin_init',            array( $this, 'download_feedback_as_csv' ) );
121
			add_action( 'admin_footer-edit.php', array( $this, 'export_form' ) );
122
			add_action( 'admin_menu',            array( $this, 'admin_menu' ) );
123
			add_action( 'current_screen',        array( $this, 'unread_count' ) );
124
		}
125
126
		// custom post type we'll use to keep copies of the feedback items
127
		register_post_type( 'feedback', array(
128
			'labels'            => array(
129
				'name'               => __( 'Feedback', 'jetpack' ),
130
				'singular_name'      => __( 'Feedback', 'jetpack' ),
131
				'search_items'       => __( 'Search Feedback', 'jetpack' ),
132
				'not_found'          => __( 'No feedback found', 'jetpack' ),
133
				'not_found_in_trash' => __( 'No feedback found', 'jetpack' ),
134
			),
135
			'menu_icon'         	=> 'dashicons-feedback',
136
			'show_ui'           	=> TRUE,
137
			'show_in_admin_bar' 	=> FALSE,
138
			'public'            	=> FALSE,
139
			'rewrite'           	=> FALSE,
140
			'query_var'         	=> FALSE,
141
			'capability_type'   	=> 'page',
142
			'show_in_rest'      	=> true,
143
			'rest_controller_class' => 'Grunion_Contact_Form_Endpoint',
144
			'capabilities'			=> array(
145
				'create_posts'        => false,
146
				'publish_posts'       => 'publish_pages',
147
				'edit_posts'          => 'edit_pages',
148
				'edit_others_posts'   => 'edit_others_pages',
149
				'delete_posts'        => 'delete_pages',
150
				'delete_others_posts' => 'delete_others_pages',
151
				'read_private_posts'  => 'read_private_pages',
152
				'edit_post'           => 'edit_page',
153
				'delete_post'         => 'delete_page',
154
				'read_post'           => 'read_page',
155
			),
156
			'map_meta_cap'			=> true,
157
		) );
158
159
		// Add to REST API post type whitelist
160
		add_filter( 'rest_api_allowed_post_types', array( $this, 'allow_feedback_rest_api_type' ) );
161
162
		// Add "spam" as a post status
163
		register_post_status( 'spam', array(
164
			'label'                  => 'Spam',
165
			'public'                 => false,
166
			'exclude_from_search'    => true,
167
			'show_in_admin_all_list' => false,
168
			'label_count'            => _n_noop( 'Spam <span class="count">(%s)</span>', 'Spam <span class="count">(%s)</span>', 'jetpack' ),
169
			'protected'              => true,
170
			'_builtin'               => false,
171
		) );
172
173
		// POST handler
174
		if (
175
			isset( $_SERVER['REQUEST_METHOD'] ) && 'POST' == strtoupper( $_SERVER['REQUEST_METHOD'] )
176
		&&
177
			isset( $_POST['action'] ) && 'grunion-contact-form' == $_POST['action']
178
		&&
179
			isset( $_POST['contact-form-id'] )
180
		) {
181
			add_action( 'template_redirect', array( $this, 'process_form_submission' ) );
182
		}
183
184
		/*
185
		 Can be dequeued by placing the following in wp-content/themes/yourtheme/functions.php
186
		 *
187
		 * 	function remove_grunion_style() {
188
		 *		wp_deregister_style('grunion.css');
189
		 *	}
190
		 *	add_action('wp_print_styles', 'remove_grunion_style');
191
		 */
192
		wp_register_style( 'grunion.css', GRUNION_PLUGIN_URL . 'css/grunion.css', array(), JETPACK__VERSION );
193
		wp_style_add_data( 'grunion.css', 'rtl', 'replace' );
194
	}
195
196
	/**
197
	 * Add the 'Export' menu item as a submenu of Feedback.
198
	 */
199
	public function admin_menu() {
200
		add_submenu_page(
201
			'edit.php?post_type=feedback',
202
			__( 'Export feedback as CSV', 'jetpack' ),
203
			__( 'Export CSV', 'jetpack' ),
204
			'export',
205
			'feedback-export',
206
			array( $this, 'export_form' )
207
		);
208
	}
209
210
	/**
211
	 * Add to REST API post type whitelist
212
	 */
213
	function allow_feedback_rest_api_type( $post_types ) {
214
		$post_types[] = 'feedback';
215
		return $post_types;
216
	}
217
218
	/**
219
	 * Display the count of new feedback entries received. It's reset when user visits the Feedback screen.
220
	 *
221
	 * @since 4.1.0
222
	 *
223
	 * @param object $screen Information about the current screen.
224
	 */
225
	function unread_count( $screen ) {
226
		if ( isset( $screen->post_type ) && 'feedback' == $screen->post_type ) {
227
			update_option( 'feedback_unread_count', 0 );
228
		} else {
229
			global $menu;
230
			if ( isset( $menu ) && is_array( $menu ) && ! empty( $menu ) ) {
231
				foreach ( $menu as $index => $menu_item ) {
232
					if ( 'edit.php?post_type=feedback' == $menu_item[2] ) {
233
						$unread = get_option( 'feedback_unread_count', 0 );
234
						if ( $unread > 0 ) {
235
							$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>' : '';
236
							$menu[ $index ][0] .= $unread_count;
237
						}
238
						break;
239
					}
240
				}
241
			}
242
		}
243
	}
244
245
	/**
246
	 * Handles all contact-form POST submissions
247
	 *
248
	 * Conditionally attached to `template_redirect`
249
	 */
250
	function process_form_submission() {
251
		// Add a filter to replace tokens in the subject field with sanitized field values
252
		add_filter( 'contact_form_subject', array( $this, 'replace_tokens_with_input' ), 10, 2 );
253
254
		$id = stripslashes( $_POST['contact-form-id'] );
255
		$hash = isset( $_POST['contact-form-hash'] ) ? $_POST['contact-form-hash'] : null;
256
		$hash = preg_replace( '/[^\da-f]/i', '', $hash );
257
258
		if ( is_user_logged_in() ) {
259
			check_admin_referer( "contact-form_{$id}" );
260
		}
261
262
		$is_widget = 0 === strpos( $id, 'widget-' );
263
264
		$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...
265
266
		if ( $is_widget ) {
267
			// It's a form embedded in a text widget
268
			$this->current_widget_id = substr( $id, 7 ); // remove "widget-"
269
			$widget_type = implode( '-', array_slice( explode( '-', $this->current_widget_id ), 0, -1 ) ); // Remove trailing -#
270
271
			// Is the widget active?
272
			$sidebar = is_active_widget( false, $this->current_widget_id, $widget_type );
273
274
			// This is lame - no core API for getting a widget by ID
275
			$widget = isset( $GLOBALS['wp_registered_widgets'][ $this->current_widget_id ] ) ? $GLOBALS['wp_registered_widgets'][ $this->current_widget_id ] : false;
276
277
			if ( $sidebar && $widget && isset( $widget['callback'] ) ) {
278
				// prevent PHP notices by populating widget args
279
				$widget_args = array(
280
					'before_widget' => '',
281
					'after_widget' => '',
282
					'before_title' => '',
283
					'after_title' => '',
284
				);
285
				// This is lamer - no API for outputting a given widget by ID
286
				ob_start();
287
				// Process the widget to populate Grunion_Contact_Form::$last
288
				call_user_func( $widget['callback'], $widget_args, $widget['params'][0] );
289
				ob_end_clean();
290
			}
291
		} else {
292
			// It's a form embedded in a post
293
			$post = get_post( $id );
294
295
			// Process the content to populate Grunion_Contact_Form::$last
296
			/** This filter is already documented in core. wp-includes/post-template.php */
297
			apply_filters( 'the_content', $post->post_content );
298
		}
299
300
		$form = isset( Grunion_Contact_Form::$forms[ $hash ] ) ? Grunion_Contact_Form::$forms[ $hash ] : null;
0 ignored issues
show
Bug introduced by
The property forms cannot be accessed from this context as it is declared private in class Grunion_Contact_Form.

This check looks for access to properties that are not accessible from the current context.

If you need to make a property accessible to another context you can either raise its visibility level or provide an accessible getter in the defining class.

Loading history...
301
302
		// No form may mean user is using do_shortcode, grab the form using the stored post meta
303
		if ( ! $form ) {
304
305
			// Get shortcode from post meta
306
			$shortcode = get_post_meta( $_POST['contact-form-id'], "_g_feedback_shortcode_{$hash}", true );
307
308
			// Format it
309
			if ( $shortcode != '' ) {
310
311
				// Get attributes from post meta.
312
				$parameters = '';
313
				$attributes = get_post_meta( $_POST['contact-form-id'], "_g_feedback_shortcode_atts_{$hash}", true );
314
				if ( ! empty( $attributes ) && is_array( $attributes ) ) {
315
					foreach( array_filter( $attributes ) as $param => $value  ) {
316
						$parameters .= " $param=\"$value\"";
317
					}
318
				}
319
320
				$shortcode = '[contact-form' . $parameters . ']' . $shortcode . '[/contact-form]';
321
				do_shortcode( $shortcode );
322
323
				// Recreate form
324
				$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...
325
			}
326
327
			if ( ! $form ) {
328
				return false;
329
			}
330
		}
331
332
		if ( is_wp_error( $form->errors ) && $form->errors->get_error_codes() ) {
333
			return $form->errors;
334
		}
335
336
		// Process the form
337
		return $form->process_submission();
338
	}
339
340
	function ajax_request() {
341
		$submission_result = self::process_form_submission();
342
343
		if ( ! $submission_result ) {
344
			header( 'HTTP/1.1 500 Server Error', 500, true );
345
			echo '<div class="form-error"><ul class="form-errors"><li class="form-error-message">';
346
			esc_html_e( 'An error occurred. Please try again later.', 'jetpack' );
347
			echo '</li></ul></div>';
348
		} elseif ( is_wp_error( $submission_result ) ) {
349
			header( 'HTTP/1.1 400 Bad Request', 403, true );
350
			echo '<div class="form-error"><ul class="form-errors"><li class="form-error-message">';
351
			echo esc_html( $submission_result->get_error_message() );
352
			echo '</li></ul></div>';
353
		} else {
354
			echo '<h3>' . esc_html__( 'Message Sent', 'jetpack' ) . '</h3>' . $submission_result;
355
		}
356
357
		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...
358
	}
359
360
	/**
361
	 * Ensure the post author is always zero for contact-form feedbacks
362
	 * Attached to `wp_insert_post_data`
363
	 *
364
	 * @see Grunion_Contact_Form::process_submission()
365
	 *
366
	 * @param array $data the data to insert
367
	 * @param array $postarr the data sent to wp_insert_post()
368
	 * @return array The filtered $data to insert
369
	 */
370
	function insert_feedback_filter( $data, $postarr ) {
371
		if ( $data['post_type'] == 'feedback' && $postarr['post_type'] == 'feedback' ) {
372
			$data['post_author'] = 0;
373
		}
374
375
		return $data;
376
	}
377
	/*
378
	 * Adds our contact-form shortcode
379
	 * The "child" contact-field shortcode is enabled as needed by the contact-form shortcode handler
380
	 */
381
	function add_shortcode() {
382
		add_shortcode( 'contact-form',         array( 'Grunion_Contact_Form', 'parse' ) );
383
		add_shortcode( 'contact-field',        array( 'Grunion_Contact_Form', 'parse_contact_field' ) );
384
	}
385
386
	static function tokenize_label( $label ) {
387
		return '{' . trim( preg_replace( '#^\d+_#', '', $label ) ) . '}';
388
	}
389
390
	static function sanitize_value( $value ) {
391
		return preg_replace( '=((<CR>|<LF>|0x0A/%0A|0x0D/%0D|\\n|\\r)\S).*=i', null, $value );
392
	}
393
394
	/**
395
	 * Replaces tokens like {city} or {City} (case insensitive) with the value
396
	 * of an input field of that name
397
	 *
398
	 * @param string $subject
399
	 * @param array  $field_values Array with field label => field value associations
400
	 *
401
	 * @return string The filtered $subject with the tokens replaced
402
	 */
403
	function replace_tokens_with_input( $subject, $field_values ) {
404
		// Wrap labels into tokens (inside {})
405
		$wrapped_labels = array_map( array( 'Grunion_Contact_Form_Plugin', 'tokenize_label' ), array_keys( $field_values ) );
406
		// Sanitize all values
407
		$sanitized_values = array_map( array( 'Grunion_Contact_Form_Plugin', 'sanitize_value' ), array_values( $field_values ) );
408
409
		foreach ( $sanitized_values as $k => $sanitized_value ) {
410
			if ( is_array( $sanitized_value ) ) {
411
				$sanitized_values[ $k ] = implode( ', ', $sanitized_value );
412
			}
413
		}
414
415
		// Search for all valid tokens (based on existing fields) and replace with the field's value
416
		$subject = str_ireplace( $wrapped_labels, $sanitized_values, $subject );
417
		return $subject;
418
	}
419
420
	/**
421
	 * Tracks the widget currently being processed.
422
	 * Attached to `dynamic_sidebar`
423
	 *
424
	 * @see $current_widget_id
425
	 *
426
	 * @param array $widget The widget data
427
	 */
428
	function track_current_widget( $widget ) {
429
		$this->current_widget_id = $widget['id'];
430
	}
431
432
	/**
433
	 * Adds a "widget" attribute to every contact-form embedded in a text widget.
434
	 * Used to tell the difference between post-embedded contact-forms and widget-embedded contact-forms
435
	 * Attached to `widget_text`
436
	 *
437
	 * @param string $text The widget text
438
	 * @return string The filtered widget text
439
	 */
440
	function widget_atts( $text ) {
441
		Grunion_Contact_Form::style( true );
442
443
		return preg_replace( '/\[contact-form([^a-zA-Z_-])/', '[contact-form widget="' . $this->current_widget_id . '"\\1', $text );
444
	}
445
446
	/**
447
	 * For sites where text widgets are not processed for shortcodes, we add this hack to process just our shortcode
448
	 * Attached to `widget_text`
449
	 *
450
	 * @param string $text The widget text
451
	 * @return string The contact-form filtered widget text
452
	 */
453
	function widget_shortcode_hack( $text ) {
454
		if ( ! preg_match( '/\[contact-form([^a-zA-Z_-])/', $text ) ) {
455
			return $text;
456
		}
457
458
		$old = $GLOBALS['shortcode_tags'];
459
		remove_all_shortcodes();
460
		Grunion_Contact_Form_Plugin::$using_contact_form_field = true;
461
		$this->add_shortcode();
462
463
		$text = do_shortcode( $text );
464
465
		Grunion_Contact_Form_Plugin::$using_contact_form_field = false;
466
		$GLOBALS['shortcode_tags'] = $old;
467
468
		return $text;
469
	}
470
471
	/**
472
	 * Populate an array with all values necessary to submit a NEW contact-form feedback to Akismet.
473
	 * Note that this includes the current user_ip etc, so this should only be called when accepting a new item via $_POST
474
	 *
475
	 * @param array $form Contact form feedback array
476
	 * @return array feedback array with additional data ready for submission to Akismet
477
	 */
478
	function prepare_for_akismet( $form ) {
479
		$form['comment_type'] = 'contact_form';
480
		$form['user_ip']      = $_SERVER['REMOTE_ADDR'];
481
		$form['user_agent']   = $_SERVER['HTTP_USER_AGENT'];
482
		$form['referrer']     = $_SERVER['HTTP_REFERER'];
483
		$form['blog']         = get_option( 'home' );
484
485
		foreach ( $_SERVER as $key => $value ) {
486
			if ( ! is_string( $value ) ) {
487
				continue;
488
			}
489
			if ( in_array( $key, array( 'HTTP_COOKIE', 'HTTP_COOKIE2', 'HTTP_USER_AGENT', 'HTTP_REFERER' ) ) ) {
490
				// We don't care about cookies, and the UA and Referrer were caught above.
491
				continue;
492
			} elseif ( in_array( $key, array( 'REMOTE_ADDR', 'REQUEST_URI', 'DOCUMENT_URI' ) ) ) {
493
				// All three of these are relevant indicators and should be passed along.
494
				$form[ $key ] = $value;
495
			} elseif ( wp_startswith( $key, 'HTTP_' ) ) {
496
				// Any other HTTP header indicators.
497
				// `wp_startswith()` is a wpcom helper function and is included in Jetpack via `functions.compat.php`
498
				$form[ $key ] = $value;
499
			}
500
		}
501
502
		return $form;
503
	}
504
505
	/**
506
	 * Submit contact-form data to Akismet to check for spam.
507
	 * If you're accepting a new item via $_POST, run it Grunion_Contact_Form_Plugin::prepare_for_akismet() first
508
	 * Attached to `jetpack_contact_form_is_spam`
509
	 *
510
	 * @param bool  $is_spam
511
	 * @param array $form
512
	 * @return bool|WP_Error TRUE => spam, FALSE => not spam, WP_Error => stop processing entirely
513
	 */
514
	function is_spam_akismet( $is_spam, $form = array() ) {
515
		global $akismet_api_host, $akismet_api_port;
516
517
		// The signature of this function changed from accepting just $form.
518
		// If something only sends an array, assume it's still using the old
519
		// signature and work around it.
520
		if ( empty( $form ) && is_array( $is_spam ) ) {
521
			$form = $is_spam;
522
			$is_spam = false;
523
		}
524
525
		// If a previous filter has alrady marked this as spam, trust that and move on.
526
		if ( $is_spam ) {
527
			return $is_spam;
528
		}
529
530
		if ( ! function_exists( 'akismet_http_post' ) && ! defined( 'AKISMET_VERSION' ) ) {
531
			return false;
532
		}
533
534
		$query_string = http_build_query( $form );
535
536
		if ( method_exists( 'Akismet', 'http_post' ) ) {
537
			$response = Akismet::http_post( $query_string, 'comment-check' );
538
		} else {
539
			$response = akismet_http_post( $query_string, $akismet_api_host, '/1.1/comment-check', $akismet_api_port );
540
		}
541
542
		$result = false;
543
544
		if ( isset( $response[0]['x-akismet-pro-tip'] ) && 'discard' === trim( $response[0]['x-akismet-pro-tip'] ) && get_option( 'akismet_strictness' ) === '1' ) {
545
			$result = new WP_Error( 'feedback-discarded', __( 'Feedback discarded.', 'jetpack' ) );
546
		} elseif ( isset( $response[1] ) && 'true' == trim( $response[1] ) ) { // 'true' is spam
547
			$result = true;
548
		}
549
550
		/**
551
		 * Filter the results returned by Akismet for each submitted contact form.
552
		 *
553
		 * @module contact-form
554
		 *
555
		 * @since 1.3.1
556
		 *
557
		 * @param WP_Error|bool $result Is the submitted feedback spam.
558
		 * @param array|bool $form Submitted feedback.
559
		 */
560
		return apply_filters( 'contact_form_is_spam_akismet', $result, $form );
561
	}
562
563
	/**
564
	 * Submit a feedback as either spam or ham
565
	 *
566
	 * @param string $as Either 'spam' or 'ham'.
567
	 * @param array  $form the contact-form data
568
	 */
569
	function akismet_submit( $as, $form ) {
570
		global $akismet_api_host, $akismet_api_port;
571
572
		if ( ! in_array( $as, array( 'ham', 'spam' ) ) ) {
573
			return false;
574
		}
575
576
		$query_string = '';
577
		if ( is_array( $form ) ) {
578
			$query_string = http_build_query( $form );
579
		}
580
		if ( method_exists( 'Akismet', 'http_post' ) ) {
581
			$response = Akismet::http_post( $query_string, "submit-{$as}" );
582
		} else {
583
			$response = akismet_http_post( $query_string, $akismet_api_host, "/1.1/submit-{$as}", $akismet_api_port );
584
		}
585
586
		return trim( $response[1] );
587
	}
588
589
	/**
590
	 * Prints the menu
591
	 */
592
	function export_form() {
593
		$current_screen = get_current_screen();
594
		if ( ! in_array( $current_screen->id, array( 'edit-feedback', 'feedback_page_feedback-export' ) ) ) {
595
			return;
596
		}
597
598
		if ( ! current_user_can( 'export' ) ) {
599
			return;
600
		}
601
602
		// if there aren't any feedbacks, bail out
603
		if ( ! (int) wp_count_posts( 'feedback' )->publish ) {
604
			return;
605
		}
606
		?>
607
608
		<div id="feedback-export" style="display:none">
609
			<h2><?php _e( 'Export feedback as CSV', 'jetpack' ) ?></h2>
610
			<div class="clear"></div>
611
			<form action="<?php echo admin_url( 'admin-post.php' ); ?>" method="post" class="form">
612
				<?php wp_nonce_field( 'feedback_export','feedback_export_nonce' ); ?>
613
614
				<input name="action" value="feedback_export" type="hidden">
615
				<label for="post"><?php _e( 'Select feedback to download', 'jetpack' ) ?></label>
616
				<select name="post">
617
					<option value="all"><?php esc_html_e( 'All posts', 'jetpack' ) ?></option>
618
					<?php echo $this->get_feedbacks_as_options() ?>
619
				</select>
620
621
				<br><br>
622
				<input type="submit" name="submit" id="submit" class="button button-primary" value="<?php esc_html_e( 'Download', 'jetpack' ); ?>">
623
			</form>
624
		</div>
625
626
		<?php
627
		// There aren't any usable actions in core to output the "export feedback" form in the correct place,
628
		// so this inline JS moves it from the top of the page to the bottom.
629
		?>
630
		<script type='text/javascript'>
631
		var menu = document.getElementById( 'feedback-export' ),
632
		wrapper = document.getElementsByClassName( 'wrap' )[0];
633
		<?php if ( 'edit-feedback' === $current_screen->id ) : ?>
634
		wrapper.appendChild(menu);
635
		<?php endif; ?>
636
		menu.style.display = 'block';
637
		</script>
638
		<?php
639
	}
640
641
	/**
642
	 * Fetch post content for a post and extract just the comment.
643
	 *
644
	 * @param int $post_id The post id to fetch the content for.
645
	 *
646
	 * @return string Trimmed post comment.
647
	 *
648
	 * @codeCoverageIgnore
649
	 */
650
	public function get_post_content_for_csv_export( $post_id ) {
651
		$post_content = get_post_field( 'post_content', $post_id );
652
		$content      = explode( '<!--more-->', $post_content );
653
654
		return trim( $content[0] );
655
	}
656
657
	/**
658
	 * Get `_feedback_extra_fields` field from post meta data.
659
	 *
660
	 * @param int $post_id Id of the post to fetch meta data for.
661
	 *
662
	 * @return mixed
663
	 *
664
	 * @codeCoverageIgnore - No need to be covered.
665
	 */
666
	public function get_post_meta_for_csv_export( $post_id ) {
667
		return get_post_meta( $post_id, '_feedback_extra_fields', true );
668
	}
669
670
	/**
671
	 * Get parsed feedback post fields.
672
	 *
673
	 * @param int $post_id Id of the post to fetch parsed contents for.
674
	 *
675
	 * @return array
676
	 *
677
	 * @codeCoverageIgnore - No need to be covered.
678
	 */
679
	public function get_parsed_field_contents_of_post( $post_id ) {
680
		return self::parse_fields_from_content( $post_id );
681
	}
682
683
	/**
684
	 * Properly maps fields that are missing from the post meta data
685
	 * to names, that are similar to those of the post meta.
686
	 *
687
	 * @param array $parsed_post_content Parsed post content
688
	 *
689
	 * @see parse_fields_from_content for how the input data is generated.
690
	 *
691
	 * @return array Mapped fields.
692
	 */
693
	public function map_parsed_field_contents_of_post_to_field_names( $parsed_post_content ) {
694
695
		$mapped_fields = array();
696
697
		$field_mapping = array(
698
			'_feedback_subject'      => __( 'Contact Form', 'jetpack' ),
699
			'_feedback_author'       => '1_Name',
700
			'_feedback_author_email' => '2_Email',
701
			'_feedback_author_url'   => '3_Website',
702
			'_feedback_main_comment' => '4_Comment',
703
		);
704
705
		foreach ( $field_mapping as $parsed_field_name => $field_name ) {
706
			if (
707
				isset( $parsed_post_content[ $parsed_field_name ] )
708
				&& ! empty( $parsed_post_content[ $parsed_field_name ] )
709
			) {
710
				$mapped_fields[ $field_name ] = $parsed_post_content[ $parsed_field_name ];
711
			}
712
		}
713
714
		return $mapped_fields;
715
	}
716
717
718
	/**
719
	 * Prepares feedback post data for CSV export.
720
	 *
721
	 * @param array $post_ids Post IDs to fetch the data for. These need to be Feedback posts.
722
	 *
723
	 * @return array
724
	 */
725
	public function get_export_data_for_posts( $post_ids ) {
726
727
		$posts_data  = array();
728
		$field_names = array();
729
		$result      = array();
730
731
		/**
732
		 * Fetch posts and get the possible field names for later use
733
		 */
734
		foreach ( $post_ids as $post_id ) {
735
736
			/**
737
			 * Fetch post main data, because we need the subject and author data for the feedback form.
738
			 */
739
			$post_real_data = $this->get_parsed_field_contents_of_post( $post_id );
740
741
			/**
742
			 * If `$post_real_data` is not an array or there is no `_feedback_subject` set,
743
			 * then something must be wrong with the feedback post. Skip it.
744
			 */
745
			if ( ! is_array( $post_real_data ) || ! isset( $post_real_data['_feedback_subject'] ) ) {
746
				continue;
747
			}
748
749
			/**
750
			 * Fetch main post comment. This is from the default textarea fields.
751
			 * If it is non-empty, then we add it to data, otherwise skip it.
752
			 */
753
			$post_comment_content = $this->get_post_content_for_csv_export( $post_id );
754
			if ( ! empty( $post_comment_content ) ) {
755
				$post_real_data['_feedback_main_comment'] = $post_comment_content;
756
			}
757
758
			/**
759
			 * Map parsed fields to proper field names
760
			 */
761
			$mapped_fields = $this->map_parsed_field_contents_of_post_to_field_names( $post_real_data );
762
763
			/**
764
			 * Fetch post meta data.
765
			 */
766
			$post_meta_data = $this->get_post_meta_for_csv_export( $post_id );
767
768
			/**
769
			 * If `$post_meta_data` is not an array or if it is empty, then there is no
770
			 * extra feedback to work with. Create an empty array.
771
			 */
772
			if ( ! is_array( $post_meta_data ) || empty( $post_meta_data ) ) {
773
				$post_meta_data = array();
774
			}
775
776
			/**
777
			 * Prepend the feedback subject to the list of fields.
778
			 */
779
			$post_meta_data = array_merge(
780
				$mapped_fields,
781
				$post_meta_data
782
			);
783
784
			/**
785
			 * Save post metadata for later usage.
786
			 */
787
			$posts_data[ $post_id ] = $post_meta_data;
788
789
			/**
790
			 * Save field names, so we can use them as header fields later in the CSV.
791
			 */
792
			$field_names = array_merge( $field_names, array_keys( $post_meta_data ) );
793
		}
794
795
		/**
796
		 * Make sure the field names are unique, because we don't want duplicate data.
797
		 */
798
		$field_names = array_unique( $field_names );
799
800
		/**
801
		 * Sort the field names by the field id number
802
		 */
803
		sort( $field_names, SORT_NUMERIC );
804
805
		/**
806
		 * Loop through every post, which is essentially CSV row.
807
		 */
808
		foreach ( $posts_data as $post_id => $single_post_data ) {
809
810
			/**
811
			 * Go through all the possible fields and check if the field is available
812
			 * in the current post.
813
			 *
814
			 * If it is - add the data as a value.
815
			 * If it is not - add an empty string, which is just a placeholder in the CSV.
816
			 */
817
			foreach ( $field_names as $single_field_name ) {
818
				if (
819
					isset( $single_post_data[ $single_field_name ] )
820
					&& ! empty( $single_post_data[ $single_field_name ] )
821
				) {
822
					$result[ $single_field_name ][] = trim( $single_post_data[ $single_field_name ] );
823
				} else {
824
					$result[ $single_field_name ][] = '';
825
				}
826
			}
827
		}
828
829
		return $result;
830
	}
831
832
	/**
833
	 * download as a csv a contact form or all of them in a csv file
834
	 */
835
	function download_feedback_as_csv() {
836
		if ( empty( $_POST['feedback_export_nonce'] ) ) {
837
			return;
838
		}
839
840
		check_admin_referer( 'feedback_export', 'feedback_export_nonce' );
841
842
		if ( ! current_user_can( 'export' ) ) {
843
			return;
844
		}
845
846
		$args = array(
847
			'posts_per_page'   => -1,
848
			'post_type'        => 'feedback',
849
			'post_status'      => 'publish',
850
			'order'            => 'ASC',
851
			'fields'           => 'ids',
852
			'suppress_filters' => false,
853
		);
854
855
		$filename = date( 'Y-m-d' ) . '-feedback-export.csv';
856
857
		// Check if we want to download all the feedbacks or just a certain contact form
858
		if ( ! empty( $_POST['post'] ) && $_POST['post'] !== 'all' ) {
859
			$args['post_parent'] = (int) $_POST['post'];
860
			$filename            = date( 'Y-m-d' ) . '-' . str_replace( '&nbsp;', '-', get_the_title( (int) $_POST['post'] ) ) . '.csv';
861
		}
862
863
		$feedbacks = get_posts( $args );
864
865
		if ( empty( $feedbacks ) ) {
866
			return;
867
		}
868
869
		$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...
870
871
		/**
872
		 * Prepare data for export.
873
		 */
874
		$data = $this->get_export_data_for_posts( $feedbacks );
875
876
		/**
877
		 * If `$data` is empty, there's nothing we can do below.
878
		 */
879
		if ( ! is_array( $data ) || empty( $data ) ) {
880
			return;
881
		}
882
883
		/**
884
		 * Extract field names from `$data` for later use.
885
		 */
886
		$fields = array_keys( $data );
887
888
		/**
889
		 * Count how many rows will be exported.
890
		 */
891
		$row_count = count( reset( $data ) );
892
893
		// Forces the download of the CSV instead of echoing
894
		header( 'Content-Disposition: attachment; filename=' . $filename );
895
		header( 'Pragma: no-cache' );
896
		header( 'Expires: 0' );
897
		header( 'Content-Type: text/csv; charset=utf-8' );
898
899
		$output = fopen( 'php://output', 'w' );
900
901
		/**
902
		 * Print CSV headers
903
		 */
904
		fputcsv( $output, $fields );
905
906
		/**
907
		 * Print rows to the output.
908
		 */
909
		for ( $i = 0; $i < $row_count; $i ++ ) {
910
911
			$current_row = array();
912
913
			/**
914
			 * Put all the fields in `$current_row` array.
915
			 */
916
			foreach ( $fields as $single_field_name ) {
917
				$current_row[] = $this->esc_csv( $data[ $single_field_name ][ $i ] );
918
			}
919
920
			/**
921
			 * Output the complete CSV row
922
			 */
923
			fputcsv( $output, $current_row );
924
		}
925
926
		fclose( $output );
927
	}
928
929
	/**
930
	 * Escape a string to be used in a CSV context
931
	 *
932
	 * Malicious input can inject formulas into CSV files, opening up the possibility for phishing attacks and
933
	 * disclosure of sensitive information.
934
	 *
935
	 * Additionally, Excel exposes the ability to launch arbitrary commands through the DDE protocol.
936
	 *
937
	 * @see http://www.contextis.com/resources/blog/comma-separated-vulnerabilities/
938
	 *
939
	 * @param string $field
940
	 *
941
	 * @return string
942
	 */
943
	public function esc_csv( $field ) {
944
		$active_content_triggers = array( '=', '+', '-', '@' );
945
946
		if ( in_array( mb_substr( $field, 0, 1 ), $active_content_triggers, true ) ) {
947
			$field = "'" . $field;
948
		}
949
950
		return $field;
951
	}
952
953
	/**
954
	 * Returns a string of HTML <option> items from an array of posts
955
	 *
956
	 * @return string a string of HTML <option> items
957
	 */
958
	protected function get_feedbacks_as_options() {
959
		$options = '';
960
961
		// Get the feedbacks' parents' post IDs
962
		$feedbacks = get_posts( array(
963
			'fields'           => 'id=>parent',
964
			'posts_per_page'   => 100000,
965
			'post_type'        => 'feedback',
966
			'post_status'      => 'publish',
967
			'suppress_filters' => false,
968
		) );
969
		$parents = array_unique( array_values( $feedbacks ) );
970
971
		$posts = get_posts( array(
972
			'orderby'          => 'ID',
973
			'posts_per_page'   => 1000,
974
			'post_type'        => 'any',
975
			'post__in'         => array_values( $parents ),
976
			'suppress_filters' => false,
977
		) );
978
979
		// creates the string of <option> elements
980
		foreach ( $posts as $post ) {
981
			$options .= sprintf( '<option value="%s">%s</option>', esc_attr( $post->ID ), esc_html( $post->post_title ) );
982
		}
983
984
		return $options;
985
	}
986
987
	/**
988
	 * Get the names of all the form's fields
989
	 *
990
	 * @param  array|int $posts the post we want the fields of
991
	 *
992
	 * @return array     the array of fields
993
	 *
994
	 * @deprecated As this is no longer necessary as of the CSV export rewrite. - 2015-12-29
995
	 */
996
	protected function get_field_names( $posts ) {
997
		$posts = (array) $posts;
998
		$all_fields = array();
999
1000
		foreach ( $posts as $post ) {
1001
			$fields = self::parse_fields_from_content( $post );
1002
1003
			if ( isset( $fields['_feedback_all_fields'] ) ) {
1004
				$extra_fields = array_keys( $fields['_feedback_all_fields'] );
1005
				$all_fields = array_merge( $all_fields, $extra_fields );
1006
			}
1007
		}
1008
1009
		$all_fields = array_unique( $all_fields );
1010
		return $all_fields;
1011
	}
1012
1013
	public static function parse_fields_from_content( $post_id ) {
1014
		static $post_fields;
1015
1016
		if ( ! is_array( $post_fields ) ) {
1017
			$post_fields = array();
1018
		}
1019
1020
		if ( isset( $post_fields[ $post_id ] ) ) {
1021
			return $post_fields[ $post_id ];
1022
		}
1023
1024
		$all_values   = array();
1025
		$post_content = get_post_field( 'post_content', $post_id );
1026
		$content      = explode( '<!--more-->', $post_content );
1027
		$lines        = array();
1028
1029
		if ( count( $content ) > 1 ) {
1030
			$content  = str_ireplace( array( '<br />', ')</p>' ), '', $content[1] );
1031
			$one_line = preg_replace( '/\s+/', ' ', $content );
1032
			$one_line = preg_replace( '/.*Array \( (.*)\)/', '$1', $one_line );
1033
1034
			preg_match_all( '/\[([^\]]+)\] =\&gt\; ([^\[]+)/', $one_line, $matches );
1035
1036
			if ( count( $matches ) > 1 ) {
1037
				$all_values = array_combine( array_map( 'trim', $matches[1] ), array_map( 'trim', $matches[2] ) );
1038
			}
1039
1040
			$lines = array_filter( explode( "\n", $content ) );
1041
		}
1042
1043
		$var_map = array(
1044
			'AUTHOR'       => '_feedback_author',
1045
			'AUTHOR EMAIL' => '_feedback_author_email',
1046
			'AUTHOR URL'   => '_feedback_author_url',
1047
			'SUBJECT'      => '_feedback_subject',
1048
			'IP'           => '_feedback_ip',
1049
		);
1050
1051
		$fields = array();
1052
1053
		foreach ( $lines as $line ) {
1054
			$vars = explode( ': ', $line, 2 );
1055
			if ( ! empty( $vars ) ) {
1056
				if ( isset( $var_map[ $vars[0] ] ) ) {
1057
					$fields[ $var_map[ $vars[0] ] ] = self::strip_tags( trim( $vars[1] ) );
1058
				}
1059
			}
1060
		}
1061
1062
		$fields['_feedback_all_fields'] = $all_values;
1063
1064
		$post_fields[ $post_id ] = $fields;
1065
1066
		return $fields;
1067
	}
1068
1069
	/**
1070
	 * Creates a valid csv row from a post id
1071
	 *
1072
	 * @param  int   $post_id The id of the post
1073
	 * @param  array $fields  An array containing the names of all the fields of the csv
1074
	 * @return String The csv row
1075
	 *
1076
	 * @deprecated This is no longer needed, as of the CSV export rewrite.
1077
	 */
1078
	protected static function make_csv_row_from_feedback( $post_id, $fields ) {
1079
		$content_fields = self::parse_fields_from_content( $post_id );
1080
		$all_fields     = array();
1081
1082
		if ( isset( $content_fields['_feedback_all_fields'] ) ) {
1083
			$all_fields = $content_fields['_feedback_all_fields'];
1084
		}
1085
1086
		// Overwrite the parsed content with the content we stored in post_meta in a better format.
1087
		$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...
1088
		foreach ( $extra_fields as $extra_field => $extra_value ) {
1089
			$all_fields[ $extra_field ] = $extra_value;
1090
		}
1091
1092
		// The first element in all of the exports will be the subject
1093
		$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...
1094
1095
		// Loop the fields array in order to fill the $row_items array correctly
1096
		foreach ( $fields as $field ) {
1097
			if ( $field === __( 'Contact Form', 'jetpack' ) ) { // the first field will ever be the contact form, so we can continue
1098
				continue;
1099
			} elseif ( array_key_exists( $field, $all_fields ) ) {
1100
				$row_items[] = $all_fields[ $field ];
1101
			} else { $row_items[] = '';
1102
			}
1103
		}
1104
1105
		return $row_items;
1106
	}
1107
1108
	public static function get_ip_address() {
1109
		return isset( $_SERVER['REMOTE_ADDR'] ) ? $_SERVER['REMOTE_ADDR'] : null;
1110
	}
1111
}
1112
1113
/**
1114
 * Generic shortcode class.
1115
 * Does nothing other than store structured data and output the shortcode as a string
1116
 *
1117
 * Not very general - specific to Grunion.
1118
 */
1119
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...
1120
	/**
1121
	 * @var string the name of the shortcode: [$shortcode_name /]
1122
	 */
1123
	public $shortcode_name;
1124
1125
	/**
1126
	 * @var array key => value pairs for the shortcode's attributes: [$shortcode_name key="value" ... /]
1127
	 */
1128
	public $attributes;
1129
1130
	/**
1131
	 * @var array key => value pair for attribute defaults
1132
	 */
1133
	public $defaults = array();
1134
1135
	/**
1136
	 * @var null|string Null for selfclosing shortcodes.  Hhe inner content of otherwise: [$shortcode_name]$content[/$shortcode_name]
1137
	 */
1138
	public $content;
1139
1140
	/**
1141
	 * @var array Associative array of inner "child" shortcodes equivalent to the $content: [$shortcode_name][child 1/][child 2/][/$shortcode_name]
1142
	 */
1143
	public $fields;
1144
1145
	/**
1146
	 * @var null|string The HTML of the parsed inner "child" shortcodes".  Null for selfclosing shortcodes.
1147
	 */
1148
	public $body;
1149
1150
	/**
1151
	 * @param array       $attributes An associative array of shortcode attributes.  @see shortcode_atts()
1152
	 * @param null|string $content Null for selfclosing shortcodes.  The inner content otherwise.
1153
	 */
1154
	function __construct( $attributes, $content = null ) {
1155
		$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...
1156
		if ( is_array( $content ) ) {
1157
			$string_content = '';
1158
			foreach ( $content as $field ) {
1159
				$string_content .= (string) $field;
1160
			}
1161
1162
			$this->content = $string_content;
1163
		} else {
1164
			$this->content = $content;
1165
		}
1166
1167
		$this->parse_content( $this->content );
1168
	}
1169
1170
	/**
1171
	 * Processes the shortcode's inner content for "child" shortcodes
1172
	 *
1173
	 * @param string $content The shortcode's inner content: [shortcode]$content[/shortcode]
1174
	 */
1175
	function parse_content( $content ) {
1176
		if ( is_null( $content ) ) {
1177
			$this->body = null;
1178
		}
1179
1180
		$this->body = do_shortcode( $content );
1181
	}
1182
1183
	/**
1184
	 * Returns the value of the requested attribute.
1185
	 *
1186
	 * @param string $key The attribute to retrieve
1187
	 * @return mixed
1188
	 */
1189
	function get_attribute( $key ) {
1190
		return isset( $this->attributes[ $key ] ) ? $this->attributes[ $key ] : null;
1191
	}
1192
1193
	function esc_attr( $value ) {
1194
		if ( is_array( $value ) ) {
1195
			return array_map( array( $this, 'esc_attr' ), $value );
1196
		}
1197
1198
		$value = Grunion_Contact_Form_Plugin::strip_tags( $value );
1199
		$value = _wp_specialchars( $value, ENT_QUOTES, false, true );
1200
1201
		// Shortcode attributes can't contain "]"
1202
		$value = str_replace( ']', '', $value );
1203
		$value = str_replace( ',', '&#x002c;', $value ); // store commas encoded
1204
		$value = strtr( $value, array( '%' => '%25', '&' => '%26' ) );
1205
1206
		// shortcode_parse_atts() does stripcslashes()
1207
		$value = addslashes( $value );
1208
		return $value;
1209
	}
1210
1211
	function unesc_attr( $value ) {
1212
		if ( is_array( $value ) ) {
1213
			return array_map( array( $this, 'unesc_attr' ), $value );
1214
		}
1215
1216
		// For back-compat with old Grunion encoding
1217
		// Also, unencode commas
1218
		$value = strtr( $value, array( '%26' => '&', '%25' => '%' ) );
1219
		$value = preg_replace( array( '/&#x0*22;/i', '/&#x0*27;/i', '/&#x0*26;/i', '/&#x0*2c;/i' ), array( '"', "'", '&', ',' ), $value );
1220
		$value = htmlspecialchars_decode( $value, ENT_QUOTES );
1221
		$value = Grunion_Contact_Form_Plugin::strip_tags( $value );
1222
1223
		return $value;
1224
	}
1225
1226
	/**
1227
	 * Generates the shortcode
1228
	 */
1229
	function __toString() {
1230
		$r = "[{$this->shortcode_name} ";
1231
1232
		foreach ( $this->attributes as $key => $value ) {
1233
			if ( ! $value ) {
1234
				continue;
1235
			}
1236
1237
			if ( isset( $this->defaults[ $key ] ) && $this->defaults[ $key ] == $value ) {
1238
				continue;
1239
			}
1240
1241
			if ( 'id' == $key ) {
1242
				continue;
1243
			}
1244
1245
			$value = $this->esc_attr( $value );
1246
1247
			if ( is_array( $value ) ) {
1248
				$value = join( ',', $value );
1249
			}
1250
1251
			if ( false === strpos( $value, "'" ) ) {
1252
				$value = "'$value'";
1253
			} elseif ( false === strpos( $value, '"' ) ) {
1254
				$value = '"' . $value . '"';
1255
			} else {
1256
				// Shortcodes can't contain both '"' and "'".  Strip one.
1257
				$value = str_replace( "'", '', $value );
1258
				$value = "'$value'";
1259
			}
1260
1261
			$r .= "{$key}={$value} ";
1262
		}
1263
1264
		$r = rtrim( $r );
1265
1266
		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...
1267
			$r .= ']';
1268
1269
			foreach ( $this->fields as $field ) {
1270
				$r .= (string) $field;
1271
			}
1272
1273
			$r .= "[/{$this->shortcode_name}]";
1274
		} else {
1275
			$r .= '/]';
1276
		}
1277
1278
		return $r;
1279
	}
1280
}
1281
1282
/**
1283
 * Class for the contact-form shortcode.
1284
 * Parses shortcode to output the contact form as HTML
1285
 * Sends email and stores the contact form response (a.k.a. "feedback")
1286
 */
1287
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...
1288
	public $shortcode_name = 'contact-form';
1289
1290
	/**
1291
	 * @var WP_Error stores form submission errors
1292
	 */
1293
	public $errors;
1294
1295
	/**
1296
	 * @var string The SHA1 hash of the attributes that comprise the form.
1297
	 */
1298
	public $hash;
1299
1300
	/**
1301
	 * @var Grunion_Contact_Form The most recent (inclusive) contact-form shortcode processed
1302
	 */
1303
	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...
1304
1305
	/**
1306
	 * @var Whatever form we are currently looking at. If processed, will become $last
1307
	 */
1308
	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...
1309
1310
	/**
1311
	 * @var array All found forms, indexed by hash.
1312
	 */
1313
	static $forms = array();
0 ignored issues
show
Coding Style introduced by
The visibility should be declared for property $forms.

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

class A {
    var $property;
}

the property is implicitly global.

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

Loading history...
1314
1315
	/**
1316
	 * @var bool Whether to print the grunion.css style when processing the contact-form shortcode
1317
	 */
1318
	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...
1319
1320
	function __construct( $attributes, $content = null ) {
1321
		global $post;
1322
1323
		$this->hash = sha1( json_encode( $attributes ) . $content );
1324
		self::$forms[ $this->hash ] = $this;
1325
1326
		// Set up the default subject and recipient for this form
1327
		$default_to = '';
1328
		$default_subject = '[' . get_option( 'blogname' ) . ']';
1329
1330
		if ( ! isset( $attributes ) || ! is_array( $attributes ) ) {
1331
			$attributes = array();
1332
		}
1333
1334
		if ( ! empty( $attributes['widget'] ) && $attributes['widget'] ) {
1335
			$default_to .= get_option( 'admin_email' );
1336
			$attributes['id'] = 'widget-' . $attributes['widget'];
1337
			$default_subject = sprintf( _x( '%1$s Sidebar', '%1$s = blog name', 'jetpack' ), $default_subject );
1338
		} elseif ( $post ) {
1339
			$attributes['id'] = $post->ID;
1340
			$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 ) );
1341
			$post_author = get_userdata( $post->post_author );
1342
			$default_to .= $post_author->user_email;
1343
		}
1344
1345
		// Keep reference to $this for parsing form fields
1346
		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...
1347
1348
		$this->defaults = array(
1349
			'to'                 => $default_to,
1350
			'subject'            => $default_subject,
1351
			'show_subject'       => 'no', // only used in back-compat mode
1352
			'widget'             => 0,    // Not exposed to the user. Works with Grunion_Contact_Form_Plugin::widget_atts()
1353
			'id'                 => null, // Not exposed to the user. Set above.
1354
			'submit_button_text' => __( 'Submit &#187;', 'jetpack' ),
1355
		);
1356
1357
		$attributes = shortcode_atts( $this->defaults, $attributes, 'contact-form' );
1358
1359
		// We only enable the contact-field shortcode temporarily while processing the contact-form shortcode
1360
		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...
1361
1362
		parent::__construct( $attributes, $content );
1363
1364
		// There were no fields in the contact form. The form was probably just [contact-form /]. Build a default form.
1365
		if ( empty( $this->fields ) ) {
1366
			// same as the original Grunion v1 form
1367
			$default_form = '
1368
				[contact-field label="' . __( 'Name', 'jetpack' ) . '" type="name"  required="true" /]
1369
				[contact-field label="' . __( 'Email', 'jetpack' ) . '" type="email" required="true" /]
1370
				[contact-field label="' . __( 'Website', 'jetpack' ) . '" type="url" /]';
1371
1372
			if ( 'yes' == strtolower( $this->get_attribute( 'show_subject' ) ) ) {
1373
				$default_form .= '
1374
					[contact-field label="' . __( 'Subject', 'jetpack' ) . '" type="subject" /]';
1375
			}
1376
1377
			$default_form .= '
1378
				[contact-field label="' . __( 'Message', 'jetpack' ) . '" type="textarea" /]';
1379
1380
			$this->parse_content( $default_form );
1381
1382
			// Store the shortcode
1383
			$this->store_shortcode( $default_form, $attributes, $this->hash );
1384
		} else {
1385
			// Store the shortcode
1386
			$this->store_shortcode( $content, $attributes, $this->hash );
1387
		}
1388
1389
		// $this->body and $this->fields have been setup.  We no longer need the contact-field shortcode.
1390
		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...
1391
	}
1392
1393
	/**
1394
	 * Store shortcode content for recall later
1395
	 *	- used to receate shortcode when user uses do_shortcode
1396
	 *
1397
	 * @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...
1398
	 * @param array $attributes
0 ignored issues
show
Documentation introduced by
Should the type for parameter $attributes not be array|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
1399
	 * @param string $hash
0 ignored issues
show
Documentation introduced by
Should the type for parameter $hash not be string|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
1400
	 */
1401
	static function store_shortcode( $content = null, $attributes = null, $hash = null ) {
1402
1403
		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...
1404
1405
			if ( empty( $hash ) ) {
1406
				$hash = sha1( json_encode( $attributes ) . $content );
1407
			}
1408
1409
			$shortcode_meta = get_post_meta( $attributes['id'], "_g_feedback_shortcode_{$hash}", true );
1410
1411
			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...
1412
				update_post_meta( $attributes['id'], "_g_feedback_shortcode_{$hash}", $content );
1413
1414
				// Save attributes to post_meta for later use. They're not available later in do_shortcode situations.
1415
				update_post_meta( $attributes['id'], "_g_feedback_shortcode_atts_{$hash}", $attributes );
1416
			}
1417
		}
1418
	}
1419
1420
	/**
1421
	 * Toggle for printing the grunion.css stylesheet
1422
	 *
1423
	 * @param bool $style
1424
	 */
1425
	static function style( $style ) {
1426
		$previous_style = self::$style;
1427
		self::$style = (bool) $style;
1428
		return $previous_style;
1429
	}
1430
1431
	/**
1432
	 * Turn on printing of grunion.css stylesheet
1433
	 *
1434
	 * @see ::style()
1435
	 * @internal
1436
	 * @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...
1437
	 */
1438
	static function _style_on() {
1439
		return self::style( true );
1440
	}
1441
1442
	/**
1443
	 * The contact-form shortcode processor
1444
	 *
1445
	 * @param array       $attributes Key => Value pairs as parsed by shortcode_parse_atts()
1446
	 * @param string|null $content The shortcode's inner content: [contact-form]$content[/contact-form]
1447
	 * @return string HTML for the concat form.
1448
	 */
1449
	static function parse( $attributes, $content ) {
1450
		require_once JETPACK__PLUGIN_DIR . '/sync/class.jetpack-sync-settings.php';
1451
		if ( Jetpack_Sync_Settings::is_syncing() ) {
1452
			return '';
1453
		}
1454
		// Create a new Grunion_Contact_Form object (this class)
1455
		$form = new Grunion_Contact_Form( $attributes, $content );
1456
1457
		$id = $form->get_attribute( 'id' );
1458
1459
		if ( ! $id ) { // something terrible has happened
1460
			return '[contact-form]';
1461
		}
1462
1463
		if ( is_feed() ) {
1464
			return '[contact-form]';
1465
		}
1466
1467
		self::$last = $form;
1468
1469
		// Enqueue the grunion.css stylesheet if self::$style allows it
1470
		if ( self::$style && ( empty( $_REQUEST['action'] ) || $_REQUEST['action'] != 'grunion_shortcode_to_json' ) ) {
1471
			// Enqueue the style here instead of printing it, because if some other plugin has run the_post()+rewind_posts(),
1472
			// (like VideoPress does), the style tag gets "printed" the first time and discarded, leaving the contact form unstyled.
1473
			// when WordPress does the real loop.
1474
			wp_enqueue_style( 'grunion.css' );
1475
		}
1476
1477
		$r = '';
1478
		$r .= "<div id='contact-form-$id'>\n";
1479
1480
		if ( is_wp_error( $form->errors ) && $form->errors->get_error_codes() ) {
1481
			// There are errors.  Display them
1482
			$r .= "<div class='form-error'>\n<h3>" . __( 'Error!', 'jetpack' ) . "</h3>\n<ul class='form-errors'>\n";
1483
			foreach ( $form->errors->get_error_messages() as $message ) {
1484
				$r .= "\t<li class='form-error-message'>" . esc_html( $message ) . "</li>\n";
1485
			}
1486
			$r .= "</ul>\n</div>\n\n";
1487
		}
1488
1489
		if ( isset( $_GET['contact-form-id'] )
1490
			&& $_GET['contact-form-id'] == self::$last->get_attribute( 'id' )
1491
			&& isset( $_GET['contact-form-sent'], $_GET['contact-form-hash'] )
1492
			&& hash_equals( $form->hash, $_GET['contact-form-hash'] ) ) {
1493
			// The contact form was submitted.  Show the success message/results
1494
			$feedback_id = (int) $_GET['contact-form-sent'];
1495
1496
			$back_url = remove_query_arg( array( 'contact-form-id', 'contact-form-sent', '_wpnonce' ) );
1497
1498
			$r_success_message =
1499
				'<h3>' . __( 'Message Sent', 'jetpack' ) .
1500
				' (<a href="' . esc_url( $back_url ) . '">' . esc_html__( 'go back', 'jetpack' ) . '</a>)' .
1501
				"</h3>\n\n";
1502
1503
			// Don't show the feedback details unless the nonce matches
1504
			if ( $feedback_id && wp_verify_nonce( stripslashes( $_GET['_wpnonce'] ), "contact-form-sent-{$feedback_id}" ) ) {
1505
				$r_success_message .= self::success_message( $feedback_id, $form );
1506
			}
1507
1508
			/**
1509
			 * Filter the message returned after a successful contact form submission.
1510
			 *
1511
			 * @module contact-form
1512
			 *
1513
			 * @since 1.3.1
1514
			 *
1515
			 * @param string $r_success_message Success message.
1516
			 */
1517
			$r .= apply_filters( 'grunion_contact_form_success_message', $r_success_message );
1518
		} else {
1519
			// Nothing special - show the normal contact form
1520
			if ( $form->get_attribute( 'widget' ) ) {
1521
				// Submit form to the current URL
1522
				$url = remove_query_arg( array( 'contact-form-id', 'contact-form-sent', 'action', '_wpnonce' ) );
1523
			} else {
1524
				// Submit form to the post permalink
1525
				$url = get_permalink();
1526
			}
1527
1528
			// For SSL/TLS page. See RFC 3986 Section 4.2
1529
			$url = set_url_scheme( $url );
1530
1531
			// May eventually want to send this to admin-post.php...
1532
			/**
1533
			 * Filter the contact form action URL.
1534
			 *
1535
			 * @module contact-form
1536
			 *
1537
			 * @since 1.3.1
1538
			 *
1539
			 * @param string $contact_form_id Contact form post URL.
1540
			 * @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...
1541
			 * @param int $id Contact Form ID.
1542
			 */
1543
			$url = apply_filters( 'grunion_contact_form_form_action', "{$url}#contact-form-{$id}", $GLOBALS['post'], $id );
1544
1545
			$r .= "<form action='" . esc_url( $url ) . "' method='post' class='contact-form commentsblock'>\n";
1546
			$r .= $form->body;
1547
			$r .= "\t<p class='contact-submit'>\n";
1548
			$r .= "\t\t<input type='submit' value='" . esc_attr( $form->get_attribute( 'submit_button_text' ) ) . "' class='pushbutton-wide'/>\n";
1549
			if ( is_user_logged_in() ) {
1550
				$r .= "\t\t" . wp_nonce_field( 'contact-form_' . $id, '_wpnonce', true, false ) . "\n"; // nonce and referer
1551
			}
1552
			$r .= "\t\t<input type='hidden' name='contact-form-id' value='$id' />\n";
1553
			$r .= "\t\t<input type='hidden' name='action' value='grunion-contact-form' />\n";
1554
			$r .= "\t\t<input type='hidden' name='contact-form-hash' value='" . esc_attr( $form->hash ) . "' />\n";
1555
			$r .= "\t</p>\n";
1556
			$r .= "</form>\n";
1557
		}
1558
1559
		$r .= '</div>';
1560
1561
		return $r;
1562
	}
1563
1564
	/**
1565
	 * Returns a success message to be returned if the form is sent via AJAX.
1566
	 *
1567
	 * @param int                         $feedback_id
1568
	 * @param object Grunion_Contact_Form $form
1569
	 *
1570
	 * @return string $message
1571
	 */
1572
	static function success_message( $feedback_id, $form ) {
1573
		return wp_kses(
1574
			'<blockquote class="contact-form-submission">'
1575
			. '<p>' . join( self::get_compiled_form( $feedback_id, $form ), '</p><p>' ) . '</p>'
1576
			. '</blockquote>',
1577
			array( 'br' => array(), 'blockquote' => array( 'class' => array() ), 'p' => array() )
1578
		);
1579
	}
1580
1581
	/**
1582
	 * Returns a compiled form with labels and values in a form of  an array
1583
	 * of lines.
1584
	 *
1585
	 * @param int                         $feedback_id
1586
	 * @param object Grunion_Contact_Form $form
1587
	 *
1588
	 * @return array $lines
1589
	 */
1590
	static function get_compiled_form( $feedback_id, $form ) {
1591
		$feedback       = get_post( $feedback_id );
1592
		$field_ids      = $form->get_field_ids();
1593
		$content_fields = Grunion_Contact_Form_Plugin::parse_fields_from_content( $feedback_id );
1594
1595
		// Maps field_ids to post_meta keys
1596
		$field_value_map = array(
1597
			'name'     => 'author',
1598
			'email'    => 'author_email',
1599
			'url'      => 'author_url',
1600
			'subject'  => 'subject',
1601
			'textarea' => false, // not a post_meta key.  This is stored in post_content
1602
		);
1603
1604
		$compiled_form = array();
1605
1606
		// "Standard" field whitelist
1607
		foreach ( $field_value_map as $type => $meta_key ) {
1608
			if ( isset( $field_ids[ $type ] ) ) {
1609
				$field = $form->fields[ $field_ids[ $type ] ];
1610
1611
				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...
1612
					if ( isset( $content_fields[ "_feedback_{$meta_key}" ] ) ) {
1613
						$value = $content_fields[ "_feedback_{$meta_key}" ];
1614
					}
1615
				} else {
1616
					// The feedback content is stored as the first "half" of post_content
1617
					$value = $feedback->post_content;
1618
					list( $value ) = explode( '<!--more-->', $value );
1619
					$value = trim( $value );
1620
				}
1621
1622
				$field_index = array_search( $field_ids[ $type ], $field_ids['all'] );
1623
				$compiled_form[ $field_index ] = sprintf(
1624
					'<b>%1$s:</b> %2$s<br /><br />',
1625
					wp_kses( $field->get_attribute( 'label' ), array() ),
1626
					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...
1627
				);
1628
			}
1629
		}
1630
1631
		// "Non-standard" fields
1632
		if ( $field_ids['extra'] ) {
1633
			// array indexed by field label (not field id)
1634
			$extra_fields = get_post_meta( $feedback_id, '_feedback_extra_fields', true );
1635
1636
			/**
1637
			 * Only get data for the compiled form if `$extra_fields` is a valid and non-empty array.
1638
			 */
1639
			if ( is_array( $extra_fields ) && ! empty( $extra_fields ) ) {
1640
1641
				$extra_field_keys = array_keys( $extra_fields );
1642
1643
				$i = 0;
1644
				foreach ( $field_ids['extra'] as $field_id ) {
1645
					$field       = $form->fields[ $field_id ];
1646
					$field_index = array_search( $field_id, $field_ids['all'] );
1647
1648
					$label = $field->get_attribute( 'label' );
1649
1650
					$compiled_form[ $field_index ] = sprintf(
1651
						'<b>%1$s:</b> %2$s<br /><br />',
1652
						wp_kses( $label, array() ),
1653
						nl2br( wp_kses( $extra_fields[ $extra_field_keys[ $i ] ], array() ) )
1654
					);
1655
1656
					$i++;
1657
				}
1658
			}
1659
		}
1660
1661
		// Sorting lines by the field index
1662
		ksort( $compiled_form );
1663
1664
		return $compiled_form;
1665
	}
1666
1667
	/**
1668
	 * The contact-field shortcode processor
1669
	 * 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.
1670
	 *
1671
	 * @param array       $attributes Key => Value pairs as parsed by shortcode_parse_atts()
1672
	 * @param string|null $content The shortcode's inner content: [contact-field]$content[/contact-field]
1673
	 * @return HTML for the contact form field
1674
	 */
1675
	static function parse_contact_field( $attributes, $content ) {
1676
		// Don't try to parse contact form fields if not inside a contact form
1677
		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...
1678
			$att_strs = array();
1679
			foreach ( $attributes as $att => $val ) {
1680
				if ( is_numeric( $att ) ) { // Is a valueless attribute
1681
					$att_strs[] = esc_html( $val );
1682
				} elseif ( isset( $val ) ) { // A regular attr - value pair
1683
					$att_strs[] = esc_html( $att ) . '=\'' . esc_html( $val ) . '\'';
1684
				}
1685
			}
1686
1687
			$html = '[contact-field ' . implode( ' ', $att_strs );
1688
1689
			if ( isset( $content ) && ! empty( $content ) ) { // If there is content, let's add a closing tag
1690
				$html .= ']' . esc_html( $content ) . '[/contact-field]';
1691
			} else { // Otherwise let's add a closing slash in the first tag
1692
				$html .= '/]';
1693
			}
1694
1695
			return $html;
1696
		}
1697
1698
		$form = Grunion_Contact_Form::$current_form;
1699
1700
		$field = new Grunion_Contact_Form_Field( $attributes, $content, $form );
1701
1702
		$field_id = $field->get_attribute( 'id' );
1703
		if ( $field_id ) {
1704
			$form->fields[ $field_id ] = $field;
1705
		} else {
1706
			$form->fields[] = $field;
1707
		}
1708
1709
		if (
1710
			isset( $_POST['action'] ) && 'grunion-contact-form' === $_POST['action']
1711
		&&
1712
			isset( $_POST['contact-form-id'] ) && $form->get_attribute( 'id' ) == $_POST['contact-form-id']
1713
		&&
1714
			isset( $_POST['contact-form-hash'] ) && hash_equals( $form->hash, $_POST['contact-form-hash'] )
1715
		) {
1716
			// If we're processing a POST submission for this contact form, validate the field value so we can show errors as necessary.
1717
			$field->validate();
1718
		}
1719
1720
		// Output HTML
1721
		return $field->render();
1722
	}
1723
1724
	/**
1725
	 * Loops through $this->fields to generate a (structured) list of field IDs.
1726
	 *
1727
	 * Important: Currently the whitelisted fields are defined as follows:
1728
	 *  `name`, `email`, `url`, `subject`, `textarea`
1729
	 *
1730
	 * If you need to add new fields to the Contact Form, please don't add them
1731
	 * to the whitelisted fields and leave them as extra fields.
1732
	 *
1733
	 * The reasoning behind this is that both the admin Feedback view and the CSV
1734
	 * export will not include any fields that are added to the list of
1735
	 * whitelisted fields without taking proper care to add them to all the
1736
	 * other places where they accessed/used/saved.
1737
	 *
1738
	 * The safest way to add new fields is to add them to the dropdown and the
1739
	 * HTML list ( @see Grunion_Contact_Form_Field::render ) and don't add them
1740
	 * to the list of whitelisted fields. This way they will become a part of the
1741
	 * `extra fields` which are saved in the post meta and will be properly
1742
	 * handled by the admin Feedback view and the CSV Export without any extra
1743
	 * work.
1744
	 *
1745
	 * If there is need to add a field to the whitelisted fields, then please
1746
	 * take proper care to add logic to handle the field in the following places:
1747
	 *
1748
	 *  - Below in the switch statement - so the field is recognized as whitelisted.
1749
	 *
1750
	 *  - Grunion_Contact_Form::process_submission - validation and logic.
1751
	 *
1752
	 *  - Grunion_Contact_Form::process_submission - add the field as an additional
1753
	 *      field in the `post_content` when saving the feedback content.
1754
	 *
1755
	 *  - Grunion_Contact_Form_Plugin::parse_fields_from_content - add mapping
1756
	 *      for the field, defined in the above method.
1757
	 *
1758
	 *  - Grunion_Contact_Form_Plugin::map_parsed_field_contents_of_post_to_field_names -
1759
	 *      add mapping of the field for the CSV Export. Otherwise it will be missing
1760
	 *      from the exported data.
1761
	 *
1762
	 *  - admin.php / grunion_manage_post_columns - add the field to the render logic.
1763
	 *      Otherwise it will be missing from the admin Feedback view.
1764
	 *
1765
	 * @return array
1766
	 */
1767
	function get_field_ids() {
1768
		$field_ids = array(
1769
			'all'   => array(), // array of all field_ids
1770
			'extra' => array(), // array of all non-whitelisted field IDs
1771
1772
			// Whitelisted "standard" field IDs:
1773
			// 'email'    => field_id,
1774
			// 'name'     => field_id,
1775
			// 'url'      => field_id,
1776
			// 'subject'  => field_id,
1777
			// 'textarea' => field_id,
1778
		);
1779
1780
		foreach ( $this->fields as $id => $field ) {
1781
			$field_ids['all'][] = $id;
1782
1783
			$type = $field->get_attribute( 'type' );
1784
			if ( isset( $field_ids[ $type ] ) ) {
1785
				// This type of field is already present in our whitelist of "standard" fields for this form
1786
				// Put it in extra
1787
				$field_ids['extra'][] = $id;
1788
				continue;
1789
			}
1790
1791
			/**
1792
			 * See method description before modifying the switch cases.
1793
			 */
1794
			switch ( $type ) {
1795
				case 'email' :
1796
				case 'name' :
1797
				case 'url' :
1798
				case 'subject' :
1799
				case 'textarea' :
1800
					$field_ids[ $type ] = $id;
1801
					break;
1802
				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...
1803
					// Put everything else in extra
1804
					$field_ids['extra'][] = $id;
1805
			}
1806
		}
1807
1808
		return $field_ids;
1809
	}
1810
1811
	/**
1812
	 * Process the contact form's POST submission
1813
	 * Stores feedback.  Sends email.
1814
	 */
1815
	function process_submission() {
1816
		global $post;
1817
1818
		$plugin = Grunion_Contact_Form_Plugin::init();
1819
1820
		$id     = $this->get_attribute( 'id' );
1821
		$to     = $this->get_attribute( 'to' );
1822
		$widget = $this->get_attribute( 'widget' );
1823
1824
		$contact_form_subject = $this->get_attribute( 'subject' );
1825
1826
		$to = str_replace( ' ', '', $to );
1827
		$emails = explode( ',', $to );
1828
1829
		$valid_emails = array();
1830
1831
		foreach ( (array) $emails as $email ) {
1832
			if ( ! is_email( $email ) ) {
1833
				continue;
1834
			}
1835
1836
			if ( function_exists( 'is_email_address_unsafe' ) && is_email_address_unsafe( $email ) ) {
1837
				continue;
1838
			}
1839
1840
			$valid_emails[] = $email;
1841
		}
1842
1843
		// No one to send it to, which means none of the "to" attributes are valid emails.
1844
		// Use default email instead.
1845
		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...
1846
			$valid_emails = $this->defaults['to'];
1847
		}
1848
1849
		$to = $valid_emails;
1850
1851
		// Last ditch effort to set a recipient if somehow none have been set.
1852
		if ( empty( $to ) ) {
1853
			$to = get_option( 'admin_email' );
1854
		}
1855
1856
		// Make sure we're processing the form we think we're processing... probably a redundant check.
1857
		if ( $widget ) {
1858
			if ( 'widget-' . $widget != $_POST['contact-form-id'] ) {
1859
				return false;
1860
			}
1861
		} else {
1862
			if ( $post->ID != $_POST['contact-form-id'] ) {
1863
				return false;
1864
			}
1865
		}
1866
1867
		$field_ids = $this->get_field_ids();
1868
1869
		// Initialize all these "standard" fields to null
1870
		$comment_author_email = $comment_author_email_label = // v
1871
		$comment_author       = $comment_author_label       = // v
1872
		$comment_author_url   = $comment_author_url_label   = // v
1873
		$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...
1874
1875
		// For each of the "standard" fields, grab their field label and value.
1876 View Code Duplication
		if ( isset( $field_ids['name'] ) ) {
1877
			$field = $this->fields[ $field_ids['name'] ];
1878
			$comment_author = Grunion_Contact_Form_Plugin::strip_tags(
1879
				stripslashes(
1880
					/** This filter is already documented in core/wp-includes/comment-functions.php */
1881
					apply_filters( 'pre_comment_author_name', addslashes( $field->value ) )
1882
				)
1883
			);
1884
			$comment_author_label = Grunion_Contact_Form_Plugin::strip_tags( $field->get_attribute( 'label' ) );
1885
		}
1886
1887 View Code Duplication
		if ( isset( $field_ids['email'] ) ) {
1888
			$field = $this->fields[ $field_ids['email'] ];
1889
			$comment_author_email = Grunion_Contact_Form_Plugin::strip_tags(
1890
				stripslashes(
1891
					/** This filter is already documented in core/wp-includes/comment-functions.php */
1892
					apply_filters( 'pre_comment_author_email', addslashes( $field->value ) )
1893
				)
1894
			);
1895
			$comment_author_email_label = Grunion_Contact_Form_Plugin::strip_tags( $field->get_attribute( 'label' ) );
1896
		}
1897
1898
		if ( isset( $field_ids['url'] ) ) {
1899
			$field = $this->fields[ $field_ids['url'] ];
1900
			$comment_author_url = Grunion_Contact_Form_Plugin::strip_tags(
1901
				stripslashes(
1902
					/** This filter is already documented in core/wp-includes/comment-functions.php */
1903
					apply_filters( 'pre_comment_author_url', addslashes( $field->value ) )
1904
				)
1905
			);
1906
			if ( 'http://' == $comment_author_url ) {
1907
				$comment_author_url = '';
1908
			}
1909
			$comment_author_url_label = Grunion_Contact_Form_Plugin::strip_tags( $field->get_attribute( 'label' ) );
1910
		}
1911
1912
		if ( isset( $field_ids['textarea'] ) ) {
1913
			$field = $this->fields[ $field_ids['textarea'] ];
1914
			$comment_content = trim( Grunion_Contact_Form_Plugin::strip_tags( $field->value ) );
1915
			$comment_content_label = Grunion_Contact_Form_Plugin::strip_tags( $field->get_attribute( 'label' ) );
1916
		}
1917
1918
		if ( isset( $field_ids['subject'] ) ) {
1919
			$field = $this->fields[ $field_ids['subject'] ];
1920
			if ( $field->value ) {
1921
				$contact_form_subject = Grunion_Contact_Form_Plugin::strip_tags( $field->value );
1922
			}
1923
		}
1924
1925
		$all_values = $extra_values = array();
1926
		$i = 1; // Prefix counter for stored metadata
1927
1928
		// For all fields, grab label and value
1929
		foreach ( $field_ids['all'] as $field_id ) {
1930
			$field = $this->fields[ $field_id ];
1931
			$label = $i . '_' . $field->get_attribute( 'label' );
1932
			$value = $field->value;
1933
1934
			$all_values[ $label ] = $value;
1935
			$i++; // Increment prefix counter for the next field
1936
		}
1937
1938
		// For the "non-standard" fields, grab label and value
1939
		// Extra fields have their prefix starting from count( $all_values ) + 1
1940
		foreach ( $field_ids['extra'] as $field_id ) {
1941
			$field = $this->fields[ $field_id ];
1942
			$label = $i . '_' . $field->get_attribute( 'label' );
1943
			$value = $field->value;
1944
1945
			if ( is_array( $value ) ) {
1946
				$value = implode( ', ', $value );
1947
			}
1948
1949
			$extra_values[ $label ] = $value;
1950
			$i++; // Increment prefix counter for the next extra field
1951
		}
1952
1953
		$contact_form_subject = trim( $contact_form_subject );
1954
1955
		$comment_author_IP = Grunion_Contact_Form_Plugin::get_ip_address();
1956
1957
		$vars = array( 'comment_author', 'comment_author_email', 'comment_author_url', 'contact_form_subject', 'comment_author_IP' );
1958
		foreach ( $vars as $var ) {
1959
			$$var = str_replace( array( "\n", "\r" ), '', $$var );
1960
		}
1961
1962
		// Ensure that Akismet gets all of the relevant information from the contact form,
1963
		// not just the textarea field and predetermined subject.
1964
		$akismet_vars = compact( $vars );
1965
		$akismet_vars['comment_content'] = $comment_content;
1966
1967
		foreach ( array_merge( $field_ids['all'], $field_ids['extra'] ) as $field_id ) {
1968
			$field = $this->fields[ $field_id ];
1969
1970
			// Skip any fields that are just a choice from a pre-defined list. They wouldn't have any value
1971
			// from a spam-filtering point of view.
1972
			if ( in_array( $field->get_attribute( 'type' ), array( 'select', 'checkbox', 'checkbox-multiple', 'radio' ) ) ) {
1973
				continue;
1974
			}
1975
1976
			// Normalize the label into a slug.
1977
			$field_slug = trim( // Strip all leading/trailing dashes.
1978
				preg_replace(   // Normalize everything to a-z0-9_-
1979
					'/[^a-z0-9_]+/',
1980
					'-',
1981
					strtolower( $field->get_attribute( 'label' ) ) // Lowercase
1982
				),
1983
				'-'
1984
			);
1985
1986
			$field_value = ( is_array( $field->value ) ) ? trim( implode( ', ', $field->value ) ) : trim( $field->value );
1987
1988
			// Skip any values that are already in the array we're sending.
1989
			if ( $field_value && in_array( $field_value, $akismet_vars ) ) {
1990
				continue;
1991
			}
1992
1993
			$akismet_vars[ 'contact_form_field_' . $field_slug ] = $field_value;
1994
		}
1995
1996
		$spam = '';
1997
		$akismet_values = $plugin->prepare_for_akismet( $akismet_vars );
1998
1999
		// Is it spam?
2000
		/** This filter is already documented in modules/contact-form/admin.php */
2001
		$is_spam = apply_filters( 'jetpack_contact_form_is_spam', false, $akismet_values );
2002
		if ( is_wp_error( $is_spam ) ) { // WP_Error to abort
2003
			return $is_spam; // abort
2004
		} elseif ( $is_spam === true ) {  // TRUE to flag a spam
2005
			$spam = '***SPAM*** ';
2006
		}
2007
2008
		if ( ! $comment_author ) {
2009
			$comment_author = $comment_author_email;
2010
		}
2011
2012
		/**
2013
		 * Filter the email where a submitted feedback is sent.
2014
		 *
2015
		 * @module contact-form
2016
		 *
2017
		 * @since 1.3.1
2018
		 *
2019
		 * @param string|array $to Array of valid email addresses, or single email address.
2020
		 */
2021
		$to = (array) apply_filters( 'contact_form_to', $to );
2022
		$reply_to_addr = $to[0]; // get just the address part before the name part is added
2023
2024
		foreach ( $to as $to_key => $to_value ) {
2025
			$to[ $to_key ] = Grunion_Contact_Form_Plugin::strip_tags( $to_value );
2026
			$to[ $to_key ] = self::add_name_to_address( $to_value );
2027
		}
2028
2029
		$blog_url = parse_url( site_url() );
2030
		$from_email_addr = 'wordpress@' . $blog_url['host'];
2031
2032
		if ( ! empty( $comment_author_email ) ) {
2033
			$reply_to_addr = $comment_author_email;
2034
		}
2035
2036
		$headers = 'From: "' . $comment_author . '" <' . $from_email_addr . ">\r\n" .
2037
					'Reply-To: "' . $comment_author . '" <' . $reply_to_addr . ">\r\n";
2038
2039
		// Build feedback reference
2040
		$feedback_time  = current_time( 'mysql' );
2041
		$feedback_title = "{$comment_author} - {$feedback_time}";
2042
		$feedback_id    = md5( $feedback_title );
2043
2044
		$all_values = array_merge( $all_values, array(
2045
			'entry_title'     => the_title_attribute( 'echo=0' ),
2046
			'entry_permalink' => esc_url( get_permalink( get_the_ID() ) ),
2047
			'feedback_id'     => $feedback_id,
2048
		) );
2049
2050
		/** This filter is already documented in modules/contact-form/admin.php */
2051
		$subject = apply_filters( 'contact_form_subject', $contact_form_subject, $all_values );
2052
		$url     = $widget ? home_url( '/' ) : get_permalink( $post->ID );
2053
2054
		$date_time_format = _x( '%1$s \a\t %2$s', '{$date_format} \a\t {$time_format}', 'jetpack' );
2055
		$date_time_format = sprintf( $date_time_format, get_option( 'date_format' ), get_option( 'time_format' ) );
2056
		$time = date_i18n( $date_time_format, current_time( 'timestamp' ) );
2057
2058
		// keep a copy of the feedback as a custom post type
2059
		$feedback_status = $is_spam === true ? 'spam' : 'publish';
2060
2061
		foreach ( (array) $akismet_values as $av_key => $av_value ) {
2062
			$akismet_values[ $av_key ] = Grunion_Contact_Form_Plugin::strip_tags( $av_value );
2063
		}
2064
2065
		foreach ( (array) $all_values as $all_key => $all_value ) {
2066
			$all_values[ $all_key ] = Grunion_Contact_Form_Plugin::strip_tags( $all_value );
2067
		}
2068
2069
		foreach ( (array) $extra_values as $ev_key => $ev_value ) {
2070
			$extra_values[ $ev_key ] = Grunion_Contact_Form_Plugin::strip_tags( $ev_value );
2071
		}
2072
2073
		/*
2074
		 We need to make sure that the post author is always zero for contact
2075
		 * form submissions.  This prevents export/import from trying to create
2076
		 * new users based on form submissions from people who were logged in
2077
		 * at the time.
2078
		 *
2079
		 * Unfortunately wp_insert_post() tries very hard to make sure the post
2080
		 * author gets the currently logged in user id.  That is how we ended up
2081
		 * with this work around. */
2082
		add_filter( 'wp_insert_post_data', array( $plugin, 'insert_feedback_filter' ), 10, 2 );
2083
2084
		$post_id = wp_insert_post( array(
2085
			'post_date'    => addslashes( $feedback_time ),
2086
			'post_type'    => 'feedback',
2087
			'post_status'  => addslashes( $feedback_status ),
2088
			'post_parent'  => (int) $post->ID,
2089
			'post_title'   => addslashes( wp_kses( $feedback_title, array() ) ),
2090
			'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
2091
			'post_name'    => $feedback_id,
2092
		) );
2093
2094
		// once insert has finished we don't need this filter any more
2095
		remove_filter( 'wp_insert_post_data', array( $plugin, 'insert_feedback_filter' ), 10 );
2096
2097
		update_post_meta( $post_id, '_feedback_extra_fields', $this->addslashes_deep( $extra_values ) );
2098
2099
		if ( 'publish' == $feedback_status ) {
2100
			// Increase count of unread feedback.
2101
			$unread = get_option( 'feedback_unread_count', 0 ) + 1;
2102
			update_option( 'feedback_unread_count', $unread );
2103
		}
2104
2105
		if ( defined( 'AKISMET_VERSION' ) ) {
2106
			update_post_meta( $post_id, '_feedback_akismet_values', $this->addslashes_deep( $akismet_values ) );
2107
		}
2108
2109
		$message = self::get_compiled_form( $post_id, $this );
2110
2111
		array_push(
2112
			$message,
2113
			"<br />",
2114
			'<hr />',
2115
			__( 'Time:', 'jetpack' ) . ' ' . $time . '<br />',
2116
			__( 'IP Address:', 'jetpack' ) . ' ' . $comment_author_IP . '<br />',
2117
			__( 'Contact Form URL:', 'jetpack' ) . ' ' . $url . '<br />'
2118
		);
2119
2120
		if ( is_user_logged_in() ) {
2121
			array_push(
2122
				$message,
2123
				sprintf(
2124
					'<p>' . __( 'Sent by a verified %s user.', 'jetpack' ) . '</p>',
2125
					isset( $GLOBALS['current_site']->site_name ) && $GLOBALS['current_site']->site_name ?
2126
						$GLOBALS['current_site']->site_name : '"' . get_option( 'blogname' ) . '"'
2127
				)
2128
			);
2129
		} else {
2130
			array_push( $message, '<p>' . __( 'Sent by an unverified visitor to your site.', 'jetpack' ) . '</p>' );
2131
		}
2132
2133
		$message = join( $message, '' );
2134
2135
		/**
2136
		 * Filters the message sent via email after a successful form submission.
2137
		 *
2138
		 * @module contact-form
2139
		 *
2140
		 * @since 1.3.1
2141
		 *
2142
		 * @param string $message Feedback email message.
2143
		 */
2144
		$message = apply_filters( 'contact_form_message', $message );
2145
2146
		// This is called after `contact_form_message`, in order to preserve back-compat
2147
		$message = self::wrap_message_in_html_tags( $message );
2148
2149
		update_post_meta( $post_id, '_feedback_email', $this->addslashes_deep( compact( 'to', 'message' ) ) );
2150
2151
		/**
2152
		 * Fires right before the contact form message is sent via email to
2153
		 * the recipient specified in the contact form.
2154
		 *
2155
		 * @module contact-form
2156
		 *
2157
		 * @since 1.3.1
2158
		 *
2159
		 * @param integer $post_id Post contact form lives on
2160
		 * @param array $all_values Contact form fields
2161
		 * @param array $extra_values Contact form fields not included in $all_values
2162
		 */
2163
		do_action( 'grunion_pre_message_sent', $post_id, $all_values, $extra_values );
2164
2165
		// schedule deletes of old spam feedbacks
2166
		if ( ! wp_next_scheduled( 'grunion_scheduled_delete' ) ) {
2167
			wp_schedule_event( time() + 250, 'daily', 'grunion_scheduled_delete' );
2168
		}
2169
2170
		if (
2171
			$is_spam !== true &&
2172
			/**
2173
			 * Filter to choose whether an email should be sent after each successful contact form submission.
2174
			 *
2175
			 * @module contact-form
2176
			 *
2177
			 * @since 2.6.0
2178
			 *
2179
			 * @param bool true Should an email be sent after a form submission. Default to true.
2180
			 * @param int $post_id Post ID.
2181
			 */
2182
			true === apply_filters( 'grunion_should_send_email', true, $post_id )
2183
		) {
2184
			self::wp_mail( $to, "{$spam}{$subject}", $message, $headers );
2185
		} elseif (
2186
			true === $is_spam &&
2187
			/**
2188
			 * Choose whether an email should be sent for each spam contact form submission.
2189
			 *
2190
			 * @module contact-form
2191
			 *
2192
			 * @since 1.3.1
2193
			 *
2194
			 * @param bool false Should an email be sent after a spam form submission. Default to false.
2195
			 */
2196
			apply_filters( 'grunion_still_email_spam', false ) == true
2197
		) { // don't send spam by default.  Filterable.
2198
			self::wp_mail( $to, "{$spam}{$subject}", $message, $headers );
2199
		}
2200
2201
		if ( defined( 'DOING_AJAX' ) && DOING_AJAX ) {
2202
			return self::success_message( $post_id, $this );
2203
		}
2204
2205
		$redirect = wp_get_referer();
2206
		if ( ! $redirect ) { // wp_get_referer() returns false if the referer is the same as the current page
2207
			$redirect = $_SERVER['REQUEST_URI'];
2208
		}
2209
2210
		$redirect = add_query_arg( urlencode_deep( array(
2211
			'contact-form-id'   => $id,
2212
			'contact-form-sent' => $post_id,
2213
			'contact-form-hash' => $this->hash,
2214
			'_wpnonce'          => wp_create_nonce( "contact-form-sent-{$post_id}" ), // wp_nonce_url HTMLencodes :(
2215
		) ), $redirect );
2216
2217
		/**
2218
		 * Filter the URL where the reader is redirected after submitting a form.
2219
		 *
2220
		 * @module contact-form
2221
		 *
2222
		 * @since 1.9.0
2223
		 *
2224
		 * @param string $redirect Post submission URL.
2225
		 * @param int $id Contact Form ID.
2226
		 * @param int $post_id Post ID.
2227
		 */
2228
		$redirect = apply_filters( 'grunion_contact_form_redirect_url', $redirect, $id, $post_id );
2229
2230
		wp_safe_redirect( $redirect );
2231
		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...
2232
	}
2233
2234
	/**
2235
	 * Wrapper for wp_mail() that enables HTML messages with text alternatives
2236
	 *
2237
	 * @param string|array $to          Array or comma-separated list of email addresses to send message.
2238
	 * @param string       $subject     Email subject.
2239
	 * @param string       $message     Message contents.
2240
	 * @param string|array $headers     Optional. Additional headers.
2241
	 * @param string|array $attachments Optional. Files to attach.
2242
	 *
2243
	 * @return bool Whether the email contents were sent successfully.
2244
	 */
2245
	public static function wp_mail( $to, $subject, $message, $headers = '', $attachments = array() ) {
2246
		add_filter( 'wp_mail_content_type', __CLASS__ . '::get_mail_content_type' );
2247
		add_action( 'phpmailer_init',       __CLASS__ . '::add_plain_text_alternative' );
2248
2249
		$result = wp_mail( $to, $subject, $message, $headers, $attachments );
2250
2251
		remove_filter( 'wp_mail_content_type', __CLASS__ . '::get_mail_content_type' );
2252
		remove_action( 'phpmailer_init',       __CLASS__ . '::add_plain_text_alternative' );
2253
2254
		return $result;
2255
	}
2256
2257
	/**
2258
	 * Add a display name part to an email address
2259
	 *
2260
	 * SpamAssassin doesn't like addresses in HTML messages that are missing display names (e.g., `[email protected]`
2261
	 * instead of `"Foo Bar" <[email protected]>`.
2262
	 *
2263
	 * @param string $address
2264
	 *
2265
	 * @return string
2266
	 */
2267
	function add_name_to_address( $address ) {
2268
		// If it's just the address, without a display name
2269
		if ( is_email( $address ) ) {
2270
			$address_parts = explode( '@', $address );
2271
			$address = sprintf( '"%s" <%s>', $address_parts[0], $address );
2272
		}
2273
2274
		return $address;
2275
	}
2276
2277
	/**
2278
	 * Get the content type that should be assigned to outbound emails
2279
	 *
2280
	 * @return string
2281
	 */
2282
	static function get_mail_content_type() {
2283
		return 'text/html';
2284
	}
2285
2286
	/**
2287
	 * Wrap a message body with the appropriate in HTML tags
2288
	 *
2289
	 * This helps to ensure correct parsing by clients, and also helps avoid triggering spam filtering rules
2290
	 *
2291
	 * @param string $body
2292
	 *
2293
	 * @return string
2294
	 */
2295
	static function wrap_message_in_html_tags( $body ) {
2296
		// Don't do anything if the message was already wrapped in HTML tags
2297
		// That could have be done by a plugin via filters
2298
		if ( false !== strpos( $body, '<html' ) ) {
2299
			return $body;
2300
		}
2301
2302
		$html_message = sprintf(
2303
			// The tabs are just here so that the raw code is correctly formatted for developers
2304
			// They're removed so that they don't affect the final message sent to users
2305
			str_replace( "\t", '',
2306
				"<!doctype html>
2307
				<html xmlns=\"http://www.w3.org/1999/xhtml\">
2308
				<body>
2309
2310
				%s
2311
2312
				</body>
2313
				</html>"
2314
			),
2315
			$body
2316
		);
2317
2318
		return $html_message;
2319
	}
2320
2321
	/**
2322
	 * Add a plain-text alternative part to an outbound email
2323
	 *
2324
	 * This makes the message more accessible to mail clients that aren't HTML-aware, and decreases the likelihood
2325
	 * that the message will be flagged as spam.
2326
	 *
2327
	 * @param PHPMailer $phpmailer
2328
	 */
2329
	static function add_plain_text_alternative( $phpmailer ) {
2330
		// Add an extra break so that the extra space above the <p> is preserved after the <p> is stripped out
2331
		$alt_body = str_replace( '<p>', '<p><br />', $phpmailer->Body );
2332
2333
		// Convert <br> to \n breaks, to preserve the space between lines that we want to keep
2334
		$alt_body = str_replace( array( '<br>', '<br />' ), "\n", $alt_body );
2335
2336
		// Convert <hr> to an plain-text equivalent, to preserve the integrity of the message
2337
		$alt_body = str_replace( array( "<hr>", "<hr />" ), "----\n", $alt_body );
2338
2339
		// Trim the plain text message to remove the \n breaks that were after <doctype>, <html>, and <body>
2340
		$phpmailer->AltBody = trim( strip_tags( $alt_body ) );
2341
	}
2342
2343
	function addslashes_deep( $value ) {
2344
		if ( is_array( $value ) ) {
2345
			return array_map( array( $this, 'addslashes_deep' ), $value );
2346
		} elseif ( is_object( $value ) ) {
2347
			$vars = get_object_vars( $value );
2348
			foreach ( $vars as $key => $data ) {
2349
				$value->{$key} = $this->addslashes_deep( $data );
2350
			}
2351
			return $value;
2352
		}
2353
2354
		return addslashes( $value );
2355
	}
2356
}
2357
2358
/**
2359
 * Class for the contact-field shortcode.
2360
 * Parses shortcode to output the contact form field as HTML.
2361
 * Validates input.
2362
 */
2363
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...
2364
	public $shortcode_name = 'contact-field';
2365
2366
	/**
2367
	 * @var Grunion_Contact_Form parent form
2368
	 */
2369
	public $form;
2370
2371
	/**
2372
	 * @var string default or POSTed value
2373
	 */
2374
	public $value;
2375
2376
	/**
2377
	 * @var bool Is the input invalid?
2378
	 */
2379
	public $error = false;
2380
2381
	/**
2382
	 * @param array                $attributes An associative array of shortcode attributes.  @see shortcode_atts()
2383
	 * @param null|string          $content Null for selfclosing shortcodes.  The inner content otherwise.
2384
	 * @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...
2385
	 */
2386
	function __construct( $attributes, $content = null, $form = null ) {
2387
		$attributes = shortcode_atts( array(
2388
					'label'       => null,
2389
					'type'        => 'text',
2390
					'required'    => false,
2391
					'options'     => array(),
2392
					'id'          => null,
2393
					'default'     => null,
2394
					'values'      => null,
2395
					'placeholder' => null,
2396
					'class'       => null,
2397
		), $attributes, 'contact-field' );
2398
2399
		// special default for subject field
2400
		if ( 'subject' == $attributes['type'] && is_null( $attributes['default'] ) && ! is_null( $form ) ) {
2401
			$attributes['default'] = $form->get_attribute( 'subject' );
2402
		}
2403
2404
		// allow required=1 or required=true
2405
		if ( '1' == $attributes['required'] || 'true' == strtolower( $attributes['required'] ) ) {
2406
			$attributes['required'] = true;
2407
		} else { $attributes['required'] = false;
2408
		}
2409
2410
		// parse out comma-separated options list (for selects, radios, and checkbox-multiples)
2411
		if ( ! empty( $attributes['options'] ) && is_string( $attributes['options'] ) ) {
2412
			$attributes['options'] = array_map( 'trim', explode( ',', $attributes['options'] ) );
2413
2414 View Code Duplication
			if ( ! empty( $attributes['values'] ) && is_string( $attributes['values'] ) ) {
2415
				$attributes['values'] = array_map( 'trim', explode( ',', $attributes['values'] ) );
2416
			}
2417
		}
2418
2419
		if ( $form ) {
2420
			// make a unique field ID based on the label, with an incrementing number if needed to avoid clashes
2421
			$form_id = $form->get_attribute( 'id' );
2422
			$id = isset( $attributes['id'] ) ? $attributes['id'] : false;
2423
2424
			$unescaped_label = $this->unesc_attr( $attributes['label'] );
2425
			$unescaped_label = str_replace( '%', '-', $unescaped_label ); // jQuery doesn't like % in IDs?
2426
			$unescaped_label = preg_replace( '/[^a-zA-Z0-9.-_:]/', '', $unescaped_label );
2427
2428
			if ( empty( $id ) ) {
2429
				$id = sanitize_title_with_dashes( 'g' . $form_id . '-' . $unescaped_label );
2430
				$i = 0;
2431
				$max_tries = 99;
2432
				while ( isset( $form->fields[ $id ] ) ) {
2433
					$i++;
2434
					$id = sanitize_title_with_dashes( 'g' . $form_id . '-' . $unescaped_label . '-' . $i );
2435
2436
					if ( $i > $max_tries ) {
2437
						break;
2438
					}
2439
				}
2440
			}
2441
2442
			$attributes['id'] = $id;
2443
		}
2444
2445
		parent::__construct( $attributes, $content );
2446
2447
		// Store parent form
2448
		$this->form = $form;
2449
	}
2450
2451
	/**
2452
	 * This field's input is invalid.  Flag as invalid and add an error to the parent form
2453
	 *
2454
	 * @param string $message The error message to display on the form.
2455
	 */
2456
	function add_error( $message ) {
2457
		$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...
2458
2459
		if ( ! is_wp_error( $this->form->errors ) ) {
2460
			$this->form->errors = new WP_Error;
2461
		}
2462
2463
		$this->form->errors->add( $this->get_attribute( 'id' ), $message );
2464
	}
2465
2466
	/**
2467
	 * Is the field input invalid?
2468
	 *
2469
	 * @see $error
2470
	 *
2471
	 * @return bool
2472
	 */
2473
	function is_error() {
2474
		return $this->error;
2475
	}
2476
2477
	/**
2478
	 * Validates the form input
2479
	 */
2480
	function validate() {
2481
		// If it's not required, there's nothing to validate
2482
		if ( ! $this->get_attribute( 'required' ) ) {
2483
			return;
2484
		}
2485
2486
		$field_id    = $this->get_attribute( 'id' );
2487
		$field_type  = $this->get_attribute( 'type' );
2488
		$field_label = $this->get_attribute( 'label' );
2489
2490
		if ( isset( $_POST[ $field_id ] ) ) {
2491
			if ( is_array( $_POST[ $field_id ] ) ) {
2492
				$field_value = array_map( 'stripslashes', $_POST[ $field_id ] );
2493
			} else {
2494
				$field_value = stripslashes( $_POST[ $field_id ] );
2495
			}
2496
		} else {
2497
			$field_value = '';
2498
		}
2499
2500
		switch ( $field_type ) {
2501
			case 'email' :
2502
				// Make sure the email address is valid
2503
				if ( ! is_email( $field_value ) ) {
2504
					/* translators: %s is the name of a form field */
2505
					$this->add_error( sprintf( __( '%s requires a valid email address', 'jetpack' ), $field_label ) );
2506
				}
2507
			break;
2508
			case 'checkbox-multiple' :
2509
				// Check that there is at least one option selected
2510
				if ( empty( $field_value ) ) {
2511
					/* translators: %s is the name of a form field */
2512
					$this->add_error( sprintf( __( '%s requires at least one selection', 'jetpack' ), $field_label ) );
2513
				}
2514
			break;
2515
			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...
2516
				// Just check for presence of any text
2517
				if ( ! strlen( trim( $field_value ) ) ) {
2518
					/* translators: %s is the name of a form field */
2519
					$this->add_error( sprintf( __( '%s is required', 'jetpack' ), $field_label ) );
2520
				}
2521
		}
2522
	}
2523
2524
2525
	/**
2526
	 * Check the default value for options field
2527
	 *
2528
	 * @param string value
2529
	 * @param int index
2530
	 * @param string default value
2531
	 *
2532
	 * @return string
2533
	 */
2534
	public function get_option_value( $value, $index, $options ) {
2535
		if ( empty( $value[ $index ] ) ) {
2536
			return $options;
2537
		}
2538
		return $value[ $index ];
2539
	}
2540
2541
	/**
2542
	 * Outputs the HTML for this form field
2543
	 *
2544
	 * @return string HTML
2545
	 */
2546
	function render() {
2547
		global $current_user, $user_identity;
2548
2549
		$r = '';
2550
2551
		$field_id          = $this->get_attribute( 'id' );
2552
		$field_type        = $this->get_attribute( 'type' );
2553
		$field_label       = $this->get_attribute( 'label' );
2554
		$field_required    = $this->get_attribute( 'required' );
2555
		$placeholder       = $this->get_attribute( 'placeholder' );
2556
		$class             = 'date' === $field_type ? 'jp-contact-form-date' : $this->get_attribute( 'class' );
2557
		$field_placeholder = ( ! empty( $placeholder ) ) ? "placeholder='" . esc_attr( $placeholder ) . "'" : '';
2558
		$field_class       = "class='" . trim( esc_attr( $field_type ) . ' ' . esc_attr( $class ) ) . "' ";
2559
2560
		if ( isset( $_POST[ $field_id ] ) ) {
2561
			if ( is_array( $_POST[ $field_id ] ) ) {
2562
				$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...
2563
			} else {
2564
				$this->value = stripslashes( (string) $_POST[ $field_id ] );
2565
			}
2566
		} elseif ( isset( $_GET[ $field_id ] ) ) {
2567
			$this->value = stripslashes( (string) $_GET[ $field_id ] );
2568
		} elseif (
2569
			is_user_logged_in() &&
2570
			( ( defined( 'IS_WPCOM' ) && IS_WPCOM ) ||
2571
			/**
2572
			 * Allow third-party tools to prefill the contact form with the user's details when they're logged in.
2573
			 *
2574
			 * @module contact-form
2575
			 *
2576
			 * @since 3.2.0
2577
			 *
2578
			 * @param bool false Should the Contact Form be prefilled with your details when you're logged in. Default to false.
2579
			 */
2580
			true === apply_filters( 'jetpack_auto_fill_logged_in_user', false )
2581
			)
2582
		) {
2583
			// Special defaults for logged-in users
2584
			switch ( $this->get_attribute( 'type' ) ) {
2585
				case 'email' :
2586
					$this->value = $current_user->data->user_email;
2587
				break;
2588
				case 'name' :
2589
					$this->value = $user_identity;
2590
				break;
2591
				case 'url' :
2592
					$this->value = $current_user->data->user_url;
2593
				break;
2594
				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...
2595
					$this->value = $this->get_attribute( 'default' );
2596
			}
2597
		} else {
2598
			$this->value = $this->get_attribute( 'default' );
2599
		}
2600
2601
		$field_value = Grunion_Contact_Form_Plugin::strip_tags( $this->value );
2602
		$field_label = Grunion_Contact_Form_Plugin::strip_tags( $field_label );
2603
2604
		/**
2605
		 * Filter the Contact Form required field text
2606
		 *
2607
		 * @module contact-form
2608
		 *
2609
		 * @since 3.8.0
2610
		 *
2611
		 * @param string $var Required field text. Default is "(required)".
2612
		 */
2613
		$required_field_text = esc_html( apply_filters( 'jetpack_required_field_text', __( '(required)', 'jetpack' ) ) );
2614
2615
		switch ( $field_type ) {
2616 View Code Duplication
			case 'email' :
2617
				$r .= "\n<div>\n";
2618
				$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";
2619
				$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";
2620
				$r .= "\t</div>\n";
2621
			break;
2622
			case 'telephone' :
2623
				$r .= "\n<div>\n";
2624
				$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";
2625
				$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";
2626
			break;
2627
			case 'url' :
2628
				$r .= "\n<div>\n";
2629
				$r .= "\t\t<label for='" . esc_attr( $field_id ) . "' class='grunion-field-label url" . ( $this->is_error() ? ' form-error' : '' ) . "'>" . esc_html( $field_label ) . ( $field_required ? '<span>' . $required_field_text . '</span>' : '' ) . "</label>\n";
2630
				$r .= "\t\t<input type='url' name='" . esc_attr( $field_id ) . "' id='" . esc_attr( $field_id ) . "' value='" . esc_attr( $field_value ) . "' " . $field_class . $field_placeholder . ' ' . ( $field_required ? "required aria-required='true'" : '' ) . "/>\n";
2631
				$r .= "\t</div>\n";
2632
			break;
2633 View Code Duplication
			case 'textarea' :
2634
				$r .= "\n<div>\n";
2635
				$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";
2636
				$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";
2637
				$r .= "\t</div>\n";
2638
			break;
2639
			case 'radio' :
2640
				$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";
2641
				foreach ( $this->get_attribute( 'options' ) as $optionIndex => $option ) {
2642
					$option = Grunion_Contact_Form_Plugin::strip_tags( $option );
2643
					$r .= "\t\t<label class='grunion-radio-label radio" . ( $this->is_error() ? ' form-error' : '' ) . "'>";
2644
					$r .= "<input type='radio' name='" . esc_attr( $field_id ) . "' value='" . esc_attr( $this->get_option_value( $this->get_attribute( 'values' ), $optionIndex, $option ) ) . "' " . $field_class . checked( $option, $field_value, false ) . ' ' . ( $field_required ? "required aria-required='true'" : '' ) . '/> ';
2645
					$r .= esc_html( $option ) . "</label>\n";
2646
					$r .= "\t\t<div class='clear-form'></div>\n";
2647
				}
2648
				$r .= "\t\t</div>\n";
2649
			break;
2650
			case 'checkbox' :
2651
				$r .= "\t<div>\n";
2652
				$r .= "\t\t<label class='grunion-field-label checkbox" . ( $this->is_error() ? ' form-error' : '' ) . "'>\n";
2653
				$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";
2654
				$r .= "\t\t" . esc_html( $field_label ) . ( $field_required ? '<span>' . $required_field_text . '</span>' : '' ) . "</label>\n";
2655
				$r .= "\t\t<div class='clear-form'></div>\n";
2656
				$r .= "\t</div>\n";
2657
			break;
2658
			case 'checkbox-multiple' :
2659
				$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";
2660
				foreach ( $this->get_attribute( 'options' ) as $optionIndex => $option ) {
2661
					$option = Grunion_Contact_Form_Plugin::strip_tags( $option );
2662
					$r .= "\t\t<label class='grunion-checkbox-multiple-label checkbox-multiple" . ( $this->is_error() ? ' form-error' : '' ) . "'>";
2663
					$r .= "<input type='checkbox' name='" . esc_attr( $field_id ) . "[]' value='" . esc_attr( $this->get_option_value( $this->get_attribute( 'values' ), $optionIndex, $option ) ) . "' " . $field_class . checked( in_array( $option, (array) $field_value ), true, false ) . ' /> ';
2664
					$r .= esc_html( $option ) . "</label>\n";
2665
					$r .= "\t\t<div class='clear-form'></div>\n";
2666
				}
2667
				$r .= "\t\t</div>\n";
2668
			break;
2669
			case 'select' :
2670
				$r .= "\n<div>\n";
2671
				$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";
2672
				$r .= "\t<select name='" . esc_attr( $field_id ) . "' id='" . esc_attr( $field_id ) . "' " . $field_class . ( $field_required ? "required aria-required='true'" : '' ) . ">\n";
2673
				foreach ( $this->get_attribute( 'options' ) as $optionIndex => $option ) {
2674
					$option = Grunion_Contact_Form_Plugin::strip_tags( $option );
2675
					$r .= "\t\t<option" . selected( $option, $field_value, false ) . " value='" . esc_attr( $this->get_option_value( $this->get_attribute( 'values' ), $optionIndex, $option ) ) . "'>" . esc_html( $option ) . "</option>\n";
2676
				}
2677
				$r .= "\t</select>\n";
2678
				$r .= "\t</div>\n";
2679
			break;
2680
			case 'date' :
2681
				$r .= "\n<div>\n";
2682
				$r .= "\t\t<label for='" . esc_attr( $field_id ) . "' class='grunion-field-label " . esc_attr( $field_type ) . ( $this->is_error() ? ' form-error' : '' ) . "'>" . esc_html( $field_label ) . ( $field_required ? '<span>' . $required_field_text . '</span>' : '' ) . "</label>\n";
2683
				$r .= "\t\t<input type='text' name='" . esc_attr( $field_id ) . "' id='" . esc_attr( $field_id ) . "' value='" . esc_attr( $field_value ) . "' " . $field_class . ( $field_required ? "required aria-required='true'" : '' ) . "/>\n";
2684
				$r .= "\t</div>\n";
2685
2686
				wp_enqueue_script( 'grunion-frontend', plugins_url( 'js/grunion-frontend.js', __FILE__ ), array( 'jquery', 'jquery-ui-datepicker' ) );
2687
				wp_enqueue_style( 'jp-jquery-ui-datepicker', plugins_url( 'css/jquery-ui-datepicker.css', __FILE__ ), array( 'dashicons' ), '1.0' );
2688
2689
				// Using Core's built-in datepicker localization routine
2690
				wp_localize_jquery_ui_datepicker();
2691
			break;
2692 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...
2693
				// note that any unknown types will produce a text input, so we can use arbitrary type names to handle
2694
				// input fields like name, email, url that require special validation or handling at POST
2695
				$r .= "\n<div>\n";
2696
				$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";
2697
				$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";
2698
				$r .= "\t</div>\n";
2699
		}
2700
2701
		/**
2702
		 * Filter the HTML of the Contact Form.
2703
		 *
2704
		 * @module contact-form
2705
		 *
2706
		 * @since 2.6.0
2707
		 *
2708
		 * @param string $r Contact Form HTML output.
2709
		 * @param string $field_label Field label.
2710
		 * @param int|null $id Post ID.
2711
		 */
2712
		return apply_filters( 'grunion_contact_form_field_html', $r, $field_label, ( in_the_loop() ? get_the_ID() : null ) );
2713
	}
2714
}
2715
2716
add_action( 'init', array( 'Grunion_Contact_Form_Plugin', 'init' ) );
2717
2718
add_action( 'grunion_scheduled_delete', 'grunion_delete_old_spam' );
2719
2720
/**
2721
 * Deletes old spam feedbacks to keep the posts table size under control
2722
 */
2723
function grunion_delete_old_spam() {
2724
	global $wpdb;
2725
2726
	$grunion_delete_limit = 100;
2727
2728
	$now_gmt = current_time( 'mysql', 1 );
2729
	$sql = $wpdb->prepare( "
2730
		SELECT `ID`
2731
		FROM $wpdb->posts
2732
		WHERE DATE_SUB( %s, INTERVAL 15 DAY ) > `post_date_gmt`
2733
			AND `post_type` = 'feedback'
2734
			AND `post_status` = 'spam'
2735
		LIMIT %d
2736
	", $now_gmt, $grunion_delete_limit );
2737
	$post_ids = $wpdb->get_col( $sql );
2738
2739
	foreach ( (array) $post_ids as $post_id ) {
2740
		// force a full delete, skip the trash
2741
		wp_delete_post( $post_id, true );
2742
	}
2743
2744
	// Arbitrary check points for running OPTIMIZE
2745
	// nothing special about 5000 or 11
2746
	// just trying to periodically recover deleted rows
2747
	$random_num = mt_rand( 1, 5000 );
2748
	if (
2749
		/**
2750
		 * Filter how often the module run OPTIMIZE TABLE on the core WP tables.
2751
		 *
2752
		 * @module contact-form
2753
		 *
2754
		 * @since 1.3.1
2755
		 *
2756
		 * @param int $random_num Random number.
2757
		 */
2758
		apply_filters( 'grunion_optimize_table', ( $random_num == 11 ) )
2759
	) {
2760
		$wpdb->query( "OPTIMIZE TABLE $wpdb->posts" );
2761
	}
2762
2763
	// if we hit the max then schedule another run
2764
	if ( count( $post_ids ) >= $grunion_delete_limit ) {
2765
		wp_schedule_single_event( time() + 700, 'grunion_scheduled_delete' );
2766
	}
2767
}
2768