set( $name, $data );
}
/**
* acf_get_form_data
*
* Gets data about the current form.
*
* @date 6/10/13
* @since 5.0.0
*
* @param string $name The store name.
* @return mixed
*/
function acf_get_form_data( $name = '' ) {
return acf_get_store( 'form' )->get( $name );
}
/**
* acf_form_data
*
* Called within a form to set important information and render hidden inputs.
*
* @date 15/10/13
* @since 5.0.0
*
* @param void
* @return void
*/
function acf_form_data( $data = array() ) {
// Apply defaults.
$data = wp_parse_args(
$data,
array(
/** @type string The current screen (post, user, taxonomy, etc). */
'screen' => 'post',
/** @type int|string The ID of current post being edited. */
'post_id' => 0,
/** @type bool Enables AJAX validation. */
'validation' => true,
)
);
// Create nonce using screen.
$data['nonce'] = wp_create_nonce( $data['screen'] );
// Append "changed" input used within "_wp_post_revision_fields" action.
$data['changed'] = 0;
// Set data.
acf_set_form_data( $data );
// Render HTML.
?>
$value ) {
acf_hidden_input(
array(
'id' => '_acf_' . $name,
'name' => '_acf_' . $name,
'value' => $value,
)
);
}
/**
* Fires within the #acf-form-data element to add extra HTML.
*
* @date 15/10/13
* @since 5.0.0
*
* @param array $data The form data.
*/
do_action( 'acf/form_data', $data );
do_action( 'acf/input/form_data', $data );
?>
0,
'render_id' => false,
);
return $state;
}
/**
* Runs a render callback, tracking whether it is the outermost render call.
*
* @since 6.8.11
* @access private
*
* @param string|false $render_id The render ID, set for the outermost call only.
* @param callable $callback The callback that renders.
* @return boolean True if this was the outermost render call.
*/
function _acf_in_field_render( $render_id, $callback ) {
$state = &_acf_field_render_state();
$is_outer = 0 === $state['depth'];
++$state['depth'];
if ( $is_outer ) {
$state['render_id'] = $render_id;
}
try {
$callback();
} finally {
--$state['depth'];
if ( $is_outer ) {
$state['render_id'] = false;
}
}
return $is_outer;
}
/**
* Returns the render ID of the fields being rendered, for inputs that sign themselves for the object.
*
* Falls back to the post ID set by acf_form_data() when fields are rendered outside acf_render_fields().
*
* @since 6.8.11
*
* @return string
*/
function acf_get_current_render_id() {
$state = &_acf_field_render_state();
if ( is_string( $state['render_id'] ) && '' !== $state['render_id'] ) {
return $state['render_id'];
}
return (string) acf_get_form_data( 'post_id' );
}
/**
* Outputs the hidden input listing the top-level field keys a render call output.
*
* The list is signed for the current user and the render ID (the object the fields were rendered
* for), so save handlers can tell which submitted keys were rendered without reconstructing the
* form. It is output outside the field wrappers so conditional logic doesn't disable it. Postboxes
* hidden by the location rule check disable it with their fields.
*
* The signature is a nonce, valid for the same 12–24 hours as `_acf_nonce`. If ACF ever refreshes
* `_acf_nonce` on long-lived screens, these lists must be refreshed with it.
*
* @since 6.8.11
* @access private
*
* @param array $keys The top-level keys.
* @param string $render_id The render ID.
* @param string $list_name The root name of the input, such as `_acf_fields`.
* @return void
*/
function _acf_render_field_list( $keys, $render_id, $list_name ) {
// Commas and pipes delimit the list.
$keys = array_filter(
array_unique( array_map( 'strval', $keys ) ),
function ( $key ) {
return '' !== $key && strpbrk( $key, ',|' ) === false;
}
);
if ( ! $keys || ! is_string( $render_id ) || '' === $render_id || ! is_string( $list_name ) || '' === $list_name ) {
return;
}
sort( $keys );
$csv = implode( ',', $keys );
acf_hidden_input(
array(
'name' => $list_name . '[' . $render_id . '][]',
'value' => $csv . '|' . wp_create_nonce( _acf_get_field_list_nonce_action( $render_id, $csv ) ),
)
);
}
/**
* Returns the top-level field keys from submitted field lists that verify for any of the given render IDs.
*
* A verified key proves the server rendered that field for the current user and object. It is
* evidence of rendering, not authorisation: the save handler still decides which render IDs and
* keys it accepts. No field lookups are made, so fields registered late (after the save handler
* runs) are still accepted.
*
* @since 6.8.11
*
* @param mixed $lists The submitted lists, keyed by render ID (usually `$_POST['_acf_fields']`, unslashed).
* @param array $render_ids The render IDs the save accepts.
* @return array The verified top-level keys.
*/
function acf_get_verified_field_keys( $lists, array $render_ids ) {
$keys = array();
if ( ! is_array( $lists ) ) {
return $keys;
}
foreach ( array_unique( array_map( 'strval', $render_ids ) ) as $render_id ) {
if ( '' === $render_id || empty( $lists[ $render_id ] ) || ! is_array( $lists[ $render_id ] ) ) {
continue;
}
foreach ( $lists[ $render_id ] as $field_list ) {
$keys = array_merge( $keys, _acf_get_verified_list_keys( $field_list, $render_id ) );
}
}
return array_values( array_unique( $keys ) );
}
/**
* Returns the keys of a submitted field list (`{csv}|{nonce}`) if it verifies for the render ID.
*
* @since 6.8.11
* @access private
*
* @param mixed $field_list The submitted list.
* @param string $render_id The render ID.
* @return array The keys, or an empty array if the list doesn't verify.
*/
function _acf_get_verified_list_keys( $field_list, $render_id ) {
$pos = is_string( $field_list ) ? strrpos( $field_list, '|' ) : false;
if ( ! $pos ) {
return array();
}
$csv = substr( $field_list, 0, $pos );
$nonce = substr( $field_list, $pos + 1 );
if ( ! wp_verify_nonce( $nonce, _acf_get_field_list_nonce_action( $render_id, $csv ) ) ) {
return array();
}
return explode( ',', $csv );
}
/**
* Returns the keys a seamless clone sub field may be submitted under when values are keyed per field
* rather than like $_POST['acf']: its cloned key (`{clone_key}_{field_key}`, as from acf_get_fields())
* and its original key, which the clone field restores in acf/prepare_field and which the datastore uses.
*
* @since 6.8.11
* @access private
*
* @param array $field The field array.
* @return array
*/
function _acf_get_flat_clone_keys( $field ) {
if ( ( $field['prefix'] ?? 'acf' ) === 'acf' ) {
return array();
}
return array_values( array_filter( array_unique( array( (string) ( $field['key'] ?? '' ), (string) ( $field['__key'] ?? '' ) ) ) ) );
}
/**
* Returns the submitted keys that belong to a top-level field of a field group located on the object.
*
* Keys are matched by top-level input key only. Field names aren't accepted: acf_update_values() looks a
* name up across every field, so it could save as a different field with the same name.
*
* @since 6.8.11
* @access private
*
* @param array $candidates The submitted top-level keys to check.
* @param array $screen The location screen args describing the object. Empty matches nothing.
* @param boolean $flat_clone_keys Whether seamless clone sub fields may also be submitted under their own keys (the datastore).
* @return array
*/
function _acf_get_located_field_keys( $candidates, $screen, $flat_clone_keys = false ) {
// An empty screen would match every field group.
if ( ! $candidates || empty( $screen ) || ! is_array( $screen ) ) {
return array();
}
$located = array();
foreach ( acf_get_field_groups( $screen ) as $field_group ) {
foreach ( (array) acf_get_fields( $field_group ) as $field ) {
$located[ acf_get_field_root_key( $field ) ] = true;
foreach ( $flat_clone_keys ? _acf_get_flat_clone_keys( $field ) : array() as $key ) {
$located[ $key ] = true;
}
}
}
return array_values( array_intersect( $candidates, array_map( 'strval', array_keys( $located ) ) ) );
}
/**
* Returns the flat keys of the sub fields of verified seamless clones that weren't submitted under the clone's key.
*
* @since 6.8.11
* @access private
*
* @param array $verified The verified top-level keys.
* @param array $submitted The submitted top-level keys.
* @return array
*/
function _acf_get_verified_flat_clone_keys( $verified, $submitted ) {
$keys = array();
foreach ( array_diff( $verified, $submitted ) as $key ) {
$field = acf_get_field( $key );
if ( $field && _acf_is_seamless_clone( $field ) ) {
$keys = array_merge( $keys, _acf_get_seamless_clone_leaf_keys( $field['sub_fields'] ) );
}
}
return $keys;
}
/**
* Returns true if a field is a seamless clone with sub fields.
*
* @since 6.8.11
* @access private
*
* @param array $field The field array.
* @return boolean
*/
function _acf_is_seamless_clone( $field ) {
return 'clone' === ( $field['type'] ?? '' ) && 'seamless' === ( $field['display'] ?? '' ) && ! empty( $field['sub_fields'] ) && is_array( $field['sub_fields'] );
}
/**
* Returns the flat keys of a seamless clone's sub fields, through any seamless clones it clones.
*
* acf_get_fields() flattens nested seamless clones to their innermost fields, which is what the datastore sends.
*
* @since 6.8.11
* @access private
*
* @param array $sub_fields The clone's sub fields.
* @return array
*/
function _acf_get_seamless_clone_leaf_keys( $sub_fields ) {
$keys = array();
foreach ( $sub_fields as $sub_field ) {
$keys = array_merge( $keys, _acf_is_seamless_clone( $sub_field ) ? _acf_get_seamless_clone_leaf_keys( $sub_field['sub_fields'] ) : _acf_get_flat_clone_keys( $sub_field ) );
}
return $keys;
}
/**
* Normalizes a list of allowed field keys, typically after it has been filtered.
*
* Drops non-scalar and empty values, casts to strings and removes duplicates so a
* misbehaving filter callback can't break array_flip() downstream.
*
* @since 6.8.11
*
* @param mixed $keys The keys to normalize.
* @return array
*/
function acf_normalize_field_keys( $keys ) {
$keys = array_filter( (array) $keys, 'is_scalar' );
return array_values( array_unique( array_filter( array_map( 'strval', $keys ) ) ) );
}
/**
* Returns the submitted top-level keys a save may accept for an object.
*
* A key is accepted if it is in a verified field list for one of the render IDs (it was rendered
* for this user and object), or, without a list, if it belongs to a top-level field of a field group
* whose location rules match the object. Everything else is dropped, and logged when WP_DEBUG is on.
*
* Fields hidden only by acf/prepare_field can still be submitted without a list. Conditional logic is
* not evaluated.
*
* @since 6.8.11
* @access private
*
* @param array $submitted The submitted top-level keys.
* @param array $args See acf_filter_save_values().
* @return array The allowed top-level keys.
*/
function _acf_get_allowed_save_keys( $submitted, $args ) {
$args = wp_parse_args(
$args,
array(
'context' => '',
'post_id' => 0,
'render_ids' => array(),
'lists' => null,
'screen' => array(),
'filter_arg' => null,
'flat_clone_keys' => false,
)
);
if ( null === $args['lists'] ) {
// phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Verified by the caller; each list is verified as a nonce.
$args['lists'] = isset( $_POST['_acf_fields'] ) ? wp_unslash( $_POST['_acf_fields'] ) : array();
}
$submitted = array_values( array_unique( array_map( 'strval', $submitted ) ) );
$verified = acf_get_verified_field_keys( $args['lists'], (array) $args['render_ids'] );
if ( $args['flat_clone_keys'] ) {
$verified = array_merge( $verified, _acf_get_verified_flat_clone_keys( $verified, $submitted ) );
}
$located = _acf_get_located_field_keys( array_values( array_diff( $submitted, $verified ) ), $args['screen'], $args['flat_clone_keys'] );
/**
* Filters the top-level field keys a save is allowed to persist.
*
* The dynamic portion of the hook name, `$context`, refers to the save context: `post`, `user`,
* `taxonomy`, `comment`, `attachment`, `nav_menu`, `nav_menu_item`, `widget`, `options_page`,
* `woo_order` or `datastore`.
*
* Use this to permit additional field keys, for example when inputs are added to a form by
* JavaScript, or for fields rendered outside a field group whose location rules match the object.
*
* @since 6.8.7 For the `user` and `comment` contexts.
* @since 6.8.11 For the other contexts.
*
* @param array $keys The allowed top-level keys.
* @param integer|string|array $post_id The object being saved. The user or comment ID for the `user` and
* `comment` contexts, the options page settings for `options_page`;
* the ACF post ID otherwise.
*/
$filter_arg = null === $args['filter_arg'] ? $args['post_id'] : $args['filter_arg'];
$keys = apply_filters( "acf/form/{$args['context']}/allowed_field_keys", array_merge( $verified, $located ), $filter_arg );
$keys = acf_normalize_field_keys( $keys );
_acf_log_dropped_field_keys( $submitted, $keys, $args );
return $keys;
}
/**
* Logs dropped keys when WP_DEBUG is on.
*
* @since 6.8.11
* @access private
*
* @param array $submitted The submitted top-level keys.
* @param array $allowed The allowed top-level keys.
* @param array $args The save args.
* @return void
*/
function _acf_log_dropped_field_keys( $submitted, $allowed, $args ) {
if ( ! defined( 'WP_DEBUG' ) || ! WP_DEBUG ) {
return;
}
$submitted = array_filter(
$submitted,
function ( $key ) {
// Basic uploader nonces are submitted as acf[{field_key}_file_nonce] and are read from $_REQUEST instead.
return substr( $key, -11 ) !== '_file_nonce';
}
);
$dropped = array_diff( $submitted, $allowed );
if ( ! $dropped ) {
return;
}
$details = sprintf(
'context=%s object=%s render_ids=%s user=%d',
$args['context'],
is_scalar( $args['post_id'] ) ? $args['post_id'] : '',
implode( ',', array_map( 'strval', (array) $args['render_ids'] ) ),
get_current_user_id()
);
acf_log( 'ACF: dropped field keys not rendered for this object', $details, 'keys=' . implode( ',', $dropped ) );
}
/**
* Returns the save args for a post. Previews and autosaves save to a revision, so they are matched as the post they belong to.
*
* @since 6.8.11
* @access private
*
* @param WP_Post $post The post or revision being saved.
* @param string $context The save context.
* @return array See acf_filter_save_values().
*/
function _acf_get_post_save_args( $post, $context ) {
$parent_id = wp_is_post_revision( $post );
$parent = $parent_id ? get_post( $parent_id ) : null;
$target = $parent ? $parent : $post;
return array(
'context' => $context,
'post_id' => $post->ID,
'render_ids' => array( $target->ID ),
'screen' => array(
'post_id' => $target->ID,
'post_type' => $target->post_type,
),
);
}
/**
* Returns the save args for an attachment.
*
* @since 6.8.11
* @access private
*
* @param integer $attachment_id The attachment ID.
* @return array See acf_filter_save_values().
*/
function _acf_get_attachment_save_args( $attachment_id ) {
return array(
'context' => 'attachment',
'post_id' => $attachment_id,
'render_ids' => array( $attachment_id ),
'screen' => array(
'attachment_id' => $attachment_id,
'attachment' => $attachment_id,
),
);
}
/**
* Returns submitted values restricted to the top-level keys a save may accept for an object.
*
* Use for saves that pass values to acf_save_post() rather than saving $_POST['acf']. The save args are:
*
* - `context` (string): The save context, used in the `acf/form/{$context}/allowed_field_keys` filter.
* - `post_id` (integer|string): The ACF post ID being saved.
* - `render_ids` (array): The render IDs whose field lists the save accepts.
* - `lists` (array|null): The submitted field lists, keyed by render ID. Defaults to `$_POST['_acf_fields']`.
* - `screen` (array): The location screen args for keys submitted without a list. Empty accepts listed keys only.
* - `filter_arg` (mixed): The second argument of the filter. Defaults to `post_id`.
* - `flat_clone_keys` (boolean): Whether seamless clone sub fields may be submitted under their own keys (the datastore).
*
* @since 6.8.11
*
* @param mixed $values The submitted values, keyed by top-level key.
* @param array $args The save args.
* @return array
*/
function acf_filter_save_values( $values, array $args ) {
if ( ! is_array( $values ) ) {
return array();
}
return array_intersect_key( $values, array_flip( _acf_get_allowed_save_keys( array_keys( $values ), $args ) ) );
}
/**
* Restricts $_POST['acf'] and $_FILES['acf'] to the top-level keys a save may accept for an object, in place.
*
* Call after verifying the nonce and before acf_validate_save_post(), so dropped keys can't cause
* validation errors. Also records the render IDs that basic uploader nonces are verified against.
*
* @since 6.8.11
*
* @param array $args See acf_filter_save_values().
* @return void
*/
function acf_restrict_save_input( array $args ) {
// phpcs:disable WordPress.Security.NonceVerification.Missing -- Verified by the caller.
$values = isset( $_POST['acf'] ) && is_array( $_POST['acf'] ) ? $_POST['acf'] : array(); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- Only keys are read here.
$keys = array_keys( $values );
if ( isset( $_FILES['acf']['name'] ) && is_array( $_FILES['acf']['name'] ) ) {
$keys = array_merge( $keys, array_keys( $_FILES['acf']['name'] ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Only keys are read here.
}
$allowed = _acf_get_allowed_save_keys( $keys, $args );
if ( isset( $_POST['acf'] ) ) {
$_POST['acf'] = array_intersect_key( $values, array_flip( $allowed ) );
}
// phpcs:enable WordPress.Security.NonceVerification.Missing
acf_restrict_uploaded_field_keys( $allowed );
acf_set_upload_render_ids( $args['render_ids'] ?? array() );
}
/**
* Sets the render IDs that basic uploader nonces are verified against in this save.
*
* @since 6.8.11
*
* @param array $render_ids The render IDs.
* @return void
*/
function acf_set_upload_render_ids( $render_ids ) {
acf_set_form_data( 'upload_render_ids', array_values( array_filter( array_map( 'strval', (array) $render_ids ) ) ) );
}
/**
* Restricts $_FILES['acf'] to the given top-level keys, in place.
*
* ACF_Media::save_files() uploads every file in $_FILES['acf'] on each acf/save_post and
* writes the attachment ID back into $_POST['acf'], so uploads must be restricted to the
* same keys as $_POST['acf'].
*
* @since 6.8.11
*
* @param array $allowed_keys The allowed top-level keys.
* @return void
*/
function acf_restrict_uploaded_field_keys( array $allowed_keys ) {
// phpcs:disable WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Verified by the caller; only filters keys.
if ( ! isset( $_FILES['acf'] ) ) {
return;
}
if ( ! is_array( $_FILES['acf'] ) ) {
$_FILES['acf'] = array();
return;
}
$allowed_keys = array_flip( acf_normalize_field_keys( $allowed_keys ) );
foreach ( $_FILES['acf'] as $attribute => $branches ) {
$_FILES['acf'][ $attribute ] = is_array( $branches ) ? array_intersect_key( $branches, $allowed_keys ) : array();
}
// phpcs:enable WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
}
/**
* Empties $_FILES['acf'] for saves that take their values from an array rather than $_POST['acf'].
*
* @since 6.8.11
*
* @return void
*/
function acf_discard_uploaded_field_values() {
if ( isset( $_FILES['acf'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Not reading input.
$_FILES['acf'] = array();
}
}