File "api-helpers.php"
Full Path: /home/shopalzfdn/public_html/wp-content/plugins/advanced-custom-fields/includes/api/api-helpers.php
File size: 134.54 KB
MIME-type: text/x-php; charset=utf-8
Charset: utf-8
<?php
/**
* @package ACF
* @author WP Engine
*
* © 2026 Advanced Custom Fields (ACF®). All rights reserved.
* "ACF" is a trademark of WP Engine.
* Licensed under the GNU General Public License v2 or later.
* https://www.gnu.org/licenses/gpl-2.0.html
*/
/**
* This function will return true for a non empty array
*
* @since 5.4.0
*
* @param mixed $array The variable to test.
* @return boolean
*/
function acf_is_array( $array ) {
return ( is_array( $array ) && ! empty( $array ) );
}
/**
* Alias of acf()->has_setting()
*
* @since 5.6.5
*
* @param string $name Name of the setting to check for.
* @return boolean
*/
function acf_has_setting( $name = '' ) {
return acf()->has_setting( $name );
}
/**
* acf_raw_setting
*
* alias of acf()->get_setting()
*
* @since 5.6.5
*
* @param n/a
* @return n/a
*/
function acf_raw_setting( $name = '' ) {
return acf()->get_setting( $name );
}
/**
* acf_update_setting
*
* alias of acf()->update_setting()
*
* @since 5.0.0
*
* @param $name (string)
* @param $value (mixed)
* @return n/a
*/
function acf_update_setting( $name, $value ) {
// validate name.
$name = acf_validate_setting( $name );
// update.
return acf()->update_setting( $name, $value );
}
/**
* acf_validate_setting
*
* Returns the changed setting name if available.
*
* @since 5.6.5
*
* @param n/a
* @return n/a
*/
function acf_validate_setting( $name = '' ) {
return apply_filters( 'acf/validate_setting', $name );
}
/**
* Alias of acf()->get_setting()
*
* @since 5.0.0
*
* @param string $name The name of the setting to test.
* @param string $value An optional default value for the setting if it doesn't exist.
* @return n/a
*/
function acf_get_setting( $name, $value = null ) {
$name = acf_validate_setting( $name );
// replace default setting value if it exists.
if ( acf_has_setting( $name ) ) {
$value = acf_raw_setting( $name );
}
// filter.
$value = apply_filters( "acf/settings/{$name}", $value );
return $value;
}
/**
* Return an array of ACF's internal post type names
*
* @since 6.1
* @return array An array of ACF's internal post type names
*/
function acf_get_internal_post_types() {
return array( 'acf-field-group', 'acf-post-type', 'acf-taxonomy', 'acf-ui-options-page' );
}
/**
* acf_append_setting
*
* This function will add a value into the settings array found in the acf object
*
* @since 5.0.0
*
* @param $name (string)
* @param $value (mixed)
* @return n/a
*/
function acf_append_setting( $name, $value ) {
// vars
$setting = acf_raw_setting( $name );
// bail early if not array
if ( ! is_array( $setting ) ) {
$setting = array();
}
// append
$setting[] = $value;
// update
return acf_update_setting( $name, $setting );
}
/**
* acf_get_data
*
* Returns data.
*
* @since 5.0.0
*
* @param string $name
* @return mixed
*/
function acf_get_data( $name ) {
return acf()->get_data( $name );
}
/**
* acf_set_data
*
* Sets data.
*
* @since 5.0.0
*
* @param string $name
* @param mixed $value
* @return n/a
*/
function acf_set_data( $name, $value ) {
return acf()->set_data( $name, $value );
}
/**
* Appends data to an existing key.
*
* @since 5.9.0
*
* @param string $name The data name.
* @param mixed $data The data to append to name.
*/
function acf_append_data( $name, $data ) {
$prev_data = acf()->get_data( $name );
if ( is_array( $prev_data ) ) {
$data = array_merge( $prev_data, $data );
}
acf()->set_data( $name, $data );
}
/**
* Alias of acf()->init() - the core ACF init function.
*
* @since 5.0.0
*/
function acf_init() {
acf()->init();
}
/**
* acf_has_done
*
* This function will return true if this action has already been done
*
* @since 5.3.2
*
* @param $name (string)
* @return (boolean)
*/
function acf_has_done( $name ) {
// return true if already done
if ( acf_raw_setting( "has_done_{$name}" ) ) {
return true;
}
// update setting and return
acf_update_setting( "has_done_{$name}", true );
return false;
}
/**
* This function will return the path to a file within an external folder
*
* @since 5.5.8
*
* @param string $file Directory path.
* @param string $path Optional file path.
* @return string File path.
*/
function acf_get_external_path( $file, $path = '' ) {
return plugin_dir_path( $file ) . $path;
}
/**
* This function will return the url to a file within an internal ACF folder
*
* @since 5.5.8
*
* @param string $file Directory path.
* @param string $path Optional file path.
* @return string File path.
*/
function acf_get_external_dir( $file, $path = '' ) {
return acf_plugin_dir_url( $file ) . $path;
}
/**
* This function will calculate the url to a plugin folder.
* Different to the WP plugin_dir_url(), this function can calculate for urls outside of the plugins folder (theme include).
*
* @since 5.6.8
*
* @param string $file A file path inside the ACF plugin to get the plugin directory path from.
* @return string The plugin directory path.
*/
function acf_plugin_dir_url( $file ) {
$path = plugin_dir_path( $file );
$path = wp_normalize_path( $path );
// check plugins.
$check_path = wp_normalize_path( realpath( WP_PLUGIN_DIR ) );
if ( strpos( $path, $check_path ) === 0 ) {
return str_replace( $check_path, plugins_url(), $path );
}
// check wp-content.
$check_path = wp_normalize_path( realpath( WP_CONTENT_DIR ) );
if ( strpos( $path, $check_path ) === 0 ) {
return str_replace( $check_path, content_url(), $path );
}
// check root.
$check_path = wp_normalize_path( realpath( ABSPATH ) );
if ( strpos( $path, $check_path ) === 0 ) {
return str_replace( $check_path, site_url( '/' ), $path );
}
// return.
return plugin_dir_url( $file );
}
/**
* This function will merge together 2 arrays and also convert any numeric values to ints
*
* @since 5.0.0
*
* @param array $args The configured arguments array.
* @param array $defaults The default properties for the passed args to inherit.
* @return array $args Parsed arguments with defaults applied.
*/
function acf_parse_args( $args, $defaults = array() ) {
$args = wp_parse_args( $args, $defaults );
// parse types
$args = acf_parse_types( $args );
return $args;
}
/**
* acf_parse_types
*
* This function will convert any numeric values to int and trim strings
*
* @since 5.0.0
*
* @param $var (mixed)
* @return $var (mixed)
*/
function acf_parse_types( $array ) {
return array_map( 'acf_parse_type', $array );
}
/**
* acf_parse_type
*
* description
*
* @since 5.0.9
*
* @param $post_id (int)
* @return $post_id (int)
*/
function acf_parse_type( $v ) {
// Check if is string.
if ( is_string( $v ) ) {
// Trim ("Word " = "Word").
$v = trim( $v );
// Convert int strings to int ("123" = 123).
if ( is_numeric( $v ) && strval( intval( $v ) ) === $v ) {
$v = intval( $v );
}
}
// return.
return $v;
}
/**
* This function will load in a file from the 'admin/views' folder and allow variables to be passed through
*
* @since 5.0.0
*
* @param string $view_path
* @param array $view_args
*/
function acf_get_view( $view_path = '', $view_args = array() ) {
// allow view file name shortcut
if ( substr( $view_path, -4 ) !== '.php' ) {
$view_path = acf_get_path( "includes/admin/views/{$view_path}.php" );
}
// include
if ( file_exists( $view_path ) ) {
// Use `EXTR_SKIP` here to prevent `$view_path` from being accidentally/maliciously overridden.
extract( $view_args, EXTR_SKIP );
include $view_path;
}
}
/**
* acf_merge_atts
*
* description
*
* @since 5.0.9
*
* @param $post_id (int)
* @return $post_id (int)
*/
function acf_merge_atts( $atts, $extra = array() ) {
// bail early if no $extra
if ( empty( $extra ) ) {
return $atts;
}
// trim
$extra = array_map( 'trim', $extra );
$extra = array_filter( $extra );
// merge in new atts
foreach ( $extra as $k => $v ) {
// append
if ( $k == 'class' || $k == 'style' ) {
$atts[ $k ] .= ' ' . $v;
// merge
} else {
$atts[ $k ] = $v;
}
}
return $atts;
}
/**
* This function will create and echo a basic nonce input
*
* @since 5.6.0
*
* @param string $nonce The nonce parameter string.
*/
function acf_nonce_input( $nonce = '' ) {
echo '<input type="hidden" name="_acf_nonce" value="' . esc_attr( wp_create_nonce( $nonce ) ) . '" />';
}
/**
* This function will remove the var from the array, and return the var
*
* @since 5.0.0
*
* @param array $extract_array an array passed as reference to be extracted.
* @param string $key The key to extract from the array.
* @param mixed $default_value The default value if it doesn't exist in the extract array.
* @return mixed Extracted var or default.
*/
function acf_extract_var( &$extract_array, $key, $default_value = null ) {
// check if exists - uses array_key_exists to extract NULL values (isset will fail).
if ( is_array( $extract_array ) && array_key_exists( $key, $extract_array ) ) {
// store and unset value.
$v = $extract_array[ $key ];
unset( $extract_array[ $key ] );
return $v;
}
return $default_value;
}
/**
* This function will remove the vars from the array, and return the vars
*
* @since 5.0.0
*
* @param array $extract_array an array passed as reference to be extracted.
* @param array $keys An array of keys to extract from the original array.
* @return array An array of extracted values.
*/
function acf_extract_vars( &$extract_array, $keys ) {
$r = array();
foreach ( $keys as $key ) {
$r[ $key ] = acf_extract_var( $extract_array, $key );
}
return $r;
}
/**
* acf_get_sub_array
*
* This function will return a sub array of data
*
* @since 5.3.2
*
* @param $post_id (int)
* @return $post_id (int)
*/
function acf_get_sub_array( $array, $keys ) {
$r = array();
foreach ( $keys as $key ) {
$r[ $key ] = $array[ $key ];
}
return $r;
}
/**
* Returns an array of post type names.
*
* @since 5.0.0
*
* @param array $args Optional. An array of key => value arguments to match against the post type objects. Default empty array.
* @return array A list of post type names.
*/
function acf_get_post_types( $args = array() ) {
$post_types = array();
// extract special arg
$exclude = acf_extract_var( $args, 'exclude', array() );
$exclude[] = 'acf-field';
$exclude[] = 'acf-field-group';
$exclude[] = 'acf-post-type';
$exclude[] = 'acf-taxonomy';
$exclude[] = 'acf-ui-options-page';
// Get post type objects.
$objects = get_post_types( $args, 'objects' );
foreach ( $objects as $i => $object ) {
// Bail early if is exclude.
if ( in_array( $i, $exclude ) ) {
continue;
}
// Bail early if is builtin (WP) private post type
// i.e. nav_menu_item, revision, customize_changeset, etc.
if ( $object->_builtin && ! $object->public ) {
continue;
}
$post_types[] = $i;
}
return apply_filters( 'acf/get_post_types', $post_types, $args );
}
function acf_get_pretty_post_types( $post_types = array() ) {
// get post types
if ( empty( $post_types ) ) {
// get all custom post types
$post_types = acf_get_post_types();
}
// get labels
$ref = array();
$r = array();
foreach ( $post_types as $post_type ) {
// vars
$label = acf_get_post_type_label( $post_type );
// append to r
$r[ $post_type ] = $label;
// increase counter
if ( ! isset( $ref[ $label ] ) ) {
$ref[ $label ] = 0;
}
++$ref[ $label ];
}
// get slugs
foreach ( array_keys( $r ) as $i ) {
// vars
$post_type = $r[ $i ];
if ( $ref[ $post_type ] > 1 ) {
$r[ $i ] .= ' (' . $i . ')';
}
}
// return
return $r;
}
/**
* Function acf_get_post_stati()
*
* Returns an array of post status names.
*
* @since 6.1.0
*
* @param array $args Optional. An array of key => value arguments to match against the post status objects. Default empty array.
* @return array A list of post status names.
*/
function acf_get_post_stati( $args = array() ) {
$args['internal'] = false;
$post_statuses = get_post_stati( $args );
unset( $post_statuses['acf-disabled'] );
$post_statuses = (array) apply_filters( 'acf/get_post_stati', $post_statuses, $args );
return $post_statuses;
}
/**
* Function acf_get_pretty_post_statuses()
*
* Returns a clean array of post status names.
*
* @since 6.1.0
*
* @param array $post_statuses Optional. An array of post status objects. Default empty array.
* @return array An array of post status names.
*/
function acf_get_pretty_post_statuses( $post_statuses = array() ) {
// Get all post statuses.
$post_statuses = array_merge( $post_statuses, acf_get_post_stati() );
$ref = array();
$result = array();
foreach ( $post_statuses as $post_status ) {
$label = acf_get_post_status_label( $post_status );
$result[ $post_status ] = $label;
if ( ! isset( $ref[ $label ] ) ) {
$ref[ $label ] = 0;
}
++$ref[ $label ];
}
foreach ( array_keys( $result ) as $i ) {
$post_status = $result[ $i ];
if ( $ref[ $post_status ] > 1 ) {
$result[ $i ] .= ' (' . $i . ')';
}
}
return $result;
}
/**
* acf_get_post_type_label
*
* This function will return a pretty label for a specific post_type
*
* @since 5.4.0
*
* @param $post_type (string)
* @return (string)
*/
function acf_get_post_type_label( $post_type ) {
// vars
$label = $post_type;
// check that object exists
// - case exists when importing field group from another install and post type does not exist
if ( post_type_exists( $post_type ) ) {
$obj = get_post_type_object( $post_type );
$label = $obj->labels->singular_name;
}
// return
return $label;
}
/**
* Function acf_get_post_status_label()
*
* This function will return a pretty label for a specific post_status
*
* @since 6.1.0
*
* @param string $post_status The post status.
* @return string The post status label.
*/
function acf_get_post_status_label( $post_status ) {
$label = $post_status;
$obj = get_post_status_object( $post_status );
$label = is_object( $obj ) ? $obj->label : '';
return $label;
}
/**
* acf_verify_nonce
*
* This function will look at the $_POST['_acf_nonce'] value and return true or false
*
* @since 5.0.0
*
* @param $nonce (string)
* @return (boolean)
*/
function acf_verify_nonce( $value ) {
// vars
$nonce = acf_maybe_get_POST( '_acf_nonce' );
// bail early nonce does not match (post|user|comment|term)
if ( ! $nonce || ! wp_verify_nonce( $nonce, $value ) ) {
return false;
}
// reset nonce (only allow 1 save)
$_POST['_acf_nonce'] = false;
// return
return true;
}
/**
* Returns true if the current AJAX request is valid.
* It's action will also allow WPML to set the lang and avoid AJAX get_posts issues
*
* @since 5.2.3
*
* @param string $nonce The nonce to check.
* @param string $action The action of the nonce.
* @param boolean $action_is_field If the action is a field, modify the action to match validate the field type.
* @param string $expected_field_type Optional field type the resolved field must be when $action_is_field is true. Prevents a nonce minted for one field type from being accepted by an AJAX handler that expects a different one. Defaults to empty (no type validation).
* @return boolean
*/
function acf_verify_ajax( $nonce = '', $action = '', $action_is_field = false, $expected_field_type = '' ) {
// Bail early if we don't have a nonce to check.
if ( empty( $nonce ) && empty( $_REQUEST['nonce'] ) ) {
return false;
}
// Build the action if we're trying to validate a specific field nonce.
if ( $action_is_field ) {
if ( ! acf_is_field_key( $action ) ) {
return false;
}
$field = acf_get_field( $action );
if ( empty( $field['type'] ) ) {
return false;
}
if ( ! empty( $expected_field_type ) && $field['type'] !== $expected_field_type ) {
return false;
}
$action = 'acf_field_' . $field['type'] . '_' . $action;
}
$nonce_to_check = ! empty( $nonce ) ? $nonce : $_REQUEST['nonce']; // phpcs:ignore WordPress.Security -- We're verifying a nonce here.
$nonce_action = ! empty( $action ) ? $action : 'acf_nonce';
// Bail if nonce can't be verified.
if ( ! wp_verify_nonce( sanitize_text_field( $nonce_to_check ), $nonce_action ) ) {
return false;
}
// Action for 3rd party customization (WPML).
do_action( 'acf/verify_ajax' );
return true;
}
/**
* acf_get_image_sizes
*
* This function will return an array of available image sizes
*
* @since 5.0.0
*
* @param n/a
* @return (array)
*/
function acf_get_image_sizes() {
// vars
$sizes = array(
'thumbnail' => __( 'Thumbnail', 'acf' ),
'medium' => __( 'Medium', 'acf' ),
'large' => __( 'Large', 'acf' ),
);
// find all sizes
$all_sizes = get_intermediate_image_sizes();
// add extra registered sizes
if ( ! empty( $all_sizes ) ) {
foreach ( $all_sizes as $size ) {
// bail early if already in array
if ( isset( $sizes[ $size ] ) ) {
continue;
}
// append to array
$label = str_replace( '-', ' ', $size );
$label = ucwords( $label );
$sizes[ $size ] = $label;
}
}
// add sizes
foreach ( array_keys( $sizes ) as $s ) {
// vars
$data = acf_get_image_size( $s );
// append
if ( $data['width'] && $data['height'] ) {
$sizes[ $s ] .= ' (' . $data['width'] . ' x ' . $data['height'] . ')';
}
}
// add full end
$sizes['full'] = __( 'Full Size', 'acf' );
// filter for 3rd party customization
$sizes = apply_filters( 'acf/get_image_sizes', $sizes );
// return
return $sizes;
}
function acf_get_image_size( $s = '' ) {
// global
global $_wp_additional_image_sizes;
// rename for nicer code
$_sizes = $_wp_additional_image_sizes;
// vars
$data = array(
'width' => isset( $_sizes[ $s ]['width'] ) ? $_sizes[ $s ]['width'] : get_option( "{$s}_size_w" ),
'height' => isset( $_sizes[ $s ]['height'] ) ? $_sizes[ $s ]['height'] : get_option( "{$s}_size_h" ),
);
// return
return $data;
}
/**
* acf_version_compare
*
* Similar to the version_compare() function but with extra functionality.
*
* @since 5.5.0
*
* @param string $left The left version number.
* @param string $compare The compare operator.
* @param string $right The right version number.
* @return boolean
*/
function acf_version_compare( $left = '', $compare = '>', $right = '' ) {
// Detect 'wp' placeholder.
if ( $left === 'wp' ) {
global $wp_version;
$left = $wp_version;
}
// Return result.
return version_compare( $left, $right, $compare );
}
/**
* acf_get_full_version
*
* This function will remove any '-beta1' or '-RC1' strings from a version
*
* @since 5.5.0
*
* @param $version (string)
* @return (string)
*/
function acf_get_full_version( $version = '1' ) {
// remove '-beta1' or '-RC1'
if ( $pos = strpos( $version, '-' ) ) {
$version = substr( $version, 0, $pos );
}
// return
return $version;
}
/**
* acf_get_terms
*
* This function is a wrapper for the get_terms() function
*
* @since 5.4.0
*
* @param $args (array)
* @return (array)
*/
function acf_get_terms( $args ) {
// defaults
$args = wp_parse_args(
$args,
array(
'taxonomy' => null,
'hide_empty' => false,
'update_term_meta_cache' => false,
)
);
// return
return get_terms( $args );
}
/**
* acf_get_taxonomy_terms
*
* This function will return an array of available taxonomy terms
*
* @since 5.0.0
*
* @param $taxonomies (array)
* @return (array)
*/
function acf_get_taxonomy_terms( $taxonomies = array() ) {
// force array
$taxonomies = acf_get_array( $taxonomies );
// get pretty taxonomy names
$taxonomies = acf_get_pretty_taxonomies( $taxonomies );
// vars
$r = array();
// populate $r
foreach ( array_keys( $taxonomies ) as $taxonomy ) {
// vars
$label = $taxonomies[ $taxonomy ];
$is_hierarchical = is_taxonomy_hierarchical( $taxonomy );
$terms = acf_get_terms(
array(
'taxonomy' => $taxonomy,
'hide_empty' => false,
)
);
// bail early i no terms
if ( empty( $terms ) ) {
continue;
}
// sort into hierachial order!
if ( $is_hierarchical ) {
$terms = _get_term_children( 0, $terms, $taxonomy );
}
// add placeholder
$r[ $label ] = array();
// add choices
foreach ( $terms as $term ) {
$k = "{$taxonomy}:{$term->slug}";
$r[ $label ][ $k ] = acf_get_term_title( $term );
}
}
// return
return $r;
}
/**
* acf_decode_taxonomy_terms
*
* This function decodes the $taxonomy:$term strings into a nested array
*
* @since 5.0.0
*
* @param $terms (array)
* @return (array)
*/
function acf_decode_taxonomy_terms( $strings = false ) {
// bail early if no terms
if ( empty( $strings ) ) {
return false;
}
// vars
$terms = array();
// loop
foreach ( $strings as $string ) {
// vars
$data = acf_decode_taxonomy_term( $string );
$taxonomy = $data['taxonomy'];
$term = $data['term'];
// create empty array
if ( ! isset( $terms[ $taxonomy ] ) ) {
$terms[ $taxonomy ] = array();
}
// append
$terms[ $taxonomy ][] = $term;
}
// return
return $terms;
}
/**
* acf_decode_taxonomy_term
*
* This function will return the taxonomy and term slug for a given value
*
* @since 5.0.0
*
* @param $string (string)
* @return (array)
*/
function acf_decode_taxonomy_term( $value ) {
// vars
$data = array(
'taxonomy' => '',
'term' => '',
);
// int
if ( is_numeric( $value ) ) {
$data['term'] = $value;
// string
} elseif ( is_string( $value ) ) {
$value = explode( ':', $value );
$data['taxonomy'] = isset( $value[0] ) ? $value[0] : '';
$data['term'] = isset( $value[1] ) ? $value[1] : '';
// error
} else {
return false;
}
// allow for term_id (Used by ACF v4)
if ( is_numeric( $data['term'] ) ) {
// global
global $wpdb;
// find taxonomy
if ( ! $data['taxonomy'] ) {
$data['taxonomy'] = $wpdb->get_var( $wpdb->prepare( "SELECT taxonomy FROM $wpdb->term_taxonomy WHERE term_id = %d LIMIT 1", $data['term'] ) );
}
// find term (may have numeric slug '123')
$term = get_term_by( 'slug', $data['term'], $data['taxonomy'] );
// attempt get term via ID (ACF4 uses ID)
if ( ! $term ) {
$term = get_term( $data['term'], $data['taxonomy'] );
}
// bail early if no term
if ( ! $term ) {
return false;
}
// update
$data['taxonomy'] = $term->taxonomy;
$data['term'] = $term->slug;
}
// return
return $data;
}
/**
* acf_array
*
* Casts the value into an array.
*
* @since 5.7.10
*
* @param mixed $val The value to cast.
* @return array
*/
function acf_array( $val = array() ) {
return (array) $val;
}
/**
* Returns a non-array value.
*
* @since 5.8.10
*
* @param mixed $val The value to review.
* @return mixed
*/
function acf_unarray( $val ) {
if ( is_array( $val ) ) {
return reset( $val );
}
return $val;
}
/**
* acf_get_array
*
* This function will force a variable to become an array
*
* @since 5.0.0
*
* @param $var (mixed)
* @return (array)
*/
function acf_get_array( $var = false, $delimiter = '' ) {
// array
if ( is_array( $var ) ) {
return $var;
}
// bail early if empty
if ( acf_is_empty( $var ) ) {
return array();
}
// string
if ( is_string( $var ) && $delimiter ) {
return explode( $delimiter, $var );
}
// place in array
return (array) $var;
}
/**
* acf_get_numeric
*
* This function will return numeric values
*
* @since 5.4.0
*
* @param $value (mixed)
* @return (mixed)
*/
function acf_get_numeric( $value = '' ) {
// vars
$numbers = array();
$is_array = is_array( $value );
// loop
foreach ( (array) $value as $v ) {
if ( is_numeric( $v ) ) {
$numbers[] = (int) $v;
}
}
// bail early if is empty
if ( empty( $numbers ) ) {
return false;
}
// convert array
if ( ! $is_array ) {
$numbers = $numbers[0];
}
// return
return $numbers;
}
/**
* acf_get_posts
*
* Similar to the get_posts() function but with extra functionality.
*
* @since 5.1.5
*
* @param array $args The query args.
* @return array
*/
function acf_get_posts( $args = array() ) {
// Vars.
$posts = array();
// Apply default args.
$args = wp_parse_args(
$args,
array(
'posts_per_page' => -1,
'post_type' => '',
'post_status' => 'any',
'update_post_meta_cache' => false,
'update_post_term_cache' => false,
)
);
// Avoid default 'post' post_type by providing all public types.
if ( ! $args['post_type'] ) {
$args['post_type'] = acf_get_post_types();
}
if ( ! $args['post_status'] ) {
$args['post_status'] = acf_get_post_stati();
}
// Check if specific post IDs have been provided.
if ( $args['post__in'] ) {
// Clean value into an array of IDs.
$args['post__in'] = array_map( 'intval', acf_array( $args['post__in'] ) );
}
/**
* Filters the args used in `acf_get_posts()` that are passed to `get_posts()`.
*
* @since 6.1.7
*
* @param array $args The args passed to `get_posts()`.
*/
$args = apply_filters( 'acf/acf_get_posts/args', $args );
// Query posts.
$posts = get_posts( $args );
// Remove any potential empty results.
$posts = array_filter( $posts );
// Manually order results.
if ( $posts && $args['post__in'] ) {
$order = array();
foreach ( $posts as $i => $post ) {
$order[ $i ] = array_search( $post->ID, $args['post__in'] );
}
array_multisort( $order, $posts );
}
/**
* Filters the results found in the `acf_get_posts()` function.
*
* @since 6.1.7
*
* @param array $posts The results from the `get_posts()` call.
*/
return apply_filters( 'acf/acf_get_posts/results', $posts );
}
/**
* _acf_query_remove_post_type
*
* This function will remove the 'wp_posts.post_type' WHERE clause completely
* When using 'post__in', this clause is unneccessary and slow.
*
* @since 5.1.5
*
* @param $sql (string)
* @return $sql
*/
function _acf_query_remove_post_type( $sql ) {
// global
global $wpdb;
// bail early if no 'wp_posts.ID IN'
if ( strpos( $sql, "$wpdb->posts.ID IN" ) === false ) {
return $sql;
}
// get bits
$glue = 'AND';
$bits = explode( $glue, $sql );
// loop through $where and remove any post_type queries
foreach ( $bits as $i => $bit ) {
if ( strpos( $bit, "$wpdb->posts.post_type" ) !== false ) {
unset( $bits[ $i ] );
}
}
// join $where back together
$sql = implode( $glue, $bits );
// return
return $sql;
}
/**
* acf_get_grouped_posts
*
* This function will return all posts grouped by post_type
* This is handy for select settings
*
* @since 5.0.0
*
* @param array $args The query arguments.
* @return array
*/
function acf_get_grouped_posts( $args ) {
// vars
$data = array();
// defaults
$args = wp_parse_args(
$args,
array(
'posts_per_page' => -1,
'paged' => 0,
'post_type' => 'post',
'orderby' => 'menu_order title',
'order' => 'ASC',
'post_status' => 'any',
'suppress_filters' => false,
'update_post_meta_cache' => false,
)
);
// Narrow anonymous callers to publicly-viewable types/statuses.
if ( ! is_user_logged_in() ) {
$requested_post_types = acf_get_array( $args['post_type'] );
if ( in_array( 'any', $requested_post_types, true ) ) {
$requested_post_types = get_post_types();
}
$viewable_post_types = array_values( array_filter( $requested_post_types, 'is_post_type_viewable' ) );
if ( empty( $viewable_post_types ) ) {
return $data;
}
$requested_post_statuses = acf_get_array( $args['post_status'] );
$public_post_statuses = array_values( array_filter( get_post_stati(), 'is_post_status_viewable' ) );
if ( empty( $requested_post_statuses ) ) {
return $data;
}
if ( in_array( 'any', $requested_post_statuses, true ) ) {
$viewable_post_statuses = $public_post_statuses;
if ( in_array( 'attachment', $viewable_post_types, true ) ) {
$viewable_post_statuses[] = 'inherit';
}
} else {
$viewable_post_statuses = array_values( array_intersect( $requested_post_statuses, $public_post_statuses ) );
if ( in_array( 'attachment', $viewable_post_types, true ) && in_array( 'inherit', $requested_post_statuses, true ) ) {
$viewable_post_statuses[] = 'inherit';
}
}
if ( empty( $viewable_post_statuses ) ) {
return $data;
}
$args['post_type'] = $viewable_post_types;
$args['post_status'] = array_values( array_unique( $viewable_post_statuses ) );
}
// `perm=readable` only covers private posts, so also hide other posts and attachments the user can't read.
if ( is_user_logged_in() && isset( $args['perm'] ) && 'readable' === $args['perm'] ) {
$args['acf_restrict_unreadable_posts'] = true;
}
// find array of post_type
$post_types = acf_get_array( $args['post_type'] );
$is_single_post_type = ( count( $post_types ) === 1 );
// WordPress 6.8+ sorts post_type arrays for cache key generation
// We need to use the same sorted order when processing results
if (
! $is_single_post_type &&
$args['posts_per_page'] !== -1 &&
version_compare( get_bloginfo( 'version' ), '6.8', '>=' )
) {
sort( $post_types );
}
$post_types_labels = acf_get_pretty_post_types( $post_types );
// attachment doesn't work if it is the only item in an array
if ( $is_single_post_type ) {
$args['post_type'] = reset( $post_types );
}
// add filter to orderby post type
if ( ! $is_single_post_type ) {
add_filter( 'posts_orderby', '_acf_orderby_post_type', 10, 2 );
}
// get posts
$posts = get_posts( $args );
// remove this filter (only once)
if ( ! $is_single_post_type ) {
remove_filter( 'posts_orderby', '_acf_orderby_post_type', 10, 2 );
}
// loop
foreach ( $post_types as $post_type ) {
// vars
$this_posts = array();
$this_group = array();
// populate $this_posts
foreach ( $posts as $post ) {
if ( $post->post_type == $post_type ) {
$this_posts[] = $post;
}
}
// bail early if no posts for this post type
if ( empty( $this_posts ) ) {
continue;
}
// sort into hierachial order!
// this will fail if a search has taken place because parents wont exist
if ( is_post_type_hierarchical( $post_type ) && empty( $args['s'] ) ) {
// vars
$post_id = $this_posts[0]->ID;
$parent_id = acf_maybe_get( $args, 'post_parent', 0 );
$offset = 0;
$length = count( $this_posts );
// get all posts from this post type
$all_posts = get_posts(
array_merge(
$args,
array(
'posts_per_page' => -1,
'paged' => 0,
'post_type' => $post_type,
)
)
);
// find starting point (offset)
foreach ( $all_posts as $i => $post ) {
if ( $post->ID == $post_id ) {
$offset = $i;
break;
}
}
// order posts
$ordered_posts = get_page_children( $parent_id, $all_posts );
// compare aray lengths
// if $ordered_posts is smaller than $all_posts, WP has lost posts during the get_page_children() function
// this is possible when get_post( $args ) filter out parents (via taxonomy, meta and other search parameters)
if ( count( $ordered_posts ) == count( $all_posts ) ) {
$this_posts = array_slice( $ordered_posts, $offset, $length );
}
}
// populate $this_posts
foreach ( $this_posts as $post ) {
$this_group[ $post->ID ] = $post;
}
// group by post type
$label = $post_types_labels[ $post_type ];
$data[ $label ] = $this_group;
}
// return
return $data;
}
/**
* Normalizes an args array so `perm=readable` actually applies inside `WP_Query`.
*
* WP_Query ignores `perm=readable` when `post_status='any'` (Trac #52094). Callers
* that opt into `perm=readable` therefore need `post_status` enumerated. This helper
* is idempotent and safe to call before and after the per-field `acf/fields/<type>/query`
* filters run — the post-filter call catches callbacks that reset `post_status`
* back to 'any' while leaving `perm` untouched.
*
* @since 6.8.7
*
* @param array $args WP_Query args.
* @return array Args with `post_status` enumerated when `perm=readable` requires it.
*/
function acf_ensure_perm_readable_post_status( $args ) {
if ( ! isset( $args['perm'] ) || 'readable' !== $args['perm'] ) {
return $args;
}
$status = isset( $args['post_status'] ) ? $args['post_status'] : '';
$is_any = empty( $status )
|| 'any' === $status
|| ( is_array( $status ) && in_array( 'any', $status, true ) );
if ( $is_any ) {
$args['post_status'] = get_post_stati( array( 'exclude_from_search' => false ) );
}
return $args;
}
/**
* The internal ACF function to return the statuses of a post type's posts that the current user can't
* read when another user wrote them, following how core maps the `read_post` capability.
*
* Core lets users read other users' draft, pending and scheduled posts only if they can edit them, which
* for scheduled posts also requires the capability to edit published posts. Other users' private posts
* require the capability to read private posts. Post types that don't map meta capabilities let users
* with the post type's read capability read all of its posts.
*
* Access granted to individual posts can't be known here, so sites that grant it can use the
* `acf/fields/relational/unreadable_post_statuses` filter.
*
* @since 6.8.11
*
* @param string $post_type The post type.
* @return string[]
*/
function _acf_get_unreadable_post_statuses( $post_type ) {
$post_type_object = get_post_type_object( $post_type );
$protected_statuses = get_post_stati( array( 'protected' => true ) );
$private_statuses = get_post_stati( array( 'private' => true ) );
if ( ! $post_type_object ) {
return array_values( array_merge( $protected_statuses, $private_statuses ) );
}
$statuses = array();
if ( ! $post_type_object->map_meta_cap ) {
// `read_post` and `read_page` are meta capabilities, which can't be checked without a post.
$read_cap = $post_type_object->cap->read_post;
$can_read = ! in_array( $read_cap, array( 'read_post', 'read_page' ), true ) && current_user_can( $read_cap );
if ( ! $can_read ) {
$statuses = array_merge( $protected_statuses, $private_statuses );
}
} else {
if ( ! current_user_can( $post_type_object->cap->edit_others_posts ) ) {
$statuses = $protected_statuses;
} elseif ( ! current_user_can( $post_type_object->cap->edit_published_posts ) ) {
$statuses = array_intersect( $protected_statuses, array( 'future' ) );
}
if ( ! current_user_can( $post_type_object->cap->read_private_posts ) ) {
$statuses = array_merge( $statuses, $private_statuses );
}
}
/**
* Filters the statuses of other users' posts that relational field searches hide from the current user.
*
* Posts with a status removed here are included in the search query, and each is then checked with
* `acf_current_user_can_read_post()`, so posts the user can't read are still left out. Use this when
* access to individual posts is granted, such as through the `acf/user_can_read_post` filter
* or a `map_meta_cap` callback. Pages of results may then contain fewer posts, or none, and the next
* page being loaded can show that the search matched posts the user can't read.
*
* @since 6.8.11
*
* @param string[] $statuses The statuses hidden from the current user.
* @param string $post_type The post type.
*/
$statuses = apply_filters( 'acf/fields/relational/unreadable_post_statuses', array_values( $statuses ), $post_type );
return array_values( array_filter( (array) $statuses, 'is_string' ) );
}
/**
* The internal ACF function to hide posts the current user can't read from `acf_get_grouped_posts()`
* queries.
*
* `WP_Query` only applies `perm=readable` to private posts, and checks an attachment's own `inherit`
* status rather than its parent's. This also hides other users' draft, pending and scheduled posts the
* current user can't edit, and attachments that inherit a status from a parent the user can't read.
*
* When the query has the field's post types in `acf_field_post_types`, it also hides other users' posts
* of post types that aren't public, unless the field lists them or the user can open their list of posts
* in the admin or edit other users' posts, matching `acf_user_can_read_post()`.
*
* @since 6.8.11
*
* @param string $where The WHERE clause of the query.
* @param WP_Query $wp_query The WP_Query.
* @return string The potentially modified WHERE clause.
*/
function _acf_query_restrict_unreadable_posts( $where, $wp_query ) {
global $wpdb;
if ( ! $wp_query->get( 'acf_restrict_unreadable_posts' ) ) {
return $where;
}
$post_types = acf_get_array( $wp_query->get( 'post_type' ) );
if ( empty( $post_types ) || in_array( 'any', $post_types, true ) ) {
$post_types = get_post_types();
}
$field_post_types = $wp_query->get( 'acf_field_post_types', null );
$readable_post_types = array();
$conditions = array( $wpdb->prepare( "{$wpdb->posts}.post_author = %d", get_current_user_id() ) );
foreach ( $post_types as $post_type ) {
// Other users' posts of these post types are left out, so only the user's own posts are found.
if ( is_array( $field_post_types ) && _acf_post_type_is_hidden_from_search( $post_type, $field_post_types ) ) {
continue;
}
$statuses = _acf_get_unreadable_post_statuses( $post_type );
if ( empty( $statuses ) ) {
$readable_post_types[] = $post_type;
continue;
}
$statuses = implode( "','", array_map( 'esc_sql', $statuses ) );
$condition = $wpdb->prepare( "{$wpdb->posts}.post_type = %s", $post_type ) . " AND {$wpdb->posts}.post_status NOT IN ('$statuses')";
// Attachments inherit their parent's status, so their parent must be readable too.
if ( 'attachment' === $post_type ) {
$condition .= " AND NOT ( {$wpdb->posts}.post_status = 'inherit' AND EXISTS ( SELECT 1 FROM {$wpdb->posts} AS acf_parent WHERE acf_parent.ID = {$wpdb->posts}.post_parent AND acf_parent.post_status IN ('$statuses') ) )";
}
$conditions[] = "( $condition )";
}
if ( count( $readable_post_types ) === count( $post_types ) ) {
return $where;
}
if ( $readable_post_types ) {
$readable_post_types = implode( "','", array_map( 'esc_sql', $readable_post_types ) );
$conditions[] = "{$wpdb->posts}.post_type IN ('$readable_post_types')";
}
return $where . ' AND (' . implode( ' OR ', $conditions ) . ')';
}
add_filter( 'posts_where', '_acf_query_restrict_unreadable_posts', 10, 2 );
/**
* The internal ACF function to check whether relational field searches leave out other users' posts of a
* post type, because it isn't public and the current user can't see its posts in the admin.
*
* @since 6.8.11
*
* @param string $post_type The post type.
* @param string[] $field_post_types The post types listed by the field's "Filter by Post Type" setting.
* @return boolean
*/
function _acf_post_type_is_hidden_from_search( $post_type, $field_post_types ) {
if ( is_post_type_viewable( $post_type ) || in_array( $post_type, $field_post_types, true ) ) {
return false;
}
$post_type_object = get_post_type_object( $post_type );
return ! $post_type_object
|| ( ! _acf_user_can_list_post_type( wp_get_current_user(), $post_type ) && ! current_user_can( $post_type_object->cap->edit_others_posts ) );
}
/**
* The internal ACF function to add order by post types for use in `acf_get_grouped_posts`
*
* @param string $orderby The current orderby value for a query.
* @param object $wp_query The WP_Query.
* @return string The potentially modified orderby string.
*/
function _acf_orderby_post_type( $orderby, $wp_query ) {
global $wpdb;
$post_types = $wp_query->get( 'post_type' );
// Prepend the SQL.
if ( is_array( $post_types ) ) {
$post_types = array_map( 'esc_sql', $post_types );
$post_types = implode( "','", $post_types );
$orderby = "FIELD({$wpdb->posts}.post_type,'$post_types')," . $orderby;
}
return $orderby;
}
function acf_get_post_title( $post = 0, $is_search = false ) {
// vars
$post = get_post( $post );
$title = '';
$prepend = '';
$append = '';
// bail early if no post
if ( ! $post ) {
return '';
}
// title
$title = get_the_title( $post->ID );
// empty
if ( $title === '' ) {
$title = __( '(no title)', 'acf' );
}
// status
if ( get_post_status( $post->ID ) != 'publish' ) {
$append .= ' (' . get_post_status( $post->ID ) . ')';
}
// ancestors
if ( $post->post_type !== 'attachment' ) {
// get ancestors
$ancestors = get_ancestors( $post->ID, $post->post_type );
$prepend .= str_repeat( '- ', count( $ancestors ) );
}
// merge
$title = $prepend . $title . $append;
// return
return $title;
}
/**
* Checks whether a user can read a post referenced by a relational field.
*
* Matches WordPress core: published posts can be read by anyone (as in the REST API, and as the
* relational field search already shows them), and other posts need to be publicly viewable or
* need the `read_post` capability (which needs edit access for other users' drafts).
*
* Posts of post types that aren't public, such as WooCommerce coupons, can only be seen in the admin.
* Published ones are readable if the field's "Filter by Post Type" setting lists their post type, or
* the user can open the post type's list of posts in the admin, which shows their titles. Others need
* the user to be able to edit them. Revisions are never public, so they need edit access, which for a
* revision means edit access to its parent.
*
* Unlike `acf_rest_reference_is_exposable()`, this doesn't require the post type to be shown in the
* REST API, because these fields can reference any post type in the editor.
*
* @since 6.8.11
*
* @param integer|WP_User $user The user ID or object. 0 for visitors who aren't logged in.
* @param integer|WP_Post $post The post ID or object.
* @param array $field Optional. The field referencing the post.
* @return boolean
*/
function acf_user_can_read_post( $user, $post, $field = array() ) {
$user = $user instanceof WP_User ? $user : new WP_User( (int) $user );
$post = get_post( $post );
if ( ! $post ) {
$can_read = false;
} elseif ( 'revision' === $post->post_type ) {
$can_read = user_can( $user, 'edit_post', $post->ID );
} elseif ( ! is_post_type_viewable( $post->post_type ) && ! in_array( $post->post_type, acf_get_array( $field['post_type'] ?? array() ), true ) ) {
$can_read = ( 'publish' === get_post_status( $post ) && _acf_user_can_list_post_type( $user, $post->post_type ) )
|| user_can( $user, 'edit_post', $post->ID );
} else {
$can_read = 'publish' === get_post_status( $post )
|| is_post_publicly_viewable( $post )
|| user_can( $user, 'read_post', $post->ID );
}
/**
* Filters whether a user can read a post referenced by a relational field.
*
* The user is usually the current user. While an ACF Block saved in a post or synced pattern renders,
* other than in a preview, it's the author of that post, so check `$user` rather than the current user.
*
* Relational field searches leave out other users' posts the user can't read before this filter runs,
* so returning true doesn't add them to search results. Use the
* `acf/fields/relational/unreadable_post_statuses` filter to include them.
*
* @since 6.8.11
*
* @param boolean $can_read Whether the user can read the post.
* @param WP_Post|null $post The post object, or null if the post doesn't exist.
* @param array $field The field referencing the post, if any.
* @param WP_User $user The user.
*/
return (bool) apply_filters( 'acf/user_can_read_post', $can_read, $post, $field, $user );
}
/**
* Checks whether the current user can read a post referenced by a relational field.
*
* @since 6.8.11
*
* @see acf_user_can_read_post()
*
* @param integer|WP_Post $post The post ID or object.
* @param array $field Optional. The field referencing the post.
* @return boolean
*/
function acf_current_user_can_read_post( $post, $field = array() ) {
return acf_user_can_read_post( wp_get_current_user(), $post, $field );
}
/**
* The internal ACF function to check whether a user can open a post type's list of posts in the admin,
* which shows the titles of all its published posts.
*
* @since 6.8.11
*
* @param integer|WP_User $user The user ID or object.
* @param string $post_type The post type.
* @return boolean
*/
function _acf_user_can_list_post_type( $user, $post_type ) {
$post_type_object = get_post_type_object( $post_type );
return $post_type_object && $post_type_object->show_ui && user_can( $user, $post_type_object->cap->edit_posts );
}
/**
* Removes posts the current user can't read from a relational field's search results.
*
* The query already hides posts core doesn't let the user read, but the `acf/user_can_read_post`
* filter can deny others. Searches that a site has opted out of `perm=readable` aren't checked.
*
* @since 6.8.11
*
* @param array $groups The posts from `acf_get_grouped_posts()`, grouped by post type label.
* @param array $args The query args.
* @param array $field The relational field being searched.
* @return array An array with `groups`, the groups without posts the user can't read, and `more`, whether
* there may be more pages of results, judged before any posts were removed.
*/
function acf_remove_unreadable_grouped_posts( $groups, $args, $field ) {
$groups = acf_get_array( $groups );
$per_page = isset( $args['posts_per_page'] ) ? (int) $args['posts_per_page'] : 0;
$more = $per_page > 0 && array_sum( array_map( 'count', $groups ) ) >= $per_page;
if ( isset( $args['perm'] ) && 'readable' === $args['perm'] ) {
foreach ( array_keys( $groups ) as $label ) {
$groups[ $label ] = array_filter(
$groups[ $label ],
function ( $post ) use ( $field ) {
return acf_current_user_can_read_post( $post, $field );
}
);
if ( empty( $groups[ $label ] ) ) {
unset( $groups[ $label ] );
}
}
}
return array(
'groups' => $groups,
'more' => $more,
);
}
/**
* Returns the label shown in place of a selected post the current user can't read.
*
* @since 6.8.11
*
* @return string
*/
function acf_get_restricted_post_title() {
return __( '(Restricted item)', 'acf' );
}
/**
* The internal ACF function to return the object whose stored values a save is compared against.
* Revisions don't store values of their own yet, so they're compared against their parent post.
*
* @since 6.8.11
*
* @param integer|string $post_id The ACF post ID being saved.
* @return integer|string
*/
function _acf_get_stored_values_post_id( $post_id ) {
$decoded = acf_decode_post_id( $post_id );
$parent = 'post' === $decoded['type'] ? wp_is_post_revision( (int) $decoded['id'] ) : false;
return $parent ? $parent : $post_id;
}
/**
* The internal ACF function to check whether relational fields should check the post IDs being saved.
* That's while ACF saves submitted values. Values code saves with update_field() during that save, such
* as back-references written by bidirectional sync, aren't checked.
*
* @since 6.8.11
*
* @return boolean
*/
function _acf_is_checking_submitted_post_ids() {
return (bool) acf_get_data( 'acf_saving_submitted_values' );
}
/**
* The internal ACF function to return whether a field is, or contains, a Relationship, Post Object or
* Page Link field, at any depth inside Repeater, Group, Clone and Flexible Content fields.
*
* @since 6.8.11
*
* @param array $field The field array.
* @return boolean
*/
function _acf_field_has_relational_fields( $field ) {
static $cache = array();
$key = isset( $field['key'] ) ? (string) $field['key'] : '';
if ( '' !== $key && isset( $cache[ $key ] ) ) {
return $cache[ $key ];
}
$has_relational = in_array( $field['type'] ?? '', array( 'relationship', 'post_object', 'page_link' ), true );
$children = acf_get_array( $field['sub_fields'] ?? array() );
foreach ( acf_get_array( $field['layouts'] ?? array() ) as $layout ) {
$children = array_merge( $children, acf_get_array( $layout['sub_fields'] ?? array() ) );
}
foreach ( $children as $child ) {
if ( $has_relational ) {
break;
}
$has_relational = is_array( $child ) && _acf_field_has_relational_fields( $child );
}
if ( '' !== $key ) {
$cache[ $key ] = $has_relational;
}
return $has_relational;
}
/**
* The internal ACF function to return the post IDs stored in an object's relational fields before the
* current user-submitted save changed them.
*
* Repeater and Flexible Content rows are saved one at a time, and can be moved, added or removed, so a
* selection may be saved to a different row than it was stored in, after that row was already updated.
* The IDs are recorded, by field key, before the first relational field is updated, and kept until the
* save finishes. A sub field has the same key in every row, so this covers moved rows at any depth.
*
* @since 6.8.11
*
* @param integer|string $post_id The ACF post ID being saved.
* @return array An array with `ids`, the sets of post IDs stored in each relational field, keyed by field
* key, and `names`, the meta names that have a field key reference.
*/
function _acf_get_stored_post_ids_snapshot( $post_id ) {
$post_id = _acf_get_stored_values_post_id( $post_id );
$snapshots = (array) acf_get_data( 'acf_stored_post_ids_snapshots' );
if ( ! isset( $snapshots[ $post_id ] ) ) {
$meta = (array) acf_get_meta( $post_id );
$relational = array();
$snapshot = array(
'ids' => array(),
'names' => array(),
);
// Each stored value has a reference to the key of the field it belongs to.
foreach ( $meta as $name => $value ) {
if ( ! isset( $meta[ '_' . $name ] ) ) {
continue;
}
$field_key = $meta[ '_' . $name ];
$snapshot['names'][ $name ] = true;
if ( ! isset( $relational[ $field_key ] ) ) {
$field = acf_get_field( $field_key );
$relational[ $field_key ] = $field && in_array( $field['type'], array( 'relationship', 'post_object', 'page_link' ), true );
}
if ( ! $relational[ $field_key ] ) {
continue;
}
foreach ( acf_get_array( acf_maybe_unserialize( $value ) ) as $id ) {
if ( is_numeric( $id ) ) {
$snapshot['ids'][ $field_key ][ (string) $id ] = true;
}
}
}
$snapshots[ $post_id ] = $snapshot;
acf_set_data( 'acf_stored_post_ids_snapshots', $snapshots );
}
return $snapshots[ $post_id ];
}
/**
* The internal ACF function to record an object's stored post IDs during a user-submitted save, when the
* first field that is, or contains, a relational field is updated, before any of them are changed. Saves
* without relational fields don't record anything.
*
* @since 6.8.11
*
* @param mixed $check The short-circuit value, returned unchanged.
* @param mixed $value The value being updated.
* @param integer|string $post_id The ACF post ID being saved.
* @param array $field The field being updated.
* @return mixed
*/
function _acf_snapshot_stored_post_ids( $check, $value, $post_id, $field ) {
if ( ! _acf_is_checking_submitted_post_ids() ) {
return $check;
}
$snapshots = (array) acf_get_data( 'acf_stored_post_ids_snapshots' );
if ( ! isset( $snapshots[ _acf_get_stored_values_post_id( $post_id ) ] ) && is_array( $field ) && _acf_field_has_relational_fields( $field ) ) {
_acf_get_stored_post_ids_snapshot( $post_id );
}
return $check;
}
add_filter( 'acf/pre_update_value', '_acf_snapshot_stored_post_ids', 1, 4 );
/**
* The internal ACF function to discard an object's stored post ID snapshot once its save has finished.
*
* @since 6.8.11
*
* @param integer|string $post_id The ACF post ID that was saved.
* @return void
*/
function _acf_clear_stored_post_ids_snapshot( $post_id ) {
$snapshots = (array) acf_get_data( 'acf_stored_post_ids_snapshots' );
unset( $snapshots[ _acf_get_stored_values_post_id( $post_id ) ] );
acf_set_data( 'acf_stored_post_ids_snapshots', $snapshots );
}
add_action( 'acf/save_post', '_acf_clear_stored_post_ids_snapshot', 99 );
/**
* The internal ACF function to discard stored post ID snapshots after each REST request (including each
* request in a batch), which updates fields without firing `acf/save_post`.
*
* REST requests made while a submitted save is still running, such as with `rest_do_request()` from an
* `acf/update_value` callback, leave the snapshots alone, as that save still needs them.
*
* @since 6.8.11
*
* @param mixed $response The REST response, returned unchanged.
* @return mixed
*/
function _acf_clear_rest_stored_post_ids_snapshots( $response ) {
if ( _acf_is_checking_submitted_post_ids() ) {
return $response;
}
acf_set_data( 'acf_stored_post_ids_snapshots', array() );
return $response;
}
add_filter( 'rest_request_after_callbacks', '_acf_clear_rest_stored_post_ids_snapshots' );
/**
* Removes newly selected post IDs that the current user isn't allowed to add to a relational field.
*
* Only runs while ACF is saving submitted values. A new ID must belong to an existing post that the
* current user can read. IDs already stored in the field, in any Repeater or Flexible Content row, are
* kept, so a selection the user can't read isn't lost when they save.
*
* Stored values are matched by field key. A value stored under a different key, such as by a field that
* was later recreated or imported with a new key, isn't matched, so a user who can't read its posts drops
* them when they save it.
*
* @since 6.8.11
*
* @param mixed $value The normalized field value: an ID, or an array of IDs.
* @param integer|string $post_id The ACF post ID being saved.
* @param array $field The field array.
* @return mixed The value with disallowed IDs removed.
*/
function acf_remove_disallowed_post_ids( $value, $post_id, $field ) {
if ( empty( $value ) || ! _acf_is_checking_submitted_post_ids() ) {
return $value;
}
$snapshot = _acf_get_stored_post_ids_snapshot( $post_id );
$stored_ids = isset( $snapshot['ids'][ $field['key'] ] ) ? $snapshot['ids'][ $field['key'] ] : array();
// A seamless Clone saved by a form references the cloned field's key, but the REST API saves it under the
// Clone's prefixed key.
if ( ! empty( $field['__key'] ) && isset( $snapshot['ids'][ $field['__key'] ] ) ) {
$stored_ids = $stored_ids + $snapshot['ids'][ $field['__key'] ];
}
// Values saved without a field key reference, such as by an import, can only be found by name. This reads
// the stored value directly, before this field is updated, so `acf/load_value` filters can't add to it.
if ( ! isset( $snapshot['names'][ $field['name'] ] ) ) {
$legacy_ids = acf_get_array( acf_get_metadata_by_field( _acf_get_stored_values_post_id( $post_id ), $field ) );
$stored_ids = $stored_ids + array_fill_keys( array_map( 'strval', array_filter( $legacy_ids, 'is_numeric' ) ), true );
}
$is_allowed = function ( $id ) use ( $stored_ids, $field ) {
// Page Link URLs aren't post IDs, so they're kept. Other fields only store post IDs, and other code,
// such as bidirectional sync, decodes values like `post_42` or `user_3` as objects to update.
if ( ! is_numeric( $id ) ) {
return 'page_link' === $field['type'];
}
// Other code, such as bidirectional sync, converts negative IDs to the positive post ID.
if ( (int) $id <= 0 ) {
return false;
}
// Only check new post IDs. Existing selections are kept.
if ( isset( $stored_ids[ (string) $id ] ) ) {
return true;
}
return acf_current_user_can_read_post( (int) $id, $field );
};
if ( ! is_array( $value ) ) {
return $is_allowed( $value ) ? $value : '';
}
$value = array_filter( $value, $is_allowed );
return acf_is_sequential_array( $value ) ? array_values( $value ) : $value;
}
/**
* Removes posts that can't be read from a relational field's value before it's formatted.
*
* Applies to values formatted while rendering an ACF Block by default. Block data is stored in the post
* content, which anyone who can edit the post controls, and the block editor renders previews from data
* that hasn't been saved yet, so selections in it are never checked when they're saved.
*
* Previews are checked against the current user. Other blocks are checked against the author of the post
* or synced pattern they're saved in, so posts that author picked show to every visitor but posts they
* couldn't have picked don't. Blocks that aren't saved in either are checked against the current user when
* rendered in the admin, by AJAX or by a REST API endpoint, and aren't checked on the front end, where
* they're in places like template parts and widgets that only administrators can edit. See
* `_acf_get_block_reader()`.
*
* The `acf/fields/relational/hide_unreadable_posts` filter can apply this to other values too, or turn it
* off.
*
* @since 6.8.11
*
* @param mixed $value The value: an ID, or an array of IDs (and Page Link URLs).
* @param integer|string $post_id The ACF post ID the value is formatted for.
* @param array $field The field array.
* @return mixed The value without posts that can't be read.
*/
function acf_remove_unreadable_formatted_post_ids( $value, $post_id, $field ) {
if ( empty( $value ) ) {
return $value;
}
$user = wp_get_current_user();
$hide = false;
if ( is_string( $post_id ) && 0 === strpos( $post_id, 'block_' ) ) {
// Set while an ACF Block renders: false when it isn't checked, otherwise the user to check.
$reader = acf_get_data( 'acf_block_reader' );
$hide = false !== $reader;
$user = $reader instanceof WP_User ? $reader : $user;
}
/**
* Filters whether a relational field's formatted value leaves out posts that can't be read.
*
* True by default for values formatted while rendering an ACF Block, such as by get_field() in a block
* template, except for blocks on the front end that aren't saved in a post or synced pattern. Return
* true to also leave them out elsewhere, such as from get_field() in theme templates, or false to show
* them in blocks again. Values other than those of blocks are checked against the current user.
* Visitors who aren't logged in can only read published posts and posts with a public status.
*
* @since 6.8.11
*
* @param boolean $hide Whether to leave out posts that can't be read.
* @param integer|string $post_id The ACF post ID the value is formatted for.
* @param array $field The field array.
*/
if ( ! apply_filters( 'acf/fields/relational/hide_unreadable_posts', $hide, $post_id, $field ) ) {
return $value;
}
$is_readable = function ( $id ) use ( $user, $field ) {
// Page Link URLs aren't post IDs, so they're kept. Other fields convert values like `123x` to post IDs.
if ( ! is_numeric( $id ) ) {
return 'page_link' === $field['type'];
}
return acf_user_can_read_post( $user, (int) $id, $field );
};
if ( ! is_array( $value ) ) {
return $is_readable( $value ) ? $value : '';
}
_prime_post_caches( array_map( 'intval', array_filter( $value, 'is_numeric' ) ), false, false );
$value = array_filter( $value, $is_readable );
return acf_is_sequential_array( $value ) ? array_values( $value ) : $value;
}
function acf_order_by_search( $array, $search ) {
// vars
$weights = array();
$needle = strtolower( $search );
// add key prefix
foreach ( array_keys( $array ) as $k ) {
$array[ '_' . $k ] = acf_extract_var( $array, $k );
}
// add search weight
foreach ( $array as $k => $v ) {
// vars
$weight = 0;
$haystack = strtolower( $v );
$strpos = strpos( $haystack, $needle );
// detect search match
if ( $strpos !== false ) {
// set eright to length of match
$weight = strlen( $search );
// increase weight if match starts at begining of string
if ( $strpos == 0 ) {
++$weight;
}
}
// append to wights
$weights[ $k ] = $weight;
}
// sort the array with menu_order ascending
array_multisort( $weights, SORT_DESC, $array );
// remove key prefix
foreach ( array_keys( $array ) as $k ) {
$array[ substr( $k, 1 ) ] = acf_extract_var( $array, $k );
}
// return
return $array;
}
/**
* acf_get_pretty_user_roles
*
* description
*
* @since 5.3.2
*
* @param $post_id (int)
* @return $post_id (int)
*/
function acf_get_pretty_user_roles( $allowed = false ) {
// vars
$editable_roles = get_editable_roles();
$allowed = acf_get_array( $allowed );
$roles = array();
// loop
foreach ( $editable_roles as $role_name => $role_details ) {
// bail early if not allowed
if ( ! empty( $allowed ) && ! in_array( $role_name, $allowed ) ) {
continue;
}
// append
$roles[ $role_name ] = translate_user_role( $role_details['name'] );
}
// return
return $roles;
}
/**
* acf_get_grouped_users
*
* This function will return all users grouped by role
* This is handy for select settings
*
* @since 5.0.0
*
* @param $args (array)
* @return (array)
*/
function acf_get_grouped_users( $args = array() ) {
// vars
$r = array();
// defaults
$args = wp_parse_args(
$args,
array(
'users_per_page' => -1,
'paged' => 0,
'role' => '',
'orderby' => 'login',
'order' => 'ASC',
)
);
// offset
$i = 0;
$min = 0;
$max = 0;
$users_per_page = acf_extract_var( $args, 'users_per_page' );
$paged = acf_extract_var( $args, 'paged' );
if ( $users_per_page > 0 ) {
// prevent paged from being -1
$paged = max( 0, $paged );
// set min / max
$min = ( ( $paged - 1 ) * $users_per_page ) + 1; // 1, 11
$max = ( $paged * $users_per_page ); // 10, 20
}
// find array of post_type
$user_roles = acf_get_pretty_user_roles( $args['role'] );
// fix role
if ( is_array( $args['role'] ) ) {
// global
global $wp_version, $wpdb;
// vars
$roles = acf_extract_var( $args, 'role' );
// new WP has role__in
if ( version_compare( $wp_version, '4.4', '>=' ) ) {
$args['role__in'] = $roles;
// old WP doesn't have role__in
} else {
// vars
$blog_id = get_current_blog_id();
$meta_query = array( 'relation' => 'OR' );
// loop
foreach ( $roles as $role ) {
$meta_query[] = array(
'key' => $wpdb->get_blog_prefix( $blog_id ) . 'capabilities',
'value' => '"' . $role . '"',
'compare' => 'LIKE',
);
}
// append
$args['meta_query'] = $meta_query;
}
}
// get posts
$users = get_users( $args );
// loop
foreach ( $user_roles as $user_role_name => $user_role_label ) {
// vars
$this_users = array();
$this_group = array();
// populate $this_posts
foreach ( array_keys( $users ) as $key ) {
// bail early if not correct role
if ( ! in_array( $user_role_name, $users[ $key ]->roles ) ) {
continue;
}
// extract user
$user = acf_extract_var( $users, $key );
// increase
++$i;
// bail early if too low
if ( $min && $i < $min ) {
continue;
}
// bail early if too high (don't bother looking at any more users)
if ( $max && $i > $max ) {
break;
}
// group by post type
$this_users[ $user->ID ] = $user;
}
// bail early if no posts for this post type
if ( empty( $this_users ) ) {
continue;
}
// append
$r[ $user_role_label ] = $this_users;
}
// return
return $r;
}
/**
* acf_json_encode
*
* Returns json_encode() ready for file / database use.
*
* @since 5.0.0
*
* @param array $json The array of data to encode.
* @return string
*/
function acf_json_encode( $json ) {
return json_encode( $json, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE );
}
/**
* acf_str_exists
*
* This function will return true if a sub string is found
*
* @since 5.0.0
*
* @param $needle (string)
* @param $haystack (string)
* @return (boolean)
*/
function acf_str_exists( $needle, $haystack ) {
// return true if $haystack contains the $needle
if ( is_string( $haystack ) && strpos( $haystack, $needle ) !== false ) {
return true;
}
// return
return false;
}
/**
* A legacy function designed for developer debugging.
*
* @deprecated 6.2.6 Removed for security, but keeping the definition in case third party devs have it in their code.
* @since 5.0.0
*
* @return false
*/
function acf_debug() {
_deprecated_function( __FUNCTION__, '6.2.7' );
return false;
}
/**
* A legacy function designed for developer debugging.
*
* @deprecated 6.2.6 Removed for security, but keeping the definition in case third party devs have it in their code.
* @since 5.0.0
*
* @return false
*/
function acf_debug_start() {
_deprecated_function( __FUNCTION__, '6.2.7' );
return false;
}
/**
* A legacy function designed for developer debugging.
*
* @deprecated 6.2.6 Removed for security, but keeping the definition in case third party devs have it in their code.
* @since 5.0.0
*
* @return false
*/
function acf_debug_end() {
_deprecated_function( __FUNCTION__, '6.2.7' );
return false;
}
/**
* acf_encode_choices
*
* description
*
* @since 5.0.0
*
* @param $post_id (int)
* @return $post_id (int)
*/
function acf_encode_choices( $array = array(), $show_keys = true ) {
// bail early if not array (maybe a single string)
if ( ! is_array( $array ) ) {
return $array;
}
// bail early if empty array
if ( empty( $array ) ) {
return '';
}
// vars
$string = '';
// if allowed to show keys (good for choices, not for default values)
if ( $show_keys ) {
// loop
foreach ( $array as $k => $v ) {
// ignore if key and value are the same
if ( strval( $k ) == strval( $v ) ) {
continue;
}
// show key in the value
$array[ $k ] = $k . ' : ' . $v;
}
}
// implode
$string = implode( "\n", $array );
// return
return $string;
}
function acf_decode_choices( $string = '', $array_keys = false ) {
// bail early if already array
if ( is_array( $string ) ) {
return $string;
// allow numeric values (same as string)
} elseif ( is_numeric( $string ) ) {
// do nothing
// bail early if not a string
} elseif ( ! is_string( $string ) ) {
return array();
// bail early if is empty string
} elseif ( $string === '' ) {
return array();
}
// vars
$array = array();
// explode
$lines = explode( "\n", $string );
// key => value
foreach ( $lines as $line ) {
// vars
$k = trim( $line );
$v = trim( $line );
// look for ' : '
if ( acf_str_exists( ' : ', $line ) ) {
$line = explode( ' : ', $line );
$k = trim( $line[0] );
$v = trim( $line[1] );
}
// append
$array[ $k ] = $v;
}
// return only array keys? (good for checkbox default_value)
if ( $array_keys ) {
return array_keys( $array );
}
// return
return $array;
}
/**
* acf_str_replace
*
* This function will replace an array of strings much like str_replace
* The difference is the extra logic to avoid replacing a string that has alread been replaced
* This is very useful for replacing date characters as they overlap with eachother
*
* @since 5.3.8
*
* @param $post_id (int)
* @return $post_id (int)
*/
function acf_str_replace( $string = '', $search_replace = array() ) {
// vars
$ignore = array();
// remove potential empty search to avoid PHP error
unset( $search_replace[''] );
// loop over conversions
foreach ( $search_replace as $search => $replace ) {
// ignore this search, it was a previous replace
if ( in_array( $search, $ignore ) ) {
continue;
}
// bail early if subsctring not found
if ( strpos( $string, $search ) === false ) {
continue;
}
// replace
$string = str_replace( $search, $replace, $string );
// append to ignore
$ignore[] = $replace;
}
// return
return $string;
}
/**
* date & time formats
*
* These settings contain an association of format strings from PHP => JS
*
* @since 5.3.8
*
* @param n/a
* @return n/a
*/
acf_update_setting(
'php_to_js_date_formats',
array(
// Year
'Y' => 'yy', // Numeric, 4 digits 1999, 2003
'y' => 'y', // Numeric, 2 digits 99, 03
// Month
'm' => 'mm', // Numeric, with leading zeros 01–12
'n' => 'm', // Numeric, without leading zeros 1–12
'F' => 'MM', // Textual full January – December
'M' => 'M', // Textual three letters Jan - Dec
// Weekday
'l' => 'DD', // Full name (lowercase 'L') Sunday – Saturday
'D' => 'D', // Three letter name Mon – Sun
// Day of Month
'd' => 'dd', // Numeric, with leading zeros 01–31
'j' => 'd', // Numeric, without leading zeros 1–31
'S' => '', // The English suffix for the day of the month st, nd or th in the 1st, 2nd or 15th.
)
);
acf_update_setting(
'php_to_js_time_formats',
array(
'a' => 'tt', // Lowercase Ante meridiem and Post meridiem am or pm
'A' => 'TT', // Uppercase Ante meridiem and Post meridiem AM or PM
'h' => 'hh', // 12-hour format of an hour with leading zeros 01 through 12
'g' => 'h', // 12-hour format of an hour without leading zeros 1 through 12
'H' => 'HH', // 24-hour format of an hour with leading zeros 00 through 23
'G' => 'H', // 24-hour format of an hour without leading zeros 0 through 23
'i' => 'mm', // Minutes with leading zeros 00 to 59
's' => 'ss', // Seconds, with leading zeros 00 through 59
)
);
/**
* acf_split_date_time
*
* This function will split a format string into seperate date and time
*
* @since 5.3.8
*
* @param $date_time (string)
* @return $formats (array)
*/
function acf_split_date_time( $date_time = '' ) {
// vars
$php_date = acf_get_setting( 'php_to_js_date_formats' );
$php_time = acf_get_setting( 'php_to_js_time_formats' );
$chars = str_split( $date_time );
$type = 'date';
// default
$data = array(
'date' => '',
'time' => '',
);
// loop
foreach ( $chars as $i => $c ) {
// find type
// - allow misc characters to append to previous type
if ( isset( $php_date[ $c ] ) ) {
$type = 'date';
} elseif ( isset( $php_time[ $c ] ) ) {
$type = 'time';
}
// append char
$data[ $type ] .= $c;
}
// trim
$data['date'] = trim( $data['date'] );
$data['time'] = trim( $data['time'] );
// return
return $data;
}
/**
* acf_convert_date_to_php
*
* This fucntion converts a date format string from JS to PHP
*
* @since 5.0.0
*
* @param $date (string)
* @return (string)
*/
function acf_convert_date_to_php( $date = '' ) {
// vars
$php_to_js = acf_get_setting( 'php_to_js_date_formats' );
$js_to_php = array_flip( $php_to_js );
// return
return acf_str_replace( $date, $js_to_php );
}
/**
* acf_convert_date_to_js
*
* This fucntion converts a date format string from PHP to JS
*
* @since 5.0.0
*
* @param $date (string)
* @return (string)
*/
function acf_convert_date_to_js( $date = '' ) {
// vars
$php_to_js = acf_get_setting( 'php_to_js_date_formats' );
// return
return acf_str_replace( $date, $php_to_js );
}
/**
* acf_convert_time_to_php
*
* This fucntion converts a time format string from JS to PHP
*
* @since 5.0.0
*
* @param $time (string)
* @return (string)
*/
function acf_convert_time_to_php( $time = '' ) {
// vars
$php_to_js = acf_get_setting( 'php_to_js_time_formats' );
$js_to_php = array_flip( $php_to_js );
// return
return acf_str_replace( $time, $js_to_php );
}
/**
* acf_convert_time_to_js
*
* This fucntion converts a date format string from PHP to JS
*
* @since 5.0.0
*
* @param $time (string)
* @return (string)
*/
function acf_convert_time_to_js( $time = '' ) {
// vars
$php_to_js = acf_get_setting( 'php_to_js_time_formats' );
// return
return acf_str_replace( $time, $php_to_js );
}
/**
* acf_update_user_setting
*
* description
*
* @since 5.0.0
*
* @param $post_id (int)
* @return $post_id (int)
*/
function acf_update_user_setting( $name, $value ) {
// get current user id
$user_id = get_current_user_id();
// get user settings
$settings = get_user_meta( $user_id, 'acf_user_settings', true );
// ensure array
$settings = acf_get_array( $settings );
// delete setting (allow 0 to save)
if ( acf_is_empty( $value ) ) {
unset( $settings[ $name ] );
// append setting
} else {
$settings[ $name ] = $value;
}
// update user data
return update_metadata( 'user', $user_id, 'acf_user_settings', $settings );
}
/**
* acf_get_user_setting
*
* description
*
* @since 5.0.0
*
* @param $post_id (int)
* @return $post_id (int)
*/
function acf_get_user_setting( $name = '', $default = false ) {
// get current user id
$user_id = get_current_user_id();
// get user settings
$settings = get_user_meta( $user_id, 'acf_user_settings', true );
// ensure array
$settings = acf_get_array( $settings );
// bail arly if no settings
if ( ! isset( $settings[ $name ] ) ) {
return $default;
}
// return
return $settings[ $name ];
}
/**
* acf_in_array
*
* description
*
* @since 5.0.0
*
* @param $post_id (int)
* @return $post_id (int)
*/
function acf_in_array( $value = '', $array = false ) {
// bail early if not array
if ( ! is_array( $array ) ) {
return false;
}
// find value in array
return in_array( $value, $array );
}
/**
* acf_get_valid_post_id
*
* This function will return a valid post_id based on the current screen / parameter
*
* @since 5.0.0
*
* @param $post_id (mixed)
* @return $post_id (mixed)
*/
function acf_get_valid_post_id( $post_id = 0 ) {
// allow filter to short-circuit load_value logic
$preload = apply_filters( 'acf/pre_load_post_id', null, $post_id );
if ( $preload !== null ) {
return $preload;
}
// vars
$_post_id = $post_id;
// if not $post_id, load queried object
if ( ! $post_id ) {
// try for global post (needed for setup_postdata)
$post_id = (int) get_the_ID();
// try for current screen
if ( ! $post_id ) {
$post_id = get_queried_object();
}
}
// $post_id may be an object.
// todo: Compare class types instead.
if ( is_object( $post_id ) ) {
// post
if ( isset( $post_id->post_type, $post_id->ID ) ) {
$post_id = $post_id->ID;
// user
} elseif ( isset( $post_id->roles, $post_id->ID ) ) {
$post_id = 'user_' . $post_id->ID;
// term
} elseif ( isset( $post_id->taxonomy, $post_id->term_id ) ) {
$post_id = 'term_' . $post_id->term_id;
// comment
} elseif ( isset( $post_id->comment_ID ) ) {
$post_id = 'comment_' . $post_id->comment_ID;
// default
} else {
$post_id = 0;
}
}
// allow for option == options
if ( $post_id === 'option' ) {
$post_id = 'options';
}
// append language code
if ( $post_id == 'options' ) {
$dl = acf_get_setting( 'default_language' );
$cl = acf_get_setting( 'current_language' );
if ( $cl && $cl !== $dl ) {
$post_id .= '_' . $cl;
}
}
// filter for 3rd party
$post_id = apply_filters( 'acf/validate_post_id', $post_id, $_post_id );
// return
return $post_id;
}
/**
* Checks whether field values stored on an ACF post ID belong to a password-protected
* post whose password has not been entered yet.
*
* @since 6.8.11
*
* @param integer|string $post_id ACF encoded post ID.
* @return boolean
*/
function acf_post_password_required( $post_id ) {
$decoded = acf_decode_post_id( $post_id );
return 'post' === $decoded['type'] && (int) $decoded['id'] > 0 && post_password_required( (int) $decoded['id'] );
}
/**
* acf_get_post_id_info
*
* This function will return the type and id for a given $post_id string
*
* @since 5.4.0
*
* @param $post_id (mixed)
* @return $info (array)
*/
function acf_get_post_id_info( $post_id = 0 ) {
// vars
$info = array(
'type' => 'post',
'id' => 0,
);
// bail early if no $post_id
if ( ! $post_id ) {
return $info;
}
// check cache
// - this function will most likely be called multiple times (saving loading fields from post)
// $cache_key = "get_post_id_info/post_id={$post_id}";
// if( acf_isset_cache($cache_key) ) return acf_get_cache($cache_key);
// numeric
if ( is_numeric( $post_id ) ) {
$info['id'] = (int) $post_id;
// string
} elseif ( is_string( $post_id ) ) {
// vars
$glue = '_';
$type = explode( $glue, $post_id );
$id = array_pop( $type );
$type = implode( $glue, $type );
$meta = array( 'post', 'user', 'comment', 'term' );
// check if is taxonomy (ACF < 5.5)
// - avoid scenario where taxonomy exists with name of meta type
if ( ! in_array( $type, $meta ) && acf_isset_termmeta( $type ) ) {
$type = 'term';
}
// meta
if ( is_numeric( $id ) && in_array( $type, $meta ) ) {
$info['type'] = $type;
$info['id'] = (int) $id;
// option
} else {
$info['type'] = 'option';
$info['id'] = $post_id;
}
}
// update cache
// acf_set_cache($cache_key, $info);
// filter
$info = apply_filters( 'acf/get_post_id_info', $info, $post_id );
// return
return $info;
}
/**
* acf_isset_termmeta
*
* This function will return true if the termmeta table exists
* https://developer.wordpress.org/reference/functions/get_term_meta/
*
* @since 5.4.0
*
* @param $post_id (int)
* @return $post_id (int)
*/
function acf_isset_termmeta( $taxonomy = '' ) {
// bail early if no table
if ( get_option( 'db_version' ) < 34370 ) {
return false;
}
// check taxonomy
if ( $taxonomy && ! taxonomy_exists( $taxonomy ) ) {
return false;
}
// return
return true;
}
/**
* This function will walk through the $_FILES data and upload each found.
*
* @since 5.0.9
* @since 6.8.11 Uploader nonces are verified against the render IDs set by acf_set_upload_render_ids() for
* this save, or the post ID being saved. See _acf_verify_file_uploader_nonce().
*
* @param array $ancestors An internal parameter, not required.
*/
function acf_upload_files( $ancestors = array() ) {
if ( empty( $_FILES['acf'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Each file's uploader nonce is verified below.
return;
}
$file = acf_sanitize_files_array( $_FILES['acf'] ); // phpcs:disable WordPress.Security.NonceVerification.Missing -- Verified upstream.
// walk through ancestors.
if ( ! empty( $ancestors ) ) {
foreach ( $ancestors as $a ) {
foreach ( array_keys( $file ) as $k ) {
$file[ $k ] = $file[ $k ][ $a ];
}
}
}
// is array?
if ( is_array( $file['name'] ) ) {
foreach ( array_keys( $file['name'] ) as $k ) {
$_ancestors = array_merge( $ancestors, array( $k ) );
acf_upload_files( $_ancestors );
}
return;
}
// Bail early if file has error (no file uploaded).
if ( $file['error'] ) {
return;
}
$field_key = end( $ancestors );
if ( ! _acf_verify_file_uploader_nonce( $field_key ) ) {
return;
}
// Assign global _acfuploader for media validation.
$_POST['_acfuploader'] = $field_key;
// file found!
$attachment_id = acf_upload_file( $file );
// update $_POST
array_unshift( $ancestors, 'acf' );
acf_update_nested_array( $_POST, $ancestors, $attachment_id );
}
/**
* Returns true if the basic uploader nonce submitted for a field verifies for the object being saved.
*
* The nonce is verified against the render IDs set by acf_set_upload_render_ids() for this save, or
* the post ID being saved. Saves that never set render IDs, such as other plugins' form handlers,
* also accept the render IDs of the field lists in the request. The nonce itself is signed for the
* current user and render, so this only accepts nonces the user was served.
*
* @since 6.8.11
* @access private
*
* @param string $field_key The field key.
* @return boolean
*/
function _acf_verify_file_uploader_nonce( $field_key ) {
$nonce_name = $field_key . '_file_nonce';
if ( empty( $_REQUEST['acf'][ $nonce_name ] ) || ! is_string( $_REQUEST['acf'][ $nonce_name ] ) ) {
return false;
}
$nonce = sanitize_text_field( wp_unslash( $_REQUEST['acf'][ $nonce_name ] ) );
$upload_render_ids = acf_get_form_data( 'upload_render_ids' );
$render_ids = is_array( $upload_render_ids ) && $upload_render_ids ? $upload_render_ids : array( (string) acf_get_form_data( 'post_id' ) );
foreach ( $render_ids as $render_id ) {
if ( '' !== (string) $render_id && wp_verify_nonce( $nonce, acf_get_file_uploader_nonce_action( (string) $render_id, $field_key ) ) ) {
return true;
}
}
if ( null !== $upload_render_ids || ! isset( $_POST['_acf_fields'] ) || ! is_array( $_POST['_acf_fields'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Only the keys are read, as render IDs to verify the uploader nonce against.
return false;
}
foreach ( array_map( 'strval', array_keys( $_POST['_acf_fields'] ) ) as $render_id ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Only the keys are read.
if ( '' !== $render_id && wp_verify_nonce( $nonce, acf_get_file_uploader_nonce_action( $render_id, $field_key ) ) ) {
return true;
}
}
return false;
}
/**
* acf_upload_file
*
* This function will uploade a $_FILE
*
* @since 5.0.9
*
* @param $uploaded_file (array) array found from $_FILE data
* @return $id (int) new attachment ID
*/
function acf_upload_file( $uploaded_file ) {
// required
// require_once( ABSPATH . "/wp-load.php" ); // WP should already be loaded
require_once ABSPATH . '/wp-admin/includes/media.php'; // video functions
require_once ABSPATH . '/wp-admin/includes/file.php';
require_once ABSPATH . '/wp-admin/includes/image.php';
// required for wp_handle_upload() to upload the file
$upload_overrides = array( 'test_form' => false );
// Restrict uploads to image MIME types for image and gallery fields.
// phpcs:disable WordPress.Security.NonceVerification.Missing -- Field key is used for validation context only.
if ( ! empty( $_POST['_acfuploader'] ) ) {
$field = acf_get_field( sanitize_text_field( wp_unslash( $_POST['_acfuploader'] ) ) );
if ( acf_is_image_field( $field ) ) {
$upload_overrides['mimes'] = acf_get_image_mime_types( $field );
}
}
// phpcs:enable WordPress.Security.NonceVerification.Missing
// upload
$file = wp_handle_upload( $uploaded_file, $upload_overrides );
// bail early if upload failed
if ( isset( $file['error'] ) ) {
return $file['error'];
}
// vars
$url = $file['url'];
$type = $file['type'];
$file = $file['file'];
$filename = basename( $file );
// Reject any PDF without a %PDF- header.
if ( 'application/pdf' === $type ) {
$head = file_get_contents( $file, false, null, 0, 1024 );
if ( false === $head || 0 !== strpos( ltrim( $head, "\x00..\x20" ), '%PDF-' ) ) {
wp_delete_file( $file );
return __( 'Sorry, this file could not be uploaded.', 'acf' );
}
}
// Construct the object array
$object = array(
'post_title' => $filename,
'post_mime_type' => $type,
'guid' => $url,
);
// Save the data
$id = wp_insert_attachment( $object, $file );
/**
* Filters whether a file uploaded through a basic uploader gets preview images.
*
* Applies to File, Image and Gallery fields using the basic uploader, such as in
* front-end forms. When false, WordPress won't render PDF previews or save cover
* art embedded in audio and video files while generating the attachment metadata.
*
* @since 6.8.11
*
* @param boolean $generate_previews Whether to generate preview images. Default true only for users who can upload files.
* @param string $file Path to the uploaded file.
* @param integer $id Attachment ID.
*/
$skip_previews = ! apply_filters( 'acf/upload/generate_previews', current_user_can( 'upload_files' ), $file, $id );
if ( $skip_previews ) {
// Dedicated callbacks, so removing them can't also remove a matching filter added by a site.
$no_preview_sizes = static function () {
return array();
};
$no_cover_art = static function ( $metadata ) {
if ( is_array( $metadata ) ) {
unset( $metadata['image'] );
}
return $metadata;
};
add_filter( 'fallback_intermediate_image_sizes', $no_preview_sizes, PHP_INT_MAX );
add_filter( 'wp_read_audio_metadata', $no_cover_art, PHP_INT_MAX );
add_filter( 'wp_read_video_metadata', $no_cover_art, PHP_INT_MAX );
}
try {
// Add the meta-data
wp_update_attachment_metadata( $id, wp_generate_attachment_metadata( $id, $file ) );
} finally {
// Always detach the temporary filters so they can't affect anything else in this request.
if ( $skip_previews ) {
remove_filter( 'fallback_intermediate_image_sizes', $no_preview_sizes, PHP_INT_MAX );
remove_filter( 'wp_read_audio_metadata', $no_cover_art, PHP_INT_MAX );
remove_filter( 'wp_read_video_metadata', $no_cover_art, PHP_INT_MAX );
}
}
/** This action is documented in wp-admin/custom-header.php */
do_action( 'wp_create_file_in_uploads', $file, $id ); // For replication
// return new ID
return $id;
}
/**
* acf_update_nested_array
*
* This function will update a nested array value. Useful for modifying the $_POST array
*
* @since 5.0.9
*
* @param $array (array) target array to be updated
* @param $ancestors (array) array of keys to navigate through to find the child
* @param $value (mixed) The new value
* @return (boolean)
*/
function acf_update_nested_array( &$array, $ancestors, $value ) {
// if no more ancestors, update the current var
if ( empty( $ancestors ) ) {
$array = $value;
// return
return true;
}
// shift the next ancestor from the array
$k = array_shift( $ancestors );
// if exists
if ( isset( $array[ $k ] ) ) {
return acf_update_nested_array( $array[ $k ], $ancestors, $value );
}
// return
return false;
}
/**
* acf_is_screen
*
* This function will return true if all args are matched for the current screen
*
* @since 5.1.5
*
* @param $post_id (int)
* @return $post_id (int)
*/
function acf_is_screen( $id = '' ) {
// bail early if not defined
if ( ! function_exists( 'get_current_screen' ) ) {
return false;
}
// vars
$current_screen = get_current_screen();
// no screen
if ( ! $current_screen ) {
return false;
// array
} elseif ( is_array( $id ) ) {
return in_array( $current_screen->id, $id );
// string
} else {
return ( $id === $current_screen->id );
}
}
/**
* Check if we're in an ACF admin screen
*
* @since 6.2.2
*
* @return boolean Returns true if the current screen is an ACF admin screen.
*/
function acf_is_acf_admin_screen() {
if ( ! is_admin() || ! function_exists( 'get_current_screen' ) ) {
return false;
}
$screen = get_current_screen();
if ( $screen && ! empty( $screen->post_type ) && substr( $screen->post_type, 0, 4 ) === 'acf-' ) {
return true;
}
return false;
}
/**
* acf_maybe_get
*
* This function will return a var if it exists in an array
*
* @since 5.1.5
*
* @param $array (array) the array to look within
* @param $key (key) the array key to look for. Nested values may be found using '/'
* @param $default (mixed) the value returned if not found
* @return $post_id (int)
*/
function acf_maybe_get( $array = array(), $key = 0, $default = null ) {
return isset( $array[ $key ] ) ? $array[ $key ] : $default;
}
function acf_maybe_get_POST( $key = '', $default = null ) {
return isset( $_POST[ $key ] ) ? acf_sanitize_request_args( $_POST[ $key ] ) : $default; // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.NonceVerification.Missing -- Checked elsewhere.
}
function acf_maybe_get_GET( $key = '', $default = null ) {
return isset( $_GET[ $key ] ) ? acf_sanitize_request_args( $_GET[ $key ] ) : $default; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Checked elsewhere.
}
/**
* Returns an array of attachment data.
*
* @since 5.1.5
*
* @param integer|WP_Post The attachment ID or object
* @return array|false
*/
function acf_get_attachment( $attachment ) {
// Allow filter to short-circuit load attachment logic.
// Alternatively, this filter may be used to switch blogs for multisite media functionality.
$response = apply_filters( 'acf/pre_load_attachment', null, $attachment );
if ( $response !== null ) {
return $response;
}
// Get the attachment post object.
$attachment = get_post( $attachment );
if ( ! $attachment ) {
return false;
}
if ( $attachment->post_type !== 'attachment' ) {
return false;
}
// Load various attachment details.
$meta = wp_get_attachment_metadata( $attachment->ID );
$attached_file = get_attached_file( $attachment->ID );
if ( strpos( $attachment->post_mime_type, '/' ) !== false ) {
list( $type, $subtype ) = explode( '/', $attachment->post_mime_type );
} else {
list( $type, $subtype ) = array( $attachment->post_mime_type, '' );
}
// Generate response.
$response = array(
'ID' => $attachment->ID,
'id' => $attachment->ID,
'title' => $attachment->post_title,
'filename' => wp_basename( $attached_file ),
'filesize' => 0,
'url' => wp_get_attachment_url( $attachment->ID ),
'link' => get_attachment_link( $attachment->ID ),
'alt' => get_post_meta( $attachment->ID, '_wp_attachment_image_alt', true ),
'author' => $attachment->post_author,
'description' => $attachment->post_content,
'caption' => $attachment->post_excerpt,
'name' => $attachment->post_name,
'status' => $attachment->post_status,
'uploaded_to' => $attachment->post_parent,
'date' => $attachment->post_date_gmt,
'modified' => $attachment->post_modified_gmt,
'menu_order' => $attachment->menu_order,
'mime_type' => $attachment->post_mime_type,
'type' => $type,
'subtype' => $subtype,
'icon' => wp_mime_type_icon( $attachment->ID ),
);
// Append filesize data.
if ( isset( $meta['filesize'] ) ) {
$response['filesize'] = $meta['filesize'];
} else {
/**
* Allows shortcutting our ACF's `filesize` call to prevent us making filesystem calls.
* Mostly useful for third party plugins which may offload media to other services, and filesize calls will induce a remote download.
*
* @since 6.2.2
*
* @param int|null The default filesize.
* @param WP_Post $attachment The attachment post object we're looking for the filesize for.
*/
$shortcut_filesize = apply_filters( 'acf/filesize', null, $attachment );
if ( $shortcut_filesize ) {
$response['filesize'] = intval( $shortcut_filesize );
} elseif ( file_exists( $attached_file ) ) {
$response['filesize'] = filesize( $attached_file );
}
}
// Restrict the loading of image "sizes".
$sizes_id = 0;
// Type specific logic.
switch ( $type ) {
case 'image':
$sizes_id = $attachment->ID;
$src = wp_get_attachment_image_src( $attachment->ID, 'full' );
if ( $src ) {
$response['url'] = $src[0];
$response['width'] = $src[1];
$response['height'] = $src[2];
}
break;
case 'video':
$response['width'] = acf_maybe_get( $meta, 'width', 0 );
$response['height'] = acf_maybe_get( $meta, 'height', 0 );
if ( $featured_id = get_post_thumbnail_id( $attachment->ID ) ) {
$sizes_id = $featured_id;
}
break;
case 'audio':
if ( $featured_id = get_post_thumbnail_id( $attachment->ID ) ) {
$sizes_id = $featured_id;
}
break;
}
// Load array of image sizes.
if ( $sizes_id ) {
$sizes = get_intermediate_image_sizes();
$sizes_data = array();
foreach ( $sizes as $size ) {
$src = wp_get_attachment_image_src( $sizes_id, $size );
if ( $src ) {
$sizes_data[ $size ] = $src[0];
$sizes_data[ $size . '-width' ] = $src[1];
$sizes_data[ $size . '-height' ] = $src[2];
}
}
$response['sizes'] = $sizes_data;
}
/**
* Filters the attachment $response after it has been loaded.
*
* @since 5.9.0
*
* @param array $response Array of loaded attachment data.
* @param WP_Post $attachment Attachment object.
* @param array|false $meta Array of attachment meta data, or false if there is none.
*/
return apply_filters( 'acf/load_attachment', $response, $attachment, $meta );
}
/**
* This function will truncate and return a string
*
* @since 5.0.0
*
* @param string $text The text to truncate.
* @param integer $length The number of characters to allow in the string.
*
* @return string
*/
function acf_get_truncated( $text, $length = 64 ) {
$text = trim( $text );
$the_length = function_exists( 'mb_strlen' ) ? mb_strlen( $text ) : strlen( $text );
$cut_length = $length - 3;
$return = function_exists( 'mb_substr' ) ? mb_substr( $text, 0, $cut_length ) : substr( $text, 0, $cut_length );
if ( $the_length > $cut_length ) {
$return .= '...';
}
return $return;
}
/**
* acf_current_user_can_admin
*
* This function will return true if the current user can administrate the ACF field groups
*
* @since 5.1.5
*
* @param $post_id (int)
* @return $post_id (int)
*/
function acf_current_user_can_admin() {
if ( acf_get_setting( 'show_admin' ) && current_user_can( acf_get_setting( 'capability' ) ) ) {
return true;
}
// return
return false;
}
/**
* Wrapper function for current_user_can( 'edit_post', $post_id ).
*
* @since 6.3.4
*
* @param integer $post_id The post ID to check.
* @return boolean
*/
function acf_current_user_can_edit_post( int $post_id ): bool {
/**
* The `edit_post` capability is a meta capability, which
* gets converted to the correct post type object `edit_post`
* equivalent.
*
* If the post type does not have `map_meta_cap` enabled and the user is
* not manually mapping the `edit_post` capability, this will fail
* unless the role has the `edit_post` capability added to a user/role.
*
* However, more (core) stuff will likely break in this scenario.
*/
$user_can_edit = current_user_can( 'edit_post', $post_id );
return (bool) apply_filters( 'acf/current_user_can_edit_post', $user_can_edit, $post_id );
}
/**
* Checks if the current user can edit a given ACF context.
*
* Handles post, user, term, comment, woo_order, block, and option contexts returned by acf_decode_post_id().
*
* @since 6.7.2
*
* @param array $post_id_info The result of acf_decode_post_id(), containing 'type' and 'id'.
* @param string $options_page_slug Optional. The options page menu slug, used to look up the page's capability.
* @return boolean
*/
function acf_current_user_can_edit_in_context( array $post_id_info, string $options_page_slug = '' ): bool {
$type = $post_id_info['type'] ?? '';
$id = $post_id_info['id'] ?? 0;
switch ( $type ) {
case 'post':
return acf_current_user_can_edit_post( (int) $id );
case 'user':
return current_user_can( 'edit_user', (int) $id );
case 'term':
return current_user_can( 'edit_term', (int) $id );
case 'comment':
return current_user_can( 'edit_comment', (int) $id );
case 'woo_order':
return current_user_can( 'edit_shop_orders' ); // phpcs:ignore
case 'block':
return current_user_can( 'edit_posts' );
case 'option':
if ( ! empty( $options_page_slug ) && function_exists( 'acf_get_options_page' ) ) {
$page = acf_get_options_page( $options_page_slug );
if ( ! empty( $page['capability'] ) && ! empty( $page['post_id'] ) ) {
// Ensure the page's post_id matches the requested post_id.
if ( acf_get_valid_post_id( $page['post_id'] ) !== $id ) {
return false;
}
return current_user_can( $page['capability'] );
}
}
return current_user_can( 'manage_options' );
default:
return (bool) apply_filters( 'acf/current_user_can_edit_in_context', false, $post_id_info );
}
}
/**
* acf_get_filesize
*
* This function will return a numeric value of bytes for a given filesize string
*
* @since 5.1.5
*
* @param $size (mixed)
* @return (int)
*/
function acf_get_filesize( $size = 1 ) {
// vars
$unit = 'MB';
$units = array(
'TB' => 4,
'GB' => 3,
'MB' => 2,
'KB' => 1,
);
// look for $unit within the $size parameter (123 KB)
if ( is_string( $size ) ) {
// vars
$custom = strtoupper( substr( $size, -2 ) );
foreach ( $units as $k => $v ) {
if ( $custom === $k ) {
$unit = $k;
$size = substr( $size, 0, -2 );
}
}
}
// calc bytes
$bytes = floatval( $size ) * pow( 1024, $units[ $unit ] );
// return
return $bytes;
}
/**
* acf_format_filesize
*
* This function will return a formatted string containing the filesize and unit
*
* @since 5.1.5
*
* @param $size (mixed)
* @return (int)
*/
function acf_format_filesize( $size = 1 ) {
// convert
$bytes = acf_get_filesize( $size );
// vars
$units = array(
'TB' => 4,
'GB' => 3,
'MB' => 2,
'KB' => 1,
);
// loop through units
foreach ( $units as $k => $v ) {
$result = $bytes / pow( 1024, $v );
if ( $result >= 1 ) {
return $result . ' ' . $k;
}
}
// return
return $bytes . ' B';
}
/**
* acf_get_valid_terms
*
* This function will replace old terms with new split term ids
*
* @since 5.1.5
*
* @param $terms (int|array)
* @param $taxonomy (string)
* @return $terms
*/
function acf_get_valid_terms( $terms = false, $taxonomy = 'category' ) {
// force into array
$terms = acf_get_array( $terms );
// force ints
$terms = array_map( 'intval', $terms );
// bail early if function does not yet exist or
if ( ! function_exists( 'wp_get_split_term' ) || empty( $terms ) ) {
return $terms;
}
// attempt to find new terms
foreach ( $terms as $i => $term_id ) {
$new_term_id = wp_get_split_term( $term_id, $taxonomy );
if ( $new_term_id ) {
$terms[ $i ] = $new_term_id;
}
}
// return
return $terms;
}
/**
* Returns true when the field only accepts image uploads.
*
* @since 6.8.7
*
* @param array $field The field array.
* @return boolean
*/
function acf_is_image_field( $field ) {
if ( ! is_array( $field ) ) {
/**
* Filters whether the supplied field should be treated as an image field.
*
* @since 6.8.7
*
* @param bool $is_image_field Whether the field only accepts image uploads.
* @param array $field The field array.
*/
return (bool) apply_filters( 'acf/is_image_field', false, $field );
}
$is_image_field = in_array( acf_maybe_get( $field, 'type', '' ), array( 'image', 'gallery' ), true );
/**
* Filters whether the supplied field should be treated as an image field.
*
* @since 6.8.7
*
* @param bool $is_image_field Whether the field only accepts image uploads.
* @param array $field The field array.
*/
$field_type = acf_maybe_get( $field, 'type', '' );
$field_name = acf_maybe_get( $field, '_name', '' );
$field_key = acf_maybe_get( $field, 'key', '' );
$is_image_field = apply_filters( "acf/is_image_field/type={$field_type}", $is_image_field, $field );
$is_image_field = apply_filters( "acf/is_image_field/name={$field_name}", $is_image_field, $field );
$is_image_field = apply_filters( "acf/is_image_field/key={$field_key}", $is_image_field, $field );
$is_image_field = apply_filters( 'acf/is_image_field', $is_image_field, $field );
return $is_image_field;
}
/**
* Returns allowed MIME types restricted to images.
*
* @since 6.8.7
*
* @param array|false $field Optional field array for field-specific filtering.
* @return array
*/
function acf_get_image_mime_types( $field = false ) {
static $image_mimes = null;
if ( null === $image_mimes ) {
$image_mimes = array();
$allowed_mimes = get_allowed_mime_types();
foreach ( $allowed_mimes as $ext => $mime ) {
if ( wp_match_mime_types( 'image', $mime ) ) {
$image_mimes[ $ext ] = $mime;
}
}
/**
* Filters the image MIME types allowed for image and gallery fields.
*
* @since 6.8.7
*
* @param array $image_mimes The allowed image MIME types keyed by extension.
*/
$image_mimes = apply_filters( 'acf/get_image_mime_types', $image_mimes );
}
if ( ! is_array( $field ) || empty( $field['type'] ) ) {
return $image_mimes;
}
/**
* Filters the image MIME types allowed for a specific field.
*
* @since 6.8.7
*
* @param array $image_mimes The allowed image MIME types keyed by extension.
* @param array $field The field array.
*/
$field_type = acf_maybe_get( $field, 'type', '' );
$field_name = acf_maybe_get( $field, '_name', '' );
$field_key = acf_maybe_get( $field, 'key', '' );
$field_mimes = apply_filters( "acf/get_image_mime_types/type={$field_type}", $image_mimes, $field );
$field_mimes = apply_filters( "acf/get_image_mime_types/name={$field_name}", $field_mimes, $field );
$field_mimes = apply_filters( "acf/get_image_mime_types/key={$field_key}", $field_mimes, $field );
return $field_mimes;
}
/**
* Applies filters to image attachment validation errors.
*
* @since 6.8.7
*
* @param array $errors Validation errors.
* @param array $file Normalized file data.
* @param array $attachment Raw attachment data for the current context.
* @param array $field The field array.
* @param string $context The validation context.
* @return array
*/
function acf_apply_validate_is_image_attachment_filters( $errors, $file, $attachment, $field, $context ) {
/**
* Filters the errors for image attachment validation.
*
* @since 6.8.7
*
* @param array $errors An array of errors.
* @param array $file An array of data for a single file.
* @param array $attachment An array of attachment data which differs based on the context.
* @param array $field The field array.
* @param string $context The current context (uploading, preparing, basic_upload).
*/
$field_type = acf_maybe_get( $field, 'type', '' );
$field_name = acf_maybe_get( $field, '_name', '' );
$field_key = acf_maybe_get( $field, 'key', '' );
$errors = apply_filters( "acf/validate_is_image_attachment/type={$field_type}", $errors, $file, $attachment, $field, $context );
$errors = apply_filters( "acf/validate_is_image_attachment/name={$field_name}", $errors, $file, $attachment, $field, $context );
$errors = apply_filters( "acf/validate_is_image_attachment/key={$field_key}", $errors, $file, $attachment, $field, $context );
$errors = apply_filters( 'acf/validate_is_image_attachment', $errors, $file, $attachment, $field, $context );
return $errors;
}
/**
* Validates image attachment data during upload.
*
* @since 6.8.7
*
* @param array $errors Validation errors.
* @param array $file Normalized file data.
* @param array $attachment Raw attachment data for the current context.
* @param array $field The field array.
* @param string $context The validation context.
* @param string $error_message The error message to use when validation fails.
* @return array
*/
function acf_validate_is_image_attachment_upload( $errors, $file, $attachment, $field, $context, $error_message ) {
if ( empty( $attachment['tmp_name'] ) || ! file_exists( $attachment['tmp_name'] ) ) {
$errors['invalid_image'] = $error_message;
return acf_apply_validate_is_image_attachment_filters( $errors, $file, $attachment, $field, $context );
}
$checked = wp_check_filetype_and_ext( $attachment['tmp_name'], $attachment['name'], acf_get_image_mime_types( $field ) );
if ( empty( $checked['ext'] ) || empty( $checked['type'] ) || empty( wp_match_mime_types( 'image', $checked['type'] ) ) ) {
$errors['invalid_image'] = $error_message;
return acf_apply_validate_is_image_attachment_filters( $errors, $file, $attachment, $field, $context );
}
/**
* Filters the list of image MIME types that cannot be validated by
* wp_get_image_mime(), which reads a binary magic number and only
* recognizes raster formats. Vector / XML image types (e.g. SVG)
* always fail that check by design and must be excluded from it.
*
* @since 6.8.8
*
* @param array $vector_mime_types MIME types to skip the binary magic-number check for.
*/
$vector_mime_types = (array) apply_filters( 'acf/validate_is_image_attachment/vector_mime_types', array( 'image/svg+xml' ) );
if ( ! in_array( $checked['type'], $vector_mime_types, true ) ) {
$real_mime = wp_get_image_mime( $attachment['tmp_name'] );
if ( ! $real_mime || empty( wp_match_mime_types( 'image', $real_mime ) ) ) {
$errors['invalid_image'] = $error_message;
}
}
return acf_apply_validate_is_image_attachment_filters( $errors, $file, $attachment, $field, $context );
}
/**
* Validates image attachment data when preparing for the media library.
*
* @since 6.8.7
*
* @param array $errors Validation errors.
* @param array $file Normalized file data.
* @param array $attachment Raw attachment data for the current context.
* @param array $field The field array.
* @param string $context The validation context.
* @param string $error_message The error message to use when validation fails.
* @return array
*/
function acf_validate_is_image_attachment_prepare( $errors, $file, $attachment, $field, $context, $error_message ) {
$attachment_id = (int) acf_maybe_get( $attachment, 'id', 0 );
if ( $attachment_id ) {
if ( ! wp_attachment_is_image( $attachment_id ) ) {
$errors['invalid_image'] = $error_message;
}
return acf_apply_validate_is_image_attachment_filters( $errors, $file, $attachment, $field, $context );
}
$mime = acf_maybe_get( $attachment, 'mime', acf_maybe_get( $attachment, 'type', '' ) );
if ( $mime && empty( wp_match_mime_types( 'image', $mime ) ) ) {
$errors['invalid_image'] = $error_message;
}
return acf_apply_validate_is_image_attachment_filters( $errors, $file, $attachment, $field, $context );
}
/**
* Validates image attachment data during basic uploader pre-save checks.
*
* @since 6.8.7
*
* @param array $errors Validation errors.
* @param array $file Normalized file data.
* @param array $attachment Raw attachment data for the current context.
* @param array $field The field array.
* @param string $context The validation context.
* @param string $error_message The error message to use when validation fails.
* @return array
*/
function acf_validate_is_image_attachment_basic_upload( $errors, $file, $attachment, $field, $context, $error_message ) {
$mime = acf_maybe_get( $attachment, 'type', '' );
if ( $mime && empty( wp_match_mime_types( 'image', $mime ) ) ) {
$errors['invalid_image'] = $error_message;
}
return acf_apply_validate_is_image_attachment_filters( $errors, $file, $attachment, $field, $context );
}
/**
* Validates that an attachment is an image for image and gallery fields.
*
* @since 6.8.7
*
* @param array $errors Existing validation errors.
* @param array $file Normalized file data.
* @param array $attachment Raw attachment data for the current context.
* @param array $field The field array.
* @param string $context The validation context.
* @return array
*/
function acf_validate_is_image_attachment( $errors, $file, $attachment, $field, $context ) {
if ( ! acf_is_image_field( $field ) ) {
return $errors;
}
/**
* Filters the error message used when an attachment is not a valid image.
*
* @since 6.8.7
*
* @param string $error_message The default error message.
* @param array $field The field array.
* @param string $context The current context (uploading, preparing, basic_upload).
* @param array $file An array of data for a single file.
* @param array $attachment An array of attachment data which differs based on the context.
*/
$error_message = apply_filters( 'acf/validate_is_image_attachment/error_message', __( 'File must be a valid image.', 'acf' ), $field, $context, $file, $attachment );
if ( 'upload' === $context ) {
return acf_validate_is_image_attachment_upload( $errors, $file, $attachment, $field, $context, $error_message );
}
if ( 'prepare' === $context ) {
return acf_validate_is_image_attachment_prepare( $errors, $file, $attachment, $field, $context, $error_message );
}
return acf_validate_is_image_attachment_basic_upload( $errors, $file, $attachment, $field, $context, $error_message );
}
/**
* acf_validate_attachment
*
* This function will validate an attachment based on a field's restrictions and return an array of errors
*
* @since 5.2.3
*
* @param $attachment (array) attachment data. Changes based on context
* @param $field (array) field settings containing restrictions
* @param context (string) $file is different when uploading / preparing
* @return $errors (array)
*/
function acf_validate_attachment( $attachment, $field, $context = 'prepare' ) {
// vars
$errors = array();
$file = array(
'type' => '',
'width' => 0,
'height' => 0,
'size' => 0,
);
// upload
if ( $context == 'upload' ) {
// vars
$file['type'] = pathinfo( $attachment['name'], PATHINFO_EXTENSION );
$file['size'] = filesize( $attachment['tmp_name'] );
if ( acf_is_image_field( $field ) || strpos( $attachment['type'], 'image' ) !== false ) {
$size = function_exists( 'wp_getimagesize' ) ? wp_getimagesize( $attachment['tmp_name'] ) : getimagesize( $attachment['tmp_name'] );
if ( $size ) {
$file['width'] = acf_maybe_get( $size, 0 );
$file['height'] = acf_maybe_get( $size, 1 );
}
}
// prepare
} elseif ( $context == 'prepare' ) {
$use_path = isset( $attachment['filename'] ) ? $attachment['filename'] : $attachment['url'];
$file['type'] = pathinfo( $use_path, PATHINFO_EXTENSION );
$file['size'] = acf_maybe_get( $attachment, 'filesizeInBytes', 0 );
$file['width'] = acf_maybe_get( $attachment, 'width', 0 );
$file['height'] = acf_maybe_get( $attachment, 'height', 0 );
// custom
} else {
$file = array_merge( $file, $attachment );
$use_path = isset( $attachment['filename'] ) ? $attachment['filename'] : $attachment['url'];
$file['type'] = pathinfo( $use_path, PATHINFO_EXTENSION );
}
// image
if ( $file['width'] || $file['height'] ) {
// width
$min_width = (int) acf_maybe_get( $field, 'min_width', 0 );
$max_width = (int) acf_maybe_get( $field, 'max_width', 0 );
if ( $file['width'] ) {
if ( $min_width && $file['width'] < $min_width ) {
// min width
$errors['min_width'] = sprintf( __( 'Image width must be at least %dpx.', 'acf' ), $min_width );
} elseif ( $max_width && $file['width'] > $max_width ) {
// min width
$errors['max_width'] = sprintf( __( 'Image width must not exceed %dpx.', 'acf' ), $max_width );
}
}
// height
$min_height = (int) acf_maybe_get( $field, 'min_height', 0 );
$max_height = (int) acf_maybe_get( $field, 'max_height', 0 );
if ( $file['height'] ) {
if ( $min_height && $file['height'] < $min_height ) {
// min height
$errors['min_height'] = sprintf( __( 'Image height must be at least %dpx.', 'acf' ), $min_height );
} elseif ( $max_height && $file['height'] > $max_height ) {
// min height
$errors['max_height'] = sprintf( __( 'Image height must not exceed %dpx.', 'acf' ), $max_height );
}
}
}
// file size
if ( $file['size'] ) {
$min_size = acf_maybe_get( $field, 'min_size', 0 );
$max_size = acf_maybe_get( $field, 'max_size', 0 );
if ( $min_size && $file['size'] < acf_get_filesize( $min_size ) ) {
// min width
$errors['min_size'] = sprintf( __( 'File size must be at least %s.', 'acf' ), acf_format_filesize( $min_size ) );
} elseif ( $max_size && $file['size'] > acf_get_filesize( $max_size ) ) {
// min width
$errors['max_size'] = sprintf( __( 'File size must not exceed %s.', 'acf' ), acf_format_filesize( $max_size ) );
}
}
// file type
if ( $file['type'] ) {
$mime_types = acf_maybe_get( $field, 'mime_types', '' );
// lower case
$file['type'] = strtolower( $file['type'] );
$mime_types = strtolower( $mime_types );
// explode
$mime_types = str_replace( array( ' ', '.' ), '', $mime_types );
$mime_types = explode( ',', $mime_types ); // split pieces
$mime_types = array_filter( $mime_types ); // remove empty pieces
if ( ! empty( $mime_types ) && ! in_array( $file['type'], $mime_types ) ) {
// glue together last 2 types
if ( count( $mime_types ) > 1 ) {
$last1 = array_pop( $mime_types );
$last2 = array_pop( $mime_types );
$mime_types[] = $last2 . ' ' . __( 'or', 'acf' ) . ' ' . $last1;
}
$errors['mime_types'] = sprintf( __( 'File type must be %s.', 'acf' ), implode( ', ', $mime_types ) );
}
}
/**
* Filters the errors for a file before it is uploaded or displayed in the media modal.
*
* @since 5.2.3
*
* @param array $errors An array of errors.
* @param array $file An array of data for a single file.
* @param array $attachment An array of attachment data which differs based on the context.
* @param array $field The field array.
* @param string $context The curent context (uploading, preparing)
*/
$errors = apply_filters( "acf/validate_attachment/type={$field['type']}", $errors, $file, $attachment, $field, $context );
$errors = apply_filters( "acf/validate_attachment/name={$field['_name']}", $errors, $file, $attachment, $field, $context );
$errors = apply_filters( "acf/validate_attachment/key={$field['key']}", $errors, $file, $attachment, $field, $context );
$errors = apply_filters( 'acf/validate_attachment', $errors, $file, $attachment, $field, $context );
// return
return $errors;
}
/**
* _acf_settings_uploader
*
* Dynamic logic for uploader setting
*
* @since 5.2.3
*
* @param $uploader (string)
* @return $uploader
*/
add_filter( 'acf/settings/uploader', '_acf_settings_uploader' );
function _acf_settings_uploader( $uploader ) {
// if can't upload files
if ( ! current_user_can( 'upload_files' ) ) {
$uploader = 'basic';
}
// return
return $uploader;
}
/**
* acf_translate
*
* This function will translate a string using the new 'l10n_textdomain' setting
* Also works for arrays which is great for fields - select -> choices
*
* @since 5.3.2
*
* @param $string (mixed) string or array containins strings to be translated
* @return $string
*/
function acf_translate( $string ) {
// vars
$l10n = acf_get_setting( 'l10n' );
$textdomain = acf_get_setting( 'l10n_textdomain' );
// bail early if not enabled
if ( ! $l10n ) {
return $string;
}
// bail early if no textdomain
if ( ! $textdomain ) {
return $string;
}
// is array
if ( is_array( $string ) ) {
return array_map( 'acf_translate', $string );
}
// bail early if not string
if ( ! is_string( $string ) ) {
return $string;
}
// bail early if empty
if ( $string === '' ) {
return $string;
}
// allow for var_export export
if ( acf_get_setting( 'l10n_var_export' ) ) {
// bail early if already translated
if ( substr( $string, 0, 7 ) === '!!__(!!' ) {
return $string;
}
// return
return "!!__(!!'" . $string . "!!', !!'" . $textdomain . "!!')!!";
}
// vars
return __( $string, $textdomain );
}
/**
* acf_maybe_add_action
*
* This function will determine if the action has already run before adding / calling the function
*
* @since 5.3.2
*
* @param $post_id (int)
* @return $post_id (int)
*/
function acf_maybe_add_action( $tag, $function_to_add, $priority = 10, $accepted_args = 1 ) {
// if action has already run, execute it
// - if currently doing action, allow $tag to be added as per usual to allow $priority ordering needed for 3rd party asset compatibility
if ( did_action( $tag ) && ! doing_action( $tag ) ) {
call_user_func( $function_to_add );
// if action has not yet run, add it
} else {
add_action( $tag, $function_to_add, $priority, $accepted_args );
}
}
/**
* acf_is_row_collapsed
*
* This function will return true if the field's row is collapsed
*
* @since 5.3.2
*
* @param $post_id (int)
* @return $post_id (int)
*/
function acf_is_row_collapsed( $field_key = '', $row_index = 0 ) {
// collapsed
$collapsed = acf_get_user_setting( 'collapsed_' . $field_key, '' );
// cookie fallback ( version < 5.3.2 )
if ( $collapsed === '' ) {
$collapsed = acf_extract_var( $_COOKIE, "acf_collapsed_{$field_key}", '' );
$collapsed = str_replace( '|', ',', $collapsed );
// update
acf_update_user_setting( 'collapsed_' . $field_key, $collapsed );
}
// explode
$collapsed = explode( ',', $collapsed );
$collapsed = array_filter( $collapsed, 'is_numeric' );
// collapsed class
return in_array( $row_index, $collapsed );
}
/**
* Return an image tag for the provided attachment ID
*
* @since 5.5.0
* @deprecated 6.3.2
*
* @param integer $attachment_id The attachment ID
* @param string $size The image size to use in the image tag.
* @return false
*/
function acf_get_attachment_image( $attachment_id = 0, $size = 'thumbnail' ) {
// report function as deprecated
_deprecated_function( __FUNCTION__, '6.3.2' );
return false;
}
/**
* acf_get_post_thumbnail
*
* This function will return a thumbail image url for a given post
*
* @since 5.3.8
*
* @param $post (obj)
* @param $size (mixed)
* @return (string)
*/
function acf_get_post_thumbnail( $post = null, $size = 'thumbnail' ) {
// vars
$data = array(
'url' => '',
'type' => '',
'html' => '',
);
// post
$post = get_post( $post );
// bail early if no post
if ( ! $post ) {
return $data;
}
// vars
$thumb_id = $post->ID;
$mime_type = acf_maybe_get( explode( '/', $post->post_mime_type ), 0 );
// attachment
if ( $post->post_type === 'attachment' ) {
// change $thumb_id
if ( $mime_type === 'audio' || $mime_type === 'video' ) {
$thumb_id = get_post_thumbnail_id( $post->ID );
}
// post
} else {
$thumb_id = get_post_thumbnail_id( $post->ID );
}
// try url
$data['url'] = wp_get_attachment_image_src( $thumb_id, $size );
$data['url'] = acf_maybe_get( $data['url'], 0 );
// default icon
if ( ! $data['url'] && $post->post_type === 'attachment' ) {
$data['url'] = wp_mime_type_icon( $post->ID );
$data['type'] = 'icon';
}
// html
$data['html'] = '<img src="' . $data['url'] . '" alt="" />';
// return
return $data;
}
/**
* acf_get_browser
*
* Returns the name of the current browser.
*
* @since 5.0.0
*
* @param void
* @return string
*/
function acf_get_browser() {
// Check server var.
if ( isset( $_SERVER['HTTP_USER_AGENT'] ) ) {
$agent = sanitize_text_field( $_SERVER['HTTP_USER_AGENT'] );
// Loop over search terms.
$browsers = array(
'Firefox' => 'firefox',
'Trident' => 'msie',
'MSIE' => 'msie',
'Edge' => 'edge',
'Chrome' => 'chrome',
'Safari' => 'safari',
);
foreach ( $browsers as $k => $v ) {
if ( strpos( $agent, $k ) !== false ) {
return $v;
}
}
}
// Return default.
return '';
}
/**
* acf_is_ajax
*
* This function will reutrn true if performing a wp ajax call
*
* @since 5.3.8
*
* @param n/a
* @return (boolean)
*/
function acf_is_ajax( $action = '' ) {
// vars
$is_ajax = false;
// check if is doing ajax
if ( defined( 'DOING_AJAX' ) && DOING_AJAX ) {
$is_ajax = true;
}
// phpcs:disable WordPress.Security.NonceVerification.Missing
// check $action
if ( $action && acf_maybe_get( $_POST, 'action' ) !== $action ) {
// phpcs:enable WordPress.Security.NonceVerification.Missing
$is_ajax = false;
}
// return
return $is_ajax;
}
/**
* Returns a date value in a formatted string.
*
* @since 5.3.8
*
* @param string $value The date value to format.
* @param string $format The format to use.
* @return string
*/
function acf_format_date( $value, $format ) {
// Bail early if no value or value is not what we expect.
if ( ! $value || ( ! is_string( $value ) && ! is_int( $value ) ) ) {
return $value;
}
// Numeric (either unix or YYYYMMDD).
if ( is_numeric( $value ) && strlen( $value ) !== 8 ) {
$unixtimestamp = $value;
} else {
$unixtimestamp = strtotime( $value );
}
return date_i18n( $format, $unixtimestamp );
}
/**
* Previously, deletes the debug.log file.
*
* @since 5.7.10
* @deprecated 6.2.7
*/
function acf_clear_log() {
_deprecated_function( __FUNCTION__, '6.2.7' );
return false;
}
/**
* acf_log
*
* description
*
* @since 5.3.8
*
* @param $post_id (int)
* @return $post_id (int)
*/
function acf_log() {
// vars
$args = func_get_args();
// loop
foreach ( $args as $i => $arg ) {
// array | object
if ( is_array( $arg ) || is_object( $arg ) ) {
$arg = print_r( $arg, true );
// bool
} elseif ( is_bool( $arg ) ) {
$arg = 'bool(' . ( $arg ? 'true' : 'false' ) . ')';
}
// update
$args[ $i ] = $arg;
}
// log
error_log( implode( ' ', $args ) );
}
/**
* acf_dev_log
*
* Used to log variables only if ACF_DEV is defined
*
* @since 5.7.4
*
* @param mixed
* @return void
*/
function acf_dev_log() {
if ( defined( 'ACF_DEV' ) && ACF_DEV ) {
call_user_func_array( 'acf_log', func_get_args() );
}
}
/**
* acf_doing
*
* This function will tell ACF what task it is doing
*
* @since 5.3.8
*
* @param $event (string)
* @param context (string)
* @return n/a
*/
function acf_doing( $event = '', $context = '' ) {
acf_update_setting( 'doing', $event );
acf_update_setting( 'doing_context', $context );
}
/**
* acf_is_doing
*
* This function can be used to state what ACF is doing, or to check
*
* @since 5.3.8
*
* @param $event (string)
* @param context (string)
* @return (boolean)
*/
function acf_is_doing( $event = '', $context = '' ) {
// vars
$doing = false;
// task
if ( acf_get_setting( 'doing' ) === $event ) {
$doing = true;
}
// context
if ( $context && acf_get_setting( 'doing_context' ) !== $context ) {
$doing = false;
}
// return
return $doing;
}
/**
* acf_is_plugin_active
*
* This function will return true if the ACF plugin is active
* - May be included within a theme or other plugin
*
* @since 5.4.0
*
* @param $basename (int)
* @return $post_id (int)
*/
function acf_is_plugin_active() {
// vars
$basename = acf_get_setting( 'basename' );
// ensure is_plugin_active() exists (not on frontend)
if ( ! function_exists( 'is_plugin_active' ) ) {
include_once ABSPATH . 'wp-admin/includes/plugin.php';
}
// return
return is_plugin_active( $basename );
}
/**
* acf_send_ajax_results
*
* This function will print JSON data for a Select2 AJAX query
*
* @since 5.4.0
*
* @param $response (array)
* @return n/a
*/
function acf_send_ajax_results( $response ) {
// validate
$response = wp_parse_args(
$response,
array(
'results' => array(),
'more' => false,
'limit' => 0,
)
);
// limit
if ( $response['limit'] && $response['results'] ) {
// vars
$total = 0;
foreach ( $response['results'] as $result ) {
// parent
++$total;
// children
if ( ! empty( $result['children'] ) ) {
$total += count( $result['children'] );
}
}
// calc
if ( $total >= $response['limit'] ) {
$response['more'] = true;
}
}
// return
wp_send_json( $response );
}
/**
* acf_is_sequential_array
*
* This function will return true if the array contains only numeric keys
*
* @source http://stackoverflow.com/questions/173400/how-to-check-if-php-array-is-associative-or-sequential
*
* @since 5.4.0
*
* @param $array (array)
* @return (boolean)
*/
function acf_is_sequential_array( $array ) {
// bail early if not array
if ( ! is_array( $array ) ) {
return false;
}
// loop
foreach ( $array as $key => $value ) {
// bail early if is string
if ( is_string( $key ) ) {
return false;
}
}
// return
return true;
}
/**
* acf_is_associative_array
*
* This function will return true if the array contains one or more string keys
*
* @source http://stackoverflow.com/questions/173400/how-to-check-if-php-array-is-associative-or-sequential
*
* @since 5.4.0
*
* @param $array (array)
* @return (boolean)
*/
function acf_is_associative_array( $array ) {
// bail early if not array
if ( ! is_array( $array ) ) {
return false;
}
// loop
foreach ( $array as $key => $value ) {
// bail early if is string
if ( is_string( $key ) ) {
return true;
}
}
// return
return false;
}
/**
* acf_add_array_key_prefix
*
* This function will add a prefix to all array keys
* Useful to preserve numeric keys when performing array_multisort
*
* @since 5.4.0
*
* @param $array (array)
* @param $prefix (string)
* @return (array)
*/
function acf_add_array_key_prefix( $array, $prefix ) {
// vars
$array2 = array();
// loop
foreach ( $array as $k => $v ) {
$k2 = $prefix . $k;
$array2[ $k2 ] = $v;
}
// return
return $array2;
}
/**
* acf_remove_array_key_prefix
*
* This function will remove a prefix to all array keys
* Useful to preserve numeric keys when performing array_multisort
*
* @since 5.4.0
*
* @param $array (array)
* @param $prefix (string)
* @return (array)
*/
function acf_remove_array_key_prefix( $array, $prefix ) {
// vars
$array2 = array();
$l = strlen( $prefix );
// loop
foreach ( $array as $k => $v ) {
$k2 = ( substr( $k, 0, $l ) === $prefix ) ? substr( $k, $l ) : $k;
$array2[ $k2 ] = $v;
}
// return
return $array2;
}
/**
* This function will remove the proticol from a url
* Used to allow licenses to remain active if a site is switched to https
*
* @since 5.5.4
*
* @param string $url The URL to strip the protocol from.
* @return string
*/
function acf_strip_protocol( $url ) {
// strip the protocol
return str_replace( array( 'http://', 'https://' ), '', $url );
}
/**
* This function will connect an attacment (image etc) to the post
* Used to connect attachements uploaded directly to media that have not been attaced to a post
*
* @since 5.8.0 Added filter to prevent connection.
* @since 5.5.4
*
* @param integer $attachment_id The attachment ID.
* @param integer $post_id The post ID.
* @return boolean True if attachment was connected.
*/
function acf_connect_attachment_to_post( $attachment_id = 0, $post_id = 0 ) {
// bail early if $attachment_id is not valid.
if ( ! $attachment_id || ! is_numeric( $attachment_id ) ) {
return false;
}
// bail early if $post_id is not valid.
if ( ! $post_id || ! is_numeric( $post_id ) ) {
return false;
}
/**
* Filters whether or not to connect the attachment.
*
* @since 5.8.0
*
* @param bool $bool Returning false will prevent the connection. Default true.
* @param int $attachment_id The attachment ID.
* @param int $post_id The post ID.
*/
if ( ! apply_filters( 'acf/connect_attachment_to_post', true, $attachment_id, $post_id ) ) {
return false;
}
// vars
$post = get_post( $attachment_id );
// Check if is valid post.
if ( $post && $post->post_type == 'attachment' && $post->post_parent == 0 ) {
// update
wp_update_post(
array(
'ID' => $post->ID,
'post_parent' => $post_id,
)
);
// return
return true;
}
// return
return true;
}
/**
* Encrypts a string using PHP.
* https://bhoover.com/using-php-openssl_encrypt-openssl_decrypt-encrypt-decrypt-data/
*
* @since 5.5.8
*
* @param string $data The data to encrypt.
* @param string $context Optional domain-separation label. When non-empty, the key derivation
* and HMAC input are bound to this label so a token minted for one
* context cannot be verified as belonging to another. When empty, the
* legacy key derivation is used for back-compat with existing callers.
* @return string|false Encrypted string, or false if encryption fails.
*/
function acf_encrypt( $data = '', string $context = '' ) {
if ( ! function_exists( 'openssl_encrypt' ) ) {
return false;
}
if ( '' === $context ) {
$key = wp_hash( 'acf_encrypt' );
$mac_key = wp_hash( 'acf_encrypt_mac' );
} else {
$key = wp_hash( 'acf_encrypt|' . $context );
$mac_key = wp_hash( 'acf_encrypt_mac|' . $context );
}
$iv = openssl_random_pseudo_bytes( openssl_cipher_iv_length( 'aes-256-cbc' ) );
if ( false === $iv ) {
return false;
}
$encrypted_data = openssl_encrypt( $data, 'aes-256-cbc', $key, 0, $iv );
if ( false === $encrypted_data ) {
return false;
}
$payload = $encrypted_data . '::' . $iv;
$mac_data = '' === $context ? $payload : ( $context . '|' . $payload );
$hmac = hash_hmac( 'sha256', $mac_data, $mac_key, true );
return base64_encode( $payload . $hmac );
}
/**
* Decrypts an encrypted string using PHP.
* https://bhoover.com/using-php-openssl_encrypt-openssl_decrypt-encrypt-decrypt-data/
*
* @since 5.5.8
*
* @param string $data The string to decrypt.
* @param string $context Optional domain-separation label. Must match the context passed to
* acf_encrypt() at mint time. When empty, the legacy key derivation is
* used for back-compat with existing callers.
* @return string|false Decrypted string, or false if the payload is malformed or decryption fails.
*/
function acf_decrypt( $data = '', string $context = '' ) {
if ( ! function_exists( 'openssl_decrypt' ) ) {
return false;
}
$raw = base64_decode( (string) $data, true );
if ( false === $raw ) {
return false;
}
if ( strlen( $raw ) <= 32 ) {
return false;
}
if ( '' === $context ) {
$key = wp_hash( 'acf_encrypt' );
$mac_key = wp_hash( 'acf_encrypt_mac' );
} else {
$key = wp_hash( 'acf_encrypt|' . $context );
$mac_key = wp_hash( 'acf_encrypt_mac|' . $context );
}
$hmac = substr( $raw, -32 );
$payload = substr( $raw, 0, -32 );
$mac_data = '' === $context ? $payload : ( $context . '|' . $payload );
$expected = hash_hmac( 'sha256', $mac_data, $mac_key, true );
if ( ! hash_equals( $expected, $hmac ) ) {
return false;
}
if ( strpos( $payload, '::' ) === false ) {
return false;
}
list( $encrypted_data, $iv ) = explode( '::', $payload, 2 );
return openssl_decrypt( $encrypted_data, 'aes-256-cbc', $key, 0, $iv );
}
/**
* acf_parse_markdown
*
* A very basic regex-based Markdown parser function based off [slimdown](https://gist.github.com/jbroadway/2836900).
*
* @since 5.7.2
*
* @param string $text The string to parse.
* @return string
*/
function acf_parse_markdown( $text = '' ) {
// trim
$text = trim( $text );
// rules
$rules = array(
'/=== (.+?) ===/' => '<h2>$1</h2>', // headings
'/== (.+?) ==/' => '<h3>$1</h3>', // headings
'/= (.+?) =/' => '<h4>$1</h4>', // headings
'/\[([^\[]+)\]\(([^\)]+)\)/' => '<a href="$2">$1</a>', // links
'/(\*\*)(.*?)\1/' => '<strong>$2</strong>', // bold
'/(\*)(.*?)\1/' => '<em>$2</em>', // intalic
'/`(.*?)`/' => '<code>$1</code>', // inline code
'/\n\*(.*)/' => "\n<ul>\n\t<li>$1</li>\n</ul>", // ul lists
'/\n[0-9]+\.(.*)/' => "\n<ol>\n\t<li>$1</li>\n</ol>", // ol lists
'/<\/ul>\s?<ul>/' => '', // fix extra ul
'/<\/ol>\s?<ol>/' => '', // fix extra ol
);
foreach ( $rules as $k => $v ) {
$text = preg_replace( $k, $v, $text );
}
// autop
$text = wpautop( $text );
// return
return $text;
}
/**
* acf_get_sites
*
* Returns an array of sites for a network.
*
* @since 5.4.0
*
* @param void
* @return array
*/
function acf_get_sites() {
$results = array();
$sites = get_sites( array( 'number' => 0 ) );
if ( $sites ) {
foreach ( $sites as $site ) {
$results[] = get_site( $site )->to_array();
}
}
return $results;
}
/**
* acf_convert_rules_to_groups
*
* Converts an array of rules from ACF4 to an array of groups for ACF5
*
* @since 5.7.4
*
* @param array $rules An array of rules.
* @param string $anyorall The anyorall setting used in ACF4. Defaults to 'any'.
* @return array
*/
function acf_convert_rules_to_groups( $rules, $anyorall = 'any' ) {
// vars
$groups = array();
$index = 0;
// loop
foreach ( $rules as $rule ) {
// extract vars
$group = acf_extract_var( $rule, 'group_no' );
$order = acf_extract_var( $rule, 'order_no' );
// calculate group if not defined
if ( $group === null ) {
$group = $index;
// use $anyorall to determine if a new group is needed
if ( $anyorall == 'any' ) {
++$index;
}
}
// calculate order if not defined
if ( $order === null ) {
$order = isset( $groups[ $group ] ) ? count( $groups[ $group ] ) : 0;
}
// append to group
$groups[ $group ][ $order ] = $rule;
// sort groups
ksort( $groups[ $group ] );
}
// sort groups
ksort( $groups );
// return
return $groups;
}
/**
* acf_register_ajax
*
* Regsiters an ajax callback.
*
* @since 5.7.7
*
* @param string $name The ajax action name.
* @param array $callback The callback function or array.
* @param boolean $public Whether to allow access to non logged in users.
* @return void
*/
function acf_register_ajax( $name = '', $callback = false, $public = false ) {
// vars
$action = "acf/ajax/$name";
// add action for logged-in users
add_action( "wp_ajax_$action", $callback );
// add action for non logged-in users
if ( $public ) {
add_action( "wp_ajax_nopriv_$action", $callback );
}
}
/**
* acf_str_camel_case
*
* Converts a string into camelCase.
* Thanks to https://stackoverflow.com/questions/31274782/convert-array-keys-from-underscore-case-to-camelcase-recursively
*
* @since 5.8.0
*
* @param string $string The string ot convert.
* @return string
*/
function acf_str_camel_case( $string = '' ) {
return lcfirst( str_replace( ' ', '', ucwords( str_replace( '_', ' ', $string ) ) ) );
}
/**
* acf_array_camel_case
*
* Converts all aray keys to camelCase.
*
* @since 5.8.0
*
* @param array $array The array to convert.
* @return array
*/
function acf_array_camel_case( $array = array() ) {
$array2 = array();
foreach ( $array as $k => $v ) {
$array2[ acf_str_camel_case( $k ) ] = $v;
}
return $array2;
}
/**
* Returns true if the current screen is using the block editor.
*
* @since 5.8.0
*
* @return boolean
*/
function acf_is_block_editor() {
if ( function_exists( 'get_current_screen' ) ) {
$screen = get_current_screen();
if ( $screen && method_exists( $screen, 'is_block_editor' ) ) {
return $screen->is_block_editor();
}
}
return false;
}
/**
* Return an array of the WordPress reserved terms
*
* @since 6.1
*
* @return array The WordPress reserved terms list.
*/
function acf_get_wp_reserved_terms() {
return array( 'action', 'attachment', 'attachment_id', 'author', 'author_name', 'calendar', 'cat', 'category', 'category__and', 'category__in', 'category__not_in', 'category_name', 'comments_per_page', 'comments_popup', 'custom', 'customize_messenger_channel', 'customized', 'cpage', 'day', 'debug', 'embed', 'error', 'exact', 'feed', 'fields', 'hour', 'link', 'link_category', 'm', 'minute', 'monthnum', 'more', 'name', 'nav_menu', 'nonce', 'nopaging', 'offset', 'order', 'orderby', 'p', 'page', 'page_id', 'paged', 'pagename', 'pb', 'perm', 'post', 'post__in', 'post__not_in', 'post_format', 'post_mime_type', 'post_status', 'post_tag', 'post_type', 'posts', 'posts_per_archive_page', 'posts_per_page', 'preview', 'robots', 's', 'search', 'second', 'sentence', 'showposts', 'static', 'status', 'subpost', 'subpost_id', 'tag', 'tag__and', 'tag__in', 'tag__not_in', 'tag_id', 'tag_slug__and', 'tag_slug__in', 'taxonomy', 'tb', 'term', 'terms', 'theme', 'themes', 'title', 'type', 'types', 'w', 'withcomments', 'withoutcomments', 'year' );
}
/**
* Detect if we're on a multisite subsite.
*
* @since 6.2.4
*
* @return boolean true if we're in a multisite install and not on the main site
*/
function acf_is_multisite_sub_site() {
if ( is_multisite() && ! is_main_site() ) {
return true;
}
return false;
}
/**
* Detect if we're on a multisite main site.
*
* @since 6.2.4
*
* @return boolean true if we're in a multisite install and on the main site
*/
function acf_is_multisite_main_site() {
if ( is_multisite() && is_main_site() ) {
return true;
}
return false;
}
/**
* Allow filterable permissions metabox callbacks.
*
* @since 6.3.10
*
* @param boolean $enable_meta_box_cb_edit Can the current user edit metabox callbacks.
* @return boolean
*/
function acf_settings_enable_meta_box_cb_edit( $enable_meta_box_cb_edit ): bool {
if ( ! is_super_admin() ) {
return false;
}
return (bool) $enable_meta_box_cb_edit;
}
add_filter( 'acf/settings/enable_meta_box_cb_edit', 'acf_settings_enable_meta_box_cb_edit', 1 );