暫無描述

class-wp-theme-json-resolver.php 16KB

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