PluginProbe
Gutenberg / trunk
Gutenberg vtrunk
24.1.0 24.0.0 23.9.1 23.9.0 23.8.0 23.7.2 23.7.1 23.7.0 23.6.1 23.6.2 23.6.0 23.5.3 23.5.2 23.5.1 23.5.0 23.4.0 23.3.2 23.3.1 23.3.0 23.2.0 23.2.1 23.2.2 23.1.1 23.1.0 23.0.1 All 404 releases
gutenberg / lib / class-wp-theme-json-resolver-gutenberg.php

class-wp-theme-json-resolver-gutenberg.php in Gutenberg trunk, at lib/class-wp-theme-json-resolver-gutenberg.php

998 lines 34.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * WP_Theme_JSON_Resolver_Gutenberg class
4 *
5 * @package gutenberg
6 * @since 5.8.0
7 */
8
9 /**
10 * Class that abstracts the processing of the different data sources
11 * for site-level config and offers an API to work with them.
12 *
13 * This class is for internal core usage and is not supposed to be used by extenders (plugins and/or themes).
14 * This is a low-level API that may need to do breaking changes. Please,
15 * use gutenberg_get_global_settings, gutenberg_get_global_styles, and gutenberg_get_global_stylesheet instead.
16 *
17 * @since 5.8.0
18 * @access private
19 */
20 #[AllowDynamicProperties]
21 class WP_Theme_JSON_Resolver_Gutenberg {
22
23 /**
24 * Container for keep track of registered blocks.
25 *
26 * @since 6.1.0
27 * @var array
28 */
29 protected static $blocks_cache = array(
30 'core' => array(),
31 'blocks' => array(),
32 'theme' => array(),
33 'user' => array(),
34 );
35
36 /**
37 * Container for data coming from core.
38 *
39 * @since 5.8.0
40 * @var WP_Theme_JSON_Gutenberg
41 */
42 protected static $core = null;
43
44 /**
45 * Container for data coming from the blocks.
46 *
47 * @since 6.1.0
48 * @var WP_Theme_JSON_Gutenberg
49 */
50 protected static $blocks = null;
51
52 /**
53 * Container for data coming from the theme.
54 *
55 * @since 5.8.0
56 * @var WP_Theme_JSON_Gutenberg
57 */
58 protected static $theme = null;
59
60 /**
61 * Container for data coming from the user.
62 *
63 * @since 5.9.0
64 * @var WP_Theme_JSON_Gutenberg
65 */
66 protected static $user = null;
67
68 /**
69 * Stores the ID of the custom post type
70 * that holds the user data.
71 *
72 * @since 5.9.0
73 * @var int
74 */
75 protected static $user_custom_post_type_id = null;
76
77 /**
78 * Container to keep loaded i18n schema for `theme.json`.
79 *
80 * @since 5.8.0 As `$theme_json_i18n`.
81 * @since 5.9.0 Renamed from `$theme_json_i18n` to `$i18n_schema`.
82 * @var array
83 */
84 protected static $i18n_schema = null;
85
86 /**
87 * `theme.json` file cache.
88 *
89 * @since 6.1.0
90 * @var array
91 */
92 protected static $theme_json_file_cache = array();
93
94 /**
95 * Processes a file that adheres to the theme.json schema
96 * and returns an array with its contents, or a void array if none found.
97 *
98 * @since 5.8.0
99 * @since 6.1.0 Added caching.
100 *
101 * @param string $file_path Path to file. Empty if no file.
102 * @return array Contents that adhere to the theme.json schema.
103 */
104 protected static function read_json_file( $file_path ) {
105 if ( $file_path ) {
106 if ( array_key_exists( $file_path, static::$theme_json_file_cache ) ) {
107 return static::$theme_json_file_cache[ $file_path ];
108 }
109
110 $decoded_file = wp_json_file_decode( $file_path, array( 'associative' => true ) );
111 if ( is_array( $decoded_file ) ) {
112 static::$theme_json_file_cache[ $file_path ] = $decoded_file;
113 return static::$theme_json_file_cache[ $file_path ];
114 }
115 }
116
117 return array();
118 }
119
120 /**
121 * Returns a data structure used in theme.json translation.
122 *
123 * @since 5.8.0
124 * @deprecated 5.9.0
125 *
126 * @return array An array of theme.json fields that are translatable and the keys that are translatable.
127 */
128 public static function get_fields_to_translate() {
129 _deprecated_function( __METHOD__, '5.9.0' );
130 return array();
131 }
132
133 /**
134 * Given a theme.json structure modifies it in place to update certain values
135 * by its translated strings according to the language set by the user.
136 *
137 * @since 5.8.0
138 *
139 * @param array $theme_json The theme.json to translate.
140 * @param string $domain Optional. Text domain. Unique identifier for retrieving translated strings.
141 * Default 'default'.
142 * @return array Returns the modified $theme_json_structure.
143 */
144 protected static function translate( $theme_json, $domain = 'default' ) {
145 if ( null === static::$i18n_schema ) {
146 $i18n_schema = wp_json_file_decode( __DIR__ . '/theme-i18n.json' );
147 static::$i18n_schema = $i18n_schema ?? array();
148 }
149
150 return translate_settings_using_i18n_schema( static::$i18n_schema, $theme_json, $domain );
151 }
152
153 /**
154 * Returns core's origin config.
155 *
156 * @since 5.8.0
157 *
158 * @return WP_Theme_JSON_Gutenberg Entity that holds core data.
159 */
160 public static function get_core_data() {
161 if ( null !== static::$core && static::has_same_registered_blocks( 'core' ) ) {
162 return static::$core;
163 }
164
165 $config = static::read_json_file( __DIR__ . '/theme.json' );
166 $config = static::translate( $config );
167
168 /**
169 * Filters the default data provided by WordPress for global styles & settings.
170 *
171 * @since 6.1.0
172 *
173 * @param WP_Theme_JSON_Data_Gutenberg Class to access and update the underlying data.
174 */
175 $theme_json = apply_filters( 'wp_theme_json_data_default', new WP_Theme_JSON_Data_Gutenberg( $config, 'default' ) );
176 static::$core = $theme_json->get_theme_json();
177
178 return static::$core;
179 }
180
181 /**
182 * Checks whether the registered blocks were already processed for this origin.
183 *
184 * @since 6.1.0
185 *
186 * @param string $origin Data source for which to cache the blocks.
187 * Valid values are 'core', 'blocks', 'theme', and 'user'.
188 * @return bool True on success, false otherwise.
189 */
190 protected static function has_same_registered_blocks( $origin ) {
191 // Bail out if the origin is invalid.
192 if ( ! isset( static::$blocks_cache[ $origin ] ) ) {
193 return false;
194 }
195
196 $registry = WP_Block_Type_Registry::get_instance();
197 $blocks = $registry->get_all_registered();
198
199 // Is there metadata for all currently registered blocks?
200 $block_diff = array_diff_key( $blocks, static::$blocks_cache[ $origin ] );
201 if ( empty( $block_diff ) ) {
202 return true;
203 }
204
205 foreach ( $blocks as $block_name => $block_type ) {
206 static::$blocks_cache[ $origin ][ $block_name ] = true;
207 }
208
209 return false;
210 }
211
212 /**
213 * Returns the theme's data.
214 *
215 * Data from theme.json will be backfilled from existing
216 * theme supports, if any. Note that if the same data
217 * is present in theme.json and in theme supports,
218 * the theme.json takes precedence.
219 *
220 * @since 5.8.0
221 * @since 5.9.0 Theme supports have been inlined and the `$theme_support_data` argument removed.
222 * @since 6.0.0 Added an `$options` parameter to allow the theme data to be returned without theme supports.
223 * @since 6.6.0 Added support for 'default-font-sizes' and 'default-spacing-sizes' theme supports.
224 * Added registration and merging of block style variations from partial theme.json files and the block styles registry.
225 *
226 * @param array $deprecated Deprecated. Not used.
227 * @param array $options {
228 * Options arguments.
229 *
230 * @type bool $with_supports Whether to include theme supports in the data. Default true.
231 * }
232 * @return WP_Theme_JSON_Gutenberg Entity that holds theme data.
233 */
234 public static function get_theme_data( $deprecated = array(), $options = array() ) {
235 if ( ! empty( $deprecated ) ) {
236 _deprecated_argument( __METHOD__, '5.9.0' );
237 }
238
239 $options = wp_parse_args( $options, array( 'with_supports' => true ) );
240
241 if ( null === static::$theme || ! static::has_same_registered_blocks( 'theme' ) ) {
242 $wp_theme = wp_get_theme();
243 $theme_json_file = $wp_theme->get_file_path( 'theme.json' );
244 if ( is_readable( $theme_json_file ) ) {
245 $theme_json_data = static::read_json_file( $theme_json_file );
246 $theme_json_data = static::translate( $theme_json_data, $wp_theme->get( 'TextDomain' ) );
247 } else {
248 $theme_json_data = array( 'version' => WP_Theme_JSON_Gutenberg::LATEST_SCHEMA );
249 }
250
251 /*
252 * Register variations defined by theme partials (theme.json files in the styles directory).
253 * This is required so the variations pass sanitization of theme.json data.
254 */
255 $variations = static::get_style_variations( 'block' );
256 gutenberg_register_block_style_variations_from_theme_json_partials( $variations );
257
258 /*
259 * Source variations from the block styles registry and block style variation files. Then, merge them into the existing theme.json data.
260 *
261 * In case the same style properties are defined in several sources, this is how we should resolve the values,
262 * from higher to lower priority:
263 *
264 * - styles.blocks.blockType.variations from theme.json
265 * - styles.variations from theme.json
266 * - variations from block style variation files
267 * - variations from block styles registry
268 *
269 * See test_add_registered_block_styles_to_theme_data and test_unwraps_block_style_variations.
270 */
271 $theme_json_data = static::inject_variations_from_block_style_variation_files( $theme_json_data, $variations );
272 $theme_json_data = static::inject_variations_from_block_styles_registry( $theme_json_data );
273
274 /**
275 * Filters the data provided by the theme for global styles and settings.
276 *
277 * @since 6.1.0
278 *
279 * @param WP_Theme_JSON_Data_Gutenberg Class to access and update the underlying data.
280 */
281 $theme_json = apply_filters( 'wp_theme_json_data_theme', new WP_Theme_JSON_Data_Gutenberg( $theme_json_data, 'theme' ) );
282 static::$theme = $theme_json->get_theme_json();
283
284 if ( $wp_theme->parent() ) {
285 // Get parent theme.json.
286 $parent_theme_json_file = $wp_theme->parent()->get_file_path( 'theme.json' );
287 if ( $theme_json_file !== $parent_theme_json_file && is_readable( $parent_theme_json_file ) ) {
288 $parent_theme_json_data = static::read_json_file( $parent_theme_json_file );
289 $parent_theme_json_data = static::translate( $parent_theme_json_data, $wp_theme->parent()->get( 'TextDomain' ) );
290 $parent_theme = new WP_Theme_JSON_Gutenberg( $parent_theme_json_data );
291
292 /*
293 * Merge the child theme.json into the parent theme.json.
294 * The child theme takes precedence over the parent.
295 */
296 $parent_theme->merge( static::$theme );
297 static::$theme = $parent_theme;
298 }
299 }
300 }
301
302 if ( ! $options['with_supports'] ) {
303 return static::$theme;
304 }
305
306 /*
307 * We want the presets and settings declared in theme.json
308 * to override the ones declared via theme supports.
309 * So we take theme supports, transform it to theme.json shape
310 * and merge the static::$theme upon that.
311 */
312 $theme_support_data = WP_Theme_JSON_Gutenberg::get_from_editor_settings( get_classic_theme_supports_block_editor_settings() );
313 if ( ! wp_theme_has_theme_json() ) {
314 /*
315 * Unlike block themes, classic themes without a theme.json disable
316 * default presets when custom preset theme support is added. This
317 * behavior can be overridden by using the corresponding default
318 * preset theme support.
319 */
320 $theme_support_data['settings']['color']['defaultPalette'] =
321 ! isset( $theme_support_data['settings']['color']['palette'] ) ||
322 current_theme_supports( 'default-color-palette' );
323 $theme_support_data['settings']['color']['defaultGradients'] =
324 ! isset( $theme_support_data['settings']['color']['gradients'] ) ||
325 current_theme_supports( 'default-gradient-presets' );
326 $theme_support_data['settings']['typography']['defaultFontSizes'] =
327 ! isset( $theme_support_data['settings']['typography']['fontSizes'] ) ||
328 current_theme_supports( 'default-font-sizes' );
329 $theme_support_data['settings']['spacing']['defaultSpacingSizes'] =
330 ! isset( $theme_support_data['settings']['spacing']['spacingSizes'] ) ||
331 current_theme_supports( 'default-spacing-sizes' );
332
333 /*
334 * Shadow presets are explicitly disabled for classic themes until a
335 * decision is made for whether the default presets should match the
336 * other presets or if they should be disabled by default in classic
337 * themes. See https://github.com/WordPress/gutenberg/issues/59989.
338 */
339 $theme_support_data['settings']['shadow']['defaultPresets'] = false;
340
341 // Allow themes to enable all border settings via theme_support.
342 if ( current_theme_supports( 'border' ) ) {
343 $theme_support_data['settings']['border']['color'] = true;
344 $theme_support_data['settings']['border']['radius'] = true;
345 $theme_support_data['settings']['border']['style'] = true;
346 $theme_support_data['settings']['border']['width'] = true;
347 }
348
349 // Allow themes to enable link colors via theme_support.
350 if ( current_theme_supports( 'link-color' ) ) {
351 $theme_support_data['settings']['color']['link'] = true;
352 }
353 if ( current_theme_supports( 'experimental-link-color' ) ) {
354 _doing_it_wrong(
355 "add_theme_support( 'experimental-link-color' )",
356 __( '`experimental-link-color` is no longer supported. Use `link-color` instead.', 'gutenberg' ),
357 '6.3.0'
358 );
359 }
360
361 // Allow themes to enable appearance tools via theme_support.
362 if ( current_theme_supports( 'appearance-tools' ) ) {
363 $theme_support_data['settings']['appearanceTools'] = true;
364 }
365 }
366 $with_theme_supports = new WP_Theme_JSON_Gutenberg( $theme_support_data );
367 $with_theme_supports->merge( static::$theme );
368 return $with_theme_supports;
369 }
370
371 /**
372 * Gets the styles for blocks from the block.json file.
373 *
374 * @since 6.1.0
375 *
376 * @return WP_Theme_JSON_Gutenberg
377 */
378 public static function get_block_data() {
379 $registry = WP_Block_Type_Registry::get_instance();
380 $blocks = $registry->get_all_registered();
381
382 if ( null !== static::$blocks && static::has_same_registered_blocks( 'blocks' ) ) {
383 return static::$blocks;
384 }
385
386 $config = array( 'version' => WP_Theme_JSON_Gutenberg::LATEST_SCHEMA );
387 foreach ( $blocks as $block_name => $block_type ) {
388 if ( isset( $block_type->supports['__experimentalStyle'] ) ) {
389 $config['styles']['blocks'][ $block_name ] = static::remove_json_comments( $block_type->supports['__experimentalStyle'] );
390 }
391
392 if (
393 isset( $block_type->supports['spacing']['blockGap']['__experimentalDefault'] ) &&
394 ! isset( $config['styles']['blocks'][ $block_name ]['spacing']['blockGap'] )
395 ) {
396 // Ensure an empty placeholder value exists for the block, if it provides a default blockGap value.
397 // The real blockGap value to be used will be determined when the styles are rendered for output.
398 $config['styles']['blocks'][ $block_name ]['spacing']['blockGap'] = null;
399 }
400 }
401
402 /**
403 * Filters the data provided by the blocks for global styles & settings.
404 *
405 * @since 6.1.0
406 *
407 * @param WP_Theme_JSON_Data_Gutenberg Class to access and update the underlying data.
408 */
409 $theme_json = apply_filters( 'wp_theme_json_data_blocks', new WP_Theme_JSON_Data_Gutenberg( $config, 'blocks' ) );
410 static::$blocks = $theme_json->get_theme_json();
411
412 return static::$blocks;
413 }
414
415 /**
416 * When given an array, this will remove any keys with the name `//`.
417 *
418 * @param array $json_array The array to filter.
419 * @return array The filtered array.
420 */
421 private static function remove_json_comments( $json_array ) {
422 unset( $json_array['//'] );
423 foreach ( $json_array as $k => $v ) {
424 if ( is_array( $v ) ) {
425 $json_array[ $k ] = static::remove_json_comments( $v );
426 }
427 }
428
429 return $json_array;
430 }
431
432 /**
433 * Returns the custom post type that contains the user's origin config
434 * for the active theme or a void array if none are found.
435 *
436 * This can also create and return a new draft custom post type.
437 *
438 * @since 5.9.0
439 *
440 * @param WP_Theme $theme The theme object. If empty, it
441 * defaults to the active theme.
442 * @param bool $create_post Optional. Whether a new custom post
443 * type should be created if none are
444 * found. Default false.
445 * @param array $post_status_filter Optional. Filter custom post type by
446 * post status. Default `array( 'publish' )`,
447 * so it only fetches published posts.
448 * @return array Custom Post Type for the user's origin config.
449 */
450 public static function get_user_data_from_wp_global_styles( $theme, $create_post = false, $post_status_filter = array( 'publish' ) ) {
451 if ( ! $theme instanceof WP_Theme ) {
452 $theme = wp_get_theme();
453 }
454
455 $user_cpt = array();
456 $post_type_filter = 'wp_global_styles';
457 $stylesheet = $theme->get_stylesheet();
458 $args = array(
459 'posts_per_page' => 1,
460 'orderby' => 'date',
461 'order' => 'desc',
462 'post_type' => $post_type_filter,
463 'post_status' => $post_status_filter,
464 'ignore_sticky_posts' => true,
465 'no_found_rows' => true,
466 'update_post_meta_cache' => false,
467 'update_post_term_cache' => false,
468 'tax_query' => array(
469 array(
470 'taxonomy' => 'wp_theme',
471 'field' => 'name',
472 'terms' => $stylesheet,
473 ),
474 ),
475 );
476
477 $global_style_query = new WP_Query();
478 $recent_posts = $global_style_query->query( $args );
479 if ( count( $recent_posts ) === 1 && $recent_posts[0] instanceof WP_Post ) {
480 $user_cpt = get_object_vars( $recent_posts[0] );
481 } elseif ( $create_post ) {
482 $cpt_post_id = wp_insert_post(
483 array(
484 'post_content' => '{"version": ' . WP_Theme_JSON_Gutenberg::LATEST_SCHEMA . ', "isGlobalStylesUserThemeJSON": true }',
485 'post_status' => 'publish',
486 'post_title' => 'Custom Styles', // Do not make string translatable, see https://core.trac.wordpress.org/ticket/54518.
487 'post_type' => $post_type_filter,
488 'post_name' => sprintf( 'wp-global-styles-%s', urlencode( $stylesheet ) ),
489 'tax_input' => array(
490 'wp_theme' => array( $stylesheet ),
491 ),
492 ),
493 true
494 );
495 if ( ! is_wp_error( $cpt_post_id ) ) {
496 $post = get_post( $cpt_post_id );
497 if ( $post instanceof WP_Post ) {
498 $user_cpt = get_object_vars( $post );
499 }
500 }
501 }
502
503 return $user_cpt;
504 }
505
506 /**
507 * Returns the user's origin config.
508 *
509 * @since 5.9.0
510 *
511 * @return WP_Theme_JSON_Gutenberg Entity that holds styles for user data.
512 */
513 public static function get_user_data() {
514 if ( null !== static::$user && static::has_same_registered_blocks( 'user' ) ) {
515 return static::$user;
516 }
517
518 $config = array();
519 $user_cpt = static::get_user_data_from_wp_global_styles( wp_get_theme() );
520
521 if ( array_key_exists( 'post_content', $user_cpt ) ) {
522 $decoded_data = json_decode( $user_cpt['post_content'], true );
523
524 $json_decoding_error = json_last_error();
525 if ( JSON_ERROR_NONE !== $json_decoding_error ) {
526 trigger_error( 'Error when decoding a theme.json schema for user data. ' . json_last_error_msg() );
527 /**
528 * Filters the data provided by the user for global styles & settings.
529 *
530 * @since 6.1.0
531 *
532 * @param WP_Theme_JSON_Data_Gutenberg Class to access and update the underlying data.
533 */
534 $theme_json = apply_filters( 'wp_theme_json_data_user', new WP_Theme_JSON_Data_Gutenberg( $config, 'custom' ) );
535
536 return $theme_json->get_theme_json();
537 }
538
539 // Very important to verify that the flag isGlobalStylesUserThemeJSON is true.
540 // If it's not true then the content was not escaped and is not safe.
541 if (
542 is_array( $decoded_data ) &&
543 isset( $decoded_data['isGlobalStylesUserThemeJSON'] ) &&
544 $decoded_data['isGlobalStylesUserThemeJSON']
545 ) {
546 unset( $decoded_data['isGlobalStylesUserThemeJSON'] );
547 $config = $decoded_data;
548 }
549 }
550
551 /** This filter is documented in wp-includes/class-wp-theme-json-resolver.php */
552 $theme_json = apply_filters( 'wp_theme_json_data_user', new WP_Theme_JSON_Data_Gutenberg( $config, 'custom' ) );
553 static::$user = $theme_json->get_theme_json();
554
555 return static::$user;
556 }
557
558 /**
559 * Returns the data merged from multiple origins.
560 *
561 * There are four sources of data (origins) for a site:
562 *
563 * - default => WordPress
564 * - blocks => each one of the blocks provides data for itself
565 * - theme => the active theme
566 * - custom => data provided by the user
567 *
568 * The custom's has higher priority than the theme's, the theme's higher than blocks',
569 * and block's higher than default's.
570 *
571 * Unlike the getters
572 * {@link https://developer.wordpress.org/reference/classes/wp_theme_json_resolver/get_core_data/ get_core_data},
573 * {@link https://developer.wordpress.org/reference/classes/wp_theme_json_resolver/get_theme_data/ get_theme_data},
574 * and {@link https://developer.wordpress.org/reference/classes/wp_theme_json_resolver/get_user_data/ get_user_data},
575 * this method returns data after it has been merged with the previous origins.
576 * This means that if the same piece of data is declared in different origins
577 * (default, blocks, theme, custom), the last origin overrides the previous.
578 *
579 * For example, if the user has set a background color
580 * for the paragraph block, and the theme has done it as well,
581 * the user preference wins.
582 *
583 * @since 5.8.0
584 * @since 5.9.0 Added user data, removed the `$settings` parameter,
585 * added the `$origin` parameter.
586 * @since 6.1.0 Added block data and generation of spacingSizes array.
587 *
588 * @param string $origin Optional. To what level should we merge data:'default', 'blocks', 'theme' or 'custom'.
589 * 'custom' is used as default value as well as fallback value if the origin is unknown.
590 *
591 * @return WP_Theme_JSON_Gutenberg
592 */
593 public static function get_merged_data( $origin = 'custom' ) {
594 if ( is_array( $origin ) ) {
595 _deprecated_argument( __FUNCTION__, '5.9.0' );
596 }
597
598 $result = new WP_Theme_JSON_Gutenberg();
599 $result->merge( static::get_core_data() );
600 if ( 'default' === $origin ) {
601 return $result;
602 }
603
604 $result->merge( static::get_block_data() );
605 if ( 'blocks' === $origin ) {
606 return $result;
607 }
608
609 $result->merge( static::get_theme_data() );
610 if ( 'theme' === $origin ) {
611 return $result;
612 }
613
614 $result->merge( static::get_user_data() );
615 return $result;
616 }
617
618 /**
619 * Returns the ID of the custom post type
620 * that stores user data.
621 *
622 * @since 5.9.0
623 *
624 * @return integer|null
625 */
626 public static function get_user_global_styles_post_id() {
627 if ( null !== static::$user_custom_post_type_id ) {
628 return static::$user_custom_post_type_id;
629 }
630
631 $user_cpt = static::get_user_data_from_wp_global_styles( wp_get_theme(), true );
632
633 if ( array_key_exists( 'ID', $user_cpt ) ) {
634 static::$user_custom_post_type_id = $user_cpt['ID'];
635 }
636
637 return static::$user_custom_post_type_id;
638 }
639
640 /**
641 * Determines whether the active theme has a theme.json file.
642 *
643 * @since 5.8.0
644 * @since 5.9.0 Added a check in the parent theme.
645 * @deprecated 6.2.0 Use wp_theme_has_theme_json() instead.
646 *
647 * @return bool
648 */
649 public static function theme_has_support() {
650 _deprecated_function( __METHOD__, '6.2.0', 'wp_theme_has_theme_json()' );
651
652 return wp_theme_has_theme_json();
653 }
654
655 /**
656 * Builds the path to the given file and checks that it is readable.
657 *
658 * If it isn't, returns an empty string, otherwise returns the whole file path.
659 *
660 * @since 5.8.0
661 * @since 5.9.0 Adapted to work with child themes, added the `$template` argument.
662 *
663 * @param string $file_name Name of the file.
664 * @param bool $template Optional. Use template theme directory. Default false.
665 * @return string The whole file path or empty if the file doesn't exist.
666 */
667 protected static function get_file_path_from_theme( $file_name, $template = false ) {
668 // TODO: Remove this method from core on 6.3 release.
669 _deprecated_function( __METHOD__, '6.3.0' );
670 $path = $template ? get_template_directory() : get_stylesheet_directory();
671 $candidate = $path . '/' . $file_name;
672
673 return is_readable( $candidate ) ? $candidate : '';
674 }
675
676 /**
677 * Cleans the cached data so it can be recalculated.
678 *
679 * @since 5.8.0
680 * @since 5.9.0 Added the `$user`, `$user_custom_post_type_id`,
681 * and `$i18n_schema` variables to reset.
682 * @since 6.1.0 Added the `$blocks` and `$blocks_cache` variables
683 * to reset.
684 */
685 public static function clean_cached_data() {
686 static::$core = null;
687 static::$blocks = null;
688 static::$blocks_cache = array(
689 'core' => array(),
690 'blocks' => array(),
691 'theme' => array(),
692 'user' => array(),
693 );
694 static::$theme = null;
695 static::$user = null;
696 static::$user_custom_post_type_id = null;
697 static::$i18n_schema = null;
698 }
699
700 /**
701 * Returns an array of all nested json files within a given directory.
702 *
703 * @since 6.2.0
704 *
705 * @param string $dir The directory to recursively iterate and list files of.
706 * @return array The merged array.
707 */
708 private static function recursively_iterate_json( $dir ) {
709 $nested_files = new RecursiveIteratorIterator( new RecursiveDirectoryIterator( $dir ) );
710 $nested_json_files = iterator_to_array( new RegexIterator( $nested_files, '/^.+\.json$/i', RecursiveRegexIterator::GET_MATCH ) );
711 return $nested_json_files;
712 }
713
714 /**
715 * Determines if a supplied style variation matches the provided scope.
716 *
717 * For backwards compatibility, if a variation does not define any scope
718 * related property, e.g. `blockTypes`, it is assumed to be a theme style
719 * variation.
720 *
721 * @since 6.6.0
722 *
723 * @param array $variation Theme.json shaped style variation object.
724 * @param string $scope Scope to check e.g. theme, block etc.
725 *
726 * @return boolean
727 */
728 private static function style_variation_has_scope( $variation, $scope ) {
729 if ( 'block' === $scope ) {
730 return isset( $variation['blockTypes'] );
731 }
732
733 if ( 'theme' === $scope ) {
734 return ! isset( $variation['blockTypes'] );
735 }
736
737 return false;
738 }
739
740 /**
741 * Returns the style variations defined by the theme (parent and child).
742 *
743 * @since 6.2.0 Returns parent theme variations if theme is a child.
744 * @since 6.6.0 Added configurable scope parameter to allow filtering
745 * theme.json partial files by the scope to which they
746 * can be applied e.g. theme vs block etc.
747 *
748 * @param string $scope The scope or type of style variation to retrieve e.g. theme, block etc.
749 * @return array
750 */
751 public static function get_style_variations( $scope = 'theme' ) {
752 return static::get_style_variations_from_directory( get_stylesheet_directory(), $scope );
753 }
754
755 /**
756 * Returns the style variation files defined by the theme (parent and child).
757 *
758 * @since 6.7.0
759 *
760 * @return array An array of style variation files.
761 */
762 protected static function get_style_variation_files_from_current_theme() {
763 $variation_files = array();
764 $base_directory = get_stylesheet_directory() . '/styles';
765 $template_directory = get_template_directory() . '/styles';
766 if ( is_dir( $base_directory ) ) {
767 $variation_files = static::recursively_iterate_json( $base_directory );
768 }
769 if ( is_dir( $template_directory ) && $template_directory !== $base_directory ) {
770 $variation_files_parent = static::recursively_iterate_json( $template_directory );
771 // If the child and parent variation file basename are the same, only include the child theme's.
772 foreach ( $variation_files_parent as $parent_path => $parent ) {
773 foreach ( $variation_files as $child_path => $child ) {
774 if ( basename( $parent_path ) === basename( $child_path ) ) {
775 unset( $variation_files_parent[ $parent_path ] );
776 }
777 }
778 }
779 $variation_files = array_merge( $variation_files, $variation_files_parent );
780 }
781
782 return $variation_files;
783 }
784
785 /**
786 * Returns the style variations in the given directory.
787 *
788 * @since 6.7.0
789 *
790 * @param string $directory The directory to get the style variations from.
791 * @param string $scope The scope or type of style variation to retrieve e.g. theme, block etc.
792 * @return array
793 */
794 public static function get_style_variations_from_directory( $directory, $scope = 'theme' ) {
795 $variation_files = array();
796 $variations = array();
797 if ( is_dir( $directory ) ) {
798 if ( get_stylesheet_directory() === $directory ) {
799 $variation_files = static::get_style_variation_files_from_current_theme();
800 } else {
801 $variation_files = static::recursively_iterate_json( $directory );
802 }
803 }
804 ksort( $variation_files );
805 foreach ( $variation_files as $path => $file ) {
806 $decoded_file = self::read_json_file( $path );
807 if ( is_array( $decoded_file ) && static::style_variation_has_scope( $decoded_file, $scope ) ) {
808 $translated = static::translate( $decoded_file, wp_get_theme()->get( 'TextDomain' ) );
809 $variation = ( new WP_Theme_JSON_Gutenberg( $translated ) )->get_raw_data();
810 if ( empty( $variation['title'] ) ) {
811 $variation['title'] = basename( $path, '.json' );
812 }
813 $variations[] = $variation;
814 }
815 }
816 return $variations;
817 }
818
819
820 /**
821 * Resolves relative paths in theme.json styles to theme absolute paths
822 * and returns them in an array that can be embedded
823 * as the value of `_link` object in REST API responses.
824 *
825 * @since 6.6.0
826 * @since 6.7.0 Added support for resolving block styles.
827 *
828 * @param WP_Theme_JSON_Gutenberg $theme_json A theme json instance.
829 * @return array An array of resolved paths.
830 */
831 public static function get_resolved_theme_uris( $theme_json ) {
832 $resolved_theme_uris = array();
833
834 if ( ! $theme_json instanceof WP_Theme_JSON_Gutenberg ) {
835 return $resolved_theme_uris;
836 }
837
838 $theme_json_data = $theme_json->get_raw_data();
839
840 // Using the same file convention when registering web fonts. See: WP_Font_Face_Resolver:: to_theme_file_uri.
841 $placeholder = 'file:./';
842
843 // Top level styles.
844 $background_image_url = $theme_json_data['styles']['background']['backgroundImage']['url'] ?? null;
845 if (
846 isset( $background_image_url ) &&
847 is_string( $background_image_url ) &&
848 // Skip if the src doesn't start with the placeholder, as there's nothing to replace.
849 str_starts_with( $background_image_url, $placeholder ) ) {
850 $file_type = wp_check_filetype( $background_image_url );
851 $src_url = str_replace( $placeholder, '', $background_image_url );
852 $resolved_theme_uri = array(
853 'name' => $background_image_url,
854 'href' => sanitize_url( get_theme_file_uri( $src_url ) ),
855 'target' => 'styles.background.backgroundImage.url',
856 );
857 if ( isset( $file_type['type'] ) ) {
858 $resolved_theme_uri['type'] = $file_type['type'];
859 }
860 $resolved_theme_uris[] = $resolved_theme_uri;
861 }
862
863 // Block styles.
864 if ( ! empty( $theme_json_data['styles']['blocks'] ) ) {
865 foreach ( $theme_json_data['styles']['blocks'] as $block_name => $block_styles ) {
866 if ( ! isset( $block_styles['background']['backgroundImage']['url'] ) ) {
867 continue;
868 }
869 $background_image_url = $block_styles['background']['backgroundImage']['url'] ?? null;
870 if (
871 isset( $background_image_url ) &&
872 is_string( $background_image_url ) &&
873 // Skip if the src doesn't start with the placeholder, as there's nothing to replace.
874 str_starts_with( $background_image_url, $placeholder ) ) {
875 $file_type = wp_check_filetype( $background_image_url );
876 $src_url = str_replace( $placeholder, '', $background_image_url );
877 $resolved_theme_uri = array(
878 'name' => $background_image_url,
879 'href' => sanitize_url( get_theme_file_uri( $src_url ) ),
880 'target' => "styles.blocks.{$block_name}.background.backgroundImage.url",
881 );
882 if ( isset( $file_type['type'] ) ) {
883 $resolved_theme_uri['type'] = $file_type['type'];
884 }
885 $resolved_theme_uris[] = $resolved_theme_uri;
886 }
887 }
888 }
889
890 return $resolved_theme_uris;
891 }
892
893 /**
894 * Resolves relative paths in theme.json styles to theme absolute paths
895 * and merges them with incoming theme JSON.
896 *
897 * @since 6.6.0
898 *
899 * @param WP_Theme_JSON_Gutenberg $theme_json A theme json instance.
900 * @return WP_Theme_JSON_Gutenberg Theme merged with resolved paths, if any found.
901 */
902 public static function resolve_theme_file_uris( $theme_json ) {
903 $resolved_urls = static::get_resolved_theme_uris( $theme_json );
904 if ( empty( $resolved_urls ) ) {
905 return $theme_json;
906 }
907
908 $resolved_theme_json_data = $theme_json->get_raw_data();
909
910 foreach ( $resolved_urls as $resolved_url ) {
911 $path = explode( '.', $resolved_url['target'] );
912 _wp_array_set( $resolved_theme_json_data, $path, $resolved_url['href'] );
913 }
914
915 return new WP_Theme_JSON_Gutenberg( $resolved_theme_json_data );
916 }
917
918 /**
919 * Adds variations sourced from block style variations files to the supplied theme.json data.
920 *
921 * @since 6.6.0
922 *
923 * @param array $data Array following the theme.json specification.
924 * @param array $variations Shared block style variations.
925 * @return array Theme json data including shared block style variation definitions.
926 */
927 private static function inject_variations_from_block_style_variation_files( $data, $variations ) {
928 if ( empty( $variations ) ) {
929 return $data;
930 }
931
932 foreach ( $variations as $variation ) {
933 if ( empty( $variation['styles'] ) || empty( $variation['blockTypes'] ) ) {
934 continue;
935 }
936
937 $variation_name = $variation['slug'] ?? _wp_to_kebab_case( $variation['title'] );
938
939 foreach ( $variation['blockTypes'] as $block_type ) {
940 // First, override partial styles with any top-level styles.
941 $top_level_data = $data['styles']['variations'][ $variation_name ] ?? array();
942 if ( ! empty( $top_level_data ) ) {
943 $variation['styles'] = array_replace_recursive( $variation['styles'], $top_level_data );
944 }
945
946 // Then, override styles so far with any block-level styles.
947 $block_level_data = $data['styles']['blocks'][ $block_type ]['variations'][ $variation_name ] ?? array();
948 if ( ! empty( $block_level_data ) ) {
949 $variation['styles'] = array_replace_recursive( $variation['styles'], $block_level_data );
950 }
951
952 $path = array( 'styles', 'blocks', $block_type, 'variations', $variation_name );
953 _wp_array_set( $data, $path, $variation['styles'] );
954 }
955 }
956
957 return $data;
958 }
959
960 /**
961 * Adds variations sourced from the block styles registry to the supplied theme.json data.
962 *
963 * @since 6.6.0
964 *
965 * @param array $data Array following the theme.json specification.
966 * @return array Theme json data including variations from the block styles registry.
967 */
968 private static function inject_variations_from_block_styles_registry( $data ) {
969 $registry = WP_Block_Styles_Registry::get_instance();
970 $styles = $registry->get_all_registered();
971
972 foreach ( $styles as $block_type => $variations ) {
973 foreach ( $variations as $variation_name => $variation ) {
974 if ( empty( $variation['style_data'] ) ) {
975 continue;
976 }
977
978 // First, override registry styles with any top-level styles.
979 $top_level_data = $data['styles']['variations'][ $variation_name ] ?? array();
980 if ( ! empty( $top_level_data ) ) {
981 $variation['style_data'] = array_replace_recursive( $variation['style_data'], $top_level_data );
982 }
983
984 // Then, override styles so far with any block-level styles.
985 $block_level_data = $data['styles']['blocks'][ $block_type ]['variations'][ $variation_name ] ?? array();
986 if ( ! empty( $block_level_data ) ) {
987 $variation['style_data'] = array_replace_recursive( $variation['style_data'], $block_level_data );
988 }
989
990 $path = array( 'styles', 'blocks', $block_type, 'variations', $variation_name );
991 _wp_array_set( $data, $path, $variation['style_data'] );
992 }
993 }
994
995 return $data;
996 }
997 }
998