diff --git a/bin/create-release-zip.sh b/bin/create-release-zip.sh index a6e90b2d..2b8916fa 100755 --- a/bin/create-release-zip.sh +++ b/bin/create-release-zip.sh @@ -116,6 +116,17 @@ for dir in "${REQUIRED_DIRS[@]}"; do cp -r "$dir" "$PLUGIN_DIR/" done +# Remove development-only assets from the staged copy: +# - assets/src: uncompiled sources, never loaded at runtime +# - *.map files under assets/build: source maps for debugging only +# Note: unminified (non-.min) build files are kept on purpose — they are +# loaded at runtime when SCF_DEVELOPMENT_MODE is enabled. +# Note: assets/inc/select2/3 is kept on purpose — the legacy 'select2_version' +# setting can still select version 3 at runtime. +echo "Removing development-only asset files..." +rm -rf "$PLUGIN_DIR/assets/src" +find "$PLUGIN_DIR/assets/build" -type f -name "*.map" -delete + # Install production dependencies echo "Installing production dependencies..." composer install --no-dev --optimize-autoloader --no-interaction --prefer-dist diff --git a/includes/acf-field-functions.php b/includes/acf-field-functions.php index 308c57cf..be66a3de 100644 --- a/includes/acf-field-functions.php +++ b/includes/acf-field-functions.php @@ -162,7 +162,14 @@ function acf_get_field_post( $id = 0 ) { // Check $post_id and return the post when possible. if ( $post_id ) { - return get_post( $post_id ); + $post = get_post( $post_id ); + + // Prime sibling field lookups to avoid one query per field. + if ( $post && $post->post_parent ) { + _acf_prime_sibling_field_posts( $post ); + } + + return $post; } } @@ -170,6 +177,65 @@ function acf_get_field_post( $id = 0 ) { return false; } +/** + * Primes the field key lookup cache for all sibling fields of the given field post. + * + * Resolving a field key normally runs one query per field. When a field belongs + * to a field group, all fields of that group are loaded in a single (cached) + * query instead, so subsequent sibling key lookups avoid further queries. + * + * @since SCF 6.8.9 + * + * @param WP_Post $post The field post object. + * @return void + */ +function _acf_prime_sibling_field_posts( $post ) { + static $primed = array(); + + // Bail early if this parent was already primed during this request. + if ( isset( $primed[ $post->post_parent ] ) ) { + return; + } + + /** + * Filters whether sibling field lookups are primed when a field is loaded by key or name. + * + * Priming bypasses the per-key query (and any filters applied to it), so + * plugins which filter field queries per language/context can opt out. + * + * @since SCF 6.8.9 + * + * @param boolean $prime True to prime sibling field lookups. Default true. + */ + if ( ! apply_filters( 'acf/prime_field_group_fields', true ) ) { + return; + } + + // Only prime when the parent is a field group. + $parent = get_post( $post->post_parent ); + if ( ! $parent || 'acf-field-group' !== $parent->post_type ) { + return; + } + + $primed[ $post->post_parent ] = true; + + // Load all fields of the group in a single (cached) query. + $raw_fields = acf_get_raw_fields( $parent->ID ); + foreach ( $raw_fields as $raw_field ) { + $field_post = get_post( $raw_field['ID'] ); + + // Only prime published fields; a per-key query would not match other statuses. + if ( ! $field_post || 'publish' !== $field_post->post_status ) { + continue; + } + + $cache_key = acf_cache_key( "acf_get_field_post:key:{$raw_field['key']}" ); + if ( false === wp_cache_get( $cache_key, 'secure-custom-fields' ) ) { + wp_cache_set( $cache_key, $field_post->ID, 'secure-custom-fields' ); + } + } +} + /** * acf_is_field_key * diff --git a/includes/acf-meta-functions.php b/includes/acf-meta-functions.php index 67b7d653..c18eb7bb 100644 --- a/includes/acf-meta-functions.php +++ b/includes/acf-meta-functions.php @@ -1,4 +1,12 @@ prop( 'multisite', true ); /** * Returns an array of "ACF only" meta for the given post_id. @@ -56,6 +64,12 @@ function acf_get_option_meta( $prefix = '' ) { // Globals. global $wpdb; + // Check store. Invalidated by _acf_flush_option_meta_cache() on option writes. + $store = acf_get_store( 'option-meta' ); + if ( $store->has( $prefix ) ) { + return $store->get( $prefix ); + } + // Vars. $meta = array(); @@ -85,10 +99,37 @@ function acf_get_option_meta( $prefix = '' ) { $meta[ substr( $row['option_name'], $len ) ][] = $row['option_value']; } + // Cache results for repeat calls during this request. + $store->set( $prefix, $meta ); + // Return results. return $meta; } +/** + * Flushes the cached option meta for any prefix matching the written option. + * + * Option values are written via update_option()/delete_option() from several + * code paths, so the core option actions are used to invalidate the cache. + * + * @since SCF 6.8.9 + * + * @param string $option The name of the option being added, updated or deleted. + * @return void + */ +function _acf_flush_option_meta_cache( $option ) { + $store = acf_get_store( 'option-meta' ); + + foreach ( array_keys( $store->get_data() ) as $prefix ) { + if ( 0 === strpos( $option, "{$prefix}_" ) || 0 === strpos( $option, "_{$prefix}_" ) ) { + $store->remove( $prefix ); + } + } +} +add_action( 'added_option', '_acf_flush_option_meta_cache' ); +add_action( 'updated_option', '_acf_flush_option_meta_cache' ); +add_action( 'deleted_option', '_acf_flush_option_meta_cache' ); + /** * Retrieves specific metadata from the database. * diff --git a/includes/admin/admin-commands.php b/includes/admin/admin-commands.php index 19970b68..5cef3f5d 100644 --- a/includes/admin/admin-commands.php +++ b/includes/admin/admin-commands.php @@ -26,8 +26,10 @@ * @since SCF 6.5.0 */ function acf_commands_init() { - // Ensure we only load our commands where the WordPress commands API is available. - if ( ! wp_script_is( 'wp-commands', 'registered' ) ) { + // Ensure we only load our commands on screens where the command palette itself loads. + // Core enqueues 'wp-commands' in the block/site editors (and on all admin screens + // since WP 6.9 via wp_enqueue_command_palette_assets()); elsewhere there is no palette. + if ( ! wp_script_is( 'wp-commands', 'enqueued' ) ) { return; } @@ -42,4 +44,5 @@ function acf_commands_init() { } } -add_action( 'admin_enqueue_scripts', 'acf_commands_init' ); +// Priority 20 ensures this runs after core has enqueued the command palette assets (priority 10). +add_action( 'admin_enqueue_scripts', 'acf_commands_init', 20 ); diff --git a/includes/admin/admin.php b/includes/admin/admin.php index 445908e5..4455a15b 100644 --- a/includes/admin/admin.php +++ b/includes/admin/admin.php @@ -55,6 +55,12 @@ public function admin_menu() { */ public function admin_enqueue_scripts() { wp_enqueue_style( 'acf-global' ); + + // Only load the escaped HTML notice assets when the notice will render. + if ( ! $this->should_show_escaped_html_notice() ) { + return; + } + wp_enqueue_script( 'acf-escaped-html-notice' ); wp_localize_script( @@ -141,26 +147,37 @@ public function maybe_show_import_from_cptui_notice() { * @since ACF 6.2.5 */ public function maybe_show_escaped_html_notice() { + // Notice for when HTML has already been escaped. + if ( $this->should_show_escaped_html_notice() ) { + acf_get_view( 'escaped-html-notice', array( 'acf_escaped' => _acf_get_escaped_html_log() ) ); + } + } + + /** + * Checks if the escaped unsafe HTML notice should be rendered. + * + * @since SCF 6.8.9 + * + * @return boolean + */ + private function should_show_escaped_html_notice() { // Only show to editors and above. if ( ! current_user_can( 'edit_others_posts' ) ) { - return; + return false; } // Allow opting-out of the notice. if ( apply_filters( 'acf/admin/prevent_escaped_html_notice', false ) ) { - return; + return false; } if ( get_option( 'acf_escaped_html_notice_dismissed' ) ) { - return; + return false; } $escaped = _acf_get_escaped_html_log(); - // Notice for when HTML has already been escaped. - if ( ! empty( $escaped ) ) { - acf_get_view( 'escaped-html-notice', array( 'acf_escaped' => $escaped ) ); - } + return ! empty( $escaped ); } /** diff --git a/includes/api/api-helpers.php b/includes/api/api-helpers.php index 0bac01e2..4e91c5da 100644 --- a/includes/api/api-helpers.php +++ b/includes/api/api-helpers.php @@ -1028,12 +1028,12 @@ function acf_decode_taxonomy_term( $value ) { // allow for term_id (Used by ACF v4) if ( is_numeric( $data['term'] ) ) { - // global - global $wpdb; - - // find taxonomy + // find taxonomy (uses the term cache instead of a direct query) if ( ! $data['taxonomy'] ) { - $data['taxonomy'] = $wpdb->get_var( $wpdb->prepare( "SELECT taxonomy FROM $wpdb->term_taxonomy WHERE term_id = %d LIMIT 1", $data['term'] ) ); + $term_object = get_term( (int) $data['term'] ); + if ( $term_object instanceof WP_Term ) { + $data['taxonomy'] = $term_object->taxonomy; + } } // find term (may have numeric slug '123') diff --git a/includes/assets.php b/includes/assets.php index 5a00953f..05275ef8 100644 --- a/includes/assets.php +++ b/includes/assets.php @@ -43,6 +43,7 @@ class ACF_Assets { */ public function __construct() { add_action( 'init', array( $this, 'register_scripts' ) ); + add_action( 'enqueue_block_editor_assets', array( $this, 'enqueue_block_editor_assets' ) ); } /** @@ -115,7 +116,7 @@ public function register_scripts() { 'base' => 'assets/build/js/%s' . $suffix . '.js', ); $css_path_patterns = array( - 'pro' => 'assets/build/css/pro/%s.css', + 'pro' => 'assets/build/css/pro/%s' . $suffix . '.css', 'base' => 'assets/build/css/%s' . $suffix . '.css', ); $asset_path_patterns = array( @@ -493,6 +494,24 @@ public function enqueue_uploader() { do_action( 'acf/enqueue_uploader' ); } + /** + * Enqueues scripts that should only load in the block editor. + * + * The JS block bindings layer relies on block editor APIs and pulls in + * the block editor script stack, so it must not load on classic admin + * screens or front-end forms. + * + * @since SCF 6.8.9 + * + * @return void + */ + public function enqueue_block_editor_assets() { + // When the datastore is enabled, the bindings layer is handled by SCF\Blocks\Bindings_Editor instead. + if ( ! acf_is_using_datastore() ) { + wp_enqueue_script( 'scf-bindings' ); + } + } + /** * Enqueues and localizes scripts. * @@ -551,9 +570,8 @@ public function enqueue_scripts() { // @todo integrate into the above. Previously, they were simply hooked into the hook below. wp_enqueue_script( 'acf-pro-input' ); - wp_enqueue_script( 'acf-pro-ui-options-page' ); - if ( ! acf_is_using_datastore() ) { - wp_enqueue_script( 'scf-bindings' ); + if ( is_admin() ) { + wp_enqueue_script( 'acf-pro-ui-options-page' ); } wp_enqueue_style( 'acf-pro-input' ); diff --git a/includes/blocks.php b/includes/blocks.php index 71d0e7f2..17e45ff2 100644 --- a/includes/blocks.php +++ b/includes/blocks.php @@ -1529,6 +1529,11 @@ function acf_get_empty_block_form_html( $block_name ) { */ function acf_parse_save_blocks( $text = '' ) { + // Bail early if no ACF block types are registered. + if ( ! acf_get_block_types() ) { + return $text; + } + // Search text for dynamic blocks and modify attrs. return addslashes( preg_replace_callback( @@ -1910,6 +1915,11 @@ function acf_add_block_meta_values( $block, $post_id ) { * @return void */ function acf_save_block_meta_values( $post_id, $post ) { + // Bail early if no ACF block types are registered. + if ( ! acf_get_block_types() ) { + return; + } + $meta_values = acf_get_block_meta_values_to_save( $post->post_content ); if ( empty( $meta_values ) ) { diff --git a/includes/datastore.php b/includes/datastore.php index f561c133..49e7b9f6 100644 --- a/includes/datastore.php +++ b/includes/datastore.php @@ -20,8 +20,17 @@ * @return boolean */ function acf_is_using_datastore() { + // The WordPress version cannot change during a request, so compare it once. + // The filter below is intentionally not memoized as callbacks may be added + // or removed at runtime. + static $wp_supports_datastore = null; + + if ( null === $wp_supports_datastore ) { + $wp_supports_datastore = version_compare( get_bloginfo( 'version' ), '6.7', '>=' ); + } + // Bail if not on WordPress 6.7+. - if ( ! version_compare( get_bloginfo( 'version' ), '6.7', '>=' ) ) { + if ( ! $wp_supports_datastore ) { return false; } diff --git a/includes/local-json.php b/includes/local-json.php index 63b31580..00179edc 100644 --- a/includes/local-json.php +++ b/includes/local-json.php @@ -62,6 +62,31 @@ public function __construct() { } } + /** + * Decodes the JSON in the given file, caching the result for the current request. + * + * Local JSON files are scanned once per internal post type and decoded again + * when included, so without caching each file is decoded multiple times per + * request. The cache is keyed on path, modified time and size so changed + * files are decoded again. + * + * @since SCF 6.8.9 + * + * @param string $file The JSON file path. + * @return mixed The decoded JSON, or null on failure. + */ + private function decode_json_file( $file ) { + static $cache = array(); + + $key = $file . ':' . filemtime( $file ) . ':' . filesize( $file ); + + if ( ! array_key_exists( $key, $cache ) ) { + $cache[ $key ] = json_decode( file_get_contents( $file ), true ); + } + + return $cache[ $key ]; + } + /** * Returns true if this component is enabled. * @@ -297,7 +322,7 @@ public function include_fields() { // Get load paths. $files = $this->scan_files( 'acf-field-group' ); foreach ( $files as $key => $file ) { - $json = json_decode( file_get_contents( $file ), true ); + $json = $this->decode_json_file( $file ); $json['local'] = 'json'; $json['local_file'] = $file; acf_add_local_field_group( $json ); @@ -318,7 +343,7 @@ public function include_post_types() { // Get load paths. $files = $this->scan_files( 'acf-post-type' ); foreach ( $files as $key => $file ) { - $json = json_decode( file_get_contents( $file ), true ); + $json = $this->decode_json_file( $file ); $json['local'] = 'json'; $json['local_file'] = $file; acf_add_local_internal_post_type( $json, 'acf-post-type' ); @@ -339,7 +364,7 @@ public function include_taxonomies() { // Get load paths. $files = $this->scan_files( 'acf-taxonomy' ); foreach ( $files as $key => $file ) { - $json = json_decode( file_get_contents( $file ), true ); + $json = $this->decode_json_file( $file ); $json['local'] = 'json'; $json['local_file'] = $file; acf_add_local_internal_post_type( $json, 'acf-taxonomy' ); @@ -394,7 +419,7 @@ function scan_files( $post_type = 'acf-field-group' ) { } // Read JSON data. - $json = json_decode( file_get_contents( $file ), true ); + $json = $this->decode_json_file( $file ); if ( ! is_array( $json ) || ! isset( $json['key'] ) ) { continue; } @@ -430,7 +455,7 @@ public function get_files( $post_type = 'acf-field-group' ) { $files[ $key ] = $path; } elseif ( 'acf-field-group' === $post_type ) { // If we can't figure out the ACF post type, make an educated guess that it's a field group. - $json = json_decode( file_get_contents( $path ), true ); + $json = $this->decode_json_file( $path ); if ( ! is_array( $json ) ) { continue; } diff --git a/includes/third-party.php b/includes/third-party.php index 929648af..2d87bbb7 100644 --- a/includes/third-party.php +++ b/includes/third-party.php @@ -144,7 +144,7 @@ public function pts_allowed_pages( $pages ) { */ public function doing_dark_mode() { $min = defined( 'SCF_DEVELOPMENT_MODE' ) && SCF_DEVELOPMENT_MODE ? '' : '.min'; - wp_enqueue_style( 'acf-dark', acf_get_url( 'assets/css/acf-dark' . $min . '.css' ), array(), ACF_VERSION ); + wp_enqueue_style( 'acf-dark', acf_get_url( 'assets/build/css/acf-dark' . $min . '.css' ), array(), ACF_VERSION ); } } diff --git a/webpack.config.js b/webpack.config.js index 21d371e5..a2a04b50 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -98,7 +98,6 @@ const unminifiedConfig = { filename: '[name].css', // Output CSS as .css } ), new DependencyExtractionWebpackPlugin( { - injectPolyfill: true, useCombinedAssetFile: true, } ), ], @@ -132,7 +131,6 @@ const minifiedConfig = { filename: '[name].min.css', // Changed to output .min.css files } ), new DependencyExtractionWebpackPlugin( { - injectPolyfill: true, useCombinedAssetFile: true, } ), ],