PluginProbe
Advanced Forms for ACF / trunk
Advanced Forms for ACF vtrunk
1.9.0 1.9.1 1.9.2.1 1.9.3 1.9.3.1 1.9.3.2 1.9.3.3 1.9.3.4 1.9.3.5 1.9.3.6 1.9.3.7 1.9.3.8 trunk 1.0.0 1.0.1 1.0.2 1.0.3 1.0.3.1 1.0.3.2 1.0.3.3 1.0.4 1.1.0 1.1.1 1.2.0 1.3.0 All 51 releases
advanced-forms / core / forms / forms-submissions.php

forms-submissions.php in Advanced Forms for ACF trunk, at core/forms/forms-submissions.php

485 lines 13.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * Handles validation, and saving of forms
5 * Refactored out of core-forms.php since 1.5.0.
6 *
7 * @since 1.0.0
8 *
9 */
10 class AF_Core_Forms_Submissions {
11
12 const DEFAULT_COOKIE_NAME = 'af_submission';
13
14 const OPTION_EXPIRY_MINUTES = 5;
15 const OPTION_DATA_PREFIX = 'af_submission_data_';
16 const OPTION_EXPIRY_PREFIX = 'af_submission_expiry_';
17
18 function __construct() {
19 add_action( 'wp_ajax_af_submission', array( $this, 'ajax_submission' ), 10, 0 );
20 add_action( 'wp_ajax_nopriv_af_submission', array( $this, 'ajax_submission' ), 10, 0 );
21 add_action( 'init', array( $this, 'pre_form' ), 10, 0 );
22 add_action( 'acf/validate_save_post', array( $this, 'validate' ), 10, 0 );
23 add_filter( 'acf/upload_prefilter', array( $this, 'intercept_upload_errors' ), 1000, 3 );
24 }
25
26 function ajax_submission() {
27 // Make sure honeypot field is empty if one exists
28 if ( ! $this->is_honeypot_valid() ) {
29 wp_send_json_error( array(
30 'errors' => array(
31 array( 'message' => 'Non-human user detected' ),
32 ),
33 ), 400 );
34 wp_die();
35 }
36
37 // Validate the posted data. This validation has already been performed once over AJAX.
38 if ( ! acf_validate_save_post() ) {
39 wp_send_json_error( array(
40 'errors' => array(
41 array( 'message' => 'Validation failed' ),
42 ),
43 ), 400 );
44 wp_die();
45 }
46
47 $form = AF()->submission['form'];
48 $args = AF()->submission['args'];
49 $fields = AF()->submission['fields'];
50
51 // Process submission. If it fails we return all errors.
52 if ( ! $this->process_submission( AF()->submission ) ) {
53 $errors = array();
54 foreach ( AF()->submission['errors'] as $message ) {
55 $errors[] = array(
56 'message' => $message,
57 );
58 }
59
60 wp_send_json_error( array(
61 'errors' => $errors,
62 ), 400 );
63 wp_die();
64 }
65
66 $response = array(
67 'type' => 'none',
68 );
69
70 // Redirect to different URL if redirect argument has been passed
71 $redirect_url = $args['redirect'];
72 if ( ! empty( $redirect_url ) ) {
73 $response = array(
74 'type' => 'redirect',
75 'redirect_url' => $redirect_url,
76 );
77 } else if ( ! empty( $args['filter_mode'] ) ) {
78 // Do nothing and let the response type be "none".
79 // Filter mode is equivalent to changing nothing after submission.
80 } else {
81 $success_message = af_form_success_message( $form, $args );
82 $response = array(
83 'type' => 'success_message',
84 'success_message' => $success_message,
85 );
86 }
87
88 $response = apply_filters( 'af/form/ajax/response', $response, $form, $args );
89 $response = apply_filters( 'af/form/ajax/response/post=' . $form['post_id'], $response, $form, $args );
90 $response = apply_filters( 'af/form/ajax/response/key=' . $form['key'], $response, $form, $args );
91
92 wp_send_json_success( $response );
93 wp_die();
94 }
95
96 /**
97 * Handles submissions and enqueue of neccessary scripts
98 * Relies on default ACF validations
99 *
100 * @since 1.0.0
101 *
102 */
103 function pre_form() {
104 // Make sure this is not an AJAX validation request
105 if ( isset ( $_POST['action'] ) ) {
106 return;
107 }
108
109 // Make sure honeypot field is empty if one exists
110 if ( ! $this->is_honeypot_valid() ) {
111 wp_die( 'Non-human user detected' );
112 exit;
113 }
114
115 // Try loading submission data
116 if ( ! $this->load_submission_data() ) {
117 return;
118 }
119
120 // Validate the posted data, this validation has already been performed once over AJAX
121 if ( ! acf_validate_save_post( true ) ) {
122 return;
123 }
124
125 $this->process_submission( AF()->submission );
126 self::handle_submission_done( AF()->submission );
127 }
128
129 static function handle_submission_done( $submission ) {
130 // Redirect to different URL if redirect argument has been passed
131 $redirect_url = $submission['args']['redirect'];
132
133 // By default the user is redirected back to the form page.
134 // Some browsers will prompt to submit the form again if the form page is reloaded.
135 // Redirecting back removes the risk of duplicate submissions.
136 if ( null === $redirect_url ) {
137 $redirect_url = $submission['origin_url'];
138 }
139
140 if ( $redirect_url && '' !== $redirect_url ) {
141 self::clear_expired_submissions();
142 self::save_submission( $submission );
143
144 wp_redirect( $redirect_url );
145 exit;
146 }
147 }
148
149 /**
150 * Check that the honeypot has not been filled.
151 *
152 * @since 1.7.2
153 */
154 function is_honeypot_valid() {
155 if ( isset( $_POST['email_for_non_humans'] ) && ! empty( $_POST['email_for_non_humans'] ) ) {
156 return false;
157 }
158
159 return true;
160 }
161
162 /**
163 * Process a form submission.
164 *
165 * @since 1.7.2
166 *
167 */
168 function process_submission( $submission ) {
169 $form = $submission['form'];
170 $args = $submission['args'];
171 $fields = $submission['fields'];
172
173 // Increase the form submissions counter
174 if ( $form['post_id'] ) {
175 $submissions = get_post_meta( $form['post_id'], 'form_num_of_submissions', true );
176 $submissions = $submissions ? $submissions + 1 : 1;
177 update_post_meta( $form['post_id'], 'form_num_of_submissions', $submissions );
178 }
179
180 do_action( 'af/form/before_submission', $form, $fields, $args );
181 do_action( 'af/form/before_submission/id=' . $form['post_id'], $form, $fields, $args );
182 do_action( 'af/form/before_submission/key=' . $form['key'], $form, $fields, $args );
183
184 if ( af_submission_failed() ) {
185 return false;
186 }
187
188 self::call_submission_handlers( $submission );
189
190 return true;
191 }
192
193 static function call_submission_handlers( $submission ) {
194 $form = $submission['form'];
195 $args = $submission['args'];
196 $fields = $submission['fields'];
197
198 do_action( 'af/form/submission', $form, $fields, $args );
199 do_action( 'af/form/submission/id=' . $form['post_id'], $form, $fields, $args );
200 do_action( 'af/form/submission/key=' . $form['key'], $form, $fields, $args );
201 }
202
203 /**
204 * Handles validation of a form.
205 * Adds custom validation actions specific to forms.
206 *
207 * @since 1.5.0
208 *
209 */
210 function validate() {
211 // Try loading submission data
212 if ( ! $this->load_submission_data() ) {
213 return;
214 }
215
216 $form = AF()->submission['form'];
217 $args = AF()->submission['args'];
218
219 do_action( 'af/form/validate', $form, $args );
220 do_action( 'af/form/validate/id=' . $form['post_id'], $form, $args );
221 do_action( 'af/form/validate/key=' . $form['key'], $form, $args );
222 }
223
224 /**
225 * Populate AF()->submission with submission data
226 * Returns boolean indicating whether a submission was loaded
227 *
228 * @since 1.5.0
229 *
230 */
231 function load_submission_data() {
232 // Check if there is a cookie-passed submission
233 if ( $submission = $this->get_submission() ) {
234 AF()->submission = $submission;
235
236 // Return false to stop the submission from being processed again
237 return false;
238 }
239
240 // Make sure a form was posted
241 if ( ! ( isset( $_POST['af_form'] ) ) ) {
242 return false;
243 }
244
245 // Bail early if already loaded
246 if ( AF()->submission ) {
247 return true;
248 }
249
250 /**
251 * Upload all files in $_FILES using ACFs helper function. Required for basic uploads to work painlessly.
252 * TODO: Move to af_save_field() to avoid saving all files?
253 *
254 * @since 1.3.1
255 *
256 */
257 if ( isset( $_FILES['acf'] ) ) {
258 $this->clear_upload_errors();
259 acf_upload_files();
260 $this->handle_upload_errors();
261 }
262
263 // Generate submission from data
264 $submission = $this->create_submission();
265 if ( ! $submission ) {
266 return false;
267 }
268
269 // Save submission data to the global AF object
270 AF()->submission = $submission;
271
272 return true;
273 }
274
275 /**
276 * Create a submission object from the request data.
277 * Returns a submission array or false on failure.
278 *
279 * @since 1.6.0
280 *
281 */
282 function create_submission() {
283 // Load form by key
284 $form_key_or_id = $_POST['af_form'];
285
286 $form = af_get_form( $form_key_or_id );
287 if ( ! $form ) {
288 return false;
289 }
290
291 // Retrieve the args used to display the form
292 $encoded_args = $_POST['af_form_args'];
293 $args = json_decode( base64_decode( $encoded_args ), true );
294
295 // Verify nonce
296 $nonce = $_POST['af_form_nonce'];
297 $hashed_args = hash( 'sha256', $encoded_args );
298 $nonce_value = sprintf( 'af_submission_%s_%s', $form['key'], $hashed_args );
299 if ( ! wp_verify_nonce( $nonce, $nonce_value ) ) {
300 wp_die( 'Your submission failed. Please reload the page and try again.' );
301 exit;
302 }
303
304 // Retrieve all form fields and load their submitted values onto the field array.
305 $fields = [];
306 if ( isset( $_POST['acf'] ) ) {
307 foreach ( $_POST['acf'] as $k => $value ) {
308 $field = acf_get_field( $k );
309 if ( empty( $field ) ) {
310 continue;
311 }
312
313 /**
314 * Filter the raw submitted value before it is formatted by ACF.
315 *
316 * This is useful for modifying the value submitted by the user. Note that this filter runs before
317 * validation so it will affect any custom validation rules you have set up using the af/form/validate
318 * filters.
319 *
320 * @since 1.9.3.4
321 */
322 $value = apply_filters( 'af/form/submission/value', $value, $field, $form, $args );
323
324 // Set the raw submitted value under the `_input` key.
325 $field['_input'] = $value;
326
327 // Format the value and set the formatted value under the `value` key.
328 $field['value'] = acf_format_value( $value, 0, $field );
329
330 $fields[] = $field;
331 }
332 }
333
334 return array(
335 'form' => $form,
336 'args' => $args,
337 'fields' => $fields,
338 'errors' => array(),
339 'origin_url' => $_POST['af_origin_url'],
340 );
341 }
342
343 /**
344 * Fetch a submission from options if the submission cookie is set.
345 * Will return false if the cookie is not set or the submission does not exist in the database.
346 *
347 * @since 1.6.6
348 *
349 */
350 private function get_submission() {
351 if ( ! isset( $_COOKIE[ self::get_cookie_name() ] ) ) {
352 return false;
353 }
354
355 $key = $_COOKIE[ self::get_cookie_name() ];
356 $submission = get_option( self::OPTION_DATA_PREFIX . $key, false );
357
358 self::delete_submission( $key );
359 setcookie( self::get_cookie_name(), '', time() - HOUR_IN_SECONDS, '/' );
360
361 return $submission;
362 }
363
364 /**
365 * Save a submission to options and set a cookie with a reference to it.
366 * Submissions are identified by a randomly generated key stored in a cookie.
367 *
368 * @since 1.6.6
369 *
370 */
371 private static function save_submission( $submission ) {
372 $key = wp_generate_password( 12, false, false );
373
374 $expiration_time = time() + self::OPTION_EXPIRY_MINUTES * MINUTE_IN_SECONDS;
375
376 add_option( self::OPTION_DATA_PREFIX . $key, $submission );
377 add_option( self::OPTION_EXPIRY_PREFIX . $key, $expiration_time );
378
379 setcookie( self::get_cookie_name(), $key, $expiration_time, '/' );
380 }
381
382 /**
383 * Delete a submission from options based on key
384 *
385 * @since 1.6.6
386 *
387 */
388 private static function delete_submission( $key ) {
389 delete_option( self::OPTION_DATA_PREFIX . $key );
390 delete_option( self::OPTION_EXPIRY_PREFIX . $key );
391 }
392
393 private static function get_cookie_name() {
394 return apply_filters( 'af/settings/cookie_name', self::DEFAULT_COOKIE_NAME );
395 }
396
397 /**
398 * Remove any expired submission from options which have not been cleared automatically.
399 * If a request fails a created submission could potentially not be removed from the database.
400 *
401 * @since 1.6.6
402 *
403 */
404 private static function clear_expired_submissions() {
405 global $wpdb;
406
407 $options_table = $wpdb->prefix . 'options';
408 $name_pattern = self::OPTION_EXPIRY_PREFIX . '%';
409 $current_time = time();
410
411 // Find all expired submissions in the options table.
412 // This query is very efficient because of the index on the name column.
413 $expired_submissions = $wpdb->get_col( "
414 SELECT option_name
415 FROM $options_table
416 WHERE option_name LIKE '$name_pattern'
417 AND option_value < $current_time
418 " );
419
420 foreach ( $expired_submissions as $option_name ) {
421 // Find submission key by removing prefix from option name.
422 $submission_key = substr( $option_name, strlen( self::OPTION_EXPIRY_PREFIX ) );
423 self::delete_submission( $submission_key );
424 }
425 }
426
427 /**
428 * ACF doesn't provide a simple way of catching upload errors when using the basic uploader.
429 * This function is hooked into the "acf/upload_prefilter" with a high priority.
430 * It will intercept all upload errors and save them together with field data.
431 *
432 * @since 1.7.0
433 *
434 */
435 function intercept_upload_errors( $errors, $file, $field ) {
436 if ( ! empty( $errors ) ) {
437 $this->upload_errors[ $field['key'] ] = array(
438 'field' => $field,
439 'messages' => $errors,
440 );
441 }
442
443 return $errors;
444 }
445
446 /**
447 * Removes all intercepted upload errors.
448 * Should be run before handling uploads using "acf_upload_files()".
449 *
450 * @since 1.7.0
451 *
452 */
453 private function clear_upload_errors() {
454 $this->upload_errors = array();
455 }
456
457 /**
458 * Checks if any upload errors have been caught and stops the submission.
459 * This is a very rudimentary way of handling upload errors but it's necessary as ACF can't handle errors when using the basic uploader.
460 * The errors checks should in the future be implemented client-side for a good user experience and this is mostly meant to be a fallback.
461 *
462 *
463 * @since 1.7.0
464 *
465 */
466 private function handle_upload_errors() {
467 if ( empty( $this->upload_errors ) ) {
468 return;
469 }
470
471 $message = sprintf( '<h2>%s</h2>', __( 'Validation failed', 'acf' ) );
472 $message .= '<ul>';
473 foreach ( $this->upload_errors as $error ) {
474 $field = $error['field'];
475 foreach ( $error['messages'] as $error_message ) {
476 $message .= '<li>' . sprintf( '%s: %s', $field['label'], $error_message ) . '</li>';
477 }
478 }
479 $message .= '</ul>';
480
481 wp_die( $message, __( 'Validation failed', 'acf' ) );
482 }
483 }
484
485 return new AF_Core_Forms_Submissions();