PluginProbe
HTML Forms – Simple WordPress Forms Plugin / 1.5.3
HTML Forms – Simple WordPress Forms Plugin v1.5.3
trunk 1.0 1.0.1 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.1 1.1.1 1.1.2 1.1.3 1.1.4 1.1.5 1.2.0 1.3.0 1.3.1 1.3.10 1.3.11 1.3.12 1.3.13 1.3.14 1.3.15 1.3.16 1.3.17 All 66 releases
html-forms / src / class-forms.php

class-forms.php in HTML Forms – Simple WordPress Forms Plugin 1.5.3, at src/class-forms.php

434 lines 12.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace HTML_Forms;
4
5 class Forms {
6
7
8 /**
9 * @var string
10 */
11 private $plugin_file;
12
13 /**
14 * @var array
15 */
16 private $settings;
17
18 /**
19 * Forms constructor.
20 *
21 * @param string $plugin_file
22 * @param array $settings
23 */
24 public function __construct( $plugin_file, array $settings ) {
25 $this->plugin_file = $plugin_file;
26 $this->settings = $settings;
27 }
28
29 public function hook() {
30 add_action( 'init', array( $this, 'register' ) );
31 add_action( 'init', array( $this, 'listen_for_submit' ) );
32 add_action( 'init', array( $this, 'register_assets' ) );
33 add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_assets' ) );
34 add_action( 'parse_request', array( $this, 'listen_for_preview' ) );
35 add_filter( 'hf_form_markup', 'hf_template' );
36 }
37
38 public function register() {
39 // register post type
40 register_post_type(
41 'html-form',
42 array(
43 'labels' => array(
44 'name' => 'HTML Forms',
45 'singular_name' => 'HTML Form',
46 ),
47 'public' => false,
48 'capability_type' => 'form',
49 )
50 );
51
52 if ( function_exists( 'register_block_type' ) ) {
53 register_block_type(
54 'html-forms/form',
55 array(
56 'render_callback' => array( $this, 'shortcode' ),
57 )
58 );
59 }
60
61 add_shortcode( 'hf_form', array( $this, 'shortcode' ) );
62 }
63
64 public function register_assets() {
65 $assets_url = plugins_url( 'assets/', $this->plugin_file );
66
67 wp_register_script( 'html-forms', $assets_url . 'js/public.js', array(), HTML_FORMS_VERSION, true );
68 wp_localize_script(
69 'html-forms',
70 'hf_js_vars',
71 array(
72 'ajax_url' => admin_url( 'admin-ajax.php?action=hf_form_submit' ),
73 )
74 );
75
76 wp_register_style( 'html-forms', $assets_url . 'css/forms.css', array(), HTML_FORMS_VERSION );
77 add_filter( 'script_loader_tag', array( $this, 'add_defer_attribute' ), 10, 2 );
78 }
79
80 public function enqueue_assets() {
81 if ( $this->settings['load_stylesheet'] ) {
82 wp_enqueue_style( 'html-forms' );
83 }
84 }
85
86 /**
87 * Adds defer attribute to our <script> element
88 */
89 public function add_defer_attribute( $tag, $handle ) {
90 if ( $handle !== 'html-forms' ) {
91 return $tag;
92 }
93
94 return str_replace( ' src=', ' defer src=', $tag );
95 }
96 /**
97 * @param Form $form
98 * @param array $data
99 * @return string
100 */
101 public function validate_form( Form $form, array $data ) {
102 // validate honeypot field
103 $honeypot_key = sprintf( '_hf_h%d', $form->ID );
104 if ( ! isset( $data[ $honeypot_key ] ) || $data[ $honeypot_key ] !== '' ) {
105 return 'spam';
106 }
107
108 // validate size of POST array
109 if ( count( $data ) > $form->get_field_count() && apply_filters( 'hf_validate_form_request_size', true ) ) {
110 return 'spam';
111 }
112
113 $was_required = (array) hf_array_get( $data, '_was_required', array() );
114 $required_fields = $form->get_required_fields();
115 foreach ( $required_fields as $field_name ) {
116 $value = hf_array_get( $data, $field_name );
117 if ( empty( $value ) && ! in_array( $field_name, $was_required ) ) {
118 return 'required_field_missing';
119 }
120 }
121
122 $email_fields = $form->get_email_fields();
123 foreach ( $email_fields as $field_name ) {
124 $value = hf_array_get( $data, $field_name );
125 if ( ! empty( $value ) && ! is_email( $value ) ) {
126 return 'invalid_email';
127 }
128 }
129
130 $error_code = '';
131
132 /**
133 * This filter allows you to perform your own form validation. The dynamic portion of the hook refers to the form slug.
134 *
135 * Return a non-empty string if you want to raise an error.
136 * Error codes with a specific error message are: "required_field_missing", "invalid_email", and "error"
137 *
138 * @param string $error_code
139 * @param Form $form
140 * @param array $data
141 */
142 $error_code = apply_filters( 'hf_validate_form_' . $form->slug, $error_code, $form, $data );
143
144 /**
145 * This filter allows you to perform your own form validation.
146 *
147 * Return a non-empty string if you want to raise an error.
148 * Error codes with a specific error message are: "required_field_missing", "invalid_email", and "error"
149 *
150 * @param string $error_code
151 * @param Form $form
152 * @param array $data
153 */
154 $error_code = apply_filters( 'hf_validate_form', $error_code, $form, $data );
155 if ( ! empty( $error_code ) ) {
156 return $error_code;
157 }
158
159 // all good: no errors!
160 return '';
161 }
162
163 /**
164 * Sanitize array with values before saving. Can be called recursively.
165 *
166 * @param mixed $value
167 * @return mixed
168 */
169 public function sanitize( $value ) {
170 if ( is_string( $value ) ) {
171 // do nothing if empty string
172 if ( $value === '' ) {
173 return $value;
174 }
175
176 // strip slashes
177 $value = stripslashes( $value );
178
179 // strip all whitespace
180 $value = trim( $value );
181
182 // convert &amp; back to &
183 $value = html_entity_decode( $value, ENT_NOQUOTES );
184 } elseif ( is_array( $value ) || is_object( $value ) ) {
185 $new_value = array();
186 $vars = is_array( $value ) ? $value : get_object_vars( $value );
187
188 // do nothing if empty array or object
189 if ( count( $vars ) === 0 ) {
190 return $value;
191 }
192
193 foreach ( $vars as $key => $sub_value ) {
194 // strip all whitespace & HTML from keys (!)
195 $key = trim( strip_tags( $key ) );
196
197 // sanitize sub value
198 $new_value[ $key ] = $this->sanitize( $sub_value );
199 }
200
201 $value = is_object( $value ) ? (object) $new_value : $new_value;
202 }
203
204 return $value;
205 }
206
207 /**
208 * @return array
209 */
210 public function get_request_data() {
211 $data = $_POST;
212
213 if ( ! empty( $_FILES ) ) {
214 foreach ( $_FILES as $field_name => $file ) {
215 // only add non-empty files so that required field validation works as expected
216 // upload could still have errored at this point
217 if ( $file['error'] !== UPLOAD_ERR_NO_FILE ) {
218 $data[ $field_name ] = $file;
219 }
220 }
221 }
222
223 return $data;
224 }
225
226 public function listen_for_submit() {
227 // only respond to AJAX requests with _hf_form_id set.
228 if ( empty( $_POST['_hf_form_id'] )
229 || empty( $_SERVER['HTTP_X_REQUESTED_WITH'] )
230 || strtolower( $_SERVER['HTTP_X_REQUESTED_WITH'] ) !== strtolower( 'XMLHttpRequest' ) ) {
231 return;
232 }
233
234 $data = $this->get_request_data();
235 $form_id = (int) $data['_hf_form_id'];
236 $form = hf_get_form( $form_id );
237 $error_code = $this->validate_form( $form, $data );
238 $submission = null;
239
240 if ( empty( $error_code ) ) {
241 /**
242 * Filters the field names that should be ignored on the Submission object.
243 * Fields starting with an underscore (_) are ignored by default.
244 *
245 * @param array $names
246 */
247 $ignored_field_names = apply_filters( 'hf_ignored_field_names', array() );
248
249 // filter out ignored field names
250 foreach ( $data as $key => $value ) {
251 if ( $key[0] === '_' || in_array( $key, $ignored_field_names ) ) {
252 unset( $data[ $key ] );
253 continue;
254 }
255
256 // this detects the WPBruiser token field to ensure it isn't stored
257 // CAVEAT: this will detect any non-uppercase string with 2 dashes in the field name and no whitespace in the field value
258 if ( class_exists( 'GoodByeCaptcha' ) && is_string( $key ) && is_string( $value ) && strtoupper( $key ) !== $key && substr_count( $key, '-' ) >= 2 && substr_count( trim( $value ), ' ' ) === 0 ) {
259 unset( $data[ $key ] );
260 continue;
261 }
262 }
263
264 // sanitize data: strip tags etc.
265 $data = $this->sanitize( $data );
266
267 // save form submission
268 $submission = new Submission();
269 $submission->form_id = $form_id;
270 $submission->data = $data;
271 $submission->ip_address = ! empty( $_SERVER['REMOTE_ADDR'] ) ? sanitize_text_field( $_SERVER['REMOTE_ADDR'] ) : '';
272 $submission->user_agent = ! empty( $_SERVER['HTTP_USER_AGENT'] ) ? sanitize_text_field( $_SERVER['HTTP_USER_AGENT'] ) : '';
273 $submission->referer_url = ! empty( $_SERVER['HTTP_REFERER'] ) ? sanitize_url( $_SERVER['HTTP_REFERER'] ) : '';
274 $submission->submitted_at = gmdate( 'Y-m-d H:i:s' );
275
276 // save submission object so that other form processor have an insert ID to work with (eg file upload)
277 if ( $form->settings['save_submissions'] ) {
278 $submission->save();
279 }
280
281 /**
282 * General purpose hook that runs before all form actions, so we can still modify the submission object that is passed to actions.
283 */
284 do_action( 'hf_process_form', $form, $submission );
285
286 // re-save submission object for convenience in form processors hooked into hf_process_form
287 if ( $form->settings['save_submissions'] ) {
288 $submission->save();
289 }
290
291 // process form actions
292 if ( isset( $form->settings['actions'] ) ) {
293 foreach ( $form->settings['actions'] as $action_settings ) {
294 /**
295 * Processes the specified form action and passes related data.
296 *
297 * @param array $action_settings
298 * @param Submission $submission
299 * @param Form $form
300 */
301 do_action( 'hf_process_form_action_' . $action_settings['type'], $action_settings, $submission, $form );
302 }
303 }
304
305 /**
306 * General purpose hook after all form actions have been processed for this specific form. The dynamic portion of the hook refers to the form slug.
307 *
308 * @param Submission $submission
309 * @param Form $form
310 */
311 do_action( "hf_form_{$form->slug}_success", $submission, $form );
312
313 /**
314 * General purpose hook after all form actions have been processed.
315 *
316 * @param Submission $submission
317 * @param Form $form
318 */
319 do_action( 'hf_form_success', $submission, $form );
320 } else {
321 /**
322 * General purpose hook for when a form error occurred
323 *
324 * @param string $error_code
325 * @param Form $form
326 * @param array $data
327 */
328 do_action( 'hf_form_error', $error_code, $form, $data );
329 }
330
331 // Delay response until "wp_loaded" hook to give other plugins a chance to process stuff.
332 add_action(
333 'wp_loaded',
334 function() use ( $error_code, $form, $data, $submission ) {
335 $response = $this->get_response_for_error_code( $error_code, $form, $data, $submission );
336
337 // clear output, some plugin or hooked code might have thrown errors by now.
338 if ( ob_get_level() > 0 ) {
339 ob_end_clean();
340 }
341
342 send_origin_headers();
343 send_nosniff_header();
344 nocache_headers();
345
346 wp_send_json( $response, 200 );
347 exit;
348 }
349 );
350 }
351
352 public function listen_for_preview() {
353 if ( empty( $_GET['hf_preview_form'] ) || ! current_user_can( 'edit_forms' ) ) {
354 return;
355 }
356
357 try {
358 $form = hf_get_form( $_GET['hf_preview_form'] );
359 } catch ( \Exception $e ) {
360 return;
361 }
362
363 show_admin_bar( false );
364 add_filter( 'pre_handle_404', '__return_true' );
365 remove_all_actions( 'template_redirect' );
366 add_action(
367 'template_redirect',
368 function() use ( $form ) {
369 // clear output, some plugin or hooked code might have thrown errors by now.
370 if ( ob_get_level() > 0 ) {
371 ob_end_clean();
372 }
373
374 status_header( 200 );
375 require dirname( $this->plugin_file ) . '/views/form-preview.php';
376 exit;
377 }
378 );
379 }
380
381 private function get_response_for_error_code( $error_code, Form $form, $data = array(), Submission $submission = null ) {
382 // return success response for empty error code string or spam (to trick bots)
383 if ( $error_code === '' || $error_code === 'spam' ) {
384 $response = array(
385 'message' => array(
386 'type' => 'success',
387 'text' => $form->get_message( 'success' ),
388 ),
389 'hide_form' => (bool) $form->settings['hide_after_success'],
390 );
391
392 if ( ! empty( $form->settings['redirect_url'] ) && $submission !== null ) {
393 $response['redirect_url'] = hf_replace_data_variables( $form->settings['redirect_url'], $submission, 'urlencode' );
394 }
395
396 return apply_filters( 'hf_form_response', $response, $form, $data );
397 }
398
399 // get error message
400 $message = $form->get_message( $error_code );
401 if ( empty( $message ) ) {
402 $message = $form->get_message( 'error' );
403 }
404
405 // return error response
406 return array(
407 'message' => array(
408 'type' => 'warning',
409 'text' => $message,
410 ),
411 'error' => $error_code,
412 );
413 }
414
415 public function shortcode( $attributes = array(), $content = '' ) {
416 if ( empty( $attributes['slug'] ) && empty( $attributes['id'] ) ) {
417 return '';
418 }
419
420 $slug_or_id = esc_attr( empty( $attributes['id'] ) ? $attributes['slug'] : $attributes['id'] );
421 try {
422 $form = hf_get_form( $slug_or_id );
423 } catch ( \Exception $e ) {
424 if ( ! current_user_can( 'manage_options' ) ) {
425 return $content;
426 }
427
428 return sprintf( '<p><strong>%s</strong> %s</p>', __( 'Error:', 'html-forms' ), sprintf( __( 'No form found with slug %s', 'html-forms' ), esc_attr( $attributes['slug'] ) ) );
429 }
430
431 return $form . $content;
432 }
433 }
434