Make WordPress Core

Changeset 58262


Ignore:
Timestamp:
05/31/2024 01:17:50 AM (2 years ago)
Author:
noisysocks
Message:

Block Themes: Add support for relative URLs in top-level theme.json styles

Allow using relative file: URLs in top-level theme.json properties such as
styles.background, and modify the REST API to provide clients with the
absolute URLs via a '​https://api.w.org/theme-file' attribute in the _links
array.

Props ramonopoly.
Fixes #61273.

Location:
trunk
Files:
7 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-includes/class-wp-theme-json-resolver.php

    r58185 r58262  
    745745                return $variations;
    746746        }
     747
     748        /**
     749         * Resolves relative paths in theme.json styles to theme absolute paths
     750         * and returns them in an array that can be embedded
     751         * as the value of `_link` object in REST API responses.
     752         *
     753         * @since 6.6.0
     754         *
     755         * @param WP_Theme_JSON  $theme_json A theme json instance.
     756         * @return array An array of resolved paths.
     757         */
     758        public static function get_resolved_theme_uris( $theme_json ) {
     759                $resolved_theme_uris = array();
     760
     761                if ( ! $theme_json instanceof WP_Theme_JSON ) {
     762                        return $resolved_theme_uris;
     763                }
     764
     765                $theme_json_data = $theme_json->get_raw_data();
     766
     767                // Top level styles.
     768                $background_image_url = isset( $theme_json_data['styles']['background']['backgroundImage']['url'] ) ? $theme_json_data['styles']['background']['backgroundImage']['url'] : null;
     769
     770                /*
     771                 * The same file convention when registering web fonts.
     772                 * See: WP_Font_Face_Resolver:: to_theme_file_uri.
     773                 */
     774                $placeholder = 'file:./';
     775                if (
     776                        isset( $background_image_url ) &&
     777                        is_string( $background_image_url ) &&
     778                        // Skip if the src doesn't start with the placeholder, as there's nothing to replace.
     779                        str_starts_with( $background_image_url, $placeholder )
     780                ) {
     781                        $file_type          = wp_check_filetype( $background_image_url );
     782                        $src_url            = str_replace( $placeholder, '', $background_image_url );
     783                        $resolved_theme_uri = array(
     784                                'name'   => $background_image_url,
     785                                'href'   => sanitize_url( get_theme_file_uri( $src_url ) ),
     786                                'target' => 'styles.background.backgroundImage.url',
     787                        );
     788                        if ( isset( $file_type['type'] ) ) {
     789                                $resolved_theme_uri['type'] = $file_type['type'];
     790                        }
     791                        $resolved_theme_uris[] = $resolved_theme_uri;
     792                }
     793
     794                return $resolved_theme_uris;
     795        }
     796
     797        /**
     798         * Resolves relative paths in theme.json styles to theme absolute paths
     799         * and merges them with incoming theme JSON.
     800         *
     801         * @since 6.6.0
     802         *
     803         * @param WP_Theme_JSON  $theme_json A theme json instance.
     804         * @return WP_Theme_JSON Theme merged with resolved paths, if any found.
     805         */
     806        public static function resolve_theme_file_uris( $theme_json ) {
     807                $resolved_urls = static::get_resolved_theme_uris( $theme_json );
     808                if ( empty( $resolved_urls ) ) {
     809                        return $theme_json;
     810                }
     811
     812                $resolved_theme_json_data = array(
     813                        'version' => WP_Theme_JSON::LATEST_SCHEMA,
     814                );
     815
     816                foreach ( $resolved_urls as $resolved_url ) {
     817                        $path = explode( '.', $resolved_url['target'] );
     818                        _wp_array_set( $resolved_theme_json_data, $path, $resolved_url['href'] );
     819                }
     820
     821                $theme_json->merge( new WP_Theme_JSON( $resolved_theme_json_data ) );
     822
     823                return $theme_json;
     824        }
    747825}
  • trunk/src/wp-includes/global-styles-and-settings.php

    r58026 r58262  
    140140 * @since 5.9.0
    141141 * @since 6.1.0 Added 'base-layout-styles' support.
     142 * @since 6.6.0 Resolves relative paths in theme.json styles to theme absolute paths.
    142143 *
    143144 * @param array $types Optional. Types of styles to load.
    … …  
    180181        }
    181182
    182         $tree = WP_Theme_JSON_Resolver::get_merged_data();
    183 
     183        $tree                = WP_Theme_JSON_Resolver::resolve_theme_file_uris( WP_Theme_JSON_Resolver::get_merged_data() );
    184184        $supports_theme_json = wp_theme_has_theme_json();
     185
    185186        if ( empty( $types ) && ! $supports_theme_json ) {
    186187                $types = array( 'variables', 'presets', 'base-layout-styles' );
  • trunk/src/wp-includes/rest-api/endpoints/class-wp-rest-global-styles-controller.php

    r58225 r58262  
    290290         *
    291291         * @since 5.9.0
     292         * @since 6.6.0 Added custom relative theme file URIs to `_links`.
    292293         *
    293294         * @param WP_Post         $post    Global Styles post object.
    … …  
    299300                $is_global_styles_user_theme_json = isset( $raw_config['isGlobalStylesUserThemeJSON'] ) && true === $raw_config['isGlobalStylesUserThemeJSON'];
    300301                $config                           = array();
     302                $theme_json                       = null;
    301303                if ( $is_global_styles_user_theme_json ) {
    302                         $config = ( new WP_Theme_JSON( $raw_config, 'custom' ) )->get_raw_data();
     304                        $theme_json = new WP_Theme_JSON( $raw_config, 'custom' );
     305                        $config     = $theme_json->get_raw_data();
    303306                }
    304307
    … …  
    342345                if ( rest_is_field_included( '_links', $fields ) || rest_is_field_included( '_embedded', $fields ) ) {
    343346                        $links = $this->prepare_links( $post->ID );
     347
     348                        // Only return resolved URIs for get requests to user theme JSON.
     349                        if ( $theme_json ) {
     350                                $resolved_theme_uris = WP_Theme_JSON_Resolver::get_resolved_theme_uris( $theme_json );
     351                                if ( ! empty( $resolved_theme_uris ) ) {
     352                                        $links['https://api.w.org/theme-file'] = $resolved_theme_uris;
     353                                }
     354                        }
     355
    344356                        $response->add_links( $links );
    345357                        if ( ! empty( $links['self']['href'] ) ) {
    … …  
    516528         *
    517529         * @since 5.9.0
     530         * @since 6.6.0 Added custom relative theme file URIs to `_links`.
    518531         *
    519532         * @param WP_REST_Request $request The request instance.
    … …  
    550563
    551564                if ( rest_is_field_included( '_links', $fields ) || rest_is_field_included( '_embedded', $fields ) ) {
    552                         $links = array(
     565                        $links               = array(
    553566                                'self' => array(
    554567                                        'href' => rest_url( sprintf( '%s/%s/themes/%s', $this->namespace, $this->rest_base, $request['stylesheet'] ) ),
    555568                                ),
    556569                        );
     570                        $resolved_theme_uris = WP_Theme_JSON_Resolver::get_resolved_theme_uris( $theme );
     571                        if ( ! empty( $resolved_theme_uris ) ) {
     572                                $links['https://api.w.org/theme-file'] = $resolved_theme_uris;
     573                        }
    557574                        $response->add_links( $links );
    558575                }
    … …  
    592609         * @since 6.0.0
    593610         * @since 6.2.0 Returns parent theme variations, if they exist.
     611         * @since 6.6.0 Added custom relative theme file URIs to `_links` for each item.
    594612         *
    595613         * @param WP_REST_Request $request The request instance.
    … …  
    607625                }
    608626
     627                $response   = array();
    609628                $variations = WP_Theme_JSON_Resolver::get_style_variations();
    610629
    611                 return rest_ensure_response( $variations );
     630                foreach ( $variations as $variation ) {
     631                        $variation_theme_json = new WP_Theme_JSON( $variation );
     632                        $resolved_theme_uris  = WP_Theme_JSON_Resolver::get_resolved_theme_uris( $variation_theme_json );
     633                        $data                 = rest_ensure_response( $variation );
     634                        if ( ! empty( $resolved_theme_uris ) ) {
     635                                $data->add_links(
     636                                        array(
     637                                                'https://api.w.org/theme-file' => $resolved_theme_uris,
     638                                        )
     639                                );
     640                        }
     641                        $response[] = $this->prepare_response_for_collection( $data );
     642                }
     643
     644                return rest_ensure_response( $response );
    612645        }
    613646
  • trunk/src/wp-includes/rest-api/endpoints/class-wp-rest-global-styles-revisions-controller.php

    r58225 r58262  
    269269         *
    270270         * @since 6.3.0
     271         * @since 6.6.0 Added resolved URI links to the response.
    271272         *
    272273         * @param WP_Post         $post    Post revision object.
    … …  
    282283                }
    283284
    284                 $fields = $this->get_fields_for_response( $request );
    285                 $data   = array();
     285                $fields     = $this->get_fields_for_response( $request );
     286                $data       = array();
     287                $theme_json = null;
    286288
    287289                if ( ! empty( $global_styles_config['styles'] ) || ! empty( $global_styles_config['settings'] ) ) {
    288                         $global_styles_config = ( new WP_Theme_JSON( $global_styles_config, 'custom' ) )->get_raw_data();
     290                        $theme_json           = new WP_Theme_JSON( $global_styles_config, 'custom' );
     291                        $global_styles_config = $theme_json->get_raw_data();
    289292                        if ( rest_is_field_included( 'settings', $fields ) ) {
    290293                                $data['settings'] = ! empty( $global_styles_config['settings'] ) ? $global_styles_config['settings'] : new stdClass();
    … …  
    323326                }
    324327
    325                 $context = ! empty( $request['context'] ) ? $request['context'] : 'view';
    326                 $data    = $this->add_additional_fields_to_object( $data, $request );
    327                 $data    = $this->filter_response_by_context( $data, $context );
    328 
    329                 return rest_ensure_response( $data );
     328                $context             = ! empty( $request['context'] ) ? $request['context'] : 'view';
     329                $data                = $this->add_additional_fields_to_object( $data, $request );
     330                $data                = $this->filter_response_by_context( $data, $context );
     331                $response            = rest_ensure_response( $data );
     332                $resolved_theme_uris = WP_Theme_JSON_Resolver::get_resolved_theme_uris( $theme_json );
     333
     334                if ( ! empty( $resolved_theme_uris ) ) {
     335                        $response->add_links(
     336                                array(
     337                                        'https://api.w.org/theme-file' => $resolved_theme_uris,
     338                                )
     339                        );
     340                }
     341
     342                return $response;
    330343        }
    331344
  • trunk/tests/phpunit/data/themedir1/block-theme/styles/variation-b.json

    r55231 r58262  
    1515                        }
    1616                }
     17        },
     18        "styles": {
     19                "background": {
     20                        "backgroundImage": {
     21                                "url": "file:./assets/sugarloaf-mountain.jpg"
     22                        }
     23                }
    1724        }
    1825}
  • trunk/tests/phpunit/tests/rest-api/rest-global-styles-controller.php

    r57662 r58262  
    3535                parent::set_up();
    3636                switch_theme( 'tt1-blocks' );
     37                add_filter( 'theme_file_uri', array( $this, 'filter_theme_file_uri' ) );
     38        }
     39
     40        public function tear_down() {
     41                remove_filter( 'theme_file_uri', array( $this, 'filter_theme_file_uri' ) );
     42                parent::tear_down();
    3743        }
    3844
    … …  
    8086        }
    8187
     88        /*
     89         * This filter callback normalizes the return value from `get_theme_file_uri`
     90         * to guard against changes in test environments.
     91         * The test suite otherwise returns full system dir path, e.g.,
     92         * /var/www/tests/phpunit/includes/../data/themedir1/block-theme/assets/sugarloaf-mountain.jpg
     93         */
     94        public function filter_theme_file_uri( $file ) {
     95                $file_name = substr( strrchr( $file, '/' ), 1 );
     96                return 'https://example.org/wp-content/themes/example-theme/assets/' . $file_name;
     97        }
     98
    8299        /**
    83100         * @covers WP_REST_Global_Styles_Controller::register_routes
    … …  
    120137        }
    121138
     139        /**
     140         * Tests a GET request to the global styles variations endpoint.
     141         *
     142         * @covers WP_REST_Global_Styles_Controller::get_theme_items
     143         * @ticket 61273
     144         */
    122145        public function test_get_theme_items() {
    123146                wp_set_current_user( self::$admin_id );
    … …  
    129152                        array(
    130153                                'version'  => 2,
    131                                 'title'    => 'variation-a',
    132154                                'settings' => array(
    133155                                        'blocks' => array(
    … …  
    147169                                        ),
    148170                                ),
     171                                'title'    => 'variation-a',
    149172                        ),
    150173                        array(
    151174                                'version'  => 2,
    152                                 'title'    => 'variation-b',
    153175                                'settings' => array(
    154176                                        'blocks' => array(
    … …  
    165187                                                                ),
    166188                                                        ),
     189                                                ),
     190                                        ),
     191                                ),
     192                                'styles'   => array(
     193                                        'background' => array(
     194                                                'backgroundImage' => array(
     195                                                        'url' => 'file:./assets/sugarloaf-mountain.jpg',
     196                                                ),
     197                                        ),
     198                                ),
     199                                'title'    => 'variation-b',
     200                                '_links'   => array(
     201                                        'curies'        => array(
     202                                                array(
     203                                                        'name'      => 'wp',
     204                                                        'href'      => 'https://api.w.org/{rel}',
     205                                                        'templated' => true,
     206                                                ),
     207                                        ),
     208                                        'wp:theme-file' => array(
     209                                                array(
     210                                                        'href'   => 'https://example.org/wp-content/themes/example-theme/assets/sugarloaf-mountain.jpg',
     211                                                        'name'   => 'file:./assets/sugarloaf-mountain.jpg',
     212                                                        'target' => 'styles.background.backgroundImage.url',
     213                                                        'type'   => 'image/jpeg',
    167214                                                ),
    168215                                        ),
  • trunk/tests/phpunit/tests/theme/wpThemeJsonResolver.php

    r57885 r58262  
    104104                add_filter( 'stylesheet_root', array( $this, 'filter_set_theme_root' ) );
    105105                add_filter( 'template_root', array( $this, 'filter_set_theme_root' ) );
     106                add_filter( 'theme_file_uri', array( $this, 'filter_theme_file_uri' ) );
    106107                $this->queries = array();
    107108                // Clear caches.
    … …  
    114115                wp_clean_themes_cache();
    115116                unset( $GLOBALS['wp_themes'] );
     117                remove_filter( 'theme_file_uri', array( $this, 'filter_theme_file_uri' ) );
    116118
    117119                // Reset data between tests.
    118120                wp_clean_theme_json_cache();
    119121                parent::tear_down();
     122        }
     123
     124        /*
     125         * This filter callback normalizes the return value from `get_theme_file_uri`
     126         * to guard against changes in test environments.
     127         * The test suite otherwise returns full system dir path, e.g.,
     128         * /var/www/tests/phpunit/includes/../data/themedir1/block-theme/assets/sugarloaf-mountain.jpg
     129         */
     130        public function filter_theme_file_uri( $file ) {
     131                $file_name = substr( strrchr( $file, '/' ), 1 );
     132                return 'https://example.org/wp-content/themes/example-theme/assets/' . $file_name;
    120133        }
    121134
    … …  
    11771190                $this->assertTrue( $default_presets_for_block );
    11781191        }
     1192
     1193        /**
     1194         * Tests that relative paths are resolved and merged into the theme.json data.
     1195         *
     1196         * @covers WP_Theme_JSON_Resolver::resolve_theme_file_uris
     1197         * @ticket 61273
     1198         */
     1199        public function test_resolve_theme_file_uris() {
     1200                $theme_json = new WP_Theme_JSON(
     1201                        array(
     1202                                'version' => WP_Theme_JSON::LATEST_SCHEMA,
     1203                                'styles'  => array(
     1204                                        'background' => array(
     1205                                                'backgroundImage' => array(
     1206                                                        'url' => 'file:./assets/image.png',
     1207                                                ),
     1208                                        ),
     1209                                ),
     1210                        )
     1211                );
     1212
     1213                $expected_data = array(
     1214                        'version' => WP_Theme_JSON::LATEST_SCHEMA,
     1215                        'styles'  => array(
     1216                                'background' => array(
     1217                                        'backgroundImage' => array(
     1218                                                'url' => 'https://example.org/wp-content/themes/example-theme/assets/image.png',
     1219                                        ),
     1220                                ),
     1221                        ),
     1222                );
     1223
     1224                $actual = WP_Theme_JSON_Resolver::resolve_theme_file_uris( $theme_json );
     1225
     1226                $this->assertSame( $expected_data, $actual->get_raw_data() );
     1227        }
     1228
     1229        /**
     1230         * Tests that them uris are resolved and bundled with other metadata in an array.
     1231         *
     1232         * @covers WP_Theme_JSON_Resolver::get_resolved_theme_uris
     1233         * @ticket 61273
     1234         */
     1235        public function test_get_resolved_theme_uris() {
     1236                $theme_json = new WP_Theme_JSON(
     1237                        array(
     1238                                'version' => WP_Theme_JSON::LATEST_SCHEMA,
     1239                                'styles'  => array(
     1240                                        'background' => array(
     1241                                                'backgroundImage' => array(
     1242                                                        'url' => 'file:./assets/image.png',
     1243                                                ),
     1244                                        ),
     1245                                ),
     1246                        )
     1247                );
     1248
     1249                $expected_data = array(
     1250                        array(
     1251                                'name'   => 'file:./assets/image.png',
     1252                                'href'   => 'https://example.org/wp-content/themes/example-theme/assets/image.png',
     1253                                'target' => 'styles.background.backgroundImage.url',
     1254                                'type'   => 'image/png',
     1255                        ),
     1256                );
     1257
     1258                $actual = WP_Theme_JSON_Resolver::get_resolved_theme_uris( $theme_json );
     1259
     1260                $this->assertSame( $expected_data, $actual );
     1261        }
    11791262}
Note: See TracChangeset for help on using the changeset viewer.