diff --git a/src/wp-includes/blocks.php b/src/wp-includes/blocks.php index 4d3ad3d4bd..31f1b22469 100644 --- a/src/wp-includes/blocks.php +++ b/src/wp-includes/blocks.php @@ -445,11 +445,13 @@ function register_block_type_from_metadata( $file_or_folder, $args = array() ) { * Using a static variable ensures that the metadata is only read once per request. */ + $file_or_folder = wp_normalize_path( $file_or_folder ); + $metadata_file = ( ! str_ends_with( $file_or_folder, 'block.json' ) ) ? trailingslashit( $file_or_folder ) . 'block.json' : $file_or_folder; - $is_core_block = str_starts_with( $file_or_folder, ABSPATH . WPINC ); + $is_core_block = str_starts_with( $file_or_folder, wp_normalize_path( ABSPATH . WPINC ) ); $metadata_file_exists = $is_core_block || file_exists( $metadata_file ); $registry_metadata = WP_Block_Metadata_Registry::get_metadata( $file_or_folder ); diff --git a/src/wp-includes/class-wp-block-metadata-registry.php b/src/wp-includes/class-wp-block-metadata-registry.php index e7e71afcd1..cba8193b7e 100644 --- a/src/wp-includes/class-wp-block-metadata-registry.php +++ b/src/wp-includes/class-wp-block-metadata-registry.php @@ -82,7 +82,7 @@ class WP_Block_Metadata_Registry { * @return bool True if the collection was registered successfully, false otherwise. */ public static function register_collection( $path, $manifest ) { - $path = wp_normalize_path( rtrim( $path, '/' ) ); + $path = rtrim( wp_normalize_path( $path ), '/' ); $collection_roots = self::get_default_collection_roots(); @@ -112,7 +112,7 @@ class WP_Block_Metadata_Registry { $collection_roots = array_unique( array_map( static function ( $allowed_root ) { - return rtrim( $allowed_root, '/' ); + return rtrim( wp_normalize_path( $allowed_root ), '/' ); }, $collection_roots ) @@ -161,6 +161,8 @@ class WP_Block_Metadata_Registry { * @return array|null The block metadata for the block, or null if not found. */ public static function get_metadata( $file_or_folder ) { + $file_or_folder = wp_normalize_path( $file_or_folder ); + $path = self::find_collection_path( $file_or_folder ); if ( ! $path ) { return null; @@ -194,7 +196,7 @@ class WP_Block_Metadata_Registry { * @return string[] List of block metadata file paths, or an empty array if the given `$path` is invalid. */ public static function get_collection_block_metadata_files( $path ) { - $path = wp_normalize_path( rtrim( $path, '/' ) ); + $path = rtrim( wp_normalize_path( $path ), '/' ); if ( ! isset( self::$collections[ $path ] ) ) { _doing_it_wrong( @@ -213,6 +215,7 @@ class WP_Block_Metadata_Registry { } return array_map( + // No normalization necessary since `$path` is already normalized and `$block_name` is just a folder name. static function ( $block_name ) use ( $path ) { return "{$path}/{$block_name}/block.json"; }, @@ -225,8 +228,8 @@ class WP_Block_Metadata_Registry { * * @since 6.7.0 * - * @param string $file_or_folder The path to the file or folder. - * @return string|null The collection path if found, or null if not found. + * @param string $file_or_folder The normalized path to the file or folder. + * @return string|null The normalized collection path if found, or null if not found. */ private static function find_collection_path( $file_or_folder ) { if ( empty( $file_or_folder ) ) { @@ -234,7 +237,7 @@ class WP_Block_Metadata_Registry { } // Check the last matched collection first, since block registration usually happens in batches per plugin or theme. - $path = wp_normalize_path( rtrim( $file_or_folder, '/' ) ); + $path = rtrim( $file_or_folder, '/' ); if ( self::$last_matched_collection && str_starts_with( $path, self::$last_matched_collection ) ) { return self::$last_matched_collection; } @@ -279,7 +282,7 @@ class WP_Block_Metadata_Registry { * * @since 6.7.0 * - * @param string $path The file or folder path to determine the block identifier from. + * @param string $path The normalized file or folder path to determine the block identifier from. * @return string The block identifier, or an empty string if the path is empty. */ private static function default_identifier_callback( $path ) { @@ -302,8 +305,8 @@ class WP_Block_Metadata_Registry { * * @since 6.7.2 * - * @param string $path Block metadata collection path, without trailing slash. - * @param string[] $collection_roots List of collection root paths, without trailing slashes. + * @param string $path Normalized block metadata collection path, without trailing slash. + * @param string[] $collection_roots List of normalized collection root paths, without trailing slashes. * @return bool True if the path is allowed, false otherwise. */ private static function is_valid_collection_path( $path, $collection_roots ) { diff --git a/tests/phpunit/tests/blocks/register.php b/tests/phpunit/tests/blocks/register.php index 7e0c391e1f..8e0b6a6409 100644 --- a/tests/phpunit/tests/blocks/register.php +++ b/tests/phpunit/tests/blocks/register.php @@ -1501,4 +1501,15 @@ class Tests_Blocks_Register extends WP_UnitTestCase { $block_type->block_hooks ); } + + /** + * @ticket 63027 + * + * @covers ::register_block_type_from_metadata + */ + public function test_register_block_type_from_metadata_with_windows_path() { + $windows_like_path = str_replace( '/', '\\', DIR_TESTDATA ) . '\\blocks\\notice'; + + $this->assertNotFalse( register_block_type_from_metadata( $windows_like_path ) ); + } } diff --git a/tests/phpunit/tests/blocks/wpBlockMetadataRegistry.php b/tests/phpunit/tests/blocks/wpBlockMetadataRegistry.php index 6c3fd5e277..4a1ddb3c94 100644 --- a/tests/phpunit/tests/blocks/wpBlockMetadataRegistry.php +++ b/tests/phpunit/tests/blocks/wpBlockMetadataRegistry.php @@ -220,4 +220,72 @@ class Tests_Blocks_WpBlockMetadataRegistry extends WP_UnitTestCase { WP_Block_Metadata_Registry::get_collection_block_metadata_files( $path ) ); } + + /** + * Tests that `register_collection()`, `get_metadata()`, and `get_collection_metadata_files()` handle Windows paths. + * + * @ticket 63027 + * @covers ::register_collection + * @covers ::get_metadata + * @covers ::get_collection_metadata_files + */ + public function test_with_windows_paths() { + // Set up a mock manifest file. + $manifest_data = array( + 'test-block' => array( + 'name' => 'test-block', + 'title' => 'Test Block', + ), + ); + file_put_contents( $this->temp_manifest_file, 'assertTrue( WP_Block_Metadata_Registry::register_collection( $plugin_path, $this->temp_manifest_file ), 'Could not register block metadata collection.' ); + $this->assertSame( $manifest_data['test-block'], WP_Block_Metadata_Registry::get_metadata( $block_path ), 'Could not find collection for provided block.json path.' ); + $this->assertSame( array( wp_normalize_path( $block_path ) ), WP_Block_Metadata_Registry::get_collection_block_metadata_files( $plugin_path ), 'Could not get correct list of block.json paths for collection.' ); + } + + /** + * Tests that `register_collection()` handles Windows paths correctly for verifying allowed roots. + * + * @ticket 63027 + * @covers ::register_collection + */ + public function test_with_windows_paths_and_disallowed_location() { + $parent_path = 'C:\\Site\\wp-content'; + $plugins_path = $parent_path . '\\plugins'; + $plugin_path = $plugins_path . '\\my-plugin\\blocks'; + + // Register the mock plugins directory as an allowed root. + add_filter( + 'wp_allowed_block_metadata_collection_roots', + static function ( $paths ) use ( $plugins_path ) { + $paths[] = $plugins_path; + return $paths; + } + ); + + $this->setExpectedIncorrectUsage( 'WP_Block_Metadata_Registry::register_collection' ); + + $result = WP_Block_Metadata_Registry::register_collection( $plugins_path, $this->temp_manifest_file ); + $this->assertFalse( $result, 'Arbitrary Windows path should not be registered if it matches a collection root' ); + + $result = WP_Block_Metadata_Registry::register_collection( $parent_path, $this->temp_manifest_file ); + $this->assertFalse( $result, 'Arbitrary Windows path should not be registered if it is a parent directory of a collection root' ); + + $result = WP_Block_Metadata_Registry::register_collection( $plugin_path, $this->temp_manifest_file ); + $this->assertTrue( $result, 'Arbitrary Windows path should be registered successfully if it is within a collection root' ); + } }