Make WordPress Core

Changeset 59730


Ignore:
Timestamp:
01/29/2025 07:39:16 PM (19 months 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.

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

Location:
trunk
Files:
2 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-includes/class-wp-block-metadata-registry.php

    r59132 r59730  
    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}
  • trunk/tests/phpunit/tests/blocks/wpBlockMetadataRegistry.php

    r59132 r59730  
    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.