Skip to content

Commit

Permalink
Background image: add support for relative theme path URLs in top-lev…
Browse files Browse the repository at this point in the history
…el theme.json styles (WordPress#61271)

* This initial commit:
- removes requirement for a `source` property in theme.json as the assumption is that, for now, all paths are paths to image files, whether absolute or relative
- checks for existence of "host" in URL and then tries to resolve background image url using get_theme_file_uri
- Adds a new public method  WP_Theme_JSON_Gutenberg::resolve_theme_file_uris to allow theme devs to optionally resolve relative paths in theme.json to a theme.

* For testing purposes, resolve in get_merged_data - should it be optional? That is, done in the global themes controller and wherever a stylesheet is generated?

* Rollback test of imperative method in resolver

* Moves resolution of file paths back to theme_json resolver

* Backend resolution of theme file URIs for global styles.

* Working on revisions

Backend resolution of theme file URIs for global styles revisions

Ensuring links are preserved when updating global styles.

* So my linter is working again

* Changed the relative link to `wp:theme-file-uris`
Always adding path to the link object so that it can dynamically resolved.

* Added some explanatory TODOs

* Adding valid link attributes to the _link object.
Updated tests

* Added some unit tests for utils

* Switching to using file: prefix, which is an established theme.json convention for relative paths to theme assets. E.g., web fonts
Adding test image to empty theme
Added theme JSON schema update

Unit tests for JS helper

Using response methods to add links to response collection

* Fix linting

* Remove TODO

* Update tests

* dump var_dump

* Check for $theme_json before resolving

* be explicit about the background value file:./

* Remove unnecessary empty check

* Abstracting getting any resolved URI to separate hook
Updating comments

* Update lib/class-wp-theme-json-resolver-gutenberg.php

* Update lib/class-wp-theme-json-resolver-gutenberg.php

* Revert useGlobalStyle changes - no longer required given the new hook

* Bad revert

* Rename wp:theme-file-uris to wp:theme-file
Rename utils file

---------

Co-authored-by: ramonjd <ramonopoly@git.wordpress.org>
Co-authored-by: andrewserong <andrewserong@git.wordpress.org>
Co-authored-by: noisysocks <noisysocks@git.wordpress.org>
Co-authored-by: tellthemachines <isabel_brison@git.wordpress.org>
Co-authored-by: oandregal <oandregal@git.wordpress.org>
Co-authored-by: TimothyBJacobs <timothyblynjacobs@git.wordpress.org>
Co-authored-by: creativecoder <grantmkin@git.wordpress.org>
  • Loading branch information
8 people authored and patil-vipul committed Jun 17, 2024
1 parent c5f68bf commit 692ddc7
Show file tree
Hide file tree
Showing 23 changed files with 525 additions and 24 deletions.
45 changes: 38 additions & 7 deletions lib/class-wp-rest-global-styles-controller-gutenberg.php
Original file line number Diff line number Diff line change
Expand Up @@ -357,6 +357,7 @@ protected function prepare_item_for_database( $request ) {
*
* @since 5.9.0
* @since 6.2.0 Handling of style.css was added to WP_Theme_JSON.
* @since 6.6.0 Added custom relative theme file URIs to `_links`.
*
* @param WP_Post $post Global Styles post object.
* @param WP_REST_Request $request Request object.
Expand All @@ -366,8 +367,10 @@ public function prepare_item_for_response( $post, $request ) { // phpcs:ignore V
$raw_config = json_decode( $post->post_content, true );
$is_global_styles_user_theme_json = isset( $raw_config['isGlobalStylesUserThemeJSON'] ) && true === $raw_config['isGlobalStylesUserThemeJSON'];
$config = array();
$theme_json = null;
if ( $is_global_styles_user_theme_json ) {
$config = ( new WP_Theme_JSON_Gutenberg( $raw_config, 'custom' ) )->get_raw_data();
$theme_json = new WP_Theme_JSON_Gutenberg( $raw_config, 'custom' );
$config = $theme_json->get_raw_data();
}

// Base fields for every post.
Expand Down Expand Up @@ -409,6 +412,13 @@ public function prepare_item_for_response( $post, $request ) { // phpcs:ignore V

if ( rest_is_field_included( '_links', $fields ) || rest_is_field_included( '_embedded', $fields ) ) {
$links = $this->prepare_links( $post->ID );
// Only return resolved URIs for get requests to user theme JSON.
if ( $theme_json ) {
$resolved_theme_uris = WP_Theme_JSON_Resolver_Gutenberg::get_resolved_theme_uris( $theme_json );
if ( ! empty( $resolved_theme_uris ) ) {
$links['https://api.w.org/theme-file'] = $resolved_theme_uris;
}
}
$response->add_links( $links );
if ( ! empty( $links['self']['href'] ) ) {
$actions = $this->get_available_actions();
Expand Down Expand Up @@ -620,18 +630,22 @@ public function get_theme_item( $request ) {
$data['styles'] = isset( $raw_data['styles'] ) ? $raw_data['styles'] : array();
}

$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
$data = $this->add_additional_fields_to_object( $data, $request );
$data = $this->filter_response_by_context( $data, $context );

$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
$data = $this->add_additional_fields_to_object( $data, $request );
$data = $this->filter_response_by_context( $data, $context );
$response = rest_ensure_response( $data );

if ( rest_is_field_included( '_links', $fields ) || rest_is_field_included( '_embedded', $fields ) ) {
$links = array(
$links = array(
'self' => array(
'href' => rest_url( sprintf( '%s/%s/themes/%s', $this->namespace, $this->rest_base, $request['stylesheet'] ) ),
),
);
$resolved_theme_uris = WP_Theme_JSON_Resolver_Gutenberg::get_resolved_theme_uris( $theme );
if ( ! empty( $resolved_theme_uris ) ) {
$links['https://api.w.org/theme-file'] = $resolved_theme_uris;
}

$response->add_links( $links );
}

Expand Down Expand Up @@ -671,6 +685,7 @@ public function get_theme_items_permissions_check( $request ) { // phpcs:ignore
* @since 6.0.0
* @since 6.2.0 Returns parent theme variations, if they exist.
* @since 6.4.0 Removed unnecessary local variable.
* @since 6.6.0 Added custom relative theme file URIs to `_links` for each item.
*
* @param WP_REST_Request $request The request instance.
*
Expand All @@ -686,9 +701,25 @@ public function get_theme_items( $request ) {
);
}

$response = array();
$variations = WP_Theme_JSON_Resolver_Gutenberg::get_style_variations();

return rest_ensure_response( $variations );
// Add resolved theme asset links.
foreach ( $variations as $variation ) {
$variation_theme_json = new WP_Theme_JSON_Gutenberg( $variation );
$resolved_theme_uris = WP_Theme_JSON_Resolver_Gutenberg::get_resolved_theme_uris( $variation_theme_json );
$data = rest_ensure_response( $variation );
if ( ! empty( $resolved_theme_uris ) ) {
$data->add_links(
array(
'https://api.w.org/theme-file' => $resolved_theme_uris,
)
);
}
$response[] = $this->prepare_response_for_collection( $data );
}

return rest_ensure_response( $response );
}

/**
Expand Down
74 changes: 74 additions & 0 deletions lib/class-wp-theme-json-resolver-gutenberg.php
Original file line number Diff line number Diff line change
Expand Up @@ -760,4 +760,78 @@ public static function get_style_variations() {
}
return $variations;
}


/**
* Resolves relative paths in theme.json styles to theme absolute paths
* and returns them in an array that can be embedded
* as the value of `_link` object in REST API responses.
*
* @since 6.6.0
*
* @param WP_Theme_JSON_Gutenberg $theme_json A theme json instance.
* @return array An array of resolved paths.
*/
public static function get_resolved_theme_uris( $theme_json ) {
$resolved_theme_uris = array();

if ( ! $theme_json instanceof WP_Theme_JSON_Gutenberg ) {
return $resolved_theme_uris;
}

$theme_json_data = $theme_json->get_raw_data();

// Top level styles.
$background_image_url = $theme_json_data['styles']['background']['backgroundImage']['url'] ?? null;
// Using the same file convention when registering web fonts. See: WP_Font_Face_Resolver:: to_theme_file_uri.
$placeholder = 'file:./';
if (
isset( $background_image_url ) &&
is_string( $background_image_url ) &&
// Skip if the src doesn't start with the placeholder, as there's nothing to replace.
str_starts_with( $background_image_url, $placeholder ) ) {
$file_type = wp_check_filetype( $background_image_url );
$src_url = str_replace( $placeholder, '', $background_image_url );
$resolved_theme_uri = array(
'name' => $background_image_url,
'href' => sanitize_url( get_theme_file_uri( $src_url ) ),
'target' => 'styles.background.backgroundImage.url',
);
if ( isset( $file_type['type'] ) ) {
$resolved_theme_uri['type'] = $file_type['type'];
}
$resolved_theme_uris[] = $resolved_theme_uri;
}

return $resolved_theme_uris;
}

/**
* Resolves relative paths in theme.json styles to theme absolute paths
* and merges them with incoming theme JSON.
*
* @since 6.6.0
*
* @param WP_Theme_JSON_Gutenberg $theme_json A theme json instance.
* @return WP_Theme_JSON_Gutenberg Theme merged with resolved paths, if any found.
*/
public static function resolve_theme_file_uris( $theme_json ) {
$resolved_urls = static::get_resolved_theme_uris( $theme_json );
if ( empty( $resolved_urls ) ) {
return $theme_json;
}

$resolved_theme_json_data = array(
'version' => WP_Theme_JSON_Gutenberg::LATEST_SCHEMA,
);

foreach ( $resolved_urls as $resolved_url ) {
$path = explode( '.', $resolved_url['target'] );
_wp_array_set( $resolved_theme_json_data, $path, $resolved_url['href'] );
}

$theme_json->merge( new WP_Theme_JSON_Gutenberg( $resolved_theme_json_data ) );

return $theme_json;
}
}
10 changes: 0 additions & 10 deletions lib/compat/wordpress-6.5/rest-api.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,6 @@
die( 'Silence is golden.' );
}

/**
* Registers the Global Styles Revisions REST API routes.
*/
function gutenberg_register_global_styles_revisions_endpoints() {
$global_styles_revisions_controller = new Gutenberg_REST_Global_Styles_Revisions_Controller_6_5();
$global_styles_revisions_controller->register_routes();
}

add_action( 'rest_api_init', 'gutenberg_register_global_styles_revisions_endpoints' );

/**
* Registers additional fields for wp_template and wp_template_part rest api.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
<?php
/**
* REST API: Gutenberg_REST_Global_Styles_Revisions_Controller class, inspired by WP_REST_Revisions_Controller.
*
* @package WordPress
* @subpackage REST_API
* @since 6.3.0
*/

/**
* Core class used to access global styles revisions via the REST API.
*
* @since 6.3.0
* @since 6.6.0 Added custom relative theme file URIs to `_links`.
*
* @see WP_REST_Controller
*/
class Gutenberg_REST_Global_Styles_Revisions_Controller_6_6 extends Gutenberg_REST_Global_Styles_Revisions_Controller_6_5 {
/**
* Prepares the revision for the REST response.
*
* @since 6.3.0
* @since 6.6.0 Added resolved URI links to the response.
*
* @param WP_Post $post Post revision object.
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response|WP_Error Response object.
*/
public function prepare_item_for_response( $post, $request ) {
$parent = $this->get_parent( $request['parent'] );
$global_styles_config = $this->get_decoded_global_styles_json( $post->post_content );

if ( is_wp_error( $global_styles_config ) ) {
return $global_styles_config;
}

$fields = $this->get_fields_for_response( $request );
$data = array();
$theme_json = array();

if ( ! empty( $global_styles_config['styles'] ) || ! empty( $global_styles_config['settings'] ) ) {
$theme_json = new WP_Theme_JSON_Gutenberg( $global_styles_config, 'custom' );
$global_styles_config = ( $theme_json )->get_raw_data();

if ( rest_is_field_included( 'settings', $fields ) ) {
$data['settings'] = ! empty( $global_styles_config['settings'] ) ? $global_styles_config['settings'] : new stdClass();
}
if ( rest_is_field_included( 'styles', $fields ) ) {
$data['styles'] = ! empty( $global_styles_config['styles'] ) ? $global_styles_config['styles'] : new stdClass();
}
}

if ( rest_is_field_included( 'author', $fields ) ) {
$data['author'] = (int) $post->post_author;
}

if ( rest_is_field_included( 'date', $fields ) ) {
$data['date'] = $this->prepare_date_response( $post->post_date_gmt, $post->post_date );
}

if ( rest_is_field_included( 'date_gmt', $fields ) ) {
$data['date_gmt'] = $this->prepare_date_response( $post->post_date_gmt );
}

if ( rest_is_field_included( 'id', $fields ) ) {
$data['id'] = (int) $post->ID;
}

if ( rest_is_field_included( 'modified', $fields ) ) {
$data['modified'] = $this->prepare_date_response( $post->post_modified_gmt, $post->post_modified );
}

if ( rest_is_field_included( 'modified_gmt', $fields ) ) {
$data['modified_gmt'] = $this->prepare_date_response( $post->post_modified_gmt );
}

if ( rest_is_field_included( 'parent', $fields ) ) {
$data['parent'] = (int) $parent->ID;
}

$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
$data = $this->add_additional_fields_to_object( $data, $request );
$data = $this->filter_response_by_context( $data, $context );

$response = rest_ensure_response( $data );

// Add resolved URIs to the response.
$links = array();
$resolved_theme_uris = WP_Theme_JSON_Resolver_Gutenberg::get_resolved_theme_uris( $theme_json );
if ( ! empty( $resolved_theme_uris ) ) {
$links['https://api.w.org/theme-file'] = $resolved_theme_uris;
}
$response->add_links( $links );

return $response;
}
}
11 changes: 11 additions & 0 deletions lib/compat/wordpress-6.6/rest-api.php
Original file line number Diff line number Diff line change
Expand Up @@ -76,3 +76,14 @@ function gutenberg_add_class_list_to_public_post_types() {
}
}
add_action( 'rest_api_init', 'gutenberg_add_class_list_to_public_post_types' );


/**
* Registers the Global Styles Revisions REST API routes.
*/
function gutenberg_register_global_styles_revisions_endpoints() {
$global_styles_revisions_controller = new Gutenberg_REST_Global_Styles_Revisions_Controller_6_6();
$global_styles_revisions_controller->register_routes();
}

add_action( 'rest_api_init', 'gutenberg_register_global_styles_revisions_endpoints' );
1 change: 1 addition & 0 deletions lib/global-styles-and-settings.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ function gutenberg_get_global_stylesheet( $types = array() ) {
}
}
$tree = WP_Theme_JSON_Resolver_Gutenberg::get_merged_data();
$tree = WP_Theme_JSON_Resolver_Gutenberg::resolve_theme_file_uris( $tree );

$supports_theme_json = wp_theme_has_theme_json();
if ( empty( $types ) && ! $supports_theme_json ) {
Expand Down
1 change: 1 addition & 0 deletions lib/load.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ function gutenberg_is_experiment_enabled( $name ) {
require_once __DIR__ . '/compat/wordpress-6.5/rest-api.php';

// WordPress 6.6 compat.
require __DIR__ . '/compat/wordpress-6.6/class-gutenberg-rest-global-styles-revisions-controller-6-6.php';
require __DIR__ . '/compat/wordpress-6.6/class-gutenberg-rest-templates-controller-6-6.php';
require __DIR__ . '/compat/wordpress-6.6/rest-api.php';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import { TOOLSPANEL_DROPDOWNMENU_PROPS } from './utils';
import { setImmutably } from '../../utils/object';
import MediaReplaceFlow from '../media-replace-flow';
import { store as blockEditorStore } from '../../store';
import { getResolvedThemeFilePath } from './theme-file-uri-utils';

const IMAGE_BACKGROUND_TYPE = 'image';
const DEFAULT_CONTROLS = {
Expand Down Expand Up @@ -191,6 +192,7 @@ function BackgroundImageToolsPanelItem( {
onChange,
style,
inheritedValue,
themeFileURIs,
} ) {
const mediaUpload = useSelect(
( select ) => select( blockEditorStore ).getSettings().mediaUpload,
Expand Down Expand Up @@ -301,7 +303,10 @@ function BackgroundImageToolsPanelItem( {
<InspectorImagePreview
label={ title }
filename={ title || __( 'Untitled' ) }
url={ url }
url={ getResolvedThemeFilePath(
url,
themeFileURIs
) }
/>
}
variant="secondary"
Expand Down Expand Up @@ -340,6 +345,7 @@ function BackgroundSizeToolsPanelItem( {
style,
inheritedValue,
defaultValues,
themeFileURIs,
} ) {
const sizeValue =
style?.background?.backgroundSize ||
Expand Down Expand Up @@ -468,7 +474,7 @@ function BackgroundSizeToolsPanelItem( {
<FocalPointPicker
__next40pxDefaultSize
label={ __( 'Position' ) }
url={ imageValue }
url={ getResolvedThemeFilePath( imageValue, themeFileURIs ) }
value={ backgroundPositionToCoords( positionValue ) }
onChange={ updateBackgroundPosition }
/>
Expand Down Expand Up @@ -553,6 +559,7 @@ export default function BackgroundPanel( {
defaultControls = DEFAULT_CONTROLS,
defaultValues = {},
headerLabel = __( 'Background image' ),
themeFileURIs,
} ) {
const resetAllFilter = useCallback( ( previousValue ) => {
return {
Expand All @@ -577,6 +584,7 @@ export default function BackgroundPanel( {
isShownByDefault={ defaultControls.backgroundImage }
style={ value }
inheritedValue={ inheritedValue }
themeFileURIs={ themeFileURIs }
/>
{ shouldShowBackgroundSizeControls && (
<BackgroundSizeToolsPanelItem
Expand All @@ -586,6 +594,7 @@ export default function BackgroundPanel( {
style={ value }
inheritedValue={ inheritedValue }
defaultValues={ defaultValues }
themeFileURIs={ themeFileURIs }
/>
) }
</Wrapper>
Expand Down
Loading

0 comments on commit 692ddc7

Please sign in to comment.