PluginProbe
Gutenberg / 19.6.1
Gutenberg v19.6.1
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 12.6.0 All 403 releases
gutenberg / lib / class-wp-theme-json-resolver-gutenberg.php

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

1,017 lines 35.0 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 = null === $i18n_schema ? array() : $i18n_schema;
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( gutenberg_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 current_theme_supports( '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 /*
463 * Bail early if the theme does not support a theme.json.
464 *
465 * Since wp_theme_has_theme_json only supports the active
466 * theme, the extra condition for whether $theme is the active theme is
467 * present here.
468 */
469 if ( $theme->get_stylesheet() === get_stylesheet() && ! wp_theme_has_theme_json() ) {
470 return array();
471 }
472
473 $user_cpt = array();
474 $post_type_filter = 'wp_global_styles';
475 $stylesheet = $theme->get_stylesheet();
476 $args = array(
477 'posts_per_page' => 1,
478 'orderby' => 'date',
479 'order' => 'desc',
480 'post_type' => $post_type_filter,
481 'post_status' => $post_status_filter,
482 'ignore_sticky_posts' => true,
483 'no_found_rows' => true,
484 'update_post_meta_cache' => false,
485 'update_post_term_cache' => false,
486 'tax_query' => array(
487 array(
488 'taxonomy' => 'wp_theme',
489 'field' => 'name',
490 'terms' => $stylesheet,
491 ),
492 ),
493 );
494
495 $global_style_query = new WP_Query();
496 $recent_posts = $global_style_query->query( $args );
497 if ( count( $recent_posts ) === 1 ) {
498 $user_cpt = get_object_vars( $recent_posts[0] );
499 } elseif ( $create_post ) {
500 $cpt_post_id = wp_insert_post(
501 array(
502 'post_content' => '{"version": ' . WP_Theme_JSON_Gutenberg::LATEST_SCHEMA . ', "isGlobalStylesUserThemeJSON": true }',
503 'post_status' => 'publish',
504 'post_title' => 'Custom Styles', // Do not make string translatable, see https://core.trac.wordpress.org/ticket/54518.
505 'post_type' => $post_type_filter,
506 'post_name' => sprintf( 'wp-global-styles-%s', urlencode( $stylesheet ) ),
507 'tax_input' => array(
508 'wp_theme' => array( $stylesheet ),
509 ),
510 ),
511 true
512 );
513 if ( ! is_wp_error( $cpt_post_id ) ) {
514 $user_cpt = get_object_vars( get_post( $cpt_post_id ) );
515 }
516 }
517
518 return $user_cpt;
519 }
520
521 /**
522 * Returns the user's origin config.
523 *
524 * @since 5.9.0
525 *
526 * @return WP_Theme_JSON_Gutenberg Entity that holds styles for user data.
527 */
528 public static function get_user_data() {
529 if ( null !== static::$user && static::has_same_registered_blocks( 'user' ) ) {
530 return static::$user;
531 }
532
533 $config = array();
534 $user_cpt = static::get_user_data_from_wp_global_styles( wp_get_theme() );
535
536 if ( array_key_exists( 'post_content', $user_cpt ) ) {
537 $decoded_data = json_decode( $user_cpt['post_content'], true );
538
539 $json_decoding_error = json_last_error();
540 if ( JSON_ERROR_NONE !== $json_decoding_error ) {
541 trigger_error( 'Error when decoding a theme.json schema for user data. ' . json_last_error_msg() );
542 /**
543 * Filters the data provided by the user for global styles & settings.
544 *
545 * @since 6.1.0
546 *
547 * @param WP_Theme_JSON_Data_Gutenberg Class to access and update the underlying data.
548 */
549 $theme_json = apply_filters( 'wp_theme_json_data_user', new WP_Theme_JSON_Data_Gutenberg( $config, 'custom' ) );
550
551 return $theme_json->get_theme_json();
552 }
553
554 // Very important to verify that the flag isGlobalStylesUserThemeJSON is true.
555 // If it's not true then the content was not escaped and is not safe.
556 if (
557 is_array( $decoded_data ) &&
558 isset( $decoded_data['isGlobalStylesUserThemeJSON'] ) &&
559 $decoded_data['isGlobalStylesUserThemeJSON']
560 ) {
561 unset( $decoded_data['isGlobalStylesUserThemeJSON'] );
562 $config = $decoded_data;
563 }
564 }
565
566 /** This filter is documented in wp-includes/class-wp-theme-json-resolver.php */
567 $theme_json = apply_filters( 'wp_theme_json_data_user', new WP_Theme_JSON_Data_Gutenberg( $config, 'custom' ) );
568 static::$user = $theme_json->get_theme_json();
569
570 return static::$user;
571 }
572
573 /**
574 * Returns the data merged from multiple origins.
575 *
576 * There are four sources of data (origins) for a site:
577 *
578 * - default => WordPress
579 * - blocks => each one of the blocks provides data for itself
580 * - theme => the active theme
581 * - custom => data provided by the user
582 *
583 * The custom's has higher priority than the theme's, the theme's higher than blocks',
584 * and block's higher than default's.
585 *
586 * Unlike the getters
587 * {@link https://developer.wordpress.org/reference/classes/wp_theme_json_resolver/get_core_data/ get_core_data},
588 * {@link https://developer.wordpress.org/reference/classes/wp_theme_json_resolver/get_theme_data/ get_theme_data},
589 * and {@link https://developer.wordpress.org/reference/classes/wp_theme_json_resolver/get_user_data/ get_user_data},
590 * this method returns data after it has been merged with the previous origins.
591 * This means that if the same piece of data is declared in different origins
592 * (default, blocks, theme, custom), the last origin overrides the previous.
593 *
594 * For example, if the user has set a background color
595 * for the paragraph block, and the theme has done it as well,
596 * the user preference wins.
597 *
598 * @since 5.8.0
599 * @since 5.9.0 Added user data, removed the `$settings` parameter,
600 * added the `$origin` parameter.
601 * @since 6.1.0 Added block data and generation of spacingSizes array.
602 *
603 * @param string $origin Optional. To what level should we merge data:'default', 'blocks', 'theme' or 'custom'.
604 * 'custom' is used as default value as well as fallback value if the origin is unknown.
605 *
606 * @return WP_Theme_JSON_Gutenberg
607 */
608 public static function get_merged_data( $origin = 'custom' ) {
609 if ( is_array( $origin ) ) {
610 _deprecated_argument( __FUNCTION__, '5.9.0' );
611 }
612
613 $result = new WP_Theme_JSON_Gutenberg();
614 $result->merge( static::get_core_data() );
615 if ( 'default' === $origin ) {
616 return $result;
617 }
618
619 $result->merge( static::get_block_data() );
620 if ( 'blocks' === $origin ) {
621 return $result;
622 }
623
624 $result->merge( static::get_theme_data() );
625 if ( 'theme' === $origin ) {
626 return $result;
627 }
628
629 $result->merge( static::get_user_data() );
630 return $result;
631 }
632
633 /**
634 * Returns the ID of the custom post type
635 * that stores user data.
636 *
637 * @since 5.9.0
638 *
639 * @return integer|null
640 */
641 public static function get_user_global_styles_post_id() {
642 if ( null !== static::$user_custom_post_type_id ) {
643 return static::$user_custom_post_type_id;
644 }
645
646 $user_cpt = static::get_user_data_from_wp_global_styles( wp_get_theme(), true );
647
648 if ( array_key_exists( 'ID', $user_cpt ) ) {
649 static::$user_custom_post_type_id = $user_cpt['ID'];
650 }
651
652 return static::$user_custom_post_type_id;
653 }
654
655 /**
656 * Determines whether the active theme has a theme.json file.
657 *
658 * @since 5.8.0
659 * @since 5.9.0 Added a check in the parent theme.
660 * @deprecated 6.2.0 Use wp_theme_has_theme_json() instead.
661 *
662 * @return bool
663 */
664 public static function theme_has_support() {
665 _deprecated_function( __METHOD__, '6.2.0', 'wp_theme_has_theme_json()' );
666
667 return wp_theme_has_theme_json();
668 }
669
670 /**
671 * Builds the path to the given file and checks that it is readable.
672 *
673 * If it isn't, returns an empty string, otherwise returns the whole file path.
674 *
675 * @since 5.8.0
676 * @since 5.9.0 Adapted to work with child themes, added the `$template` argument.
677 *
678 * @param string $file_name Name of the file.
679 * @param bool $template Optional. Use template theme directory. Default false.
680 * @return string The whole file path or empty if the file doesn't exist.
681 */
682 protected static function get_file_path_from_theme( $file_name, $template = false ) {
683 // TODO: Remove this method from core on 6.3 release.
684 _deprecated_function( __METHOD__, '6.3.0' );
685 $path = $template ? get_template_directory() : get_stylesheet_directory();
686 $candidate = $path . '/' . $file_name;
687
688 return is_readable( $candidate ) ? $candidate : '';
689 }
690
691 /**
692 * Cleans the cached data so it can be recalculated.
693 *
694 * @since 5.8.0
695 * @since 5.9.0 Added the `$user`, `$user_custom_post_type_id`,
696 * and `$i18n_schema` variables to reset.
697 * @since 6.1.0 Added the `$blocks` and `$blocks_cache` variables
698 * to reset.
699 */
700 public static function clean_cached_data() {
701 static::$core = null;
702 static::$blocks = null;
703 static::$blocks_cache = array(
704 'core' => array(),
705 'blocks' => array(),
706 'theme' => array(),
707 'user' => array(),
708 );
709 static::$theme = null;
710 static::$user = null;
711 static::$user_custom_post_type_id = null;
712 static::$i18n_schema = null;
713 }
714
715 /**
716 * Returns an array of all nested json files within a given directory.
717 *
718 * @since 6.2.0
719 *
720 * @param string $dir The directory to recursively iterate and list files of.
721 * @return array The merged array.
722 */
723 private static function recursively_iterate_json( $dir ) {
724 $nested_files = new RecursiveIteratorIterator( new RecursiveDirectoryIterator( $dir ) );
725 $nested_json_files = iterator_to_array( new RegexIterator( $nested_files, '/^.+\.json$/i', RecursiveRegexIterator::GET_MATCH ) );
726 return $nested_json_files;
727 }
728
729 /**
730 * Determines if a supplied style variation matches the provided scope.
731 *
732 * For backwards compatibility, if a variation does not define any scope
733 * related property, e.g. `blockTypes`, it is assumed to be a theme style
734 * variation.
735 *
736 * @since 6.6.0
737 *
738 * @param array $variation Theme.json shaped style variation object.
739 * @param string $scope Scope to check e.g. theme, block etc.
740 *
741 * @return boolean
742 */
743 private static function style_variation_has_scope( $variation, $scope ) {
744 if ( 'block' === $scope ) {
745 return isset( $variation['blockTypes'] );
746 }
747
748 if ( 'theme' === $scope ) {
749 return ! isset( $variation['blockTypes'] );
750 }
751
752 return false;
753 }
754
755 /**
756 * Returns the style variations defined by the theme (parent and child).
757 *
758 * @since 6.2.0 Returns parent theme variations if theme is a child.
759 * @since 6.6.0 Added configurable scope parameter to allow filtering
760 * theme.json partial files by the scope to which they
761 * can be applied e.g. theme vs block etc.
762 *
763 * @param string $scope The scope or type of style variation to retrieve e.g. theme, block etc.
764 * @return array
765 */
766 public static function get_style_variations( $scope = 'theme' ) {
767 return static::get_style_variations_from_directory( get_stylesheet_directory(), $scope );
768 }
769
770 /**
771 * Returns the style variation files defined by the theme (parent and child).
772 *
773 * @since 6.7.0
774 *
775 * @return array An array of style variation files.
776 */
777 protected static function get_style_variation_files_from_current_theme() {
778 $variation_files = array();
779 $base_directory = get_stylesheet_directory() . '/styles';
780 $template_directory = get_template_directory() . '/styles';
781 if ( is_dir( $base_directory ) ) {
782 $variation_files = static::recursively_iterate_json( $base_directory );
783 }
784 if ( is_dir( $template_directory ) && $template_directory !== $base_directory ) {
785 $variation_files_parent = static::recursively_iterate_json( $template_directory );
786 // If the child and parent variation file basename are the same, only include the child theme's.
787 foreach ( $variation_files_parent as $parent_path => $parent ) {
788 foreach ( $variation_files as $child_path => $child ) {
789 if ( basename( $parent_path ) === basename( $child_path ) ) {
790 unset( $variation_files_parent[ $parent_path ] );
791 }
792 }
793 }
794 $variation_files = array_merge( $variation_files, $variation_files_parent );
795 }
796
797 return $variation_files;
798 }
799
800 /**
801 * Returns the style variations in the given directory.
802 *
803 * @since 6.7.0
804 *
805 * @param string $directory The directory to get the style variations from.
806 * @param string $scope The scope or type of style variation to retrieve e.g. theme, block etc.
807 * @return array
808 */
809 public static function get_style_variations_from_directory( $directory, $scope = 'theme' ) {
810 $variation_files = array();
811 $variations = array();
812 if ( is_dir( $directory ) ) {
813 if ( get_stylesheet_directory() === $directory ) {
814 $variation_files = static::get_style_variation_files_from_current_theme();
815 } else {
816 $variation_files = static::recursively_iterate_json( $directory );
817 }
818 }
819 ksort( $variation_files );
820 foreach ( $variation_files as $path => $file ) {
821 $decoded_file = self::read_json_file( $path );
822 if ( is_array( $decoded_file ) && static::style_variation_has_scope( $decoded_file, $scope ) ) {
823 $translated = static::translate( $decoded_file, wp_get_theme()->get( 'TextDomain' ) );
824 $variation = ( new WP_Theme_JSON_Gutenberg( $translated ) )->get_raw_data();
825 if ( empty( $variation['title'] ) ) {
826 $variation['title'] = basename( $path, '.json' );
827 }
828 $variations[] = $variation;
829 }
830 }
831 return $variations;
832 }
833
834
835 /**
836 * Resolves relative paths in theme.json styles to theme absolute paths
837 * and returns them in an array that can be embedded
838 * as the value of `_link` object in REST API responses.
839 *
840 * @since 6.6.0
841 * @since 6.7.0 Added support for resolving block styles.
842 *
843 * @param WP_Theme_JSON_Gutenberg $theme_json A theme json instance.
844 * @return array An array of resolved paths.
845 */
846 public static function get_resolved_theme_uris( $theme_json ) {
847 $resolved_theme_uris = array();
848
849 if ( ! $theme_json instanceof WP_Theme_JSON_Gutenberg ) {
850 return $resolved_theme_uris;
851 }
852
853 $theme_json_data = $theme_json->get_raw_data();
854
855 // Using the same file convention when registering web fonts. See: WP_Font_Face_Resolver:: to_theme_file_uri.
856 $placeholder = 'file:./';
857
858 // Top level styles.
859 $background_image_url = $theme_json_data['styles']['background']['backgroundImage']['url'] ?? null;
860 if (
861 isset( $background_image_url ) &&
862 is_string( $background_image_url ) &&
863 // Skip if the src doesn't start with the placeholder, as there's nothing to replace.
864 str_starts_with( $background_image_url, $placeholder ) ) {
865 $file_type = wp_check_filetype( $background_image_url );
866 $src_url = str_replace( $placeholder, '', $background_image_url );
867 $resolved_theme_uri = array(
868 'name' => $background_image_url,
869 'href' => sanitize_url( get_theme_file_uri( $src_url ) ),
870 'target' => 'styles.background.backgroundImage.url',
871 );
872 if ( isset( $file_type['type'] ) ) {
873 $resolved_theme_uri['type'] = $file_type['type'];
874 }
875 $resolved_theme_uris[] = $resolved_theme_uri;
876 }
877
878 // Block styles.
879 if ( ! empty( $theme_json_data['styles']['blocks'] ) ) {
880 foreach ( $theme_json_data['styles']['blocks'] as $block_name => $block_styles ) {
881 if ( ! isset( $block_styles['background']['backgroundImage']['url'] ) ) {
882 continue;
883 }
884 $background_image_url = $block_styles['background']['backgroundImage']['url'] ?? null;
885 if (
886 isset( $background_image_url ) &&
887 is_string( $background_image_url ) &&
888 // Skip if the src doesn't start with the placeholder, as there's nothing to replace.
889 str_starts_with( $background_image_url, $placeholder ) ) {
890 $file_type = wp_check_filetype( $background_image_url );
891 $src_url = str_replace( $placeholder, '', $background_image_url );
892 $resolved_theme_uri = array(
893 'name' => $background_image_url,
894 'href' => sanitize_url( get_theme_file_uri( $src_url ) ),
895 'target' => "styles.blocks.{$block_name}.background.backgroundImage.url",
896 );
897 if ( isset( $file_type['type'] ) ) {
898 $resolved_theme_uri['type'] = $file_type['type'];
899 }
900 $resolved_theme_uris[] = $resolved_theme_uri;
901 }
902 }
903 }
904
905 return $resolved_theme_uris;
906 }
907
908 /**
909 * Resolves relative paths in theme.json styles to theme absolute paths
910 * and merges them with incoming theme JSON.
911 *
912 * @since 6.6.0
913 *
914 * @param WP_Theme_JSON_Gutenberg $theme_json A theme json instance.
915 * @return WP_Theme_JSON_Gutenberg Theme merged with resolved paths, if any found.
916 */
917 public static function resolve_theme_file_uris( $theme_json ) {
918 $resolved_urls = static::get_resolved_theme_uris( $theme_json );
919 if ( empty( $resolved_urls ) ) {
920 return $theme_json;
921 }
922
923 $resolved_theme_json_data = array(
924 'version' => WP_Theme_JSON_Gutenberg::LATEST_SCHEMA,
925 );
926
927 foreach ( $resolved_urls as $resolved_url ) {
928 $path = explode( '.', $resolved_url['target'] );
929 _wp_array_set( $resolved_theme_json_data, $path, $resolved_url['href'] );
930 }
931
932 $theme_json->merge( new WP_Theme_JSON_Gutenberg( $resolved_theme_json_data ) );
933
934 return $theme_json;
935 }
936
937 /**
938 * Adds variations sourced from block style variations files to the supplied theme.json data.
939 *
940 * @since 6.6.0
941 *
942 * @param array $data Array following the theme.json specification.
943 * @param array $variations Shared block style variations.
944 * @return array Theme json data including shared block style variation definitions.
945 */
946 private static function inject_variations_from_block_style_variation_files( $data, $variations ) {
947 if ( empty( $variations ) ) {
948 return $data;
949 }
950
951 foreach ( $variations as $variation ) {
952 if ( empty( $variation['styles'] ) || empty( $variation['blockTypes'] ) ) {
953 continue;
954 }
955
956 $variation_name = $variation['slug'] ?? _wp_to_kebab_case( $variation['title'] );
957
958 foreach ( $variation['blockTypes'] as $block_type ) {
959 // First, override partial styles with any top-level styles.
960 $top_level_data = $data['styles']['variations'][ $variation_name ] ?? array();
961 if ( ! empty( $top_level_data ) ) {
962 $variation['styles'] = array_replace_recursive( $variation['styles'], $top_level_data );
963 }
964
965 // Then, override styles so far with any block-level styles.
966 $block_level_data = $data['styles']['blocks'][ $block_type ]['variations'][ $variation_name ] ?? array();
967 if ( ! empty( $block_level_data ) ) {
968 $variation['styles'] = array_replace_recursive( $variation['styles'], $block_level_data );
969 }
970
971 $path = array( 'styles', 'blocks', $block_type, 'variations', $variation_name );
972 _wp_array_set( $data, $path, $variation['styles'] );
973 }
974 }
975
976 return $data;
977 }
978
979 /**
980 * Adds variations sourced from the block styles registry to the supplied theme.json data.
981 *
982 * @since 6.6.0
983 *
984 * @param array $data Array following the theme.json specification.
985 * @return array Theme json data including variations from the block styles registry.
986 */
987 private static function inject_variations_from_block_styles_registry( $data ) {
988 $registry = WP_Block_Styles_Registry::get_instance();
989 $styles = $registry->get_all_registered();
990
991 foreach ( $styles as $block_type => $variations ) {
992 foreach ( $variations as $variation_name => $variation ) {
993 if ( empty( $variation['style_data'] ) ) {
994 continue;
995 }
996
997 // First, override registry styles with any top-level styles.
998 $top_level_data = $data['styles']['variations'][ $variation_name ] ?? array();
999 if ( ! empty( $top_level_data ) ) {
1000 $variation['style_data'] = array_replace_recursive( $variation['style_data'], $top_level_data );
1001 }
1002
1003 // Then, override styles so far with any block-level styles.
1004 $block_level_data = $data['styles']['blocks'][ $block_type ]['variations'][ $variation_name ] ?? array();
1005 if ( ! empty( $block_level_data ) ) {
1006 $variation['style_data'] = array_replace_recursive( $variation['style_data'], $block_level_data );
1007 }
1008
1009 $path = array( 'styles', 'blocks', $block_type, 'variations', $variation_name );
1010 _wp_array_set( $data, $path, $variation['style_data'] );
1011 }
1012 }
1013
1014 return $data;
1015 }
1016 }
1017