Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix block type registration bugs on Windows by correctly normalizing paths #8417

Open
wants to merge 3 commits into
base: trunk
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/wp-includes/blocks.php
Original file line number Diff line number Diff line change
Expand Up @@ -445,6 +445,8 @@ 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;
Expand Down
21 changes: 12 additions & 9 deletions src/wp-includes/class-wp-block-metadata-registry.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down Expand Up @@ -112,7 +112,7 @@ public static function register_collection( $path, $manifest ) {
$collection_roots = array_unique(
array_map(
static function ( $allowed_root ) {
return rtrim( $allowed_root, '/' );
return rtrim( wp_normalize_path( $allowed_root ), '/' );
},
$collection_roots
)
Expand Down Expand Up @@ -161,6 +161,8 @@ static function ( $allowed_root ) {
* @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;
Expand Down Expand Up @@ -194,7 +196,7 @@ public static function get_metadata( $file_or_folder ) {
* @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(
Expand All @@ -213,6 +215,7 @@ public static function get_collection_block_metadata_files( $path ) {
}

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";
},
Expand All @@ -225,16 +228,16 @@ static function ( $block_name ) use ( $path ) {
*
* @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 ) ) {
return null;
}

// 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, '/' );
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense, because normalization is happening in the public method ::get_metadata() before passing the value to this and the also private ::default_identifier_callback() methods. Thanks for clarifying in the docs for both ✨

if ( self::$last_matched_collection && str_starts_with( $path, self::$last_matched_collection ) ) {
return self::$last_matched_collection;
}
Expand Down Expand Up @@ -279,7 +282,7 @@ public static function has_metadata( $file_or_folder ) {
*
* @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 ) {
Expand All @@ -302,8 +305,8 @@ private static function default_identifier_callback( $path ) {
*
* @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 ) {
Expand Down
11 changes: 11 additions & 0 deletions tests/phpunit/tests/blocks/register.php
Original file line number Diff line number Diff line change
Expand Up @@ -1501,4 +1501,15 @@ public function test_register_block_hooks_targeting_itself() {
$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 ) );
}
}
68 changes: 68 additions & 0 deletions tests/phpunit/tests/blocks/wpBlockMetadataRegistry.php
Original file line number Diff line number Diff line change
Expand Up @@ -220,4 +220,72 @@ public function test_get_collection_block_metadata_files() {
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, '<?php return ' . var_export( $manifest_data, true ) . ';' );

$plugins_path = 'C:\\Site\\wp-content\\plugins';
$plugin_path = $plugins_path . '\\my-plugin\\blocks';
$block_path = $plugin_path . '\\test-block\\block.json';

// 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->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' );
}
}
Loading