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' ), 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']; + } } } 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']; + } } }