Make WordPress Core

Changeset 58262


Ignore:
Timestamp:
05/31/2024 01:17:50 AM (8 weeks 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.