wp/wp-includes/class-wp-theme-json-resolver.php
changeset 19 3d72ae0968f4
parent 18 be944660c56a
child 21 48c4eec2b7e6
equal deleted inserted replaced
18:be944660c56a 19:3d72ae0968f4
     9 
     9 
    10 /**
    10 /**
    11  * Class that abstracts the processing of the different data sources
    11  * Class that abstracts the processing of the different data sources
    12  * for site-level config and offers an API to work with them.
    12  * for site-level config and offers an API to work with them.
    13  *
    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  *
    14  * @access private
    18  * @access private
    15  */
    19  */
    16 class WP_Theme_JSON_Resolver {
    20 class WP_Theme_JSON_Resolver {
    17 
    21 
    18 	/**
    22 	/**
    19 	 * Container for data coming from core.
    23 	 * Container for data coming from core.
    20 	 *
    24 	 *
    21 	 * @since 5.8.0
    25 	 * @since 5.8.0
    22 	 * @var WP_Theme_JSON
    26 	 * @var WP_Theme_JSON
    23 	 */
    27 	 */
    24 	private static $core = null;
    28 	protected static $core = null;
    25 
    29 
    26 	/**
    30 	/**
    27 	 * Container for data coming from the theme.
    31 	 * Container for data coming from the theme.
    28 	 *
    32 	 *
    29 	 * @since 5.8.0
    33 	 * @since 5.8.0
    30 	 * @var WP_Theme_JSON
    34 	 * @var WP_Theme_JSON
    31 	 */
    35 	 */
    32 	private static $theme = null;
    36 	protected static $theme = null;
    33 
    37 
    34 	/**
    38 	/**
    35 	 * Whether or not the theme supports theme.json.
    39 	 * Whether or not the theme supports theme.json.
    36 	 *
    40 	 *
    37 	 * @since 5.8.0
    41 	 * @since 5.8.0
    38 	 * @var bool
    42 	 * @var bool
    39 	 */
    43 	 */
    40 	private static $theme_has_support = null;
    44 	protected static $theme_has_support = null;
    41 
    45 
    42 	/**
    46 	/**
    43 	 * Structure to hold i18n metadata.
    47 	 * Container for data coming from the user.
    44 	 *
    48 	 *
    45 	 * @since 5.8.0
    49 	 * @since 5.9.0
       
    50 	 * @var WP_Theme_JSON
       
    51 	 */
       
    52 	protected static $user = null;
       
    53 
       
    54 	/**
       
    55 	 * Stores the ID of the custom post type
       
    56 	 * that holds the user data.
       
    57 	 *
       
    58 	 * @since 5.9.0
       
    59 	 * @var int
       
    60 	 */
       
    61 	protected static $user_custom_post_type_id = null;
       
    62 
       
    63 	/**
       
    64 	 * Container to keep loaded i18n schema for `theme.json`.
       
    65 	 *
       
    66 	 * @since 5.8.0 As `$theme_json_i18n`.
       
    67 	 * @since 5.9.0 Renamed from `$theme_json_i18n` to `$i18n_schema`.
    46 	 * @var array
    68 	 * @var array
    47 	 */
    69 	 */
    48 	private static $theme_json_i18n = null;
    70 	protected static $i18n_schema = null;
    49 
    71 
    50 	/**
    72 	/**
    51 	 * Processes a file that adheres to the theme.json schema
    73 	 * Processes a file that adheres to the theme.json schema
    52 	 * and returns an array with its contents, or a void array if none found.
    74 	 * and returns an array with its contents, or a void array if none found.
    53 	 *
    75 	 *
    54 	 * @since 5.8.0
    76 	 * @since 5.8.0
    55 	 *
    77 	 *
    56 	 * @param string $file_path Path to file. Empty if no file.
    78 	 * @param string $file_path Path to file. Empty if no file.
    57 	 * @return array Contents that adhere to the theme.json schema.
    79 	 * @return array Contents that adhere to the theme.json schema.
    58 	 */
    80 	 */
    59 	private static function read_json_file( $file_path ) {
    81 	protected static function read_json_file( $file_path ) {
    60 		$config = array();
    82 		$config = array();
    61 		if ( $file_path ) {
    83 		if ( $file_path ) {
    62 			$decoded_file = json_decode(
    84 			$decoded_file = wp_json_file_decode( $file_path, array( 'associative' => true ) );
    63 				file_get_contents( $file_path ),
       
    64 				true
       
    65 			);
       
    66 
       
    67 			$json_decoding_error = json_last_error();
       
    68 			if ( JSON_ERROR_NONE !== $json_decoding_error ) {
       
    69 				trigger_error( "Error when decoding a theme.json schema at path $file_path " . json_last_error_msg() );
       
    70 				return $config;
       
    71 			}
       
    72 
       
    73 			if ( is_array( $decoded_file ) ) {
    85 			if ( is_array( $decoded_file ) ) {
    74 				$config = $decoded_file;
    86 				$config = $decoded_file;
    75 			}
    87 			}
    76 		}
    88 		}
    77 		return $config;
    89 		return $config;
    78 	}
    90 	}
    79 
    91 
    80 	/**
    92 	/**
    81 	 * Converts a tree as in i18n-theme.json into a linear array
       
    82 	 * containing metadata to translate a theme.json file.
       
    83 	 *
       
    84 	 * For example, given this input:
       
    85 	 *
       
    86 	 *     {
       
    87 	 *       "settings": {
       
    88 	 *         "*": {
       
    89 	 *           "typography": {
       
    90 	 *             "fontSizes": [ { "name": "Font size name" } ],
       
    91 	 *             "fontStyles": [ { "name": "Font size name" } ]
       
    92 	 *           }
       
    93 	 *         }
       
    94 	 *       }
       
    95 	 *     }
       
    96 	 *
       
    97 	 * will return this output:
       
    98 	 *
       
    99 	 *     [
       
   100 	 *       0 => [
       
   101 	 *         'path'    => [ 'settings', '*', 'typography', 'fontSizes' ],
       
   102 	 *         'key'     => 'name',
       
   103 	 *         'context' => 'Font size name'
       
   104 	 *       ],
       
   105 	 *       1 => [
       
   106 	 *         'path'    => [ 'settings', '*', 'typography', 'fontStyles' ],
       
   107 	 *         'key'     => 'name',
       
   108 	 *         'context' => 'Font style name'
       
   109 	 *       ]
       
   110 	 *     ]
       
   111 	 *
       
   112 	 * @since 5.8.0
       
   113 	 *
       
   114 	 * @param array $i18n_partial A tree that follows the format of i18n-theme.json.
       
   115 	 * @param array $current_path Optional. Keeps track of the path as we walk down the given tree.
       
   116 	 *                            Default empty array.
       
   117 	 * @return array A linear array containing the paths to translate.
       
   118 	 */
       
   119 	private static function extract_paths_to_translate( $i18n_partial, $current_path = array() ) {
       
   120 		$result = array();
       
   121 		foreach ( $i18n_partial as $property => $partial_child ) {
       
   122 			if ( is_numeric( $property ) ) {
       
   123 				foreach ( $partial_child as $key => $context ) {
       
   124 					$result[] = array(
       
   125 						'path'    => $current_path,
       
   126 						'key'     => $key,
       
   127 						'context' => $context,
       
   128 					);
       
   129 				}
       
   130 				return $result;
       
   131 			}
       
   132 			$result = array_merge(
       
   133 				$result,
       
   134 				self::extract_paths_to_translate( $partial_child, array_merge( $current_path, array( $property ) ) )
       
   135 			);
       
   136 		}
       
   137 		return $result;
       
   138 	}
       
   139 
       
   140 	/**
       
   141 	 * Returns a data structure used in theme.json translation.
    93 	 * Returns a data structure used in theme.json translation.
   142 	 *
    94 	 *
   143 	 * @since 5.8.0
    95 	 * @since 5.8.0
       
    96 	 * @deprecated 5.9.0
   144 	 *
    97 	 *
   145 	 * @return array An array of theme.json fields that are translatable and the keys that are translatable.
    98 	 * @return array An array of theme.json fields that are translatable and the keys that are translatable.
   146 	 */
    99 	 */
   147 	public static function get_fields_to_translate() {
   100 	public static function get_fields_to_translate() {
   148 		if ( null === self::$theme_json_i18n ) {
   101 		_deprecated_function( __METHOD__, '5.9.0' );
   149 			$file_structure        = self::read_json_file( __DIR__ . '/theme-i18n.json' );
   102 		return array();
   150 			self::$theme_json_i18n = self::extract_paths_to_translate( $file_structure );
       
   151 		}
       
   152 		return self::$theme_json_i18n;
       
   153 	}
       
   154 
       
   155 	/**
       
   156 	 * Translates a chunk of the loaded theme.json structure.
       
   157 	 *
       
   158 	 * @since 5.8.0
       
   159 	 *
       
   160 	 * @param array  $array_to_translate The chunk of theme.json to translate.
       
   161 	 * @param string $key                The key of the field that contains the string to translate.
       
   162 	 * @param string $context            The context to apply in the translation call.
       
   163 	 * @param string $domain             Text domain. Unique identifier for retrieving translated strings.
       
   164 	 * @return array Returns the modified $theme_json chunk.
       
   165 	 */
       
   166 	private static function translate_theme_json_chunk( array $array_to_translate, $key, $context, $domain ) {
       
   167 		foreach ( $array_to_translate as $item_key => $item_to_translate ) {
       
   168 			if ( empty( $item_to_translate[ $key ] ) ) {
       
   169 				continue;
       
   170 			}
       
   171 
       
   172 			// phpcs:ignore WordPress.WP.I18n.LowLevelTranslationFunction,WordPress.WP.I18n.NonSingularStringLiteralText,WordPress.WP.I18n.NonSingularStringLiteralContext,WordPress.WP.I18n.NonSingularStringLiteralDomain
       
   173 			$array_to_translate[ $item_key ][ $key ] = translate_with_gettext_context( $array_to_translate[ $item_key ][ $key ], $context, $domain );
       
   174 		}
       
   175 
       
   176 		return $array_to_translate;
       
   177 	}
   103 	}
   178 
   104 
   179 	/**
   105 	/**
   180 	 * Given a theme.json structure modifies it in place to update certain values
   106 	 * Given a theme.json structure modifies it in place to update certain values
   181 	 * by its translated strings according to the language set by the user.
   107 	 * by its translated strings according to the language set by the user.
   185 	 * @param array  $theme_json The theme.json to translate.
   111 	 * @param array  $theme_json The theme.json to translate.
   186 	 * @param string $domain     Optional. Text domain. Unique identifier for retrieving translated strings.
   112 	 * @param string $domain     Optional. Text domain. Unique identifier for retrieving translated strings.
   187 	 *                           Default 'default'.
   113 	 *                           Default 'default'.
   188 	 * @return array Returns the modified $theme_json_structure.
   114 	 * @return array Returns the modified $theme_json_structure.
   189 	 */
   115 	 */
   190 	private static function translate( $theme_json, $domain = 'default' ) {
   116 	protected static function translate( $theme_json, $domain = 'default' ) {
   191 		$fields = self::get_fields_to_translate();
   117 		if ( null === static::$i18n_schema ) {
   192 		foreach ( $fields as $field ) {
   118 			$i18n_schema         = wp_json_file_decode( __DIR__ . '/theme-i18n.json' );
   193 			$path    = $field['path'];
   119 			static::$i18n_schema = null === $i18n_schema ? array() : $i18n_schema;
   194 			$key     = $field['key'];
   120 		}
   195 			$context = $field['context'];
   121 
   196 
   122 		return translate_settings_using_i18n_schema( static::$i18n_schema, $theme_json, $domain );
   197 			/*
   123 	}
   198 			 * We need to process the paths that include '*' separately.
   124 
   199 			 * One example of such a path would be:
   125 	/**
   200 			 * [ 'settings', 'blocks', '*', 'color', 'palette' ]
   126 	 * Returns core's origin config.
   201 			 */
       
   202 			$nodes_to_iterate = array_keys( $path, '*', true );
       
   203 			if ( ! empty( $nodes_to_iterate ) ) {
       
   204 				/*
       
   205 				 * At the moment, we only need to support one '*' in the path, so take it directly.
       
   206 				 * - base will be [ 'settings', 'blocks' ]
       
   207 				 * - data will be [ 'color', 'palette' ]
       
   208 				 */
       
   209 				$base_path = array_slice( $path, 0, $nodes_to_iterate[0] );
       
   210 				$data_path = array_slice( $path, $nodes_to_iterate[0] + 1 );
       
   211 				$base_tree = _wp_array_get( $theme_json, $base_path, array() );
       
   212 				foreach ( $base_tree as $node_name => $node_data ) {
       
   213 					$array_to_translate = _wp_array_get( $node_data, $data_path, null );
       
   214 					if ( is_null( $array_to_translate ) ) {
       
   215 						continue;
       
   216 					}
       
   217 
       
   218 					// Whole path will be [ 'settings', 'blocks', 'core/paragraph', 'color', 'palette' ].
       
   219 					$whole_path       = array_merge( $base_path, array( $node_name ), $data_path );
       
   220 					$translated_array = self::translate_theme_json_chunk( $array_to_translate, $key, $context, $domain );
       
   221 					_wp_array_set( $theme_json, $whole_path, $translated_array );
       
   222 				}
       
   223 			} else {
       
   224 				$array_to_translate = _wp_array_get( $theme_json, $path, null );
       
   225 				if ( is_null( $array_to_translate ) ) {
       
   226 					continue;
       
   227 				}
       
   228 
       
   229 				$translated_array = self::translate_theme_json_chunk( $array_to_translate, $key, $context, $domain );
       
   230 				_wp_array_set( $theme_json, $path, $translated_array );
       
   231 			}
       
   232 		}
       
   233 
       
   234 		return $theme_json;
       
   235 	}
       
   236 
       
   237 	/**
       
   238 	 * Return core's origin config.
       
   239 	 *
   127 	 *
   240 	 * @since 5.8.0
   128 	 * @since 5.8.0
   241 	 *
   129 	 *
   242 	 * @return WP_Theme_JSON Entity that holds core data.
   130 	 * @return WP_Theme_JSON Entity that holds core data.
   243 	 */
   131 	 */
   244 	public static function get_core_data() {
   132 	public static function get_core_data() {
   245 		if ( null !== self::$core ) {
   133 		if ( null !== static::$core ) {
   246 			return self::$core;
   134 			return static::$core;
   247 		}
   135 		}
   248 
   136 
   249 		$config     = self::read_json_file( __DIR__ . '/theme.json' );
   137 		$config       = static::read_json_file( __DIR__ . '/theme.json' );
   250 		$config     = self::translate( $config );
   138 		$config       = static::translate( $config );
   251 		self::$core = new WP_Theme_JSON( $config, 'core' );
   139 		static::$core = new WP_Theme_JSON( $config, 'default' );
   252 
   140 
   253 		return self::$core;
   141 		return static::$core;
   254 	}
   142 	}
   255 
   143 
   256 	/**
   144 	/**
   257 	 * Returns the theme's data.
   145 	 * Returns the theme's data.
   258 	 *
   146 	 *
   259 	 * Data from theme.json can be augmented via the $theme_support_data variable.
   147 	 * Data from theme.json will be backfilled from existing
   260 	 * This is useful, for example, to backfill the gaps in theme.json that a theme
   148 	 * theme supports, if any. Note that if the same data
   261 	 * has declared via add_theme_supports.
   149 	 * is present in theme.json and in theme supports,
   262 	 *
   150 	 * the theme.json takes precedence.
   263 	 * Note that if the same data is present in theme.json and in $theme_support_data,
   151 	 *
   264 	 * the theme.json's is not overwritten.
   152 	 * @since 5.8.0
   265 	 *
   153 	 * @since 5.9.0 Theme supports have been inlined and the `$theme_support_data` argument removed.
   266 	 * @since 5.8.0
   154 	 * @since 6.0.0 Added an `$options` parameter to allow the theme data to be returned without theme supports.
   267 	 *
   155 	 *
   268 	 * @param array $theme_support_data Optional. Theme support data in theme.json format.
   156 	 * @param array $deprecated Deprecated. Not used.
   269 	 *                                  Default empty array.
   157 	 * @param array $options {
       
   158 	 *     Options arguments.
       
   159 	 *
       
   160 	 *     @type bool $with_supports Whether to include theme supports in the data. Default true.
       
   161 	 * }
   270 	 * @return WP_Theme_JSON Entity that holds theme data.
   162 	 * @return WP_Theme_JSON Entity that holds theme data.
   271 	 */
   163 	 */
   272 	public static function get_theme_data( $theme_support_data = array() ) {
   164 	public static function get_theme_data( $deprecated = array(), $options = array() ) {
   273 		if ( null === self::$theme ) {
   165 		if ( ! empty( $deprecated ) ) {
   274 			$theme_json_data = self::read_json_file( self::get_file_path_from_theme( 'theme.json' ) );
   166 			_deprecated_argument( __METHOD__, '5.9.0' );
   275 			$theme_json_data = self::translate( $theme_json_data, wp_get_theme()->get( 'TextDomain' ) );
   167 		}
   276 			self::$theme     = new WP_Theme_JSON( $theme_json_data );
   168 
   277 		}
   169 		$options = wp_parse_args( $options, array( 'with_supports' => true ) );
   278 
   170 
   279 		if ( empty( $theme_support_data ) ) {
   171 		if ( null === static::$theme ) {
   280 			return self::$theme;
   172 			$theme_json_data = static::read_json_file( static::get_file_path_from_theme( 'theme.json' ) );
       
   173 			$theme_json_data = static::translate( $theme_json_data, wp_get_theme()->get( 'TextDomain' ) );
       
   174 			static::$theme   = new WP_Theme_JSON( $theme_json_data );
       
   175 
       
   176 			if ( wp_get_theme()->parent() ) {
       
   177 				// Get parent theme.json.
       
   178 				$parent_theme_json_data = static::read_json_file( static::get_file_path_from_theme( 'theme.json', true ) );
       
   179 				$parent_theme_json_data = static::translate( $parent_theme_json_data, wp_get_theme()->parent()->get( 'TextDomain' ) );
       
   180 				$parent_theme           = new WP_Theme_JSON( $parent_theme_json_data );
       
   181 
       
   182 				// Merge the child theme.json into the parent theme.json.
       
   183 				// The child theme takes precedence over the parent.
       
   184 				$parent_theme->merge( static::$theme );
       
   185 				static::$theme = $parent_theme;
       
   186 			}
       
   187 		}
       
   188 
       
   189 		if ( ! $options['with_supports'] ) {
       
   190 			return static::$theme;
   281 		}
   191 		}
   282 
   192 
   283 		/*
   193 		/*
   284 		 * We want the presets and settings declared in theme.json
   194 		 * We want the presets and settings declared in theme.json
   285 		 * to override the ones declared via add_theme_support.
   195 		 * to override the ones declared via theme supports.
       
   196 		 * So we take theme supports, transform it to theme.json shape
       
   197 		 * and merge the static::$theme upon that.
   286 		 */
   198 		 */
       
   199 		$theme_support_data = WP_Theme_JSON::get_from_editor_settings( get_default_block_editor_settings() );
       
   200 		if ( ! static::theme_has_support() ) {
       
   201 			if ( ! isset( $theme_support_data['settings']['color'] ) ) {
       
   202 				$theme_support_data['settings']['color'] = array();
       
   203 			}
       
   204 
       
   205 			$default_palette = false;
       
   206 			if ( current_theme_supports( 'default-color-palette' ) ) {
       
   207 				$default_palette = true;
       
   208 			}
       
   209 			if ( ! isset( $theme_support_data['settings']['color']['palette'] ) ) {
       
   210 				// If the theme does not have any palette, we still want to show the core one.
       
   211 				$default_palette = true;
       
   212 			}
       
   213 			$theme_support_data['settings']['color']['defaultPalette'] = $default_palette;
       
   214 
       
   215 			$default_gradients = false;
       
   216 			if ( current_theme_supports( 'default-gradient-presets' ) ) {
       
   217 				$default_gradients = true;
       
   218 			}
       
   219 			if ( ! isset( $theme_support_data['settings']['color']['gradients'] ) ) {
       
   220 				// If the theme does not have any gradients, we still want to show the core ones.
       
   221 				$default_gradients = true;
       
   222 			}
       
   223 			$theme_support_data['settings']['color']['defaultGradients'] = $default_gradients;
       
   224 
       
   225 			// Classic themes without a theme.json don't support global duotone.
       
   226 			$theme_support_data['settings']['color']['defaultDuotone'] = false;
       
   227 		}
   287 		$with_theme_supports = new WP_Theme_JSON( $theme_support_data );
   228 		$with_theme_supports = new WP_Theme_JSON( $theme_support_data );
   288 		$with_theme_supports->merge( self::$theme );
   229 		$with_theme_supports->merge( static::$theme );
   289 
   230 
   290 		return $with_theme_supports;
   231 		return $with_theme_supports;
   291 	}
   232 	}
   292 
   233 
   293 	/**
   234 	/**
   294 	 * There are different sources of data for a site: core and theme.
   235 	 * Returns the custom post type that contains the user's origin config
   295 	 *
   236 	 * for the active theme or a void array if none are found.
   296 	 * While the getters {@link get_core_data}, {@link get_theme_data} return the raw data
   237 	 *
   297 	 * from the respective origins, this method merges them all together.
   238 	 * This can also create and return a new draft custom post type.
   298 	 *
   239 	 *
   299 	 * If the same piece of data is declared in different origins (core and theme),
   240 	 * @since 5.9.0
   300 	 * the last origin overrides the previous. For example, if core disables custom colors
   241 	 *
   301 	 * but a theme enables them, the theme config wins.
   242 	 * @param WP_Theme $theme              The theme object. If empty, it
   302 	 *
   243 	 *                                     defaults to the active theme.
   303 	 * @since 5.8.0
   244 	 * @param bool     $create_post        Optional. Whether a new custom post
   304 	 *
   245 	 *                                     type should be created if none are
   305 	 * @param array $settings Optional. Existing block editor settings. Default empty array.
   246 	 *                                     found. Default false.
       
   247 	 * @param array    $post_status_filter Optional. Filter custom post type by
       
   248 	 *                                     post status. Default `array( 'publish' )`,
       
   249 	 *                                     so it only fetches published posts.
       
   250 	 * @return array Custom Post Type for the user's origin config.
       
   251 	 */
       
   252 	public static function get_user_data_from_wp_global_styles( $theme, $create_post = false, $post_status_filter = array( 'publish' ) ) {
       
   253 		if ( ! $theme instanceof WP_Theme ) {
       
   254 			$theme = wp_get_theme();
       
   255 		}
       
   256 		$user_cpt         = array();
       
   257 		$post_type_filter = 'wp_global_styles';
       
   258 		$args             = array(
       
   259 			'numberposts' => 1,
       
   260 			'orderby'     => 'date',
       
   261 			'order'       => 'desc',
       
   262 			'post_type'   => $post_type_filter,
       
   263 			'post_status' => $post_status_filter,
       
   264 			'tax_query'   => array(
       
   265 				array(
       
   266 					'taxonomy' => 'wp_theme',
       
   267 					'field'    => 'name',
       
   268 					'terms'    => $theme->get_stylesheet(),
       
   269 				),
       
   270 			),
       
   271 		);
       
   272 
       
   273 		$cache_key = sprintf( 'wp_global_styles_%s', md5( serialize( $args ) ) );
       
   274 		$post_id   = wp_cache_get( $cache_key );
       
   275 
       
   276 		if ( (int) $post_id > 0 ) {
       
   277 			return get_post( $post_id, ARRAY_A );
       
   278 		}
       
   279 
       
   280 		// Special case: '-1' is a results not found.
       
   281 		if ( -1 === $post_id && ! $create_post ) {
       
   282 			return $user_cpt;
       
   283 		}
       
   284 
       
   285 		$recent_posts = wp_get_recent_posts( $args );
       
   286 		if ( is_array( $recent_posts ) && ( count( $recent_posts ) === 1 ) ) {
       
   287 			$user_cpt = $recent_posts[0];
       
   288 		} elseif ( $create_post ) {
       
   289 			$cpt_post_id = wp_insert_post(
       
   290 				array(
       
   291 					'post_content' => '{"version": ' . WP_Theme_JSON::LATEST_SCHEMA . ', "isGlobalStylesUserThemeJSON": true }',
       
   292 					'post_status'  => 'publish',
       
   293 					'post_title'   => 'Custom Styles',
       
   294 					'post_type'    => $post_type_filter,
       
   295 					'post_name'    => 'wp-global-styles-' . urlencode( wp_get_theme()->get_stylesheet() ),
       
   296 					'tax_input'    => array(
       
   297 						'wp_theme' => array( wp_get_theme()->get_stylesheet() ),
       
   298 					),
       
   299 				),
       
   300 				true
       
   301 			);
       
   302 			$user_cpt    = get_post( $cpt_post_id, ARRAY_A );
       
   303 		}
       
   304 		$cache_expiration = $user_cpt ? DAY_IN_SECONDS : HOUR_IN_SECONDS;
       
   305 		wp_cache_set( $cache_key, $user_cpt ? $user_cpt['ID'] : -1, '', $cache_expiration );
       
   306 
       
   307 		return $user_cpt;
       
   308 	}
       
   309 
       
   310 	/**
       
   311 	 * Returns the user's origin config.
       
   312 	 *
       
   313 	 * @since 5.9.0
       
   314 	 *
       
   315 	 * @return WP_Theme_JSON Entity that holds styles for user data.
       
   316 	 */
       
   317 	public static function get_user_data() {
       
   318 		if ( null !== static::$user ) {
       
   319 			return static::$user;
       
   320 		}
       
   321 
       
   322 		$config   = array();
       
   323 		$user_cpt = static::get_user_data_from_wp_global_styles( wp_get_theme() );
       
   324 
       
   325 		if ( array_key_exists( 'post_content', $user_cpt ) ) {
       
   326 			$decoded_data = json_decode( $user_cpt['post_content'], true );
       
   327 
       
   328 			$json_decoding_error = json_last_error();
       
   329 			if ( JSON_ERROR_NONE !== $json_decoding_error ) {
       
   330 				trigger_error( 'Error when decoding a theme.json schema for user data. ' . json_last_error_msg() );
       
   331 				return new WP_Theme_JSON( $config, 'custom' );
       
   332 			}
       
   333 
       
   334 			// Very important to verify that the flag isGlobalStylesUserThemeJSON is true.
       
   335 			// If it's not true then the content was not escaped and is not safe.
       
   336 			if (
       
   337 				is_array( $decoded_data ) &&
       
   338 				isset( $decoded_data['isGlobalStylesUserThemeJSON'] ) &&
       
   339 				$decoded_data['isGlobalStylesUserThemeJSON']
       
   340 			) {
       
   341 				unset( $decoded_data['isGlobalStylesUserThemeJSON'] );
       
   342 				$config = $decoded_data;
       
   343 			}
       
   344 		}
       
   345 		static::$user = new WP_Theme_JSON( $config, 'custom' );
       
   346 
       
   347 		return static::$user;
       
   348 	}
       
   349 
       
   350 	/**
       
   351 	 * Returns the data merged from multiple origins.
       
   352 	 *
       
   353 	 * There are three sources of data (origins) for a site:
       
   354 	 * default, theme, and custom. The custom's has higher priority
       
   355 	 * than the theme's, and the theme's higher than default's.
       
   356 	 *
       
   357 	 * Unlike the getters
       
   358 	 * {@link https://developer.wordpress.org/reference/classes/wp_theme_json_resolver/get_core_data/ get_core_data},
       
   359 	 * {@link https://developer.wordpress.org/reference/classes/wp_theme_json_resolver/get_theme_data/ get_theme_data},
       
   360 	 * and {@link https://developer.wordpress.org/reference/classes/wp_theme_json_resolver/get_user_data/ get_user_data},
       
   361 	 * this method returns data after it has been merged with the previous origins.
       
   362 	 * This means that if the same piece of data is declared in different origins
       
   363 	 * (user, theme, and core), the last origin overrides the previous.
       
   364 	 *
       
   365 	 * For example, if the user has set a background color
       
   366 	 * for the paragraph block, and the theme has done it as well,
       
   367 	 * the user preference wins.
       
   368 	 *
       
   369 	 * @since 5.8.0
       
   370 	 * @since 5.9.0 Added user data, removed the `$settings` parameter,
       
   371 	 *              added the `$origin` parameter.
       
   372 	 *
       
   373 	 * @param string $origin Optional. To what level should we merge data.
       
   374 	 *                       Valid values are 'theme' or 'custom'. Default 'custom'.
   306 	 * @return WP_Theme_JSON
   375 	 * @return WP_Theme_JSON
   307 	 */
   376 	 */
   308 	public static function get_merged_data( $settings = array() ) {
   377 	public static function get_merged_data( $origin = 'custom' ) {
   309 		$theme_support_data = WP_Theme_JSON::get_from_editor_settings( $settings );
   378 		if ( is_array( $origin ) ) {
       
   379 			_deprecated_argument( __FUNCTION__, '5.9.0' );
       
   380 		}
   310 
   381 
   311 		$result = new WP_Theme_JSON();
   382 		$result = new WP_Theme_JSON();
   312 		$result->merge( self::get_core_data() );
   383 		$result->merge( static::get_core_data() );
   313 		$result->merge( self::get_theme_data( $theme_support_data ) );
   384 		$result->merge( static::get_theme_data() );
       
   385 
       
   386 		if ( 'custom' === $origin ) {
       
   387 			$result->merge( static::get_user_data() );
       
   388 		}
   314 
   389 
   315 		return $result;
   390 		return $result;
   316 	}
   391 	}
   317 
   392 
   318 	/**
   393 	/**
   319 	 * Whether the current theme has a theme.json file.
   394 	 * Returns the ID of the custom post type
   320 	 *
   395 	 * that stores user data.
   321 	 * @since 5.8.0
   396 	 *
       
   397 	 * @since 5.9.0
       
   398 	 *
       
   399 	 * @return integer|null
       
   400 	 */
       
   401 	public static function get_user_global_styles_post_id() {
       
   402 		if ( null !== static::$user_custom_post_type_id ) {
       
   403 			return static::$user_custom_post_type_id;
       
   404 		}
       
   405 
       
   406 		$user_cpt = static::get_user_data_from_wp_global_styles( wp_get_theme(), true );
       
   407 
       
   408 		if ( array_key_exists( 'ID', $user_cpt ) ) {
       
   409 			static::$user_custom_post_type_id = $user_cpt['ID'];
       
   410 		}
       
   411 
       
   412 		return static::$user_custom_post_type_id;
       
   413 	}
       
   414 
       
   415 	/**
       
   416 	 * Determines whether the active theme has a theme.json file.
       
   417 	 *
       
   418 	 * @since 5.8.0
       
   419 	 * @since 5.9.0 Added a check in the parent theme.
   322 	 *
   420 	 *
   323 	 * @return bool
   421 	 * @return bool
   324 	 */
   422 	 */
   325 	public static function theme_has_support() {
   423 	public static function theme_has_support() {
   326 		if ( ! isset( self::$theme_has_support ) ) {
   424 		if ( ! isset( static::$theme_has_support ) ) {
   327 			self::$theme_has_support = (bool) self::get_file_path_from_theme( 'theme.json' );
   425 			static::$theme_has_support = (
   328 		}
   426 				is_readable( static::get_file_path_from_theme( 'theme.json' ) ) ||
   329 
   427 				is_readable( static::get_file_path_from_theme( 'theme.json', true ) )
   330 		return self::$theme_has_support;
   428 			);
       
   429 		}
       
   430 
       
   431 		return static::$theme_has_support;
   331 	}
   432 	}
   332 
   433 
   333 	/**
   434 	/**
   334 	 * Builds the path to the given file and checks that it is readable.
   435 	 * Builds the path to the given file and checks that it is readable.
   335 	 *
   436 	 *
   336 	 * If it isn't, returns an empty string, otherwise returns the whole file path.
   437 	 * If it isn't, returns an empty string, otherwise returns the whole file path.
   337 	 *
   438 	 *
   338 	 * @since 5.8.0
   439 	 * @since 5.8.0
       
   440 	 * @since 5.9.0 Adapted to work with child themes, added the `$template` argument.
   339 	 *
   441 	 *
   340 	 * @param string $file_name Name of the file.
   442 	 * @param string $file_name Name of the file.
       
   443 	 * @param bool   $template  Optional. Use template theme directory. Default false.
   341 	 * @return string The whole file path or empty if the file doesn't exist.
   444 	 * @return string The whole file path or empty if the file doesn't exist.
   342 	 */
   445 	 */
   343 	private static function get_file_path_from_theme( $file_name ) {
   446 	protected static function get_file_path_from_theme( $file_name, $template = false ) {
   344 		/*
   447 		$path      = $template ? get_template_directory() : get_stylesheet_directory();
   345 		 * This used to be a locate_template call. However, that method proved problematic
   448 		$candidate = $path . '/' . $file_name;
   346 		 * due to its use of constants (STYLESHEETPATH) that threw errors in some scenarios.
   449 
   347 		 *
   450 		return is_readable( $candidate ) ? $candidate : '';
   348 		 * When the theme.json merge algorithm properly supports child themes,
       
   349 		 * this should also fall back to the template path, as locate_template did.
       
   350 		 */
       
   351 		$located   = '';
       
   352 		$candidate = get_stylesheet_directory() . '/' . $file_name;
       
   353 		if ( is_readable( $candidate ) ) {
       
   354 			$located = $candidate;
       
   355 		}
       
   356 		return $located;
       
   357 	}
   451 	}
   358 
   452 
   359 	/**
   453 	/**
   360 	 * Cleans the cached data so it can be recalculated.
   454 	 * Cleans the cached data so it can be recalculated.
   361 	 *
   455 	 *
   362 	 * @since 5.8.0
   456 	 * @since 5.8.0
       
   457 	 * @since 5.9.0 Added the `$user`, `$user_custom_post_type_id`,
       
   458 	 *              and `$i18n_schema` variables to reset.
   363 	 */
   459 	 */
   364 	public static function clean_cached_data() {
   460 	public static function clean_cached_data() {
   365 		self::$core              = null;
   461 		static::$core                     = null;
   366 		self::$theme             = null;
   462 		static::$theme                    = null;
   367 		self::$theme_has_support = null;
   463 		static::$user                     = null;
   368 		self::$theme_json_i18n   = null;
   464 		static::$user_custom_post_type_id = null;
       
   465 		static::$theme_has_support        = null;
       
   466 		static::$i18n_schema              = null;
       
   467 	}
       
   468 
       
   469 	/**
       
   470 	 * Returns the style variations defined by the theme.
       
   471 	 *
       
   472 	 * @since 6.0.0
       
   473 	 *
       
   474 	 * @return array
       
   475 	 */
       
   476 	public static function get_style_variations() {
       
   477 		$variations     = array();
       
   478 		$base_directory = get_stylesheet_directory() . '/styles';
       
   479 		if ( is_dir( $base_directory ) ) {
       
   480 			$nested_files      = new RecursiveIteratorIterator( new RecursiveDirectoryIterator( $base_directory ) );
       
   481 			$nested_html_files = iterator_to_array( new RegexIterator( $nested_files, '/^.+\.json$/i', RecursiveRegexIterator::GET_MATCH ) );
       
   482 			ksort( $nested_html_files );
       
   483 			foreach ( $nested_html_files as $path => $file ) {
       
   484 				$decoded_file = wp_json_file_decode( $path, array( 'associative' => true ) );
       
   485 				if ( is_array( $decoded_file ) ) {
       
   486 					$translated = static::translate( $decoded_file, wp_get_theme()->get( 'TextDomain' ) );
       
   487 					$variation  = ( new WP_Theme_JSON( $translated ) )->get_raw_data();
       
   488 					if ( empty( $variation['title'] ) ) {
       
   489 						$variation['title'] = basename( $path, '.json' );
       
   490 					}
       
   491 					$variations[] = $variation;
       
   492 				}
       
   493 			}
       
   494 		}
       
   495 		return $variations;
   369 	}
   496 	}
   370 
   497 
   371 }
   498 }