PluginProbe ʕ •ᴥ•ʔ
Advanced Custom Fields (ACF®) / 6.8.9
Advanced Custom Fields (ACF®) v6.8.9
6.8.9 6.8.8 6.8.7 6.8.6 6.8.5 6.8.4 6.8.3 6.8.2 6.8.1 5.8.5 5.8.6 5.8.7 5.8.8 5.8.9 5.9.0 5.9.1 5.9.2 5.9.3 5.9.4 5.9.5 5.9.6 5.9.7 5.9.8 5.9.9 6.0.0 6.0.1 6.0.2 6.0.3 6.0.4 6.0.5 6.0.6 6.0.7 6.1.0 6.1.1 6.1.2 6.1.3 6.1.4 6.1.5 6.1.6 6.1.7 6.1.8 6.2.0 6.2.1 6.2.2 6.2.3 6.2.4 6.2.5 6.2.6 6.2.6.1 6.2.7 6.2.8 6.2.9 6.3.0 6.3.1 6.3.10.2 6.3.11 6.3.12 6.3.2 6.3.3 6.3.4 6.3.5 6.3.6 6.3.6.1 6.4.0 6.4.0.1 6.4.1 6.4.2 6.4.3 6.5.0 6.5.1 6.6.0 6.6.1 6.6.2 6.7.0 6.7.1 6.7.2 6.8.0 trunk 1.0.0 1.0.2 1.0.3 1.0.5 1.1.0 1.1.1 1.1.2 1.1.3 1.1.4 2.0.0 2.0.1 2.0.2 2.0.3 2.0.4 2.0.5 2.1.1 2.1.3 2.1.4 3.0.0 3.0.1 3.0.2 3.0.3 3.0.4 3.0.6 3.0.7 3.1.0 3.1.1 3.1.2 3.1.3 3.1.4 3.1.5 3.1.6 3.1.7 3.1.8 3.1.9 3.2.0 3.2.2 3.2.3 3.2.4 3.2.5 3.2.6 3.2.7 3.2.8 3.2.9 3.3.0 3.3.1 3.3.2 3.3.3 3.3.4 3.3.5 3.3.6 3.3.7 3.3.8 3.3.9 3.4.0 3.4.1 3.4.2 3.4.3 3.5.0 3.5.1 3.5.2 3.5.3 3.5.4 3.5.5 3.5.6 3.5.7 3.5.8 4.0.0 4.0.1 4.0.2 4.0.3 4.1.0 4.1.1 4.1.2 4.1.3 4.1.4 4.1.5 4.1.6 4.1.8 4.2.0 4.2.1 4.2.2 4.3.0 4.3.1 4.3.2 4.3.3 4.3.4 4.3.5 4.3.6 4.3.7 4.3.8 4.3.9 4.4.0 4.4.1 4.4.10 4.4.11 4.4.12 4.4.2 4.4.3 4.4.4 4.4.5 4.4.6 4.4.7 4.4.8 4.4.9 5.10 5.10.1 5.10.2 5.11 5.11.1 5.11.2 5.11.3 5.11.4 5.12 5.12.1 5.12.2 5.12.3 5.12.4 5.12.5 5.12.6 5.6.10 5.6.2 5.6.3 5.6.4 5.6.5 5.6.6 5.6.7 5.6.8 5.6.9 5.7.0 5.7.1 5.7.10 5.7.12 5.7.13 5.7.2 5.7.3 5.7.4 5.7.5 5.7.6 5.7.7 5.7.8 5.7.9 5.8.0 5.8.1 5.8.10 5.8.11 5.8.12 5.8.13 5.8.14 5.8.2 5.8.3 5.8.4
advanced-custom-fields / includes / forms / form-front.php
advanced-custom-fields / includes / forms Last commit date
form-attachment.php 5 months ago form-comment.php 4 weeks ago form-customizer.php 5 months ago form-front.php 2 months ago form-gutenberg.php 5 months ago form-nav-menu.php 5 months ago form-post.php 3 months ago form-taxonomy.php 2 months ago form-user.php 4 weeks ago form-widget.php 5 months ago index.php 2 years ago
form-front.php
884 lines
1 <?php
2 /**
3 * @package ACF
4 * @author WP Engine
5 *
6 * © 2026 Advanced Custom Fields (ACF®). All rights reserved.
7 * "ACF" is a trademark of WP Engine.
8 * Licensed under the GNU General Public License v2 or later.
9 * https://www.gnu.org/licenses/gpl-2.0.html
10 */
11
12 if ( ! defined( 'ABSPATH' ) ) {
13 exit; // Exit if accessed directly
14 }
15
16 if ( ! class_exists( 'acf_form_front' ) ) :
17 class acf_form_front {
18
19 /**
20 * An array of registered form settings.
21 * @var array
22 */
23 private $forms = array();
24
25 /**
26 * An array of default fields.
27 * @var array
28 */
29 public $fields = array();
30
31 /**
32 * Per-request render id, shared across every render_form() call in this request.
33 *
34 * @var string|null
35 */
36 private $render_id = null;
37
38 /**
39 * Constructs the class.
40 *
41 * @since 5.0.0
42 */
43 public function __construct() {
44 add_action( 'acf/validate_save_post', array( $this, 'validate_save_post' ), 1 );
45 add_filter( 'acf/pre_save_post', array( $this, 'pre_save_post' ), 5, 2 );
46 }
47
48 /**
49 * Returns fields used by frontend forms.
50 *
51 * @since 6.4
52 *
53 * @return array
54 */
55 public function get_default_fields(): array {
56 $this->fields = array(
57 '_post_title' => array(
58 'prefix' => 'acf',
59 'name' => '_post_title',
60 'key' => '_post_title',
61 'label' => __( 'Title', 'acf' ),
62 'type' => 'text',
63 'required' => true,
64 ),
65
66 '_post_content' => array(
67 'prefix' => 'acf',
68 'name' => '_post_content',
69 'key' => '_post_content',
70 'label' => __( 'Content', 'acf' ),
71 'type' => 'wysiwyg',
72 ),
73
74 '_validate_email' => array(
75 'prefix' => 'acf',
76 'name' => '_validate_email',
77 'key' => '_validate_email',
78 'label' => __( 'Validate Email', 'acf' ),
79 'type' => 'text',
80 'value' => '',
81 'wrapper' => array( 'style' => 'display:none !important;' ),
82 ),
83 );
84
85 return $this->fields;
86 }
87
88 /**
89 * description
90 *
91 * @type function
92 * @date 28/2/17
93 * @since 5.5.8
94 *
95 * @param $post_id (int)
96 * @return $post_id (int)
97 */
98 function validate_form( $args ) {
99
100 // defaults
101 // Todo: Allow message and button text to be generated by CPT settings.
102 $args = wp_parse_args(
103 $args,
104 array(
105 'id' => 'acf-form',
106 'post_id' => false,
107 'new_post' => false,
108 'field_groups' => false,
109 'fields' => false,
110 'post_title' => false,
111 'post_content' => false,
112 'form' => true,
113 'form_attributes' => array(),
114 'return' => add_query_arg( 'updated', 'true', acf_get_current_url() ),
115 'html_before_fields' => '',
116 'html_after_fields' => '',
117 'submit_value' => __( 'Update', 'acf' ),
118 'updated_message' => __( 'Post updated', 'acf' ),
119 'label_placement' => 'top',
120 'instruction_placement' => 'label',
121 'field_el' => 'div',
122 'uploader' => 'wp',
123 'honeypot' => true,
124 'html_updated_message' => '<div id="message" class="updated"><p>%s</p></div>', // 5.5.10
125 'html_submit_button' => '<input type="submit" class="acf-button button button-primary button-large" value="%s" />', // 5.5.10
126 'html_submit_spinner' => '<span class="acf-spinner"></span>', // 5.5.10
127 'kses' => true, // 5.6.5
128 )
129 );
130
131 $args['form_attributes'] = wp_parse_args(
132 $args['form_attributes'],
133 array(
134 'id' => $args['id'],
135 'class' => 'acf-form',
136 'action' => '',
137 'method' => 'post',
138 )
139 );
140
141 // filter post_id
142 $args['post_id'] = acf_get_valid_post_id( $args['post_id'] );
143
144 // new post?
145 if ( $args['post_id'] === 'new_post' ) {
146 $args['new_post'] = wp_parse_args(
147 $args['new_post'],
148 array(
149 'post_type' => 'post',
150 'post_status' => 'draft',
151 )
152 );
153 }
154
155 // filter
156 $args = apply_filters( 'acf/validate_form', $args );
157
158 // return
159 return $args;
160 }
161
162
163 /**
164 * description
165 *
166 * @type function
167 * @date 28/2/17
168 * @since 5.5.8
169 *
170 * @param $post_id (int)
171 * @return $post_id (int)
172 */
173 function add_form( $args = array() ) {
174
175 // validate
176 $args = $this->validate_form( $args );
177
178 // append
179 $this->forms[ $args['id'] ] = $args;
180 }
181
182
183 /**
184 * description
185 *
186 * @type function
187 * @date 28/2/17
188 * @since 5.5.8
189 *
190 * @param $post_id (int)
191 * @return $post_id (int)
192 */
193 function get_form( $id = '' ) {
194
195 // bail early if not set
196 if ( ! isset( $this->forms[ $id ] ) ) {
197 return false;
198 }
199
200 // return
201 return $this->forms[ $id ];
202 }
203
204 function get_forms() {
205 return $this->forms;
206 }
207
208 /**
209 * This function will validate fields from the above array
210 *
211 * @type function
212 * @date 7/09/2016
213 * @since 5.4.0
214 *
215 * @param $post_id (int)
216 * @return $post_id (int)
217 */
218 function validate_save_post() {
219
220 // register field if isset in $_POST
221 foreach ( $this->get_default_fields() as $k => $field ) {
222
223 // bail early if no in $_POST
224 if ( ! isset( $_POST['acf'][ $k ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Verified elsewhere.
225 continue;
226 }
227
228 // register
229 acf_add_local_field( $field );
230 }
231
232 // honeypot
233 if ( ! empty( $_POST['acf']['_validate_email'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Data not used; presence indicates spam.
234
235 acf_add_validation_error( '', __( 'Spam Detected', 'acf' ) );
236 }
237 }
238
239
240 /**
241 * description
242 *
243 * @type function
244 * @date 7/09/2016
245 * @since 5.4.0
246 *
247 * @param $post_id (int)
248 * @return $post_id (int)
249 */
250 function pre_save_post( $post_id, $form ) {
251
252 // vars
253 $save = array(
254 'ID' => 0,
255 );
256
257 // determine save data
258 if ( is_numeric( $post_id ) ) {
259
260 // update post
261 $save['ID'] = $post_id;
262 } elseif ( $post_id == 'new_post' ) {
263
264 // merge in new post data
265 $save = array_merge( $save, $form['new_post'] );
266 } else {
267
268 // not post
269 return $post_id;
270 }
271
272 // phpcs:disable WordPress.Security.NonceVerification.Missing -- Verified in check_submit_form().
273 // Always extract the special _post_title / _post_content fields from $_POST['acf'] so they
274 // cannot leak into acf_update_values() downstream, but only apply them to the post when the
275 // form was rendered with the corresponding option enabled (mirrors render_form()).
276 if ( isset( $_POST['acf']['_post_title'] ) ) {
277 $post_title = acf_extract_var( $_POST['acf'], '_post_title' ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitized by WP when saved.
278 if ( ! empty( $form['post_title'] ) ) {
279 $save['post_title'] = $post_title;
280 }
281 }
282
283 if ( isset( $_POST['acf']['_post_content'] ) ) {
284 $post_content = acf_extract_var( $_POST['acf'], '_post_content' ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitized by WP when saved.
285 if ( ! empty( $form['post_content'] ) ) {
286 $save['post_content'] = $post_content;
287 }
288 }
289 // phpcs:enable WordPress.Security.NonceVerification.Missing
290
291 // honeypot
292 if ( ! empty( $_POST['acf']['_validate_email'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Data not used; presence indicates spam.
293 return false;
294 }
295
296 // validate
297 if ( count( $save ) == 1 ) {
298 return $post_id;
299 }
300
301 // save
302 if ( $save['ID'] ) {
303 wp_update_post( $save );
304 } else {
305 $post_id = wp_insert_post( $save );
306 }
307
308 // return
309 return $post_id;
310 }
311
312
313 /**
314 * This function will enqueue a form
315 *
316 * @type function
317 * @date 7/09/2016
318 * @since 5.4.0
319 *
320 * @param $post_id (int)
321 * @return $post_id (int)
322 */
323 function enqueue_form() {
324
325 // check
326 $this->check_submit_form();
327
328 // load acf scripts
329 acf_enqueue_scripts();
330 }
331
332
333 /**
334 * This function will maybe submit form data
335 *
336 * @type function
337 * @date 3/3/17
338 * @since 5.5.10
339 *
340 * @param n/a
341 * @return n/a
342 */
343 function check_submit_form() {
344
345 // Verify nonce.
346 if ( ! acf_verify_nonce( 'acf_form' ) ) {
347 return false;
348 }
349
350 // Confirm form was submit.
351 if ( ! isset( $_POST['_acf_form'] ) ) {
352 return false;
353 }
354
355 // Load registered form using id.
356 $form = $this->get_form( acf_sanitize_request_args( $_POST['_acf_form'] ) );
357
358 // Fallback to encrypted JSON.
359 if ( ! $form ) {
360 $form = json_decode( acf_decrypt( sanitize_text_field( $_POST['_acf_form'] ) ), true );
361 if ( ! $form ) {
362 return false;
363 }
364 }
365
366 $form = $this->merge_form_meta( $form );
367
368 // Run kses on all $_POST data.
369 if ( $form['kses'] && isset( $_POST['acf'] ) ) {
370 $_POST['acf'] = wp_kses_post_deep( $_POST['acf'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- False positive.
371 }
372
373 // Validate data and show errors.
374 // Todo: Return WP_Error and show above form, keeping input values.
375 acf_validate_save_post( true );
376
377 // Submit form.
378 $this->submit_form( $form );
379 }
380
381
382 /**
383 * This function will submit form data
384 *
385 * @type function
386 * @date 3/3/17
387 * @since 5.5.10
388 *
389 * @param n/a
390 * @return n/a
391 */
392 function submit_form( $form ) {
393
394 // filter
395 $form = apply_filters( 'acf/pre_submit_form', $form );
396
397 // vars
398 $post_id = acf_maybe_get( $form, 'post_id', 0 );
399
400 // add global for backwards compatibility
401 $GLOBALS['acf_form'] = $form;
402
403 // allow for custom save
404 $post_id = apply_filters( 'acf/pre_save_post', $post_id, $form );
405
406 // Restrict $_POST['acf'] to the field keys the form actually exposed, so the
407 // save path cannot accept values for fields the form did not render.
408 // phpcs:disable WordPress.Security.NonceVerification.Missing -- Verified in check_submit_form().
409 if ( isset( $_POST['acf'] ) && is_array( $_POST['acf'] ) ) {
410 $allowed_keys = $this->get_allowed_field_keys( $form );
411 $_POST['acf'] = array_intersect_key( $_POST['acf'], array_flip( $allowed_keys ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- Sanitized downstream; save pipeline expects slashed input.
412 }
413 // phpcs:enable WordPress.Security.NonceVerification.Missing
414
415 // save
416 acf_save_post( $post_id );
417
418 // restore form (potentially modified)
419 $form = $GLOBALS['acf_form'];
420
421 // action
422 do_action( 'acf/submit_form', $form, $post_id );
423
424 // vars
425 $return = acf_maybe_get( $form, 'return', '' );
426
427 // redirect
428 if ( $return ) {
429
430 // update %placeholders%
431 $return = str_replace( '%post_id%', $post_id, $return );
432 $return = str_replace( '%post_url%', get_permalink( $post_id ), $return );
433
434 // redirect
435 wp_redirect( $return ); //phpcs:ignore WordPress.Security.SafeRedirect.wp_redirect_wp_redirect -- unsafe redirects allowed.
436 exit;
437 }
438 }
439
440 /**
441 * Returns the per-request render ID, generating one if necessary.
442 *
443 * @since 6.8.4
444 *
445 * @return string
446 */
447 protected function get_render_id(): string {
448 if ( $this->render_id === null ) {
449 $this->render_id = wp_generate_uuid4();
450 }
451 return $this->render_id;
452 }
453
454 /**
455 * Folds metadata from `_acf_form_meta[]` inputs into the primary form
456 * configuration so the multi-`acf_form()`-in-one-outer-`<form>` pattern works.
457 *
458 * Non-field request-level args (`post_id`, `return`, `new_post`, `kses`) stay
459 * "last wins" via the primary form.
460 *
461 * @since 6.8.4
462 *
463 * @param array $form The primary form configuration loaded from `_acf_form`.
464 * @return array The primary form with allowed-key extras and OR'd title/content flags.
465 */
466 protected function merge_form_meta( array $form ): array {
467 // phpcs:disable WordPress.Security.NonceVerification.Missing -- Verified above in check_submit_form().
468 if ( empty( $_POST['_acf_form_meta'] ) || ! is_array( $_POST['_acf_form_meta'] ) ) {
469 return $form;
470 }
471
472 if ( empty( $_POST['_acf_render_id'] ) || ! is_scalar( $_POST['_acf_render_id'] ) ) {
473 return $form;
474 }
475 $expected_render_id = sanitize_text_field( wp_unslash( $_POST['_acf_render_id'] ) );
476
477 // wp_unslash only — sanitize_text_field would desync from the raw
478 // $acf_form_value hashed render-side.
479 $primary_form_value = ( isset( $_POST['_acf_form'] ) && is_scalar( $_POST['_acf_form'] ) )
480 ? (string) wp_unslash( $_POST['_acf_form'] ) // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Must match render-side bytes hashed into the form anchor.
481 : '';
482
483 $expected_anchor = hash( 'sha256', $primary_form_value );
484 $primary_post_id = isset( $form['post_id'] ) ? (string) $form['post_id'] : '';
485
486 /**
487 * Filters how long a `_acf_form_meta[]` payload remains valid after the page that
488 * emitted it was rendered.
489 *
490 * @since 6.8.4
491 *
492 * @param int $ttl Allowed age of a meta payload, in seconds.
493 */
494 $ttl = (int) apply_filters( 'acf/form/meta_ttl', DAY_IN_SECONDS );
495 $now = time();
496
497 $valid_metas = array();
498 $primary_present = false;
499
500 foreach ( $_POST['_acf_form_meta'] as $token ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- Each $token is sanitized below before use.
501 if ( ! is_scalar( $token ) ) {
502 continue;
503 }
504
505 $decoded = json_decode( acf_decrypt( sanitize_text_field( $token ) ), true );
506 if ( ! is_array( $decoded ) ) {
507 continue;
508 }
509
510 if ( empty( $decoded['render_id'] ) || ! is_string( $decoded['render_id'] ) ) {
511 continue;
512 }
513
514 if ( ! hash_equals( $expected_render_id, $decoded['render_id'] ) ) {
515 continue;
516 }
517
518 if ( ! isset( $decoded['issued_at'] ) || ! is_numeric( $decoded['issued_at'] ) ) {
519 continue;
520 }
521 if ( ( $now - (int) $decoded['issued_at'] ) >= $ttl ) {
522 continue;
523 }
524
525 if ( ! empty( $decoded['form_anchor'] )
526 && is_string( $decoded['form_anchor'] )
527 && hash_equals( $expected_anchor, $decoded['form_anchor'] )
528 ) {
529 $primary_present = true;
530 }
531
532 $valid_metas[] = $decoded;
533 }
534
535 if ( ! $primary_present ) {
536 return $form;
537 }
538
539 $extra_keys = array();
540
541 foreach ( $valid_metas as $decoded ) {
542 $target_post_id = isset( $decoded['target_post_id'] ) ? (string) $decoded['target_post_id'] : '';
543 if ( ! hash_equals( $primary_post_id, $target_post_id ) ) {
544 continue;
545 }
546
547 if ( ! empty( $decoded['allowed_field_keys'] ) && is_array( $decoded['allowed_field_keys'] ) ) {
548 foreach ( $decoded['allowed_field_keys'] as $key ) {
549 if ( is_scalar( $key ) ) {
550 $extra_keys[] = (string) $key;
551 }
552 }
553 }
554
555 if ( ! empty( $decoded['post_title'] ) ) {
556 $form['post_title'] = true;
557 }
558 if ( ! empty( $decoded['post_content'] ) ) {
559 $form['post_content'] = true;
560 }
561 }
562 // phpcs:enable WordPress.Security.NonceVerification.Missing
563
564 if ( $extra_keys ) {
565 $form['_additional_allowed_field_keys'] = array_values( array_unique( $extra_keys ) );
566 }
567
568 return $form;
569 }
570
571 /**
572 * Returns the fields a given form configuration will expose, mirroring the
573 * selection logic used by render_form().
574 *
575 * Used by render_form() to discover what to render, and by submit_form() to
576 * derive the set of $_POST['acf'] keys the save path will accept.
577 *
578 * @since 6.8.2
579 *
580 * @param array $args The validated form configuration.
581 * @return array
582 */
583 protected function get_form_fields( array $args ): array {
584 $fields = array();
585 $field_groups = array();
586 $post_id = $args['post_id'];
587
588 // Prevent ACF from loading values for "new_post".
589 if ( $post_id === 'new_post' ) {
590 $post_id = false;
591 }
592
593 // Register local default fields so the special _post_title / _post_content / _validate_email
594 // keys are resolvable via acf_get_field().
595 foreach ( $this->get_default_fields() as $field ) {
596 acf_add_local_field( $field );
597 }
598
599 // Append post_title field.
600 if ( $args['post_title'] ) {
601 $fields[] = acf_get_field( '_post_title' );
602 }
603
604 // Append post_content field.
605 if ( $args['post_content'] ) {
606 $fields[] = acf_get_field( '_post_content' );
607 }
608
609 // Load specific fields.
610 if ( $args['fields'] ) {
611 foreach ( $args['fields'] as $selector ) {
612 if ( $post_id ) {
613 // Lookup fields using $strict = false for better compatibility with field names.
614 $fields[] = acf_maybe_get_field( $selector, $post_id, false );
615 } else {
616 // No post to resolve meta references against — skip acf_maybe_get_field()'s
617 // post_id resolution, which can fatal in get_queried_object() if submit_form()
618 // runs before WordPress's main query is built.
619 $fields[] = acf_get_field( $selector );
620 }
621 }
622
623 // Load specific field groups.
624 } elseif ( $args['field_groups'] ) {
625 foreach ( $args['field_groups'] as $selector ) {
626 $field_groups[] = acf_get_field_group( $selector );
627 }
628
629 // Load fields for the given "new_post" args.
630 } elseif ( $args['post_id'] === 'new_post' ) {
631 $field_groups = acf_get_field_groups( $args['new_post'] );
632
633 // Load fields for the given "post_id" arg.
634 } else {
635 $field_groups = acf_get_field_groups(
636 array(
637 'post_id' => $args['post_id'],
638 )
639 );
640 }
641
642 // Load fields from the found field groups.
643 if ( $field_groups ) {
644 foreach ( $field_groups as $field_group ) {
645 $_fields = acf_get_fields( $field_group );
646 if ( $_fields ) {
647 foreach ( $_fields as $_field ) {
648 $fields[] = $_field;
649 }
650 }
651 }
652 }
653
654 // Add honeypot field.
655 if ( $args['honeypot'] ) {
656 $fields[] = acf_get_field( '_validate_email' );
657 }
658
659 return array_filter( $fields );
660 }
661
662 /**
663 * Returns the top-level $_POST['acf'] keys a given form configuration will accept on save.
664 *
665 * Derived from the same field discovery render_form() uses, so the set of save-acceptable
666 * keys matches the set of keys the form actually rendered. For seamless clone fields whose
667 * subfield input names nest under the parent clone's key (e.g. acf[clone_key][subkey]),
668 * the parent's top-level key is what gets returned.
669 *
670 * @since 6.8.2
671 *
672 * @param array $form The validated form configuration.
673 * @param array $fields Optional pre-discovered fields for this form to avoid a
674 * redundant get_form_fields() call when the caller already has them.
675 * @return array
676 */
677 public function get_allowed_field_keys( array $form, array $fields = array() ): array {
678 $keys = array();
679 $fields = $fields ?: $this->get_form_fields( $form );
680
681 foreach ( $fields as $field ) {
682 $prefix = $field['prefix'] ?? 'acf';
683
684 if ( $prefix === 'acf' ) {
685 if ( ! empty( $field['key'] ) ) {
686 $keys[] = $field['key'];
687 }
688 } elseif ( preg_match( '/^acf\[([^]]+)]$/', $prefix, $matches ) ) {
689 $keys[] = $matches[1];
690 }
691 }
692
693 // Include keys folded in from sibling acf_form() calls on the same page.
694 if ( ! empty( $form['_additional_allowed_field_keys'] ) && is_array( $form['_additional_allowed_field_keys'] ) ) {
695 $keys = array_merge( $keys, $form['_additional_allowed_field_keys'] );
696 }
697
698 $keys = array_values( array_unique( array_filter( $keys ) ) );
699
700 /**
701 * Filters the list of $_POST['acf'] keys a front-end form submission is allowed to save.
702 *
703 * Use this to permit additional field keys when a developer dynamically injects fields
704 * into a form via JavaScript that aren't part of the form's declared field configuration.
705 *
706 * @since 6.8.2
707 *
708 * @param array $keys The allowed top-level $_POST['acf'] keys.
709 * @param array $form The validated form configuration.
710 */
711 $keys = apply_filters( 'acf/form/allowed_field_keys', $keys, $form );
712
713 // Re-normalize after the filter so a misbehaving callback can't break array_flip()
714 // downstream in submit_form() with non-scalar or empty values.
715 $keys = array_filter( (array) $keys, 'is_scalar' );
716 return array_values( array_unique( array_filter( array_map( 'strval', $keys ) ) ) );
717 }
718
719 /**
720 * Renders a front-end ACF form.
721 *
722 * Accepts either an array of form configuration (validated via validate_form()) or the
723 * string id of a form previously registered with acf_register_form(). Outputs the form
724 * HTML directly.
725 *
726 * @since 5.4.0
727 *
728 * @param array|string $args Form configuration array, or the id of a registered form.
729 * @return false|void False if a registered form id was passed and no matching form exists;
730 * otherwise outputs the form and returns no value.
731 */
732 function render_form( $args = array() ) {
733
734 // Vars.
735 $is_registered = false;
736
737 // Allow form settings to be directly provided.
738 if ( is_array( $args ) ) {
739 $args = $this->validate_form( $args );
740
741 // Otherwise, lookup registered form.
742 } else {
743 $is_registered = true;
744 $args = $this->get_form( $args );
745 if ( ! $args ) {
746 return false;
747 }
748 }
749
750 // Extract vars.
751 $post_id = $args['post_id'];
752
753 // Prevent ACF from loading values for "new_post".
754 if ( $post_id === 'new_post' ) {
755 $post_id = false;
756 }
757
758 // Set uploader type.
759 acf_update_setting( 'uploader', $args['uploader'] );
760
761 // Discover the fields this form will expose.
762 $fields = $this->get_form_fields( $args );
763
764 // Load values for the special _post_title / _post_content fields so they
765 // render pre-populated with the current post's data.
766 foreach ( $fields as &$field ) {
767 if ( ! isset( $field['key'] ) ) {
768 continue;
769 }
770 if ( $field['key'] === '_post_title' ) {
771 $field['value'] = $post_id ? get_post_field( 'post_title', $post_id ) : '';
772 } elseif ( $field['key'] === '_post_content' ) {
773 $field['value'] = $post_id ? get_post_field( 'post_content', $post_id ) : '';
774 }
775 }
776 unset( $field );
777
778 // Display updated_message
779 if ( ! empty( $_GET['updated'] ) && $args['updated_message'] ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Used as a flag; data not used.
780 printf( $args['html_updated_message'], $args['updated_message'] ); //phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- designed to contain potentially unsafe HTML, set by developers.
781 }
782
783 // display form
784 if ( $args['form'] ) : ?>
785 <form <?php echo acf_esc_attrs( $args['form_attributes'] ); ?>>
786 <?php
787 endif;
788
789 // Render hidden form data.
790 $render_id = $this->get_render_id();
791 $acf_form_value = $is_registered ? $args['id'] : acf_encrypt( wp_json_encode( $args ) );
792 acf_form_data(
793 array(
794 'screen' => 'acf_form',
795 'post_id' => $args['post_id'],
796 'form' => $acf_form_value,
797 'render_id' => $render_id,
798 )
799 );
800
801 /**
802 * Emit a per-form metadata token. PHP keeps only the last `_acf_form` input
803 * after browser-level dedup, so multiple acf_form() calls inside a single
804 * outer <form> would otherwise lose all but one form's allow-list —
805 * `_acf_form_meta[]` uses the array form to survive that and carries each
806 * form's contribution.
807 */
808 $meta = wp_json_encode(
809 array(
810 'render_id' => $render_id,
811 'form_anchor' => hash( 'sha256', (string) $acf_form_value ),
812 'target_post_id' => (string) $args['post_id'],
813 'issued_at' => time(),
814 'allowed_field_keys' => $this->get_allowed_field_keys( $args, $fields ),
815 'post_title' => (bool) $args['post_title'],
816 'post_content' => (bool) $args['post_content'],
817 )
818 );
819 acf_hidden_input(
820 array(
821 'name' => '_acf_form_meta[]',
822 'value' => acf_encrypt( $meta ),
823 )
824 );
825
826 ?>
827 <div class="acf-fields acf-form-fields -<?php echo esc_attr( $args['label_placement'] ); ?>">
828 <?php echo $args['html_before_fields']; ?><?php //phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- designed to contain potentially unsafe HTML, set by developers. ?>
829 <?php acf_render_fields( $fields, $post_id, $args['field_el'], $args['instruction_placement'] ); ?>
830 <?php echo $args['html_after_fields']; ?><?php //phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- designed to contain potentially unsafe HTML, set by developers. ?>
831 </div>
832 <?php if ( $args['form'] ) : ?>
833 <div class="acf-form-submit">
834 <?php printf( $args['html_submit_button'], $args['submit_value'] ); ?><?php //phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- designed to contain potentially unsafe HTML, set by developers. ?>
835 <?php echo $args['html_submit_spinner']; ?><?php //phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- designed to contain potentially unsafe HTML, set by developers. ?>
836 </div>
837 </form>
838 <?php endif;
839 }
840 }
841
842 // initialize
843 acf()->form_front = new acf_form_front();
844 endif; // class_exists check
845
846
847 /**
848 * Functions
849 *
850 * alias of acf()->form->functions
851 *
852 * @type function
853 * @date 11/06/2014
854 * @since 5.0.0
855 *
856 * @param n/a
857 * @return n/a
858 */
859 function acf_form_head() {
860
861 acf()->form_front->enqueue_form();
862 }
863
864 function acf_form( $args = array() ) {
865
866 acf()->form_front->render_form( $args );
867 }
868
869 function acf_get_form( $id = '' ) {
870
871 return acf()->form_front->get_form( $id );
872 }
873
874 function acf_get_forms() {
875 return acf()->form_front->get_forms();
876 }
877
878 function acf_register_form( $args ) {
879
880 acf()->form_front->add_form( $args );
881 }
882
883 ?>
884