[ Index ]

PHP Cross Reference of WordPress Trunk (Updated Daily)

Search

title

Body

[close]

/wp-includes/ -> class-wp-theme-json-resolver.php (source)

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


Generated : Fri Oct 30 08:20:34 2026 Cross-referenced by PHPXref