PluginProbe
Gutenberg / 23.3.0
Gutenberg v23.3.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 12.6.0 7.4.0 All 402 releases
gutenberg / lib / class-wp-theme-json-resolver-gutenberg.php

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

1,005 lines 34.7 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 // BEGIN OF EXPERIMENTAL CODE. Not to backport to core.
302 if ( ! class_exists( 'WP_Font_Face' ) && class_exists( 'WP_Fonts_Resolver' ) ) {
303 static::$theme = WP_Fonts_Resolver::add_missing_fonts_to_theme_json( static::$theme );
304 }
305 // END OF EXPERIMENTAL CODE.
306
307 }
308
309 if ( ! $options['with_supports'] ) {
310 return static::$theme;
311 }
312
313 /*
314 * We want the presets and settings declared in theme.json
315 * to override the ones declared via theme supports.
316 * So we take theme supports, transform it to theme.json shape
317 * and merge the static::$theme upon that.
318 */
319 $theme_support_data = WP_Theme_JSON_Gutenberg::get_from_editor_settings( get_classic_theme_supports_block_editor_settings() );
320 if ( ! wp_theme_has_theme_json() ) {
321 /*
322 * Unlike block themes, classic themes without a theme.json disable
323 * default presets when custom preset theme support is added. This
324 * behavior can be overridden by using the corresponding default
325 * preset theme support.
326 */
327 $theme_support_data['settings']['color']['defaultPalette'] =
328 ! isset( $theme_support_data['settings']['color']['palette'] ) ||
329 current_theme_supports( 'default-color-palette' );
330 $theme_support_data['settings']['color']['defaultGradients'] =
331 ! isset( $theme_support_data['settings']['color']['gradients'] ) ||
332 current_theme_supports( 'default-gradient-presets' );
333 $theme_support_data['settings']['typography']['defaultFontSizes'] =
334 ! isset( $theme_support_data['settings']['typography']['fontSizes'] ) ||
335 current_theme_supports( 'default-font-sizes' );
336 $theme_support_data['settings']['spacing']['defaultSpacingSizes'] =
337 ! isset( $theme_support_data['settings']['spacing']['spacingSizes'] ) ||
338 current_theme_supports( 'default-spacing-sizes' );
339
340 /*
341 * Shadow presets are explicitly disabled for classic themes until a
342 * decision is made for whether the default presets should match the
343 * other presets or if they should be disabled by default in classic
344 * themes. See https://github.com/WordPress/gutenberg/issues/59989.
345 */
346 $theme_support_data['settings']['shadow']['defaultPresets'] = false;
347
348 // Allow themes to enable all border settings via theme_support.
349 if ( current_theme_supports( 'border' ) ) {
350 $theme_support_data['settings']['border']['color'] = true;
351 $theme_support_data['settings']['border']['radius'] = true;
352 $theme_support_data['settings']['border']['style'] = true;
353 $theme_support_data['settings']['border']['width'] = true;
354 }
355
356 // Allow themes to enable link colors via theme_support.
357 if ( current_theme_supports( 'link-color' ) ) {
358 $theme_support_data['settings']['color']['link'] = true;
359 }
360 if ( current_theme_supports( 'experimental-link-color' ) ) {
361 _doing_it_wrong(
362 "add_theme_support( 'experimental-link-color' )",
363 __( '`experimental-link-color` is no longer supported. Use `link-color` instead.', 'gutenberg' ),
364 '6.3.0'
365 );
366 }
367
368 // Allow themes to enable appearance tools via theme_support.
369 if ( current_theme_supports( 'appearance-tools' ) ) {
370 $theme_support_data['settings']['appearanceTools'] = true;
371 }
372 }
373 $with_theme_supports = new WP_Theme_JSON_Gutenberg( $theme_support_data );
374 $with_theme_supports->merge( static::$theme );
375 return $with_theme_supports;
376 }
377
378 /**
379 * Gets the styles for blocks from the block.json file.
380 *
381 * @since 6.1.0
382 *
383 * @return WP_Theme_JSON_Gutenberg
384 */
385 public static function get_block_data() {
386 $registry = WP_Block_Type_Registry::get_instance();
387 $blocks = $registry->get_all_registered();
388
389 if ( null !== static::$blocks && static::has_same_registered_blocks( 'blocks' ) ) {
390 return static::$blocks;
391 }
392
393 $config = array( 'version' => WP_Theme_JSON_Gutenberg::LATEST_SCHEMA );
394 foreach ( $blocks as $block_name => $block_type ) {
395 if ( isset( $block_type->supports['__experimentalStyle'] ) ) {
396 $config['styles']['blocks'][ $block_name ] = static::remove_json_comments( $block_type->supports['__experimentalStyle'] );
397 }
398
399 if (
400 isset( $block_type->supports['spacing']['blockGap']['__experimentalDefault'] ) &&
401 ! isset( $config['styles']['blocks'][ $block_name ]['spacing']['blockGap'] )
402 ) {
403 // Ensure an empty placeholder value exists for the block, if it provides a default blockGap value.
404 // The real blockGap value to be used will be determined when the styles are rendered for output.
405 $config['styles']['blocks'][ $block_name ]['spacing']['blockGap'] = null;
406 }
407 }
408
409 /**
410 * Filters the data provided by the blocks for global styles & settings.
411 *
412 * @since 6.1.0
413 *
414 * @param WP_Theme_JSON_Data_Gutenberg Class to access and update the underlying data.
415 */
416 $theme_json = apply_filters( 'wp_theme_json_data_blocks', new WP_Theme_JSON_Data_Gutenberg( $config, 'blocks' ) );
417 static::$blocks = $theme_json->get_theme_json();
418
419 return static::$blocks;
420 }
421
422 /**
423 * When given an array, this will remove any keys with the name `//`.
424 *
425 * @param array $json_array The array to filter.
426 * @return array The filtered array.
427 */
428 private static function remove_json_comments( $json_array ) {
429 unset( $json_array['//'] );
430 foreach ( $json_array as $k => $v ) {
431 if ( is_array( $v ) ) {
432 $json_array[ $k ] = static::remove_json_comments( $v );
433 }
434 }
435
436 return $json_array;
437 }
438
439 /**
440 * Returns the custom post type that contains the user's origin config
441 * for the active theme or a void array if none are found.
442 *
443 * This can also create and return a new draft custom post type.
444 *
445 * @since 5.9.0
446 *
447 * @param WP_Theme $theme The theme object. If empty, it
448 * defaults to the active theme.
449 * @param bool $create_post Optional. Whether a new custom post
450 * type should be created if none are
451 * found. Default false.
452 * @param array $post_status_filter Optional. Filter custom post type by
453 * post status. Default `array( 'publish' )`,
454 * so it only fetches published posts.
455 * @return array Custom Post Type for the user's origin config.
456 */
457 public static function get_user_data_from_wp_global_styles( $theme, $create_post = false, $post_status_filter = array( 'publish' ) ) {
458 if ( ! $theme instanceof WP_Theme ) {
459 $theme = wp_get_theme();
460 }
461
462 $user_cpt = array();
463 $post_type_filter = 'wp_global_styles';
464 $stylesheet = $theme->get_stylesheet();
465 $args = array(
466 'posts_per_page' => 1,
467 'orderby' => 'date',
468 'order' => 'desc',
469 'post_type' => $post_type_filter,
470 'post_status' => $post_status_filter,
471 'ignore_sticky_posts' => true,
472 'no_found_rows' => true,
473 'update_post_meta_cache' => false,
474 'update_post_term_cache' => false,
475 'tax_query' => array(
476 array(
477 'taxonomy' => 'wp_theme',
478 'field' => 'name',
479 'terms' => $stylesheet,
480 ),
481 ),
482 );
483
484 $global_style_query = new WP_Query();
485 $recent_posts = $global_style_query->query( $args );
486 if ( count( $recent_posts ) === 1 && $recent_posts[0] instanceof WP_Post ) {
487 $user_cpt = get_object_vars( $recent_posts[0] );
488 } elseif ( $create_post ) {
489 $cpt_post_id = wp_insert_post(
490 array(
491 'post_content' => '{"version": ' . WP_Theme_JSON_Gutenberg::LATEST_SCHEMA . ', "isGlobalStylesUserThemeJSON": true }',
492 'post_status' => 'publish',
493 'post_title' => 'Custom Styles', // Do not make string translatable, see https://core.trac.wordpress.org/ticket/54518.
494 'post_type' => $post_type_filter,
495 'post_name' => sprintf( 'wp-global-styles-%s', urlencode( $stylesheet ) ),
496 'tax_input' => array(
497 'wp_theme' => array( $stylesheet ),
498 ),
499 ),
500 true
501 );
502 if ( ! is_wp_error( $cpt_post_id ) ) {
503 $post = get_post( $cpt_post_id );
504 if ( $post instanceof WP_Post ) {
505 $user_cpt = get_object_vars( $post );
506 }
507 }
508 }
509
510 return $user_cpt;
511 }
512
513 /**
514 * Returns the user's origin config.
515 *
516 * @since 5.9.0
517 *
518 * @return WP_Theme_JSON_Gutenberg Entity that holds styles for user data.
519 */
520 public static function get_user_data() {
521 if ( null !== static::$user && static::has_same_registered_blocks( 'user' ) ) {
522 return static::$user;
523 }
524
525 $config = array();
526 $user_cpt = static::get_user_data_from_wp_global_styles( wp_get_theme() );
527
528 if ( array_key_exists( 'post_content', $user_cpt ) ) {
529 $decoded_data = json_decode( $user_cpt['post_content'], true );
530
531 $json_decoding_error = json_last_error();
532 if ( JSON_ERROR_NONE !== $json_decoding_error ) {
533 trigger_error( 'Error when decoding a theme.json schema for user data. ' . json_last_error_msg() );
534 /**
535 * Filters the data provided by the user for global styles & settings.
536 *
537 * @since 6.1.0
538 *
539 * @param WP_Theme_JSON_Data_Gutenberg Class to access and update the underlying data.
540 */
541 $theme_json = apply_filters( 'wp_theme_json_data_user', new WP_Theme_JSON_Data_Gutenberg( $config, 'custom' ) );
542
543 return $theme_json->get_theme_json();
544 }
545
546 // Very important to verify that the flag isGlobalStylesUserThemeJSON is true.
547 // If it's not true then the content was not escaped and is not safe.
548 if (
549 is_array( $decoded_data ) &&
550 isset( $decoded_data['isGlobalStylesUserThemeJSON'] ) &&
551 $decoded_data['isGlobalStylesUserThemeJSON']
552 ) {
553 unset( $decoded_data['isGlobalStylesUserThemeJSON'] );
554 $config = $decoded_data;
555 }
556 }
557
558 /** This filter is documented in wp-includes/class-wp-theme-json-resolver.php */
559 $theme_json = apply_filters( 'wp_theme_json_data_user', new WP_Theme_JSON_Data_Gutenberg( $config, 'custom' ) );
560 static::$user = $theme_json->get_theme_json();
561
562 return static::$user;
563 }
564
565 /**
566 * Returns the data merged from multiple origins.
567 *
568 * There are four sources of data (origins) for a site:
569 *
570 * - default => WordPress
571 * - blocks => each one of the blocks provides data for itself
572 * - theme => the active theme
573 * - custom => data provided by the user
574 *
575 * The custom's has higher priority than the theme's, the theme's higher than blocks',
576 * and block's higher than default's.
577 *
578 * Unlike the getters
579 * {@link https://developer.wordpress.org/reference/classes/wp_theme_json_resolver/get_core_data/ get_core_data},
580 * {@link https://developer.wordpress.org/reference/classes/wp_theme_json_resolver/get_theme_data/ get_theme_data},
581 * and {@link https://developer.wordpress.org/reference/classes/wp_theme_json_resolver/get_user_data/ get_user_data},
582 * this method returns data after it has been merged with the previous origins.
583 * This means that if the same piece of data is declared in different origins
584 * (default, blocks, theme, custom), the last origin overrides the previous.
585 *
586 * For example, if the user has set a background color
587 * for the paragraph block, and the theme has done it as well,
588 * the user preference wins.
589 *
590 * @since 5.8.0
591 * @since 5.9.0 Added user data, removed the `$settings` parameter,
592 * added the `$origin` parameter.
593 * @since 6.1.0 Added block data and generation of spacingSizes array.
594 *
595 * @param string $origin Optional. To what level should we merge data:'default', 'blocks', 'theme' or 'custom'.
596 * 'custom' is used as default value as well as fallback value if the origin is unknown.
597 *
598 * @return WP_Theme_JSON_Gutenberg
599 */
600 public static function get_merged_data( $origin = 'custom' ) {
601 if ( is_array( $origin ) ) {
602 _deprecated_argument( __FUNCTION__, '5.9.0' );
603 }
604
605 $result = new WP_Theme_JSON_Gutenberg();
606 $result->merge( static::get_core_data() );
607 if ( 'default' === $origin ) {
608 return $result;
609 }
610
611 $result->merge( static::get_block_data() );
612 if ( 'blocks' === $origin ) {
613 return $result;
614 }
615
616 $result->merge( static::get_theme_data() );
617 if ( 'theme' === $origin ) {
618 return $result;
619 }
620
621 $result->merge( static::get_user_data() );
622 return $result;
623 }
624
625 /**
626 * Returns the ID of the custom post type
627 * that stores user data.
628 *
629 * @since 5.9.0
630 *
631 * @return integer|null
632 */
633 public static function get_user_global_styles_post_id() {
634 if ( null !== static::$user_custom_post_type_id ) {
635 return static::$user_custom_post_type_id;
636 }
637
638 $user_cpt = static::get_user_data_from_wp_global_styles( wp_get_theme(), true );
639
640 if ( array_key_exists( 'ID', $user_cpt ) ) {
641 static::$user_custom_post_type_id = $user_cpt['ID'];
642 }
643
644 return static::$user_custom_post_type_id;
645 }
646
647 /**
648 * Determines whether the active theme has a theme.json file.
649 *
650 * @since 5.8.0
651 * @since 5.9.0 Added a check in the parent theme.
652 * @deprecated 6.2.0 Use wp_theme_has_theme_json() instead.
653 *
654 * @return bool
655 */
656 public static function theme_has_support() {
657 _deprecated_function( __METHOD__, '6.2.0', 'wp_theme_has_theme_json()' );
658
659 return wp_theme_has_theme_json();
660 }
661
662 /**
663 * Builds the path to the given file and checks that it is readable.
664 *
665 * If it isn't, returns an empty string, otherwise returns the whole file path.
666 *
667 * @since 5.8.0
668 * @since 5.9.0 Adapted to work with child themes, added the `$template` argument.
669 *
670 * @param string $file_name Name of the file.
671 * @param bool $template Optional. Use template theme directory. Default false.
672 * @return string The whole file path or empty if the file doesn't exist.
673 */
674 protected static function get_file_path_from_theme( $file_name, $template = false ) {
675 // TODO: Remove this method from core on 6.3 release.
676 _deprecated_function( __METHOD__, '6.3.0' );
677 $path = $template ? get_template_directory() : get_stylesheet_directory();
678 $candidate = $path . '/' . $file_name;
679
680 return is_readable( $candidate ) ? $candidate : '';
681 }
682
683 /**
684 * Cleans the cached data so it can be recalculated.
685 *
686 * @since 5.8.0
687 * @since 5.9.0 Added the `$user`, `$user_custom_post_type_id`,
688 * and `$i18n_schema` variables to reset.
689 * @since 6.1.0 Added the `$blocks` and `$blocks_cache` variables
690 * to reset.
691 */
692 public static function clean_cached_data() {
693 static::$core = null;
694 static::$blocks = null;
695 static::$blocks_cache = array(
696 'core' => array(),
697 'blocks' => array(),
698 'theme' => array(),
699 'user' => array(),
700 );
701 static::$theme = null;
702 static::$user = null;
703 static::$user_custom_post_type_id = null;
704 static::$i18n_schema = null;
705 }
706
707 /**
708 * Returns an array of all nested json files within a given directory.
709 *
710 * @since 6.2.0
711 *
712 * @param string $dir The directory to recursively iterate and list files of.
713 * @return array The merged array.
714 */
715 private static function recursively_iterate_json( $dir ) {
716 $nested_files = new RecursiveIteratorIterator( new RecursiveDirectoryIterator( $dir ) );
717 $nested_json_files = iterator_to_array( new RegexIterator( $nested_files, '/^.+\.json$/i', RecursiveRegexIterator::GET_MATCH ) );
718 return $nested_json_files;
719 }
720
721 /**
722 * Determines if a supplied style variation matches the provided scope.
723 *
724 * For backwards compatibility, if a variation does not define any scope
725 * related property, e.g. `blockTypes`, it is assumed to be a theme style
726 * variation.
727 *
728 * @since 6.6.0
729 *
730 * @param array $variation Theme.json shaped style variation object.
731 * @param string $scope Scope to check e.g. theme, block etc.
732 *
733 * @return boolean
734 */
735 private static function style_variation_has_scope( $variation, $scope ) {
736 if ( 'block' === $scope ) {
737 return isset( $variation['blockTypes'] );
738 }
739
740 if ( 'theme' === $scope ) {
741 return ! isset( $variation['blockTypes'] );
742 }
743
744 return false;
745 }
746
747 /**
748 * Returns the style variations defined by the theme (parent and child).
749 *
750 * @since 6.2.0 Returns parent theme variations if theme is a child.
751 * @since 6.6.0 Added configurable scope parameter to allow filtering
752 * theme.json partial files by the scope to which they
753 * can be applied e.g. theme vs block etc.
754 *
755 * @param string $scope The scope or type of style variation to retrieve e.g. theme, block etc.
756 * @return array
757 */
758 public static function get_style_variations( $scope = 'theme' ) {
759 return static::get_style_variations_from_directory( get_stylesheet_directory(), $scope );
760 }
761
762 /**
763 * Returns the style variation files defined by the theme (parent and child).
764 *
765 * @since 6.7.0
766 *
767 * @return array An array of style variation files.
768 */
769 protected static function get_style_variation_files_from_current_theme() {
770 $variation_files = array();
771 $base_directory = get_stylesheet_directory() . '/styles';
772 $template_directory = get_template_directory() . '/styles';
773 if ( is_dir( $base_directory ) ) {
774 $variation_files = static::recursively_iterate_json( $base_directory );
775 }
776 if ( is_dir( $template_directory ) && $template_directory !== $base_directory ) {
777 $variation_files_parent = static::recursively_iterate_json( $template_directory );
778 // If the child and parent variation file basename are the same, only include the child theme's.
779 foreach ( $variation_files_parent as $parent_path => $parent ) {
780 foreach ( $variation_files as $child_path => $child ) {
781 if ( basename( $parent_path ) === basename( $child_path ) ) {
782 unset( $variation_files_parent[ $parent_path ] );
783 }
784 }
785 }
786 $variation_files = array_merge( $variation_files, $variation_files_parent );
787 }
788
789 return $variation_files;
790 }
791
792 /**
793 * Returns the style variations in the given directory.
794 *
795 * @since 6.7.0
796 *
797 * @param string $directory The directory to get the style variations from.
798 * @param string $scope The scope or type of style variation to retrieve e.g. theme, block etc.
799 * @return array
800 */
801 public static function get_style_variations_from_directory( $directory, $scope = 'theme' ) {
802 $variation_files = array();
803 $variations = array();
804 if ( is_dir( $directory ) ) {
805 if ( get_stylesheet_directory() === $directory ) {
806 $variation_files = static::get_style_variation_files_from_current_theme();
807 } else {
808 $variation_files = static::recursively_iterate_json( $directory );
809 }
810 }
811 ksort( $variation_files );
812 foreach ( $variation_files as $path => $file ) {
813 $decoded_file = self::read_json_file( $path );
814 if ( is_array( $decoded_file ) && static::style_variation_has_scope( $decoded_file, $scope ) ) {
815 $translated = static::translate( $decoded_file, wp_get_theme()->get( 'TextDomain' ) );
816 $variation = ( new WP_Theme_JSON_Gutenberg( $translated ) )->get_raw_data();
817 if ( empty( $variation['title'] ) ) {
818 $variation['title'] = basename( $path, '.json' );
819 }
820 $variations[] = $variation;
821 }
822 }
823 return $variations;
824 }
825
826
827 /**
828 * Resolves relative paths in theme.json styles to theme absolute paths
829 * and returns them in an array that can be embedded
830 * as the value of `_link` object in REST API responses.
831 *
832 * @since 6.6.0
833 * @since 6.7.0 Added support for resolving block styles.
834 *
835 * @param WP_Theme_JSON_Gutenberg $theme_json A theme json instance.
836 * @return array An array of resolved paths.
837 */
838 public static function get_resolved_theme_uris( $theme_json ) {
839 $resolved_theme_uris = array();
840
841 if ( ! $theme_json instanceof WP_Theme_JSON_Gutenberg ) {
842 return $resolved_theme_uris;
843 }
844
845 $theme_json_data = $theme_json->get_raw_data();
846
847 // Using the same file convention when registering web fonts. See: WP_Font_Face_Resolver:: to_theme_file_uri.
848 $placeholder = 'file:./';
849
850 // Top level styles.
851 $background_image_url = $theme_json_data['styles']['background']['backgroundImage']['url'] ?? null;
852 if (
853 isset( $background_image_url ) &&
854 is_string( $background_image_url ) &&
855 // Skip if the src doesn't start with the placeholder, as there's nothing to replace.
856 str_starts_with( $background_image_url, $placeholder ) ) {
857 $file_type = wp_check_filetype( $background_image_url );
858 $src_url = str_replace( $placeholder, '', $background_image_url );
859 $resolved_theme_uri = array(
860 'name' => $background_image_url,
861 'href' => sanitize_url( get_theme_file_uri( $src_url ) ),
862 'target' => 'styles.background.backgroundImage.url',
863 );
864 if ( isset( $file_type['type'] ) ) {
865 $resolved_theme_uri['type'] = $file_type['type'];
866 }
867 $resolved_theme_uris[] = $resolved_theme_uri;
868 }
869
870 // Block styles.
871 if ( ! empty( $theme_json_data['styles']['blocks'] ) ) {
872 foreach ( $theme_json_data['styles']['blocks'] as $block_name => $block_styles ) {
873 if ( ! isset( $block_styles['background']['backgroundImage']['url'] ) ) {
874 continue;
875 }
876 $background_image_url = $block_styles['background']['backgroundImage']['url'] ?? null;
877 if (
878 isset( $background_image_url ) &&
879 is_string( $background_image_url ) &&
880 // Skip if the src doesn't start with the placeholder, as there's nothing to replace.
881 str_starts_with( $background_image_url, $placeholder ) ) {
882 $file_type = wp_check_filetype( $background_image_url );
883 $src_url = str_replace( $placeholder, '', $background_image_url );
884 $resolved_theme_uri = array(
885 'name' => $background_image_url,
886 'href' => sanitize_url( get_theme_file_uri( $src_url ) ),
887 'target' => "styles.blocks.{$block_name}.background.backgroundImage.url",
888 );
889 if ( isset( $file_type['type'] ) ) {
890 $resolved_theme_uri['type'] = $file_type['type'];
891 }
892 $resolved_theme_uris[] = $resolved_theme_uri;
893 }
894 }
895 }
896
897 return $resolved_theme_uris;
898 }
899
900 /**
901 * Resolves relative paths in theme.json styles to theme absolute paths
902 * and merges them with incoming theme JSON.
903 *
904 * @since 6.6.0
905 *
906 * @param WP_Theme_JSON_Gutenberg $theme_json A theme json instance.
907 * @return WP_Theme_JSON_Gutenberg Theme merged with resolved paths, if any found.
908 */
909 public static function resolve_theme_file_uris( $theme_json ) {
910 $resolved_urls = static::get_resolved_theme_uris( $theme_json );
911 if ( empty( $resolved_urls ) ) {
912 return $theme_json;
913 }
914
915 $resolved_theme_json_data = $theme_json->get_raw_data();
916
917 foreach ( $resolved_urls as $resolved_url ) {
918 $path = explode( '.', $resolved_url['target'] );
919 _wp_array_set( $resolved_theme_json_data, $path, $resolved_url['href'] );
920 }
921
922 return new WP_Theme_JSON_Gutenberg( $resolved_theme_json_data );
923 }
924
925 /**
926 * Adds variations sourced from block style variations files to the supplied theme.json data.
927 *
928 * @since 6.6.0
929 *
930 * @param array $data Array following the theme.json specification.
931 * @param array $variations Shared block style variations.
932 * @return array Theme json data including shared block style variation definitions.
933 */
934 private static function inject_variations_from_block_style_variation_files( $data, $variations ) {
935 if ( empty( $variations ) ) {
936 return $data;
937 }
938
939 foreach ( $variations as $variation ) {
940 if ( empty( $variation['styles'] ) || empty( $variation['blockTypes'] ) ) {
941 continue;
942 }
943
944 $variation_name = $variation['slug'] ?? _wp_to_kebab_case( $variation['title'] );
945
946 foreach ( $variation['blockTypes'] as $block_type ) {
947 // First, override partial styles with any top-level styles.
948 $top_level_data = $data['styles']['variations'][ $variation_name ] ?? array();
949 if ( ! empty( $top_level_data ) ) {
950 $variation['styles'] = array_replace_recursive( $variation['styles'], $top_level_data );
951 }
952
953 // Then, override styles so far with any block-level styles.
954 $block_level_data = $data['styles']['blocks'][ $block_type ]['variations'][ $variation_name ] ?? array();
955 if ( ! empty( $block_level_data ) ) {
956 $variation['styles'] = array_replace_recursive( $variation['styles'], $block_level_data );
957 }
958
959 $path = array( 'styles', 'blocks', $block_type, 'variations', $variation_name );
960 _wp_array_set( $data, $path, $variation['styles'] );
961 }
962 }
963
964 return $data;
965 }
966
967 /**
968 * Adds variations sourced from the block styles registry to the supplied theme.json data.
969 *
970 * @since 6.6.0
971 *
972 * @param array $data Array following the theme.json specification.
973 * @return array Theme json data including variations from the block styles registry.
974 */
975 private static function inject_variations_from_block_styles_registry( $data ) {
976 $registry = WP_Block_Styles_Registry::get_instance();
977 $styles = $registry->get_all_registered();
978
979 foreach ( $styles as $block_type => $variations ) {
980 foreach ( $variations as $variation_name => $variation ) {
981 if ( empty( $variation['style_data'] ) ) {
982 continue;
983 }
984
985 // First, override registry styles with any top-level styles.
986 $top_level_data = $data['styles']['variations'][ $variation_name ] ?? array();
987 if ( ! empty( $top_level_data ) ) {
988 $variation['style_data'] = array_replace_recursive( $variation['style_data'], $top_level_data );
989 }
990
991 // Then, override styles so far with any block-level styles.
992 $block_level_data = $data['styles']['blocks'][ $block_type ]['variations'][ $variation_name ] ?? array();
993 if ( ! empty( $block_level_data ) ) {
994 $variation['style_data'] = array_replace_recursive( $variation['style_data'], $block_level_data );
995 }
996
997 $path = array( 'styles', 'blocks', $block_type, 'variations', $variation_name );
998 _wp_array_set( $data, $path, $variation['style_data'] );
999 }
1000 }
1001
1002 return $data;
1003 }
1004 }
1005