Make WordPress Core

Changeset 59731


Ignore:
Timestamp:
01/29/2025 09:14:53 PM (2 weeks ago)
Author:
flixos90
Message:

Editor: Relax restrictions around registration of block metadata collections.

This changeset allows for block metadata collections to be registered for almost any source, such as MU plugins, themes, or custom directories with e.g. symlinked plugins or symlinked themes. Prior to the change, block metadata collections could only be registered for plugins and WordPress Core.

There are still safeguards in place to prevent registration of collections in locations that would cause conflicts. For example, it is not possible to register a collection for the entire wp-content/plugins directory or the entire wp-content/themes directory, since such a collection would conflict with any specific plugin's or theme's collection. In case developers would like to enable this safeguard for their own custom directories, they can use the new wp_allowed_block_metadata_collection_roots filter.

Reviewed by jorbin.
Merges [59730] to the 6.7 branch.

Props assassinateur, bowedk, desrosj, dougwollison, flixos90, glynnquelch, gziolo, jorbin, mreishus, swissspidy.
Fixes #62140.

Location:
branches/6.7
Files:
3 edited

Legend:

Unmodified
Added
Removed
  • branches/6.7

  • branches/6.7/src/wp-includes/class-wp-block-metadata-registry.php

    r59132 r59731  
    3939
    4040    /**
    41      * Stores the WordPress 'wp-includes' directory path.
    42      *
    43      * @since 6.7.0
    44      * @var string|null
    45      */
    46     private static $wpinc_dir = null;
    47 
    48     /**
    49      * Stores the normalized WordPress plugin directory path.
    50      *
    51      * @since 6.7.0
    52      * @var string|null
    53      */
    54     private static $plugin_dir = null;
     41     * Stores the default allowed collection root paths.
     42     *
     43     * @since 6.7.2
     44     * @var string[]|null
     45     */
     46    private static $default_collection_roots = null;
    5547
    5648    /**
     
    9385        $path = wp_normalize_path( rtrim( $path, '/' ) );
    9486
    95         $wpinc_dir  = self::get_wpinc_dir();
    96         $plugin_dir = self::get_plugin_dir();
     87        $collection_roots = self::get_default_collection_roots();
     88
     89        /**
     90         * Filters the root directory paths for block metadata collections.
     91         *
     92         * Any block metadata collection that is registered must not use any of these paths, or any parent directory
     93         * path of them. Most commonly, block metadata collections should reside within one of these paths, though in
     94         * some scenarios they may also reside in entirely different directories (e.g. in case of symlinked plugins).
     95         *
     96         * Example:
     97         * * It is allowed to register a collection with path `WP_PLUGIN_DIR . '/my-plugin'`.
     98         * * It is not allowed to register a collection with path `WP_PLUGIN_DIR`.
     99         * * It is not allowed to register a collection with path `dirname( WP_PLUGIN_DIR )`.
     100         *
     101         * The default list encompasses the `wp-includes` directory, as well as the root directories for plugins,
     102         * must-use plugins, and themes. This filter can be used to expand the list, e.g. to custom directories that
     103         * contain symlinked plugins, so that these root directories cannot be used themselves for a block metadata
     104         * collection either.
     105         *
     106         * @since 6.7.2
     107         *
     108         * @param string[] $collection_roots List of allowed metadata collection root paths.
     109         */
     110        $collection_roots = apply_filters( 'wp_allowed_block_metadata_collection_roots', $collection_roots );
     111
     112        $collection_roots = array_unique(
     113            array_map(
     114                static function ( $allowed_root ) {
     115                    return rtrim( $allowed_root, '/' );
     116                },
     117                $collection_roots
     118            )
     119        );
    97120
    98121        // Check if the path is valid:
    99         if ( str_starts_with( $path, $plugin_dir ) ) {
    100             // For plugins, ensure the path is within a specific plugin directory and not the base plugin directory.
    101             $relative_path = substr( $path, strlen( $plugin_dir ) + 1 );
    102             $plugin_name   = strtok( $relative_path, '/' );
    103 
    104             if ( empty( $plugin_name ) || $plugin_name === $relative_path ) {
    105                 _doing_it_wrong(
    106                     __METHOD__,
    107                     __( 'Block metadata collections can only be registered for a specific plugin. The provided path is neither a core path nor a valid plugin path.' ),
    108                     '6.7.0'
    109                 );
    110                 return false;
    111             }
    112         } elseif ( ! str_starts_with( $path, $wpinc_dir ) ) {
    113             // If it's neither a plugin directory path nor within 'wp-includes', the path is invalid.
     122        if ( ! self::is_valid_collection_path( $path, $collection_roots ) ) {
    114123            _doing_it_wrong(
    115124                __METHOD__,
    116                 __( 'Block metadata collections can only be registered for a specific plugin. The provided path is neither a core path nor a valid plugin path.' ),
    117                 '6.7.0'
     125                sprintf(
     126                    /* translators: %s: list of allowed collection roots */
     127                    __( 'Block metadata collections cannot be registered as one of the following directories or their parent directories: %s' ),
     128                    esc_html( implode( wp_get_list_item_separator(), $collection_roots ) )
     129                ),
     130                '6.7.2'
    118131            );
    119132            return false;
     
    245258
    246259    /**
    247      * Gets the WordPress 'wp-includes' directory path.
    248      *
    249      * @since 6.7.0
    250      *
    251      * @return string The WordPress 'wp-includes' directory path.
    252      */
    253     private static function get_wpinc_dir() {
    254         if ( ! isset( self::$wpinc_dir ) ) {
    255             self::$wpinc_dir = wp_normalize_path( ABSPATH . WPINC );
    256         }
    257         return self::$wpinc_dir;
    258     }
    259 
    260     /**
    261      * Gets the normalized WordPress plugin directory path.
    262      *
    263      * @since 6.7.0
    264      *
    265      * @return string The normalized WordPress plugin directory path.
    266      */
    267     private static function get_plugin_dir() {
    268         if ( ! isset( self::$plugin_dir ) ) {
    269             self::$plugin_dir = wp_normalize_path( WP_PLUGIN_DIR );
    270         }
    271         return self::$plugin_dir;
     260     * Checks whether the given block metadata collection path is valid against the list of collection roots.
     261     *
     262     * @since 6.7.2
     263     *
     264     * @param string   $path             Block metadata collection path, without trailing slash.
     265     * @param string[] $collection_roots List of collection root paths, without trailing slashes.
     266     * @return bool True if the path is allowed, false otherwise.
     267     */
     268    private static function is_valid_collection_path( $path, $collection_roots ) {
     269        foreach ( $collection_roots as $allowed_root ) {
     270            // If the path matches any root exactly, it is invalid.
     271            if ( $allowed_root === $path ) {
     272                return false;
     273            }
     274
     275            // If the path is a parent path of any of the roots, it is invalid.
     276            if ( str_starts_with( $allowed_root, $path ) ) {
     277                return false;
     278            }
     279        }
     280
     281        return true;
     282    }
     283
     284    /**
     285     * Gets the default collection root directory paths.
     286     *
     287     * @since 6.7.2
     288     *
     289     * @return string[] List of directory paths within which metadata collections are allowed.
     290     */
     291    private static function get_default_collection_roots() {
     292        if ( isset( self::$default_collection_roots ) ) {
     293            return self::$default_collection_roots;
     294        }
     295
     296        $collection_roots = array(
     297            wp_normalize_path( ABSPATH . WPINC ),
     298            wp_normalize_path( WP_CONTENT_DIR ),
     299            wp_normalize_path( WPMU_PLUGIN_DIR ),
     300            wp_normalize_path( WP_PLUGIN_DIR ),
     301        );
     302
     303        $theme_roots = get_theme_roots();
     304        if ( ! is_array( $theme_roots ) ) {
     305            $theme_roots = array( $theme_roots );
     306        }
     307        foreach ( $theme_roots as $theme_root ) {
     308            $collection_roots[] = trailingslashit( wp_normalize_path( WP_CONTENT_DIR ) ) . ltrim( wp_normalize_path( $theme_root ), '/' );
     309        }
     310
     311        self::$default_collection_roots = array_unique( $collection_roots );
     312        return self::$default_collection_roots;
    272313    }
    273314}
  • branches/6.7/tests/phpunit/tests/blocks/wpBlockMetadataRegistry.php

    r59132 r59731  
    8181    }
    8282
    83     public function test_register_collection_with_non_existent_path() {
    84         $non_existent_path = '/path/that/does/not/exist';
     83    /**
     84     * @ticket 62140
     85     */
     86    public function test_register_collection_with_valid_muplugin_path() {
     87        $plugin_path = WPMU_PLUGIN_DIR . '/my-plugin/blocks';
     88        $result      = WP_Block_Metadata_Registry::register_collection( $plugin_path, $this->temp_manifest_file );
     89        $this->assertTrue( $result, 'Valid must-use plugin path should be registered successfully' );
     90    }
     91
     92    /**
     93     * @ticket 62140
     94     */
     95    public function test_register_collection_with_invalid_muplugin_path() {
     96        $invalid_plugin_path = WPMU_PLUGIN_DIR;
    8597
    8698        $this->setExpectedIncorrectUsage( 'WP_Block_Metadata_Registry::register_collection' );
    8799
    88         $result = WP_Block_Metadata_Registry::register_collection( $non_existent_path, $this->temp_manifest_file );
    89         $this->assertFalse( $result, 'Non-existent path should not be registered' );
     100        $result = WP_Block_Metadata_Registry::register_collection( $invalid_plugin_path, $this->temp_manifest_file );
     101        $this->assertFalse( $result, 'Invalid must-use plugin path should not be registered' );
     102    }
     103
     104    /**
     105     * @ticket 62140
     106     */
     107    public function test_register_collection_with_valid_theme_path() {
     108        $theme_path = WP_CONTENT_DIR . '/themes/my-theme/blocks';
     109        $result     = WP_Block_Metadata_Registry::register_collection( $theme_path, $this->temp_manifest_file );
     110        $this->assertTrue( $result, 'Valid theme path should be registered successfully' );
     111    }
     112
     113    /**
     114     * @ticket 62140
     115     */
     116    public function test_register_collection_with_invalid_theme_path() {
     117        $invalid_theme_path = WP_CONTENT_DIR . '/themes';
     118
     119        $this->setExpectedIncorrectUsage( 'WP_Block_Metadata_Registry::register_collection' );
     120
     121        $result = WP_Block_Metadata_Registry::register_collection( $invalid_theme_path, $this->temp_manifest_file );
     122        $this->assertFalse( $result, 'Invalid theme path should not be registered' );
     123    }
     124
     125    /**
     126     * @ticket 62140
     127     */
     128    public function test_register_collection_with_arbitrary_path() {
     129        $arbitrary_path = '/var/arbitrary/path';
     130        $result         = WP_Block_Metadata_Registry::register_collection( $arbitrary_path, $this->temp_manifest_file );
     131        $this->assertTrue( $result, 'Arbitrary path should be registered successfully' );
     132    }
     133
     134    /**
     135     * @ticket 62140
     136     */
     137    public function test_register_collection_with_arbitrary_path_and_collection_roots_filter() {
     138        $arbitrary_path = '/var/arbitrary/path';
     139        add_filter(
     140            'wp_allowed_block_metadata_collection_roots',
     141            static function ( $paths ) use ( $arbitrary_path ) {
     142                $paths[] = $arbitrary_path;
     143                return $paths;
     144            }
     145        );
     146
     147        $this->setExpectedIncorrectUsage( 'WP_Block_Metadata_Registry::register_collection' );
     148
     149        $result = WP_Block_Metadata_Registry::register_collection( $arbitrary_path, $this->temp_manifest_file );
     150        $this->assertFalse( $result, 'Arbitrary path should not be registered if it matches a collection root' );
     151
     152        $result = WP_Block_Metadata_Registry::register_collection( dirname( $arbitrary_path ), $this->temp_manifest_file );
     153        $this->assertFalse( $result, 'Arbitrary path should not be registered if it is a parent directory of a collection root' );
     154
     155        $result = WP_Block_Metadata_Registry::register_collection( $arbitrary_path . '/my-plugin/blocks', $this->temp_manifest_file );
     156        $this->assertTrue( $result, 'Arbitrary path should be registered successfully if it is within a collection root' );
     157    }
     158
     159    /**
     160     * @ticket 62140
     161     */
     162    public function test_register_collection_with_wp_content_parent_directory_path() {
     163        $invalid_path = dirname( WP_CONTENT_DIR );
     164
     165        $this->setExpectedIncorrectUsage( 'WP_Block_Metadata_Registry::register_collection' );
     166
     167        $result = WP_Block_Metadata_Registry::register_collection( $invalid_path, $this->temp_manifest_file );
     168        $this->assertFalse( $result, 'Invalid path (parent directory of "wp-content") should not be registered' );
     169    }
     170
     171    /**
     172     * @ticket 62140
     173     */
     174    public function test_register_collection_with_wp_includes_parent_directory_path() {
     175        $invalid_path = ABSPATH;
     176
     177        $this->setExpectedIncorrectUsage( 'WP_Block_Metadata_Registry::register_collection' );
     178
     179        $result = WP_Block_Metadata_Registry::register_collection( $invalid_path, $this->temp_manifest_file );
     180        $this->assertFalse( $result, 'Invalid path (parent directory of "wp-includes") should not be registered' );
     181    }
     182
     183    public function test_register_collection_with_non_existent_manifest() {
     184        $non_existent_manifest = '/path/that/does/not/exist/block-manifest.php';
     185
     186        $this->setExpectedIncorrectUsage( 'WP_Block_Metadata_Registry::register_collection' );
     187
     188        $result = WP_Block_Metadata_Registry::register_collection( '/var/arbitrary/path', $non_existent_manifest );
     189        $this->assertFalse( $result, 'Non-existent manifest should not be registered' );
    90190    }
    91191}
Note: See TracChangeset for help on using the changeset viewer.