diff --git a/src/wp-admin/includes/class-wp-site-health.php b/src/wp-admin/includes/class-wp-site-health.php index dd537296a8655..a54010f836373 100644 --- a/src/wp-admin/includes/class-wp-site-health.php +++ b/src/wp-admin/includes/class-wp-site-health.php @@ -29,6 +29,21 @@ class WP_Site_Health { private $timeout_missed_cron = null; private $timeout_late_cron = null; + /** + * @var bool + */ + private $wp_debug; + + /** + * @var bool|string + */ + private $wp_debug_log; + + /** + * @var bool|null + */ + private $wp_debug_display; + /** * WP_Site_Health constructor. * @@ -54,6 +69,10 @@ public function __construct() { add_action( 'wp_site_health_scheduled_check', array( $this, 'wp_cron_scheduled_check' ) ); add_action( 'site_health_tab_content', array( $this, 'show_site_health_tab' ) ); + + $this->wp_debug = defined( 'WP_DEBUG' ) && WP_DEBUG; + $this->wp_debug_log = defined( 'WP_DEBUG_LOG' ) ? WP_DEBUG_LOG : false; + $this->wp_debug_display = defined( 'WP_DEBUG_DISPLAY' ) ? WP_DEBUG_DISPLAY : null; } /** @@ -1383,7 +1402,17 @@ public function get_test_dotorg_communication() { * * @since 5.2.0 * - * @return array The test results. + * @return array{ + * label: string, + * status: string, + * badge: array{ + * label: string, + * color: string + * }, + * description: string, + * actions: string, + * test: string + * } The test results. */ public function get_test_is_in_debug_mode() { $result = array( @@ -1408,23 +1437,80 @@ public function get_test_is_in_debug_mode() { 'test' => 'is_in_debug_mode', ); - if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { - if ( defined( 'WP_DEBUG_LOG' ) && WP_DEBUG_LOG ) { - $result['label'] = __( 'Your site is set to log errors to a potentially public file' ); + if ( $this->wp_debug ) { + if ( ! empty( ini_get( 'error_log' ) ) ) { + $debug_log_dir = realpath( dirname( ini_get( 'error_log' ) ) ); + $absolute_path = realpath( ABSPATH ) . DIRECTORY_SEPARATOR; - $result['status'] = str_starts_with( ini_get( 'error_log' ), ABSPATH ) ? 'critical' : 'recommended'; + if ( false === $debug_log_dir ) { + $log_path_status = 'error'; + } elseif ( str_starts_with( $debug_log_dir . DIRECTORY_SEPARATOR, $absolute_path ) ) { + $log_path_status = 'public'; + } else { + $log_path_status = 'private'; + } - $result['description'] .= sprintf( - '
%s
', - sprintf( - /* translators: %s: WP_DEBUG_LOG */ - __( 'The value, %s, has been added to this website’s configuration file. This means any errors on the site will be written to a file which is potentially available to all users.' ), - 'WP_DEBUG_LOG'
- )
- );
+ if ( 'public' === $log_path_status ) {
+ $result['label'] = __( 'Your site is set to log errors to a potentially public file' );
+ $result['status'] = 'critical';
+
+ if ( $this->wp_debug_log ) {
+ $result['description'] .= sprintf(
+ '%s
', + sprintf( + /* translators: %s: WP_DEBUG_LOG */ + __( 'The constant, %s, has been added to this website’s configuration file. This means any errors on the site will be written to a file which is likely publicly accessible.' ), + 'WP_DEBUG_LOG'
+ )
+ );
+ } else {
+ $result['description'] .= sprintf(
+ '%s
', + __( 'The error log path has been configured to a file within the WordPress directory. This means any errors on the site will be written to a file which is likely publicly accessible.' ) + ); + } + } elseif ( 'private' === $log_path_status ) { + $result['label'] = __( 'Your site is set to log errors to a file outside the document root' ); + $result['status'] = 'good'; + + if ( $this->wp_debug_log ) { + $result['description'] .= sprintf( + '%s
', + sprintf( + /* translators: %s: WP_DEBUG_LOG */ + __( 'The configuration constant, %s, is enabled. In addition, your site is set to write errors to a file outside the WordPress directory, which is a good practice as the log file should not be publicly accessible.' ), + 'WP_DEBUG_LOG'
+ )
+ );
+ } else {
+ $result['description'] .= sprintf(
+ '%s
', + __( 'The error log path has been configured to a file outside the WordPress directory. This is a good practice as the log file should not be publicly accessible.' ) + ); + } + } else { + $result['label'] = __( 'Unable to determine error log file location' ); + $result['status'] = 'critical'; + + if ( $this->wp_debug_log ) { + $result['description'] .= sprintf( + '%s
', + sprintf( + /* translators: %s: WP_DEBUG_LOG */ + __( 'The configuration constant, %s, is enabled, but the log file location could not be determined.' ), + 'WP_DEBUG_LOG'
+ )
+ );
+ } else {
+ $result['description'] .= sprintf(
+ '%s
', + __( 'The error log path could not be determined. Please check your PHP configuration.' ) + ); + } + } } - if ( defined( 'WP_DEBUG_DISPLAY' ) && WP_DEBUG_DISPLAY ) { + if ( $this->wp_debug_display ) { $result['label'] = __( 'Your site is set to display errors to site visitors' ); $result['status'] = 'critical'; diff --git a/tests/phpunit/tests/admin/wpSiteHealth.php b/tests/phpunit/tests/admin/wpSiteHealth.php index 0c6a42f71bea3..87476f1258b1c 100644 --- a/tests/phpunit/tests/admin/wpSiteHealth.php +++ b/tests/phpunit/tests/admin/wpSiteHealth.php @@ -572,4 +572,424 @@ public static function set_autoloaded_option( $bytes = 800000 ) { // Force autoloading so that WordPress core does not override it. See https://core.trac.wordpress.org/changeset/57920. add_option( 'test_set_autoloaded_option', $heavy_option_string, '', true ); } + + /** + * Helper method to set up WP_Site_Health instance with debug properties. + * + * @ticket 64071 + * + * @param bool $wp_debug Value for wp_debug property. + * @param bool|string $wp_debug_log Value for wp_debug_log property. + * @param bool|null $wp_debug_display Value for wp_debug_display property. + * + * @return WP_Site_Health + */ + private function setup_site_health_with_debug_properties( bool $wp_debug = false, $wp_debug_log = false, ?bool $wp_debug_display = null ) { + $site_health = new WP_Site_Health(); + $reflection = new ReflectionClass( $site_health ); + + $wp_debug_property = $reflection->getProperty( 'wp_debug' ); + if ( PHP_VERSION_ID < 80100 ) { + $wp_debug_property->setAccessible( true ); + } + $wp_debug_property->setValue( $site_health, $wp_debug ); + + $wp_debug_log_property = $reflection->getProperty( 'wp_debug_log' ); + if ( PHP_VERSION_ID < 80100 ) { + $wp_debug_log_property->setAccessible( true ); + } + $wp_debug_log_property->setValue( $site_health, $wp_debug_log ); + + $wp_debug_display_property = $reflection->getProperty( 'wp_debug_display' ); + if ( PHP_VERSION_ID < 80100 ) { + $wp_debug_display_property->setAccessible( true ); + } + $wp_debug_display_property->setValue( $site_health, $wp_debug_display ); + + return $site_health; + } + + /** + * Helper method to set error_log ini setting and restore it later. + * + * @ticket 64071 + * + * @param string $log_path Path to set for error_log. + * + * @return string|false Original error_log value. + */ + private function set_error_log_path( string $log_path = '' ) { + $original_error_log = ini_get( 'error_log' ); + ini_set( 'error_log', $log_path ); + return $original_error_log; + } + + /** + * Helper method to restore error_log ini setting. + * + * @ticket 64071 + * + * @param string|false $original_value Original error_log value. + */ + private function restore_error_log_path( $original_value ) { + ini_set( 'error_log', $original_value ); + } + + /** + * Returns the expected result array when debug mode is disabled. + * + * @ticket 64071 + * + * @return array + */ + private function get_debug_mode_disabled_result() { + return array( + 'status' => 'good', + 'label' => 'Your site is not set to output debug information', + 'test' => 'is_in_debug_mode', + 'badge' => array( + 'label' => 'Security', + 'color' => 'blue', + ), + ); + } + + /** + * Returns the expected result array when debug log is in a public location. + * + * @ticket 64071 + * + * @param bool $wp_debug_log_defined Whether WP_DEBUG_LOG is defined. + * + * @return array + */ + private function get_debug_error_log_public_result( bool $wp_debug_log_defined = true ) { + + $result = array( + 'status' => 'critical', + 'label' => 'Your site is set to log errors to a potentially public file', + 'description' => 'The constant,WP_DEBUG_LOG, has been added to this website’s configuration file. This means any errors on the site will be written to a file which is likely publicly accessible.',
+ 'test' => 'is_in_debug_mode',
+ );
+
+ if ( ! $wp_debug_log_defined ) {
+ $result['description'] = 'The error log path has been configured to a file within the WordPress directory. This means any errors on the site will be written to a file which is likely publicly accessible.';
+ }
+
+ return $result;
+ }
+
+ /**
+ * Returns the expected result array when debug log is in a private location.
+ *
+ * @ticket 64071
+ *
+ * @param bool $wp_debug_log_defined Whether WP_DEBUG_LOG is defined.
+ *
+ * @return array
+ */
+ private function get_debug_error_log_private_result( bool $wp_debug_log_defined = true ) {
+
+ $result = array(
+ 'status' => 'good',
+ 'label' => 'Your site is set to log errors to a file outside the document root',
+ 'description' => 'The configuration constant, WP_DEBUG_LOG, is enabled. In addition, your site is set to write errors to a file outside the WordPress directory, which is a good practice as the log file should not be publicly accessible.',
+ 'test' => 'is_in_debug_mode',
+ );
+
+ if ( ! $wp_debug_log_defined ) {
+ $result['description'] = 'The error log path has been configured to a file outside the WordPress directory. This is a good practice as the log file should not be publicly accessible.';
+ }
+
+ return $result;
+ }
+
+ /**
+ * Returns the expected result array when debug log path does not exist.
+ *
+ * @ticket 64071
+ *
+ * @param bool $wp_debug_log_defined Whether WP_DEBUG_LOG is defined.
+ *
+ * @return array
+ */
+ private function get_debug_log_non_existent_path_result( bool $wp_debug_log_defined = true ) {
+
+ $result = array(
+ 'status' => 'critical',
+ 'label' => 'Unable to determine error log file location',
+ 'description' => 'The configuration constant, WP_DEBUG_LOG, is enabled, but the log file location could not be determined.',
+ 'test' => 'is_in_debug_mode',
+ );
+
+ if ( ! $wp_debug_log_defined ) {
+ $result['description'] = 'The error log path could not be determined. Please check your PHP configuration.';
+ }
+
+ return $result;
+ }
+
+ /**
+ * Tests get_test_is_in_debug_mode() when debug mode is disabled.
+ *
+ * @ticket 64071
+ *
+ * @covers ::get_test_is_in_debug_mode()
+ */
+ public function test_is_in_debug_mode_disabled() {
+ $site_health = $this->setup_site_health_with_debug_properties( false, false, null );
+ $actual_result = $site_health->get_test_is_in_debug_mode();
+ $expected_result = $this->get_debug_mode_disabled_result();
+
+ $this->assertSame( $expected_result['status'], $actual_result['status'], 'Status should be "good" when debug mode is disabled.' );
+ $this->assertSame( $expected_result['label'], $actual_result['label'], 'Label should indicate debug mode is disabled.' );
+ $this->assertSame( $expected_result['test'], $actual_result['test'], 'Test identifier should be "is_in_debug_mode".' );
+ $this->assertArrayHasKey( 'badge', $actual_result, 'Result should have a badge.' );
+ $this->assertSame( $expected_result['badge']['label'], $actual_result['badge']['label'], 'Badge label should be "Security".' );
+ $this->assertSame( $expected_result['badge']['color'], $actual_result['badge']['color'], 'Badge color should be "blue".' );
+ }
+
+ /**
+ * Tests get_test_is_in_debug_mode() when WP_DEBUG is enabled without error logging.
+ *
+ * @ticket 64071
+ *
+ * @covers ::get_test_is_in_debug_mode()
+ */
+ public function test_is_in_debug_mode_enabled_no_error_log() {
+ $site_health = $this->setup_site_health_with_debug_properties( true, false, null );
+ $original_error_log = $this->set_error_log_path( '' );
+ $actual_result = $site_health->get_test_is_in_debug_mode();
+ $expected_result = $this->get_debug_mode_disabled_result();
+
+ $this->restore_error_log_path( $original_error_log );
+
+ $this->assertSame( $expected_result['status'], $actual_result['status'], 'Status should be "good" when no error log is configured.' );
+ $this->assertSame( $expected_result['label'], $actual_result['label'], 'Label should indicate no error log is configured.' );
+ $this->assertSame( $expected_result['test'], $actual_result['test'], 'Test identifier should be "is_in_debug_mode".' );
+ $this->assertArrayHasKey( 'badge', $actual_result, 'Result should have a badge.' );
+ }
+
+ /**
+ * Tests get_test_is_in_debug_mode() when error log is in a public location.
+ *
+ * @ticket 64071
+ *
+ * @covers ::get_test_is_in_debug_mode()
+ */
+ public function test_is_in_debug_mode_error_log_public() {
+ $site_health = $this->setup_site_health_with_debug_properties( true, true, null );
+ $public_log_path = ABSPATH . 'wp-content/debug.log';
+ $original_error_log = $this->set_error_log_path( $public_log_path );
+ $actual_result = $site_health->get_test_is_in_debug_mode();
+ $expected_result = $this->get_debug_error_log_public_result( true );
+
+ $this->restore_error_log_path( $original_error_log );
+
+ $this->assertSame( $expected_result['status'], $actual_result['status'], 'Status should be "critical" when error log is in a public location.' );
+ $this->assertSame( $expected_result['label'], $actual_result['label'], 'Label should indicate error log is in a public location.' );
+ $this->assertStringContainsString( $expected_result['description'], $actual_result['description'], 'Description should display error log is configured with WP_DEBUG_LOG and is in a public directory.' );
+ }
+
+ /**
+ * Tests get_test_is_in_debug_mode() when error log is public without WP_DEBUG_LOG.
+ *
+ * @ticket 64071
+ *
+ * @covers ::get_test_is_in_debug_mode()
+ */
+ public function test_is_in_debug_mode_error_log_public_without_wp_debug_log() {
+ $site_health = $this->setup_site_health_with_debug_properties( true, false, null );
+ $public_log_path = ABSPATH . 'wp-content/debug.log';
+ $original_error_log = $this->set_error_log_path( $public_log_path );
+ $actual_result = $site_health->get_test_is_in_debug_mode();
+ $expected_result = $this->get_debug_error_log_public_result( false );
+
+ $this->restore_error_log_path( $original_error_log );
+
+ $this->assertSame( $expected_result['status'], $actual_result['status'], 'Status should be "critical" when error log is in a public location.' );
+ $this->assertSame( $expected_result['label'], $actual_result['label'], 'Label should indicate error log is in a public location.' );
+ $this->assertStringContainsString( $expected_result['description'], $actual_result['description'], 'Description should mention error log is configured without WP_DEBUG_LOG and in public directory.' );
+ }
+
+ /**
+ * Tests get_test_is_in_debug_mode() when error log is in a private location.
+ *
+ * @ticket 64071
+ *
+ * @covers ::get_test_is_in_debug_mode()
+ */
+ public function test_is_in_debug_mode_error_log_private() {
+ $site_health = $this->setup_site_health_with_debug_properties( true, true, null );
+ $private_log_path = '/var/log/php-error.log';
+ $original_error_log = $this->set_error_log_path( $private_log_path );
+ $actual_result = $site_health->get_test_is_in_debug_mode();
+ $expected_result = $this->get_debug_error_log_private_result( true );
+
+ $this->restore_error_log_path( $original_error_log );
+
+ $this->assertSame( $expected_result['status'], $actual_result['status'], 'Status should be "good" when error log is in a private location.' );
+ $this->assertSame( $expected_result['label'], $actual_result['label'], 'Label should indicate error log is in a private location.' );
+ $this->assertStringContainsString( $expected_result['description'], $actual_result['description'], 'Description should mention error log is configured outside WordPress directory.' );
+ }
+
+ /**
+ * Tests get_test_is_in_debug_mode() when error log is private without WP_DEBUG_LOG.
+ *
+ * @ticket 64071
+ *
+ * @covers ::get_test_is_in_debug_mode()
+ */
+ public function test_is_in_debug_mode_error_log_private_without_wp_debug_log() {
+ $site_health = $this->setup_site_health_with_debug_properties( true, false, null );
+ $private_log_path = '/var/log/php-error.log';
+ $original_error_log = $this->set_error_log_path( $private_log_path );
+ $actual_result = $site_health->get_test_is_in_debug_mode();
+ $expected_result = $this->get_debug_error_log_private_result( false );
+
+ $this->restore_error_log_path( $original_error_log );
+
+ $this->assertSame( $expected_result['status'], $actual_result['status'], 'Status should be "good" when error log is in a private location.' );
+ $this->assertSame( $expected_result['label'], $actual_result['label'], 'Label should indicate error log is in a private location.' );
+ $this->assertStringContainsString( $expected_result['description'], $actual_result['description'], 'Description should mention error log is configured outside WordPress directory.' );
+ }
+
+ /**
+ * Tests get_test_is_in_debug_mode() when error log path cannot be determined.
+ *
+ * @ticket 64071
+ *
+ * @covers ::get_test_is_in_debug_mode()
+ */
+ public function test_is_in_debug_mode_error_log_non_existent() {
+ $site_health = $this->setup_site_health_with_debug_properties( true, true, null );
+ $invalid_log_path = '/nonexistent/path/that/does/not/exist/debug.log';
+ $original_error_log = $this->set_error_log_path( $invalid_log_path );
+ $actual_result = $site_health->get_test_is_in_debug_mode();
+ $expected_result = $this->get_debug_log_non_existent_path_result( true );
+
+ $this->restore_error_log_path( $original_error_log );
+
+ $this->assertSame( $expected_result['status'], $actual_result['status'], 'Status should be "critical" when error log location cannot be determined.' );
+ $this->assertSame( $expected_result['label'], $actual_result['label'], 'Label should indicate that error log location could not be determined.' );
+ $this->assertStringContainsString( $expected_result['description'], $actual_result['description'], 'Description should mention error log path is nonexistent.' );
+ }
+
+ /**
+ * Tests get_test_is_in_debug_mode() when error log path cannot be determined and WP_DEBUG_LOG is not defined.
+ *
+ * @ticket 64071
+ *
+ * @covers ::get_test_is_in_debug_mode()
+ */
+ public function test_is_in_debug_mode_error_log_non_existent_without_wp_debug_log() {
+ $site_health = $this->setup_site_health_with_debug_properties( true, false, null );
+ $invalid_log_path = '/nonexistent/path/that/does/not/exist/debug.log';
+ $original_error_log = $this->set_error_log_path( $invalid_log_path );
+ $actual_result = $site_health->get_test_is_in_debug_mode();
+ $expected_result = $this->get_debug_log_non_existent_path_result( false );
+
+ $this->restore_error_log_path( $original_error_log );
+
+ $this->assertSame( $expected_result['status'], $actual_result['status'], 'Status should be "critical" when error log location cannot be determined.' );
+ $this->assertSame( $expected_result['label'], $actual_result['label'], 'Label should indicate that error log location could not be determined.' );
+ $this->assertStringContainsString( $expected_result['description'], $actual_result['description'], 'Description should mention error log path is nonexistent.' );
+ }
+
+ /**
+ * Tests get_test_is_in_debug_mode() when WP_DEBUG_DISPLAY is enabled in production.
+ *
+ * @ticket 64071
+ *
+ * @covers ::get_test_is_in_debug_mode()
+ */
+ public function test_is_in_debug_mode_display_enabled_production() {
+ $site_health_mock = $this->getMockBuilder( 'WP_Site_Health' )
+ ->onlyMethods( array( 'is_development_environment' ) )
+ ->getMock();
+
+ $site_health_mock->method( 'is_development_environment' )
+ ->willReturn( false );
+
+ $site_health = new WP_Site_Health();
+ $reflection_mock = new ReflectionClass( $site_health );
+
+ $wp_debug_property_mock = $reflection_mock->getProperty( 'wp_debug' );
+ if ( PHP_VERSION_ID < 80100 ) {
+ $wp_debug_property_mock->setAccessible( true );
+ }
+ $wp_debug_property_mock->setValue( $site_health_mock, true );
+
+ $wp_debug_display_property_mock = $reflection_mock->getProperty( 'wp_debug_display' );
+ if ( PHP_VERSION_ID < 80100 ) {
+ $wp_debug_display_property_mock->setAccessible( true );
+ }
+ $wp_debug_display_property_mock->setValue( $site_health_mock, true );
+
+ $actual_result = $site_health_mock->get_test_is_in_debug_mode();
+
+ $this->assertSame( 'critical', $actual_result['status'], 'Status should be "critical" when WP_DEBUG_DISPLAY is enabled in production.' );
+ $this->assertSame( 'Your site is set to display errors to site visitors', $actual_result['label'], 'Label should indicate that errors are displayed to visitors.' );
+ $this->assertStringContainsString( 'WP_DEBUG_DISPLAY', $actual_result['description'], 'Description should contain WP_DEBUG_DISPLAY.' );
+ }
+
+ /**
+ * Tests get_test_is_in_debug_mode() when WP_DEBUG_DISPLAY is enabled in development.
+ *
+ * @ticket 64071
+ *
+ * @covers ::get_test_is_in_debug_mode()
+ */
+ public function test_is_in_debug_mode_display_enabled_development() {
+ $site_health_mock = $this->getMockBuilder( 'WP_Site_Health' )
+ ->onlyMethods( array( 'is_development_environment' ) )
+ ->getMock();
+
+ $site_health_mock->method( 'is_development_environment' )
+ ->willReturn( true );
+
+ $site_health = new WP_Site_Health();
+ $reflection_mock = new ReflectionClass( $site_health );
+
+ $wp_debug_property_mock = $reflection_mock->getProperty( 'wp_debug' );
+ if ( PHP_VERSION_ID < 80100 ) {
+ $wp_debug_property_mock->setAccessible( true );
+ }
+ $wp_debug_property_mock->setValue( $site_health_mock, true );
+
+ $wp_debug_display_property_mock = $reflection_mock->getProperty( 'wp_debug_display' );
+ if ( PHP_VERSION_ID < 80100 ) {
+ $wp_debug_display_property_mock->setAccessible( true );
+ }
+ $wp_debug_display_property_mock->setValue( $site_health_mock, true );
+
+ $actual_result = $site_health_mock->get_test_is_in_debug_mode();
+
+ $this->assertSame( 'recommended', $actual_result['status'], 'Status should be "recommended" when WP_DEBUG_DISPLAY is enabled in development.' );
+ $this->assertSame( 'Your site is set to display errors to site visitors', $actual_result['label'], 'Label should indicate that errors are displayed to visitors.' );
+ $this->assertStringContainsString( 'WP_DEBUG_DISPLAY', $actual_result['description'], 'Description should contain WP_DEBUG_DISPLAY.' );
+ }
+
+ /**
+ * Tests get_test_is_in_debug_mode() validates actual_result structure.
+ *
+ * @ticket 64071
+ *
+ * @covers ::get_test_is_in_debug_mode()
+ */
+ public function test_is_in_debug_mode_result_structure() {
+ $site_health = new WP_Site_Health();
+ $actual_result = $site_health->get_test_is_in_debug_mode();
+
+ $this->assertArrayHasKey( 'label', $actual_result, 'Result should have a label.' );
+ $this->assertArrayHasKey( 'status', $actual_result, 'Result should have a status.' );
+ $this->assertArrayHasKey( 'badge', $actual_result, 'Result should have a badge.' );
+ $this->assertArrayHasKey( 'description', $actual_result, 'Result should have a description.' );
+ $this->assertArrayHasKey( 'actions', $actual_result, 'Result should have actions.' );
+ $this->assertArrayHasKey( 'test', $actual_result, 'Result should have a test identifier.' );
+ $this->assertIsArray( $actual_result['badge'], 'Badge should be an array.' );
+ $this->assertArrayHasKey( 'label', $actual_result['badge'], 'Badge should have a label.' );
+ $this->assertArrayHasKey( 'color', $actual_result['badge'], 'Badge should have a color.' );
+ $this->assertContains( $actual_result['status'], array( 'good', 'recommended', 'critical' ), 'Status should be one of: good, recommended, critical.' );
+ }
}