@@ -745,7 +798,7 @@ public function print_assets_manager_exceptions( $type, $handle ) {
@@ -817,7 +870,7 @@ public function save_assets_manager_settings() {
! empty( $post_data['perform_assets_manager'] )
) {
- $current_id = get_queried_object_id();
+ $current_id = $this->get_current_object_id();
$filters = [ 'js', 'css', 'plugins', 'themes' ];
$options = get_option( 'perform_assets_manager_options' );
$settings = get_option( 'perform_assets_manager_settings' );
@@ -857,35 +910,35 @@ public function save_assets_manager_settings() {
'disabled' === $status &&
! empty( $value )
) {
- if ( 'everywhere' === $value ) {
- $options['disabled'][ $type ][ $handle ]['everywhere'] = 1;
+ if ( 'everywhere' === $value ) {
+ $options['disabled'][ $type ][ $handle ]['everywhere'] = 1;
- if ( ! empty( $options['disabled'][ $type ][ $handle ]['current'] ) ) {
- unset( $options['disabled'][ $type ][ $handle ]['current'] );
- }
- } elseif ( 'current' === $value ) {
+ if ( ! empty( $options['disabled'][ $type ][ $handle ]['current'] ) ) {
+ unset( $options['disabled'][ $type ][ $handle ]['current'] );
+ }
+ } elseif ( 'current' === $value ) {
- if ( isset( $options['disabled'][ $type ][ $handle ]['everywhere'] ) ) {
- unset( $options['disabled'][ $type ][ $handle ]['everywhere'] );
- }
+ if ( isset( $options['disabled'][ $type ][ $handle ]['everywhere'] ) ) {
+ unset( $options['disabled'][ $type ][ $handle ]['everywhere'] );
+ }
- if ( ! isset( $options['disabled'][ $type ][ $handle ]['current'] ) || ! is_array( $options['disabled'][ $type ][ $handle ]['current'] ) ) {
- $options['disabled'][ $type ][ $handle ]['current'] = [];
- }
+ $options['disabled'][ $type ][ $handle ]['current'] = $this->normalize_object_id_list( $options['disabled'][ $type ][ $handle ]['current'] ?? array() );
- if ( ! in_array( $current_id, $options['disabled'][ $type ][ $handle ]['current'], true ) ) {
- array_push( $options['disabled'][ $type ][ $handle ]['current'], $current_id );
+ if ( 0 < $current_id && ! in_array( $current_id, $options['disabled'][ $type ][ $handle ]['current'], true ) ) {
+ array_push( $options['disabled'][ $type ][ $handle ]['current'], $current_id );
+ }
}
- }
} else {
unset( $options['disabled'][ $type ][ $handle ]['everywhere'] );
if ( isset( $options['disabled'][ $type ][ $handle ]['current'] ) ) {
+ $options['disabled'][ $type ][ $handle ]['current'] = $this->normalize_object_id_list( $options['disabled'][ $type ][ $handle ]['current'] );
$current_key = array_search( $current_id, $options['disabled'][ $type ][ $handle ]['current'], true );
if ( false !== $current_key ) {
unset( $options['disabled'][ $type ][ $handle ]['current'][ $current_key ] );
+ $options['disabled'][ $type ][ $handle ]['current'] = array_values( $options['disabled'][ $type ][ $handle ]['current'] );
if ( empty( $options['disabled'][ $type ][ $handle ]['current'] ) ) {
unset( $options['disabled'][ $type ][ $handle ]['current'] );
@@ -928,26 +981,27 @@ public function save_assets_manager_settings() {
}
$current_value = is_array( $value ) && array_key_exists( 'current', $value ) ? $value['current'] : '';
- $has_current_exception = '' !== (string) $current_value;
+ $current_value = is_scalar( $current_value ) && is_numeric( $current_value ) ? (int) $current_value : 0;
+ $has_current_exception = 0 < $current_value;
if (
! $group_disabled &&
'disabled' === $status &&
$has_current_exception
) {
- if ( ! isset( $options['enabled'][ $type ][ $handle ]['current'] ) || ! is_array( $options['enabled'][ $type ][ $handle ]['current'] ) ) {
- $options['enabled'][ $type ][ $handle ]['current'] = [];
- }
+ $options['enabled'][ $type ][ $handle ]['current'] = $this->normalize_object_id_list( $options['enabled'][ $type ][ $handle ]['current'] ?? array() );
if ( ! in_array( $current_value, $options['enabled'][ $type ][ $handle ]['current'], true ) ) {
array_push( $options['enabled'][ $type ][ $handle ]['current'], $current_value );
}
} else {
if ( isset( $options['enabled'][ $type ][ $handle ]['current'] ) ) {
+ $options['enabled'][ $type ][ $handle ]['current'] = $this->normalize_object_id_list( $options['enabled'][ $type ][ $handle ]['current'] );
$current_key = array_search( $current_id, $options['enabled'][ $type ][ $handle ]['current'], true );
if ( false !== $current_key ) {
unset( $options['enabled'][ $type ][ $handle ]['current'][ $current_key ] );
+ $options['enabled'][ $type ][ $handle ]['current'] = array_values( $options['enabled'][ $type ][ $handle ]['current'] );
if ( empty( $options['enabled'][ $type ][ $handle ]['current'] ) ) {
unset( $options['enabled'][ $type ][ $handle ]['current'] );
@@ -1050,7 +1104,7 @@ public function dequeue_assets( $src, $handle ) {
// Load Assets Manager settings.
$options = get_option( 'perform_assets_manager_options' );
- $current_id = get_queried_object_id();
+ $current_id = $this->get_current_object_id();
$content_dirname = Helpers::get_content_dir_name();
// Get category + group from src.
@@ -1076,11 +1130,11 @@ public function dequeue_assets( $src, $handle ) {
) ||
(
! empty( $options['disabled'][ $type ][ $handle ]['current'] ) &&
- in_array( $current_id, $options['disabled'][ $type ][ $handle ]['current'], true )
+ $this->has_current_object_id( $current_id, $options['disabled'][ $type ][ $handle ]['current'] )
)
) {
- if ( ! empty( $options['enabled'][ $type ][ $handle ]['current'] ) && in_array( $current_id, $options['enabled'][ $type ][ $handle ]['current'], true ) ) {
+ if ( ! empty( $options['enabled'][ $type ][ $handle ]['current'] ) && $this->has_current_object_id( $current_id, $options['enabled'][ $type ][ $handle ]['current'] ) ) {
return $src;
}
diff --git a/src/Modules/Cache/PageCache.php b/src/Modules/Cache/PageCache.php
index a71c15e..69c144d 100644
--- a/src/Modules/Cache/PageCache.php
+++ b/src/Modules/Cache/PageCache.php
@@ -89,8 +89,8 @@ public function should_load(): bool {
* @return void
*/
public function register(): void {
- $this->cache_dir = trailingslashit( WP_CONTENT_DIR ) . 'cache/perform/';
- $this->stats_store = new StatsStore();
+ $this->cache_dir = trailingslashit( WP_CONTENT_DIR ) . 'cache/perform/';
+ $this->stats_store = new StatsStore();
$this->url_normalizer = new UrlNormalizer();
add_filter( 'cron_schedules', [ $this, 'register_cron_schedule' ] );
@@ -182,22 +182,22 @@ public function maybe_serve_cache() {
if ( $meta && is_string( $html ) && ! $is_regen_request ) {
$now = time();
- if ( ! empty( $meta['expires'] ) && $now <= (int) $meta['expires'] ) {
- $this->increment_stat( 'hits' );
- $this->send_cache_headers( 'HIT' );
- $this->flush_stats();
- echo $html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
- exit;
- }
+ if ( ! empty( $meta['expires'] ) && $now <= (int) $meta['expires'] ) {
+ $this->increment_stat( 'hits' );
+ $this->send_cache_headers( 'HIT' );
+ $this->flush_stats();
+ echo $html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
+ exit;
+ }
- if ( ! empty( $meta['swr_expires'] ) && $now <= (int) $meta['swr_expires'] ) {
- $this->increment_stat( 'stale_hits' );
- $this->send_cache_headers( 'STALE' );
- $this->maybe_trigger_async_regeneration( $this->current_url );
- $this->flush_stats();
- echo $html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
- exit;
- }
+ if ( ! empty( $meta['swr_expires'] ) && $now <= (int) $meta['swr_expires'] ) {
+ $this->increment_stat( 'stale_hits' );
+ $this->send_cache_headers( 'STALE' );
+ $this->maybe_trigger_async_regeneration( $this->current_url );
+ $this->flush_stats();
+ echo $html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
+ exit;
+ }
}
// Miss path: acquire lock to avoid stampede.
@@ -205,22 +205,26 @@ public function maybe_serve_cache() {
$this->increment_stat( 'lock_waits' );
// If lock is held and stale exists, prefer stale over full uncached render.
- if ( $meta && is_string( $html ) ) {
- $this->increment_stat( 'stale_hits' );
- $this->send_cache_headers( 'STALE-LOCK' );
- $this->flush_stats();
- echo $html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
- exit;
- }
+ if ( $meta && is_string( $html ) ) {
+ $this->increment_stat( 'stale_hits' );
+ $this->send_cache_headers( 'STALE-LOCK' );
+ $this->flush_stats();
+ echo $html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
+ exit;
+ }
- $this->increment_stat( 'misses' );
- $this->record_top_miss( $this->current_url );
+ $miss_increment = $this->increment_stat( 'misses' );
+ if ( 0 < $miss_increment ) {
+ $this->record_top_miss( $this->current_url, $miss_increment );
+ }
return;
}
$this->should_write_cache = true;
- $this->increment_stat( 'misses' );
- $this->record_top_miss( $this->current_url );
+ $miss_increment = $this->increment_stat( 'misses' );
+ if ( 0 < $miss_increment ) {
+ $this->record_top_miss( $this->current_url, $miss_increment );
+ }
}
/**
@@ -295,8 +299,8 @@ public function record_slow_uncached_request() {
return;
}
- $url = '' !== $this->current_url ? $this->current_url : $this->get_normalized_request_url();
- $slow = $this->get_stat_map( 'slow_uncached' );
+ $url = '' !== $this->current_url ? $this->current_url : $this->get_normalized_request_url();
+ $slow = $this->get_stat_map( 'slow_uncached' );
$slow[ $url ] = round( $duration_ms );
arsort( $slow );
$slow = array_slice( $slow, 0, 30, true );
@@ -428,7 +432,7 @@ public function run_preload_batch() {
]
);
- $processed++;
+ ++$processed;
$this->increment_stat( 'preload_requests' );
}
@@ -851,7 +855,7 @@ private function is_cacheable_request() {
$path = isset( $_SERVER['REQUEST_URI'] ) ? wp_parse_url( wp_unslash( $_SERVER['REQUEST_URI'] ), PHP_URL_PATH ) : '';
if ( is_string( $path ) ) {
- $path = untrailingslashit( strtolower( $path ) );
+ $path = untrailingslashit( strtolower( $path ) );
$blocked_paths = [
'/cart',
'/checkout',
@@ -869,7 +873,7 @@ private function is_cacheable_request() {
return false;
}
- $cookies = $_COOKIE; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
+ $cookies = $_COOKIE; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
$bypass_cookies = [
'woocommerce_items_in_cart',
'woocommerce_cart_hash',
@@ -1155,11 +1159,11 @@ private function write_file_atomically( $path, $contents ) {
*
* @param string $key Stat key.
*
- * @return void
+ * @return int Increment size applied to the counter.
*/
private function increment_stat( $key ) {
$store = $this->get_stats_store();
- $store->increment( $key );
+ return $store->increment( $key );
}
/**
@@ -1179,15 +1183,16 @@ private function set_stat_value( $key, $value ) {
* Record top miss URLs.
*
* @param string $url URL.
+ * @param int $increment Count increment.
*
* @return void
*/
- private function record_top_miss( $url ) {
+ private function record_top_miss( $url, $increment = 1 ) {
$map = $this->get_stat_map( 'top_misses' );
if ( ! isset( $map[ $url ] ) ) {
$map[ $url ] = 0;
}
- $map[ $url ]++;
+ $map[ $url ] += max( 1, (int) $increment );
arsort( $map );
$map = array_slice( $map, 0, 50, true );
$this->set_stat_map( 'top_misses', $map );
diff --git a/src/Modules/Cache/StatsStore.php b/src/Modules/Cache/StatsStore.php
index d1dc015..95e1cff 100644
--- a/src/Modules/Cache/StatsStore.php
+++ b/src/Modules/Cache/StatsStore.php
@@ -28,6 +28,34 @@ final class StatsStore {
*/
private $stats = [];
+ /**
+ * Stats snapshot loaded at request start.
+ *
+ * @var array
+ */
+ private $base_stats = [];
+
+ /**
+ * Buffered numeric counter increments.
+ *
+ * @var array
+ */
+ private $increments = [];
+
+ /**
+ * Buffered scalar values that should replace the stored value.
+ *
+ * @var array
+ */
+ private $values = [];
+
+ /**
+ * Buffered map stats.
+ *
+ * @var array>
+ */
+ private $maps = [];
+
/**
* Whether in-memory stats changed and need flush.
*
@@ -42,17 +70,38 @@ final class StatsStore {
*/
private $sampled_counter_keys = [
'hits' => true,
+ 'misses' => true,
'stale_hits' => true,
'bypasses' => true,
'lock_waits' => true,
];
+ /**
+ * Map stats whose values are additive counters.
+ *
+ * @var array
+ */
+ private $additive_map_keys = [
+ 'top_misses' => true,
+ ];
+
+ /**
+ * Maximum number of entries to persist for map stats.
+ *
+ * @var array
+ */
+ private $map_limits = [
+ 'top_misses' => 50,
+ 'slow_uncached' => 30,
+ ];
+
/**
* Constructor.
*/
public function __construct() {
- $stats = get_option( $this->option_key, [] );
- $this->stats = is_array( $stats ) ? $stats : [];
+ $stats = get_option( $this->option_key, [] );
+ $this->stats = is_array( $stats ) ? $stats : [];
+ $this->base_stats = $this->stats;
}
/**
@@ -60,16 +109,19 @@ public function __construct() {
*
* @param string $key Stat key.
*
- * @return void
+ * @return int Increment size applied to the counter.
*/
public function increment( $key ) {
$increment = $this->get_increment_sample_size( $key );
if ( 0 === $increment ) {
- return;
+ return 0;
}
- $this->stats[ $key ] = isset( $this->stats[ $key ] ) ? ( (int) $this->stats[ $key ] + $increment ) : $increment;
- $this->dirty = true;
+ $this->stats[ $key ] = isset( $this->stats[ $key ] ) ? ( (int) $this->stats[ $key ] + $increment ) : $increment;
+ $this->increments[ $key ] = isset( $this->increments[ $key ] ) ? ( $this->increments[ $key ] + $increment ) : $increment;
+ $this->dirty = true;
+
+ return $increment;
}
/**
@@ -103,8 +155,9 @@ private function get_increment_sample_size( $key ) {
* @return void
*/
public function set_value( $key, $value ) {
- $this->stats[ $key ] = $value;
- $this->dirty = true;
+ $this->stats[ $key ] = $value;
+ $this->values[ $key ] = $value;
+ $this->dirty = true;
}
/**
@@ -131,8 +184,11 @@ public function get_map( $key ) {
* @return void
*/
public function set_map( $key, $value ) {
+ $value = $this->sort_and_limit_map( $key, $value );
+
$this->stats[ $key ] = $value;
- $this->dirty = true;
+ $this->maps[ $key ] = $value;
+ $this->dirty = true;
}
/**
@@ -145,7 +201,89 @@ public function flush() {
return;
}
- update_option( $this->option_key, $this->stats, false );
- $this->dirty = false;
+ $current = get_option( $this->option_key, [] );
+ $current = is_array( $current ) ? $current : [];
+
+ foreach ( $this->increments as $key => $increment ) {
+ $current[ $key ] = isset( $current[ $key ] ) ? ( (int) $current[ $key ] + $increment ) : $increment;
+ }
+
+ foreach ( $this->values as $key => $value ) {
+ $current[ $key ] = $value;
+ }
+
+ foreach ( $this->maps as $key => $map ) {
+ $current_map = $this->normalize_map( $current[ $key ] ?? [] );
+
+ if ( ! empty( $this->additive_map_keys[ $key ] ) ) {
+ $base_map = $this->normalize_map( $this->base_stats[ $key ] ?? [] );
+ foreach ( $map as $map_key => $value ) {
+ $delta = $value - ( $base_map[ $map_key ] ?? 0 );
+ if ( 0 >= $delta ) {
+ continue;
+ }
+
+ $current_map[ $map_key ] = ( $current_map[ $map_key ] ?? 0 ) + $delta;
+ }
+ } else {
+ foreach ( $map as $map_key => $value ) {
+ $current_map[ $map_key ] = isset( $current_map[ $map_key ] ) ? max( $current_map[ $map_key ], $value ) : $value;
+ }
+ }
+
+ $current[ $key ] = $this->sort_and_limit_map( $key, $current_map );
+ }
+
+ update_option( $this->option_key, $current, false );
+
+ $this->stats = $current;
+ $this->base_stats = $current;
+ $this->increments = [];
+ $this->values = [];
+ $this->maps = [];
+ $this->dirty = false;
+ }
+
+ /**
+ * Normalize a map stat to numeric values.
+ *
+ * @param mixed $map Map value.
+ *
+ * @return array
+ */
+ private function normalize_map( $map ) {
+ if ( ! is_array( $map ) ) {
+ return [];
+ }
+
+ $normalized = [];
+ foreach ( $map as $key => $value ) {
+ if ( ! is_numeric( $value ) ) {
+ continue;
+ }
+
+ $normalized[ (string) $key ] = $value + 0;
+ }
+
+ return $normalized;
+ }
+
+ /**
+ * Sort and limit a map stat.
+ *
+ * @param string $key Stat key.
+ * @param array $map Map value.
+ *
+ * @return array
+ */
+ private function sort_and_limit_map( $key, array $map ) {
+ arsort( $map );
+
+ $limit = $this->map_limits[ $key ] ?? 0;
+ if ( 0 < $limit ) {
+ $map = array_slice( $map, 0, $limit, true );
+ }
+
+ return $map;
}
}
diff --git a/src/Modules/MenuCache/MenuCache.php b/src/Modules/MenuCache/MenuCache.php
index 0ffa870..4a3e663 100644
--- a/src/Modules/MenuCache/MenuCache.php
+++ b/src/Modules/MenuCache/MenuCache.php
@@ -92,7 +92,11 @@ class MenuCache implements ModuleInterface {
*/
public function should_load(): bool {
$settings = Helpers::get_settings();
- return isset( $settings['enable_navigation_menu_cache'] ) && ! empty( $settings['enable_navigation_menu_cache'] );
+ if ( empty( $settings['enable_navigation_menu_cache'] ) ) {
+ return false;
+ }
+
+ return $this->supports_current_theme();
}
/**
@@ -106,6 +110,32 @@ public function register(): void {
add_action( 'wp_update_nav_menu', [ $this, 'update_nav_menu_cache' ], 10, 2 );
}
+ /**
+ * Determine whether menu caching is appropriate for the active theme.
+ *
+ * Block themes render navigation through block output, so wp_nav_menu()
+ * caching is only enabled by default for classic/non-block themes.
+ *
+ * @return bool
+ */
+ private function supports_current_theme(): bool {
+ $is_block_theme = function_exists( 'wp_is_block_theme' ) && wp_is_block_theme();
+ $supported = ! $is_block_theme;
+
+ return (bool) apply_filters( 'perform_menu_cache_supports_current_theme', $supported, $is_block_theme );
+ }
+
+ /**
+ * Determine whether the current request can safely use shared menu output.
+ *
+ * @return bool
+ */
+ private function is_cacheable_request(): bool {
+ $cacheable = ! is_admin() && ! is_user_logged_in();
+
+ return (bool) apply_filters( 'perform_menu_cache_is_cacheable_request', $cacheable );
+ }
+
/**
* This function is used to output the cached navigation menu.
*
@@ -119,8 +149,8 @@ public function register(): void {
*/
public function cache_nav_menu_output( $output, $args ) {
- // Validate input arguments.
- if ( empty( $args ) || ! is_object( $args ) ) {
+ // Validate input arguments.
+ if ( ! $this->is_cacheable_request() || empty( $args ) || ! is_object( $args ) ) {
return $output;
}
@@ -182,7 +212,7 @@ public function cache_nav_menu_output( $output, $args ) {
public function cache_nav_menu( $nav_menu, $args ) {
// Validate input arguments.
- if ( empty( $args ) || ! is_object( $args ) || empty( $args->menu->term_id ) ) {
+ if ( ! $this->is_cacheable_request() || empty( $args ) || ! is_object( $args ) || empty( $args->menu->term_id ) ) {
return $nav_menu;
}
diff --git a/tests/bootstrap.php b/tests/bootstrap.php
index 0c975ec..e3ecaad 100644
--- a/tests/bootstrap.php
+++ b/tests/bootstrap.php
@@ -8,9 +8,9 @@
}
if ( ! function_exists( 'get_option' ) ) {
- function get_option( $name, $default = false ) {
+ function get_option( $name, $default_value = false ) {
$store = isset( $GLOBALS['perform_test_options'] ) && is_array( $GLOBALS['perform_test_options'] ) ? $GLOBALS['perform_test_options'] : [];
- return array_key_exists( $name, $store ) ? $store[ $name ] : $default;
+ return array_key_exists( $name, $store ) ? $store[ $name ] : $default_value;
}
}
@@ -24,6 +24,74 @@ function update_option( $name, $value, $autoload = null ) {
}
}
+if ( ! function_exists( 'add_action' ) ) {
+ function add_action( $hook_name, $callback, $priority = 10, $accepted_args = 1 ) {
+ return true;
+ }
+}
+
+if ( ! function_exists( 'wp_enqueue_style' ) ) {
+ function wp_enqueue_style( $handle, $src = '', $deps = [], $ver = false, $media = 'all' ) {
+ $GLOBALS['perform_test_enqueued_styles'][] = $handle;
+ }
+}
+
+if ( ! function_exists( 'wp_enqueue_script' ) ) {
+ function wp_enqueue_script( $handle, $src = '', $deps = [], $ver = false, $in_footer = false ) {
+ $GLOBALS['perform_test_enqueued_scripts'][] = $handle;
+ }
+}
+
+if ( ! function_exists( 'current_user_can' ) ) {
+ function current_user_can( $capability ) {
+ $capabilities = isset( $GLOBALS['perform_test_current_user_can'] ) && is_array( $GLOBALS['perform_test_current_user_can'] ) ? $GLOBALS['perform_test_current_user_can'] : [];
+ return ! empty( $capabilities[ $capability ] );
+ }
+}
+
+if ( ! function_exists( 'is_admin' ) ) {
+ function is_admin() {
+ return ! empty( $GLOBALS['perform_test_is_admin'] );
+ }
+}
+
+if ( ! function_exists( 'is_user_logged_in' ) ) {
+ function is_user_logged_in() {
+ return ! empty( $GLOBALS['perform_test_is_user_logged_in'] );
+ }
+}
+
+if ( ! function_exists( 'wp_is_block_theme' ) ) {
+ function wp_is_block_theme() {
+ return ! empty( $GLOBALS['perform_test_is_block_theme'] );
+ }
+}
+
+if ( ! function_exists( 'apply_filters' ) ) {
+ function apply_filters( $hook_name, $value, ...$args ) {
+ $filters = isset( $GLOBALS['perform_test_filters'] ) && is_array( $GLOBALS['perform_test_filters'] ) ? $GLOBALS['perform_test_filters'] : [];
+ if ( ! array_key_exists( $hook_name, $filters ) ) {
+ return $value;
+ }
+
+ if ( is_callable( $filters[ $hook_name ] ) ) {
+ return $filters[ $hook_name ]( $value, ...$args );
+ }
+
+ return $filters[ $hook_name ];
+ }
+}
+
+if ( ! function_exists( 'wp_rand' ) ) {
+ function wp_rand( $min = 0, $max = 0 ) {
+ if ( isset( $GLOBALS['perform_test_wp_rand'] ) ) {
+ return (int) $GLOBALS['perform_test_wp_rand'];
+ }
+
+ return (int) $min;
+ }
+}
+
if ( ! function_exists( 'wp_parse_url' ) ) {
function wp_parse_url( $url, $component = -1 ) {
return parse_url( $url, $component );
@@ -42,6 +110,12 @@ function sanitize_text_field( $value ) {
}
}
+if ( ! function_exists( 'sanitize_textarea_field' ) ) {
+ function sanitize_textarea_field( $value ) {
+ return is_scalar( $value ) ? trim( (string) $value ) : $value;
+ }
+}
+
if ( ! function_exists( 'wp_unslash' ) ) {
function wp_unslash( $value ) {
return $value;
diff --git a/tests/unit-tests/tests-actions.php b/tests/unit-tests/tests-actions.php
new file mode 100644
index 0000000..bc12ca4
--- /dev/null
+++ b/tests/unit-tests/tests-actions.php
@@ -0,0 +1,34 @@
+ false,
+ ];
+ $GLOBALS['perform_test_enqueued_scripts'] = [];
+ $GLOBALS['perform_test_enqueued_styles'] = [];
+ }
+
+ protected function tearDown(): void {
+ unset(
+ $_GET['perform'],
+ $GLOBALS['perform_test_current_user_can'],
+ $GLOBALS['perform_test_enqueued_scripts'],
+ $GLOBALS['perform_test_enqueued_styles']
+ );
+ }
+
+ public function test_assets_manager_assets_require_manage_options() {
+ $actions = new Actions();
+
+ $actions->enqueue_scripts();
+ $actions->enqueue_styles();
+
+ $this->assertSame( [], $GLOBALS['perform_test_enqueued_scripts'] );
+ $this->assertSame( [], $GLOBALS['perform_test_enqueued_styles'] );
+ }
+}
diff --git a/tests/unit-tests/tests-assets-manager.php b/tests/unit-tests/tests-assets-manager.php
new file mode 100644
index 0000000..2de9dbd
--- /dev/null
+++ b/tests/unit-tests/tests-assets-manager.php
@@ -0,0 +1,23 @@
+setAccessible( true );
+
+ $this->assertSame( [ 42, 7 ], $method->invoke( $manager, [ '42', 42, '7', 'invalid', 0 ] ) );
+ }
+
+ public function test_current_object_id_lookup_matches_legacy_string_ids() {
+ $manager = new AssetsManager();
+ $method = new ReflectionMethod( $manager, 'has_current_object_id' );
+ $method->setAccessible( true );
+
+ $this->assertTrue( $method->invoke( $manager, 42, [ '42' ] ) );
+ $this->assertFalse( $method->invoke( $manager, 11, [ '42' ] ) );
+ }
+}
diff --git a/tests/unit-tests/tests-cache-stats-store.php b/tests/unit-tests/tests-cache-stats-store.php
new file mode 100644
index 0000000..59faee2
--- /dev/null
+++ b/tests/unit-tests/tests-cache-stats-store.php
@@ -0,0 +1,65 @@
+ 1,
+ ];
+ unset( $GLOBALS['perform_test_wp_rand'] );
+ }
+
+ protected function tearDown(): void {
+ unset( $GLOBALS['perform_test_filters'], $GLOBALS['perform_test_wp_rand'] );
+ }
+
+ public function test_flush_merges_buffered_increments_with_latest_option() {
+ $GLOBALS['perform_test_options']['perform_cache_stats'] = [
+ 'hits' => 10,
+ 'top_misses' => [
+ '/a' => 2,
+ ],
+ ];
+
+ $store = new StatsStore();
+ $store->increment( 'hits' );
+
+ $top_misses = $store->get_map( 'top_misses' );
+ $top_misses['/a'] = 3;
+ $top_misses['/b'] = 1;
+ $store->set_map( 'top_misses', $top_misses );
+
+ $GLOBALS['perform_test_options']['perform_cache_stats'] = [
+ 'hits' => 20,
+ 'top_misses' => [
+ '/a' => 5,
+ '/c' => 4,
+ ],
+ ];
+
+ $store->flush();
+
+ $this->assertSame( 21, $GLOBALS['perform_test_options']['perform_cache_stats']['hits'] );
+ $this->assertSame( 6, $GLOBALS['perform_test_options']['perform_cache_stats']['top_misses']['/a'] );
+ $this->assertSame( 1, $GLOBALS['perform_test_options']['perform_cache_stats']['top_misses']['/b'] );
+ $this->assertSame( 4, $GLOBALS['perform_test_options']['perform_cache_stats']['top_misses']['/c'] );
+ }
+
+ public function test_sampled_counter_does_not_dirty_when_sample_is_skipped() {
+ $GLOBALS['perform_test_options']['perform_cache_stats'] = [
+ 'hits' => 10,
+ ];
+ $GLOBALS['perform_test_filters']['perform_cache_stats_sample_rate'] = 20;
+ $GLOBALS['perform_test_wp_rand'] = 2;
+
+ $store = new StatsStore();
+
+ $this->assertSame( 0, $store->increment( 'hits' ) );
+ $store->flush();
+
+ $this->assertSame( 10, $GLOBALS['perform_test_options']['perform_cache_stats']['hits'] );
+ }
+}
diff --git a/tests/unit-tests/tests-menu-cache.php b/tests/unit-tests/tests-menu-cache.php
new file mode 100644
index 0000000..62c4bb1
--- /dev/null
+++ b/tests/unit-tests/tests-menu-cache.php
@@ -0,0 +1,38 @@
+ [
+ 'enable_navigation_menu_cache' => 1,
+ ],
+ ];
+ unset( $GLOBALS['perform_test_filters'], $GLOBALS['perform_test_is_block_theme'] );
+ }
+
+ protected function tearDown(): void {
+ unset( $GLOBALS['perform_test_filters'], $GLOBALS['perform_test_is_block_theme'] );
+ }
+
+ public function test_menu_cache_does_not_load_for_block_themes_by_default() {
+ $GLOBALS['perform_test_is_block_theme'] = true;
+
+ $menu_cache = new MenuCache();
+
+ $this->assertFalse( $menu_cache->should_load() );
+ }
+
+ public function test_menu_cache_filter_can_opt_in_hybrid_block_themes() {
+ $GLOBALS['perform_test_is_block_theme'] = true;
+ $GLOBALS['perform_test_filters'] = [
+ 'perform_menu_cache_supports_current_theme' => true,
+ ];
+
+ $menu_cache = new MenuCache();
+
+ $this->assertTrue( $menu_cache->should_load() );
+ }
+}
diff --git a/tests/unit-tests/tests-settings-menu.php b/tests/unit-tests/tests-settings-menu.php
new file mode 100644
index 0000000..efef3a0
--- /dev/null
+++ b/tests/unit-tests/tests-settings-menu.php
@@ -0,0 +1,34 @@
+setAccessible( true );
+
+ $this->assertSame(
+ [
+ 'https://fonts.example.com',
+ 'https://cdn.example.com',
+ ],
+ $method->invoke( $menu, "https://fonts.example.com\nhttps://cdn.example.com" )
+ );
+ }
+
+ public function test_existing_multiline_arrays_keep_their_storage_shape() {
+ $menu = new Menu();
+ $method = new ReflectionMethod( $menu, 'normalize_multiline_setting' );
+ $method->setAccessible( true );
+
+ $this->assertSame(
+ [
+ 'https://fonts.example.com',
+ 'https://cdn.example.com',
+ ],
+ $method->invoke( $menu, [ 'https://fonts.example.com', '', 'https://cdn.example.com' ] )
+ );
+ }
+}