From abc044879eae2b9df519489be0b2f48eaf650671 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Mon, 15 Jun 2026 17:26:41 -0700 Subject: [PATCH 1/4] Site Health: Fix false negative in opcode cache test for file cache mode. The Site Health opcode cache test relied solely on `opcache_get_status()` to determine whether OPcache is active. However, that function returns `false` when OPcache operates in file cache only mode (`opcache.file_cache_only=1`), even though opcode caching is active. As a result, Site Health incorrectly reported "Opcode cache is not enabled" on such configurations. Detection now checks whether the Zend OPcache extension is loaded and `opcache.enable` is on, treating that as sufficient, and only defers to `opcache_get_status()` as authoritative when it returns usable data. Co-Authored-By: Claude Opus 4.8 --- .../includes/class-wp-site-health.php | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/src/wp-admin/includes/class-wp-site-health.php b/src/wp-admin/includes/class-wp-site-health.php index 75e046ef8ffa7..e222c5ff2927d 100644 --- a/src/wp-admin/includes/class-wp-site-health.php +++ b/src/wp-admin/includes/class-wp-site-health.php @@ -2810,10 +2810,21 @@ public function get_test_search_engine_visibility() { */ public function get_test_opcode_cache(): array { $opcode_cache_enabled = false; - if ( function_exists( 'opcache_get_status' ) ) { - $status = @opcache_get_status( false ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- Warning emitted in failure case. - if ( $status && true === $status['opcache_enabled'] ) { - $opcode_cache_enabled = true; + if ( extension_loaded( 'Zend OPcache' ) && ini_get( 'opcache.enable' ) ) { + /* + * OPcache is enabled. When it operates in file cache only mode + * (opcache.file_cache_only=1), opcache_get_status() returns false + * even though opcode caching is active, so treat the extension being + * enabled as sufficient and only defer to opcache_get_status() when + * it returns usable data. + */ + $opcode_cache_enabled = true; + + if ( function_exists( 'opcache_get_status' ) ) { + $status = @opcache_get_status( false ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- Warning emitted in failure case. + if ( is_array( $status ) && isset( $status['opcache_enabled'] ) ) { + $opcode_cache_enabled = (bool) $status['opcache_enabled']; + } } } From 5edc9f2578470a4b57088bdbd06496498e144805 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Mon, 15 Jun 2026 20:13:01 -0700 Subject: [PATCH 2/4] Site Health: Update opcode cache test to mirror file cache detection. The test recomputed its own expectation using the previous detection logic that relied solely on opcache_get_status(). Update it to mirror the new detection (extension loaded and opcache.enable on, deferring to opcache_get_status() only when it returns usable data) so the expected status matches the production result in file cache only and CLI environments. Co-Authored-By: Claude Opus 4.8 --- tests/phpunit/tests/admin/wpSiteHealth.php | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/tests/phpunit/tests/admin/wpSiteHealth.php b/tests/phpunit/tests/admin/wpSiteHealth.php index 6080b477f54c3..c0851e6963bc6 100644 --- a/tests/phpunit/tests/admin/wpSiteHealth.php +++ b/tests/phpunit/tests/admin/wpSiteHealth.php @@ -681,9 +681,12 @@ public function test_get_test_opcode_cache_return_structure() { /** * Tests get_test_opcode_cache() result when opcode cache is enabled or not. * - * Covers: opcache enabled, disabled, not available, and opcache_get_status() returns false. + * Covers: opcache enabled, disabled, not available, file cache only mode + * (where opcache_get_status() returns false), and opcache_get_status() + * being unavailable via disable_functions. * * @ticket 63697 + * @ticket 64707 * * @covers ::get_test_opcode_cache() */ @@ -691,10 +694,14 @@ public function test_get_test_opcode_cache_result_by_environment() { $result = $this->instance->get_test_opcode_cache(); $opcache_enabled = false; - if ( function_exists( 'opcache_get_status' ) ) { - $status = @opcache_get_status( false ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- Warning emitted in failure case. - if ( $status && true === $status['opcache_enabled'] ) { - $opcache_enabled = true; + if ( extension_loaded( 'Zend OPcache' ) && ini_get( 'opcache.enable' ) ) { + $opcache_enabled = true; + + if ( function_exists( 'opcache_get_status' ) ) { + $status = @opcache_get_status( false ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- Warning emitted in failure case. + if ( is_array( $status ) && isset( $status['opcache_enabled'] ) ) { + $opcache_enabled = (bool) $status['opcache_enabled']; + } } } From 030690d866dec5510219f59247583e346f00b0c4 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Mon, 15 Jun 2026 20:27:11 -0700 Subject: [PATCH 3/4] Site Health: Fix opcode cache debug data false negative in file cache mode. The Site Health Info debug data reported the opcode cache as "Disabled by configuration" whenever opcache_get_status() returned false. That happens in file cache only mode (opcache.file_cache_only=1) and when the function is listed in disable_functions, even though OPcache is active. Detection now reports the cache as enabled when the Zend OPcache extension is loaded and opcache.enable is on but opcache_get_status() returns no usable data. The detailed statistics (memory usage, hit rate, etc.) remain gated on a successful opcache_get_status() call, since that data is only available for the shared memory cache. Co-Authored-By: siliconforks Co-Authored-By: Claude Opus 4.8 --- src/wp-admin/includes/class-wp-debug-data.php | 125 ++++++++++-------- 1 file changed, 67 insertions(+), 58 deletions(-) diff --git a/src/wp-admin/includes/class-wp-debug-data.php b/src/wp-admin/includes/class-wp-debug-data.php index f2399b30550c9..7a65cdaef7979 100644 --- a/src/wp-admin/includes/class-wp-debug-data.php +++ b/src/wp-admin/includes/class-wp-debug-data.php @@ -472,74 +472,83 @@ private static function get_wp_server(): array { ); // Opcode Cache. - if ( function_exists( 'opcache_get_status' ) ) { - $opcache_status = @opcache_get_status( false ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- Warning emitted in failure case. - - if ( false === $opcache_status ) { - $fields['opcode_cache'] = array( - 'label' => __( 'Opcode cache' ), - 'value' => __( 'Disabled by configuration' ), - 'debug' => 'not available', - ); - } else { - $fields['opcode_cache'] = array( - 'label' => __( 'Opcode cache' ), - 'value' => $opcache_status['opcache_enabled'] ? __( 'Enabled' ) : __( 'Disabled' ), - 'debug' => $opcache_status['opcache_enabled'], + // Only query opcache_get_status() when the genuine Zend OPcache extension + // is loaded, so a userland opcache_get_status() polyfill cannot spoof the + // reported status. + $opcache_status = ( extension_loaded( 'Zend OPcache' ) && function_exists( 'opcache_get_status' ) ) + ? @opcache_get_status( false ) // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- Warning emitted in failure case. + : false; + + if ( is_array( $opcache_status ) ) { + $fields['opcode_cache'] = array( + 'label' => __( 'Opcode cache' ), + 'value' => $opcache_status['opcache_enabled'] ? __( 'Enabled' ) : __( 'Disabled' ), + 'debug' => $opcache_status['opcache_enabled'], + ); + + if ( true === $opcache_status['opcache_enabled'] ) { + $fields['opcode_cache_memory_usage'] = array( + 'label' => __( 'Opcode cache memory usage' ), + 'value' => sprintf( + /* translators: 1: Used memory, 2: Total memory */ + __( '%1$s of %2$s' ), + size_format( $opcache_status['memory_usage']['used_memory'] ), + size_format( $opcache_status['memory_usage']['free_memory'] + $opcache_status['memory_usage']['used_memory'] ) + ), + 'debug' => sprintf( + '%s of %s', + $opcache_status['memory_usage']['used_memory'], + $opcache_status['memory_usage']['free_memory'] + $opcache_status['memory_usage']['used_memory'] + ), ); - if ( true === $opcache_status['opcache_enabled'] ) { - $fields['opcode_cache_memory_usage'] = array( - 'label' => __( 'Opcode cache memory usage' ), + if ( 0 !== $opcache_status['interned_strings_usage']['buffer_size'] ) { + $fields['opcode_cache_interned_strings_usage'] = array( + 'label' => __( 'Opcode cache interned strings usage' ), 'value' => sprintf( - /* translators: 1: Used memory, 2: Total memory */ - __( '%1$s of %2$s' ), - size_format( $opcache_status['memory_usage']['used_memory'] ), - size_format( $opcache_status['memory_usage']['free_memory'] + $opcache_status['memory_usage']['used_memory'] ) + /* translators: 1: Percentage used, 2: Total memory, 3: Free memory */ + __( '%1$s%% of %2$s (%3$s free)' ), + number_format_i18n( ( $opcache_status['interned_strings_usage']['used_memory'] / $opcache_status['interned_strings_usage']['buffer_size'] ) * 100, 2 ), + size_format( $opcache_status['interned_strings_usage']['buffer_size'] ), + size_format( $opcache_status['interned_strings_usage']['free_memory'] ) ), 'debug' => sprintf( - '%s of %s', - $opcache_status['memory_usage']['used_memory'], - $opcache_status['memory_usage']['free_memory'] + $opcache_status['memory_usage']['used_memory'] + '%s%% of %s (%s free)', + round( ( $opcache_status['interned_strings_usage']['used_memory'] / $opcache_status['interned_strings_usage']['buffer_size'] ) * 100, 2 ), + $opcache_status['interned_strings_usage']['buffer_size'], + $opcache_status['interned_strings_usage']['free_memory'] ), ); + } - if ( 0 !== $opcache_status['interned_strings_usage']['buffer_size'] ) { - $fields['opcode_cache_interned_strings_usage'] = array( - 'label' => __( 'Opcode cache interned strings usage' ), - 'value' => sprintf( - /* translators: 1: Percentage used, 2: Total memory, 3: Free memory */ - __( '%1$s%% of %2$s (%3$s free)' ), - number_format_i18n( ( $opcache_status['interned_strings_usage']['used_memory'] / $opcache_status['interned_strings_usage']['buffer_size'] ) * 100, 2 ), - size_format( $opcache_status['interned_strings_usage']['buffer_size'] ), - size_format( $opcache_status['interned_strings_usage']['free_memory'] ) - ), - 'debug' => sprintf( - '%s%% of %s (%s free)', - round( ( $opcache_status['interned_strings_usage']['used_memory'] / $opcache_status['interned_strings_usage']['buffer_size'] ) * 100, 2 ), - $opcache_status['interned_strings_usage']['buffer_size'], - $opcache_status['interned_strings_usage']['free_memory'] - ), - ); - } - - $fields['opcode_cache_hit_rate'] = array( - 'label' => __( 'Opcode cache hit rate' ), - 'value' => sprintf( - /* translators: %s: Hit rate percentage */ - __( '%s%%' ), - number_format_i18n( $opcache_status['opcache_statistics']['opcache_hit_rate'], 2 ) - ), - 'debug' => round( $opcache_status['opcache_statistics']['opcache_hit_rate'], 2 ), - ); + $fields['opcode_cache_hit_rate'] = array( + 'label' => __( 'Opcode cache hit rate' ), + 'value' => sprintf( + /* translators: %s: Hit rate percentage */ + __( '%s%%' ), + number_format_i18n( $opcache_status['opcache_statistics']['opcache_hit_rate'], 2 ) + ), + 'debug' => round( $opcache_status['opcache_statistics']['opcache_hit_rate'], 2 ), + ); - $fields['opcode_cache_full'] = array( - 'label' => __( 'Is the Opcode cache full?' ), - 'value' => $opcache_status['cache_full'] ? __( 'Yes' ) : __( 'No' ), - 'debug' => $opcache_status['cache_full'], - ); - } + $fields['opcode_cache_full'] = array( + 'label' => __( 'Is the Opcode cache full?' ), + 'value' => $opcache_status['cache_full'] ? __( 'Yes' ) : __( 'No' ), + 'debug' => $opcache_status['cache_full'], + ); } + } elseif ( extension_loaded( 'Zend OPcache' ) && ini_get( 'opcache.enable' ) ) { + /* + * OPcache is enabled but opcache_get_status() returned no data, for + * example in file cache only mode (opcache.file_cache_only=1) or when the + * function is listed in disable_functions. Detailed statistics are + * unavailable in this case. + */ + $fields['opcode_cache'] = array( + 'label' => __( 'Opcode cache' ), + 'value' => __( 'Enabled' ), + 'debug' => true, + ); } else { $fields['opcode_cache'] = array( 'label' => __( 'Opcode cache' ), From dbcff5974d0ecf40c2ac1975ec0d94131c719b21 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 16 Jun 2026 10:07:04 -0700 Subject: [PATCH 4/4] Site Health: Restore "Disabled by configuration" opcode cache state. The previous change collapsed two distinct disabled states into a single "Disabled" value. Restore the distinction between the Zend OPcache extension being loaded but opcache.enable being off ("Disabled by configuration") and the extension not being loaded at all ("Disabled"), while still reporting file cache only mode as enabled. Co-Authored-By: siliconforks Co-Authored-By: Claude Opus 4.8 --- src/wp-admin/includes/class-wp-debug-data.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/wp-admin/includes/class-wp-debug-data.php b/src/wp-admin/includes/class-wp-debug-data.php index 7a65cdaef7979..afcad3258ce50 100644 --- a/src/wp-admin/includes/class-wp-debug-data.php +++ b/src/wp-admin/includes/class-wp-debug-data.php @@ -549,7 +549,15 @@ private static function get_wp_server(): array { 'value' => __( 'Enabled' ), 'debug' => true, ); + } elseif ( extension_loaded( 'Zend OPcache' ) ) { + // The extension is loaded but opcache.enable is off. + $fields['opcode_cache'] = array( + 'label' => __( 'Opcode cache' ), + 'value' => __( 'Disabled by configuration' ), + 'debug' => 'not available', + ); } else { + // The Zend OPcache extension is not loaded. $fields['opcode_cache'] = array( 'label' => __( 'Opcode cache' ), 'value' => __( 'Disabled' ),