PluginProbe
HTML Forms – Simple WordPress Forms Plugin / trunk
HTML Forms – Simple WordPress Forms Plugin vtrunk
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 trunk, at src/class-forms.php

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