diff --git a/src/wp-admin/includes/class-wp-automatic-updater.php b/src/wp-admin/includes/class-wp-automatic-updater.php index 2facbeb1d522f..edb6cc617634c 100644 --- a/src/wp-admin/includes/class-wp-automatic-updater.php +++ b/src/wp-admin/includes/class-wp-automatic-updater.php @@ -107,24 +107,34 @@ public function is_allowed_dir( $dir ) { } /** - * Checks for version control checkouts. + * Discovers version control checkouts without applying filters. * - * Checks for Subversion, Git, Mercurial, and Bazaar. It recursively looks up the - * filesystem to the top of the drive, erring on the side of detecting a VCS - * checkout somewhere. + * Checks for Subversion, Git, Mercurial, and Bazaar. It walks up from each + * relevant directory to the top of the drive, erring on the side of detecting + * a VCS checkout somewhere. * * ABSPATH is always checked in addition to whatever `$context` is (which may be the * wp-content directory, for example). The underlying assumption is that if you are * using version control *anywhere*, then you should be making decisions for * how things get updated. * - * @since 3.7.0 + * @since 7.1.0 * * @param string $context The filesystem path to check, in addition to ABSPATH. - * @return bool True if a VCS checkout was discovered at `$context` or ABSPATH, - * or anywhere higher. False otherwise. + * @return array{ + * checkout: bool, + * check_dir: string, + * vcs_dir: string + * } { + * @type bool $checkout Whether a VCS checkout was discovered at `$context` + * or ABSPATH, or anywhere higher. + * @type string $check_dir Directory where the VCS metadata directory was found. + * Empty string when `$checkout` is false. + * @type string $vcs_dir Version control metadata directory name (e.g. `.git`). + * Empty string when `$checkout` is false. + * } */ - public function is_vcs_checkout( $context ) { + public function get_vcs_checkout_details( $context ) { $context_dirs = array( untrailingslashit( $context ) ); if ( ABSPATH !== $context ) { $context_dirs[] = untrailingslashit( ABSPATH ); @@ -149,21 +159,53 @@ public function is_vcs_checkout( $context ) { $check_dirs = array_unique( $check_dirs ); $checkout = false; + $check_dir = ''; + $vcs_dir = ''; // Search all directories we've found for evidence of version control. - foreach ( $vcs_dirs as $vcs_dir ) { - foreach ( $check_dirs as $check_dir ) { - if ( ! $this->is_allowed_dir( $check_dir ) ) { + foreach ( $vcs_dirs as $vcs_dir_candidate ) { + foreach ( $check_dirs as $check_dir_candidate ) { + if ( ! $this->is_allowed_dir( $check_dir_candidate ) ) { continue; } - $checkout = is_dir( rtrim( $check_dir, '\\/' ) . "/$vcs_dir" ); + $checkout = is_dir( rtrim( $check_dir_candidate, '\\/' ) . "/$vcs_dir_candidate" ); if ( $checkout ) { + $check_dir = $check_dir_candidate; + $vcs_dir = $vcs_dir_candidate; break 2; } } } + return array( + 'checkout' => $checkout, + 'check_dir' => $check_dir, + 'vcs_dir' => $vcs_dir, + ); + } + + /** + * Checks for version control checkouts. + * + * Checks for Subversion, Git, Mercurial, and Bazaar. It recursively looks up the + * filesystem to the top of the drive, erring on the side of detecting a VCS + * checkout somewhere. + * + * ABSPATH is always checked in addition to whatever `$context` is (which may be the + * wp-content directory, for example). The underlying assumption is that if you are + * using version control *anywhere*, then you should be making decisions for + * how things get updated. + * + * @since 3.7.0 + * + * @param string $context The filesystem path to check, in addition to ABSPATH. + * @return bool True if a VCS checkout was discovered at `$context` or ABSPATH, + * or anywhere higher. False otherwise. + */ + public function is_vcs_checkout( $context ) { + $details = $this->get_vcs_checkout_details( $context ); + /** * Filters whether the automatic updater should consider a filesystem * location to be potentially managed by a version control system. @@ -175,7 +217,7 @@ public function is_vcs_checkout( $context ) { * @param string $context The filesystem context (a path) against which * filesystem status should be checked. */ - return apply_filters( 'automatic_updates_is_vcs_checkout', $checkout, $context ); + return apply_filters( 'automatic_updates_is_vcs_checkout', $details['checkout'], $context ); } /** diff --git a/src/wp-admin/includes/class-wp-site-health-auto-updates.php b/src/wp-admin/includes/class-wp-site-health-auto-updates.php index 1904acd4e08c5..23c064ba3dc8b 100644 --- a/src/wp-admin/includes/class-wp-site-health-auto-updates.php +++ b/src/wp-admin/includes/class-wp-site-health-auto-updates.php @@ -214,44 +214,20 @@ public function test_if_failed_update() { * @return array The test results. */ public function test_vcs_abspath() { - $context_dirs = array( ABSPATH ); - $vcs_dirs = array( '.svn', '.git', '.hg', '.bzr' ); - $check_dirs = array(); - - foreach ( $context_dirs as $context_dir ) { - // Walk up from $context_dir to the root. - do { - $check_dirs[] = $context_dir; - - // Once we've hit '/' or 'C:\', we need to stop. dirname will keep returning the input here. - if ( dirname( $context_dir ) === $context_dir ) { - break; - } - - // Continue one level at a time. - } while ( $context_dir = dirname( $context_dir ) ); + if ( ! class_exists( 'WP_Automatic_Updater' ) ) { + require_once ABSPATH . 'wp-admin/includes/class-wp-automatic-updater.php'; } - $check_dirs = array_unique( $check_dirs ); - $updater = new WP_Automatic_Updater(); - $checkout = false; - - // Search all directories we've found for evidence of version control. - foreach ( $vcs_dirs as $vcs_dir ) { - foreach ( $check_dirs as $check_dir ) { - if ( ! $updater->is_allowed_dir( $check_dir ) ) { - continue; - } - - $checkout = is_dir( rtrim( $check_dir, '\\/' ) . "/$vcs_dir" ); - if ( $checkout ) { - break 2; - } - } - } + $updater = new WP_Automatic_Updater(); + $details = $updater->get_vcs_checkout_details( ABSPATH ); + $checkout = $details['checkout']; + $check_dir = $details['check_dir']; + $vcs_dir = $details['vcs_dir']; /** This filter is documented in wp-admin/includes/class-wp-automatic-updater.php */ - if ( $checkout && ! apply_filters( 'automatic_updates_is_vcs_checkout', true, ABSPATH ) ) { + $filtered = apply_filters( 'automatic_updates_is_vcs_checkout', $checkout, ABSPATH ); + + if ( $checkout && ! $filtered ) { return array( 'description' => sprintf( /* translators: 1: Folder name. 2: Version control directory. 3: Filter name. */ diff --git a/tests/phpunit/tests/admin/wpAutomaticUpdater.php b/tests/phpunit/tests/admin/wpAutomaticUpdater.php index ee52f44daf9c1..49385faad6f52 100644 --- a/tests/phpunit/tests/admin/wpAutomaticUpdater.php +++ b/tests/phpunit/tests/admin/wpAutomaticUpdater.php @@ -733,4 +733,45 @@ public function test_is_vcs_checkout_should_return_false_when_no_directories_are $this->assertFalse( $updater_mock->is_vcs_checkout( get_temp_dir() ) ); } + + /** + * Tests that `WP_Automatic_Updater::get_vcs_checkout_details()` reports no checkout + * when none of the checked directories are allowed. + * + * @ticket 47428 + * + * @covers WP_Automatic_Updater::get_vcs_checkout_details + */ + public function test_get_vcs_checkout_details_should_report_no_checkout_when_no_directories_are_allowed() { + $updater_mock = $this->getMockBuilder( 'WP_Automatic_Updater' ) + ->setMethods( array( 'is_allowed_dir' ) ) + ->getMock(); + + $updater_mock->expects( $this->any() )->method( 'is_allowed_dir' )->willReturn( false ); + + $details = $updater_mock->get_vcs_checkout_details( get_temp_dir() ); + + $this->assertIsArray( $details ); + $this->assertFalse( $details['checkout'] ); + $this->assertSame( '', $details['check_dir'] ); + $this->assertSame( '', $details['vcs_dir'] ); + } + + /** + * Tests that `is_vcs_checkout()` applies the same filter input as a direct + * `apply_filters()` call using raw discovery from `get_vcs_checkout_details()`. + * + * @ticket 47428 + * + * @covers WP_Automatic_Updater::get_vcs_checkout_details + * @covers WP_Automatic_Updater::is_vcs_checkout + */ + public function test_is_vcs_checkout_matches_filtered_get_vcs_checkout_details_checkout() { + $context = ABSPATH; + + $details = self::$updater->get_vcs_checkout_details( $context ); + $expected = apply_filters( 'automatic_updates_is_vcs_checkout', $details['checkout'], $context ); + + $this->assertSame( $expected, self::$updater->is_vcs_checkout( $context ) ); + } }