PluginProbe
Gutenberg / 9.8.0
Gutenberg v9.8.0
24.0.0 23.9.1 23.9.0 23.8.0 23.7.2 23.7.1 23.7.0 23.6.1 23.6.2 23.6.0 23.5.3 23.5.2 23.5.1 23.5.0 23.4.0 23.3.2 23.3.1 23.3.0 23.2.0 23.2.1 23.2.2 23.1.1 23.1.0 23.0.1 12.6.0 All 403 releases
gutenberg / lib / class-wp-theme-json-resolver.php

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

436 lines 14.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Process the different data sources for site-level
4 * config and offers and API to work with them.
5 *
6 * @package gutenberg
7 */
8
9 /**
10 * Class that abstracts the processing
11 * of the different data sources.
12 */
13 class WP_Theme_JSON_Resolver {
14
15 /**
16 * Container for data coming from core.
17 *
18 * @var WP_Theme_JSON
19 */
20 private static $core = null;
21
22 /**
23 * Container for data coming from the theme.
24 *
25 * @var WP_Theme_JSON
26 */
27 private $theme = null;
28
29 /**
30 * Container for data coming from the user.
31 *
32 * @var WP_Theme_JSON
33 */
34 private static $user = null;
35
36 /**
37 * Stores the ID of the custom post type
38 * that holds the user data.
39 *
40 * @var integer
41 */
42 private static $user_custom_post_type_id = null;
43
44 /**
45 * Processes a file that adheres to the theme.json
46 * schema and returns an array with its contents,
47 * or a void array if none found.
48 *
49 * @param string $file_path Path to file.
50 *
51 * @return array Contents that adhere to the theme.json schema.
52 */
53 private static function get_from_file( $file_path ) {
54 $config = array();
55 if ( file_exists( $file_path ) ) {
56 $decoded_file = json_decode(
57 file_get_contents( $file_path ),
58 true
59 );
60
61 $json_decoding_error = json_last_error();
62 if ( JSON_ERROR_NONE !== $json_decoding_error ) {
63 error_log( 'Error when decoding file schema: ' . json_last_error_msg() );
64 return $config;
65 }
66
67 if ( is_array( $decoded_file ) ) {
68 $config = $decoded_file;
69 }
70 }
71 return $config;
72 }
73
74 /**
75 * Processes a tree from i18n-theme.json into a linear array
76 * containing the a translatable path from theme.json and an array
77 * of properties that are translatable.
78 *
79 * @param array $file_structure_partial A part of a theme.json i18n tree.
80 * @param array $current_path An array with a path on the theme.json i18n tree.
81 *
82 * @return array An array of arrays each one containing a translatable path and an array of properties that are translatable.
83 */
84 private static function theme_json_i18_file_structure_to_preset_paths( $file_structure_partial, $current_path = array() ) {
85 $result = array();
86 foreach ( $file_structure_partial as $property => $partial_child ) {
87 if ( is_numeric( $property ) ) {
88 return array(
89 array(
90 'path' => $current_path,
91 'translatable_keys' => $file_structure_partial,
92 ),
93 );
94 }
95 $result = array_merge(
96 $result,
97 self::theme_json_i18_file_structure_to_preset_paths( $partial_child, array_merge( $current_path, array( $property ) ) )
98 );
99 }
100 return $result;
101 }
102
103 /**
104 * Returns a data structure used in theme.json translation.
105 *
106 * @return array An array of theme.json paths that are translatable and the keys that are translatable
107 */
108 private static function get_presets_to_translate() {
109 static $theme_json_i18n = null;
110 if ( null === $theme_json_i18n ) {
111 $file_structure = self::get_from_file( __DIR__ . '/experimental-i18n-theme.json' );
112 $theme_json_i18n = self::theme_json_i18_file_structure_to_preset_paths( $file_structure );
113
114 }
115 return $theme_json_i18n;
116 }
117
118 /**
119 * Translates a theme.json structure.
120 *
121 * @param array $theme_json_structure A theme.json structure that is going to be translatable.
122 * @param string $domain Optional. Text domain. Unique identifier for retrieving translated strings.
123 * Default 'default'.
124 */
125 private static function translate_presets( &$theme_json_structure, $domain = 'default' ) {
126 $preset_to_translate = self::get_presets_to_translate();
127 foreach ( $theme_json_structure as &$context_value ) {
128 if ( empty( $context_value ) ) {
129 continue;
130 }
131 foreach ( $preset_to_translate as $preset ) {
132 $path = $preset['path'];
133 $translatable_keys = $preset['translatable_keys'];
134 $array_to_translate = gutenberg_experimental_get( $context_value, $path, null );
135 if ( null === $array_to_translate ) {
136 continue;
137 }
138 foreach ( $array_to_translate as &$item_to_translate ) {
139 foreach ( $translatable_keys as $translatable_key ) {
140 if ( empty( $item_to_translate[ $translatable_key ] ) ) {
141 continue;
142 }
143 // phpcs:ignore WordPress.WP.I18n.LowLevelTranslationFunction,WordPress.WP.I18n.NonSingularStringLiteralText,WordPress.WP.I18n.NonSingularStringLiteralDomain
144 $item_to_translate[ $translatable_key ] = translate( $item_to_translate[ $translatable_key ], $domain );
145 // phpcs:enable
146 }
147 }
148 gutenberg_experimental_set( $context_value, $path, $array_to_translate );
149 }
150 }
151 }
152
153 /**
154 * Return core's origin config.
155 *
156 * @return WP_Theme_JSON Entity that holds core data.
157 */
158 private static function get_core_origin() {
159 if ( null !== self::$core ) {
160 return self::$core;
161 }
162
163 $config = self::get_from_file( __DIR__ . '/experimental-default-theme.json' );
164 self::translate_presets( $config );
165
166 // Start i18n logic to remove when JSON i18 strings are extracted.
167 $default_colors_i18n = array(
168 'black' => __( 'Black', 'gutenberg' ),
169 'cyan-bluish-gray' => __( 'Cyan bluish gray', 'gutenberg' ),
170 'white' => __( 'White', 'gutenberg' ),
171 'pale-pink' => __( 'Pale pink', 'gutenberg' ),
172 'vivid-red' => __( 'Vivid red', 'gutenberg' ),
173 'luminous-vivid-orange' => __( 'Luminous vivid orange', 'gutenberg' ),
174 'luminous-vivid-amber' => __( 'Luminous vivid amber', 'gutenberg' ),
175 'light-green-cyan' => __( 'Light green cyan', 'gutenberg' ),
176 'vivid-green-cyan' => __( 'Vivid green cyan', 'gutenberg' ),
177 'pale-cyan-blue' => __( 'Pale cyan blue', 'gutenberg' ),
178 'vivid-cyan-blue' => __( 'Vivid cyan blue', 'gutenberg' ),
179 'vivid-purple' => __( 'Vivid purple', 'gutenberg' ),
180 );
181 if ( ! empty( $config['global']['settings']['color']['palette'] ) ) {
182 foreach ( $config['global']['settings']['color']['palette'] as &$color ) {
183 $color['name'] = $default_colors_i18n[ $color['slug'] ];
184 }
185 }
186
187 $default_gradients_i18n = array(
188 'vivid-cyan-blue-to-vivid-purple' => __( 'Vivid cyan blue to vivid purple', 'gutenberg' ),
189 'light-green-cyan-to-vivid-green-cyan' => __( 'Light green cyan to vivid green cyan', 'gutenberg' ),
190 'luminous-vivid-amber-to-luminous-vivid-orange' => __( 'Luminous vivid amber to luminous vivid orange', 'gutenberg' ),
191 'luminous-vivid-orange-to-vivid-red' => __( 'Luminous vivid orange to vivid red', 'gutenberg' ),
192 'very-light-gray-to-cyan-bluish-gray' => __( 'Very light gray to cyan bluish gray', 'gutenberg' ),
193 'cool-to-warm-spectrum' => __( 'Cool to warm spectrum', 'gutenberg' ),
194 'blush-light-purple' => __( 'Blush light purple', 'gutenberg' ),
195 'blush-bordeaux' => __( 'Blush bordeaux', 'gutenberg' ),
196 'luminous-dusk' => __( 'Luminous dusk', 'gutenberg' ),
197 'pale-ocean' => __( 'Pale ocean', 'gutenberg' ),
198 'electric-grass' => __( 'Electric grass', 'gutenberg' ),
199 'midnight' => __( 'Midnight', 'gutenberg' ),
200 );
201 if ( ! empty( $config['global']['settings']['color']['gradients'] ) ) {
202 foreach ( $config['global']['settings']['color']['gradients'] as &$gradient ) {
203 $gradient['name'] = $default_gradients_i18n[ $gradient['slug'] ];
204 }
205 }
206
207 $default_font_sizes_i18n = array(
208 'small' => __( 'Small', 'gutenberg' ),
209 'normal' => __( 'Normal', 'gutenberg' ),
210 'medium' => __( 'Medium', 'gutenberg' ),
211 'large' => __( 'Large', 'gutenberg' ),
212 'huge' => __( 'Huge', 'gutenberg' ),
213 );
214 if ( ! empty( $config['global']['settings']['typography']['fontSizes'] ) ) {
215 foreach ( $config['global']['settings']['typography']['fontSizes'] as &$font_size ) {
216 $font_size['name'] = $default_font_sizes_i18n[ $font_size['slug'] ];
217 }
218 }
219 // End i18n logic to remove when JSON i18 strings are extracted.
220
221 self::$core = new WP_Theme_JSON( $config );
222
223 return self::$core;
224 }
225
226 /**
227 * Returns the theme's origin config.
228 *
229 * It uses the theme support data if
230 * the theme hasn't declared any via theme.json.
231 *
232 * @param array $theme_support_data Theme support data in theme.json format.
233 *
234 * @return WP_Theme_JSON Entity that holds theme data.
235 */
236 private function get_theme_origin( $theme_support_data = array() ) {
237 $theme_json_data = self::get_from_file( locate_template( 'experimental-theme.json' ) );
238 self::translate_presets( $theme_json_data, wp_get_theme()->get( 'TextDomain' ) );
239
240 /*
241 * We want the presets and settings declared in theme.json
242 * to override the ones declared via add_theme_support.
243 */
244 $this->theme = new WP_Theme_JSON( $theme_support_data );
245 $this->theme->merge( new WP_Theme_JSON( $theme_json_data ) );
246
247 return $this->theme;
248 }
249
250 /**
251 * Returns the CPT that contains the user's origin config
252 * for the current theme or a void array if none found.
253 *
254 * It can also create and return a new draft CPT.
255 *
256 * @param bool $should_create_cpt Whether a new CPT should be created if no one was found.
257 * False by default.
258 * @param array $post_status_filter Filter CPT by post status.
259 * ['publish'] by default, so it only fetches published posts.
260 *
261 * @return array Custom Post Type for the user's origin config.
262 */
263 private static function get_user_data_from_custom_post_type( $should_create_cpt = false, $post_status_filter = array( 'publish' ) ) {
264 $user_cpt = array();
265 $post_type_filter = 'wp_global_styles';
266 $post_name_filter = 'wp-global-styles-' . urlencode( wp_get_theme()->get_stylesheet() );
267 $recent_posts = wp_get_recent_posts(
268 array(
269 'numberposts' => 1,
270 'orderby' => 'date',
271 'order' => 'desc',
272 'post_type' => $post_type_filter,
273 'post_status' => $post_status_filter,
274 'name' => $post_name_filter,
275 )
276 );
277
278 if ( is_array( $recent_posts ) && ( count( $recent_posts ) === 1 ) ) {
279 $user_cpt = $recent_posts[0];
280 } elseif ( $should_create_cpt ) {
281 $cpt_post_id = wp_insert_post(
282 array(
283 'post_content' => '{}',
284 'post_status' => 'publish',
285 'post_type' => $post_type_filter,
286 'post_name' => $post_name_filter,
287 ),
288 true
289 );
290 $user_cpt = get_post( $cpt_post_id, ARRAY_A );
291 }
292
293 return $user_cpt;
294 }
295
296 /**
297 * Returns the user's origin config.
298 *
299 * @return WP_Theme_JSON Entity that holds user data.
300 */
301 private static function get_user_origin() {
302 if ( null !== self::$user ) {
303 return self::$user;
304 }
305
306 $config = array();
307 $user_cpt = self::get_user_data_from_custom_post_type();
308 if ( array_key_exists( 'post_content', $user_cpt ) ) {
309 $decoded_data = json_decode( $user_cpt['post_content'], true );
310
311 $json_decoding_error = json_last_error();
312 if ( JSON_ERROR_NONE !== $json_decoding_error ) {
313 error_log( 'Error when decoding user schema: ' . json_last_error_msg() );
314 return $config;
315 }
316
317 // Very important to verify if the flag isGlobalStylesUserThemeJSON is true.
318 // If is not true the content was not escaped and is not safe.
319 if (
320 is_array( $decoded_data ) &&
321 isset( $decoded_data['isGlobalStylesUserThemeJSON'] ) &&
322 $decoded_data['isGlobalStylesUserThemeJSON']
323 ) {
324 unset( $decoded_data['isGlobalStylesUserThemeJSON'] );
325 $config = $decoded_data;
326 }
327 }
328 self::$user = new WP_Theme_JSON( $config, true );
329
330 return self::$user;
331 }
332
333 /**
334 * There are three sources of data for a site:
335 * core, theme, and user.
336 *
337 * The main function of the resolver is to
338 * merge all this data following this algorithm:
339 * theme overrides core, and user overrides
340 * data coming from either theme or core.
341 *
342 * user data > theme data > core data
343 *
344 * The main use case for the resolver is to return
345 * the merged data up to the user level.However,
346 * there are situations in which we need the
347 * data merged up to a different level (theme)
348 * or no merged at all.
349 *
350 * @param array $theme_support_data Existing block editor settings.
351 * Empty array by default.
352 * @param string $origin The source of data the consumer wants.
353 * Valid values are 'core', 'theme', 'user'.
354 * Default is 'user'.
355 * @param boolean $merged Whether the data should be merged
356 * with the previous origins (the default).
357 *
358 * @return WP_Theme_JSON
359 */
360 public function get_origin( $theme_support_data = array(), $origin = 'user', $merged = true ) {
361 if ( ( 'user' === $origin ) && $merged ) {
362 $result = new WP_Theme_JSON();
363 $result->merge( self::get_core_origin() );
364 $result->merge( $this->get_theme_origin( $theme_support_data ) );
365 $result->merge( self::get_user_origin() );
366 return $result;
367 }
368
369 if ( ( 'theme' === $origin ) && $merged ) {
370 $result = new WP_Theme_JSON();
371 $result->merge( self::get_core_origin() );
372 $result->merge( $this->get_theme_origin( $theme_support_data ) );
373 return $result;
374 }
375
376 if ( 'user' === $origin ) {
377 return self::get_user_origin();
378 }
379
380 if ( 'theme' === $origin ) {
381 return $this->get_theme_origin( $theme_support_data );
382 }
383
384 return self::get_core_origin();
385 }
386
387 /**
388 * Registers a Custom Post Type to store the user's origin config.
389 */
390 public static function register_user_custom_post_type() {
391 $args = array(
392 'label' => __( 'Global Styles', 'gutenberg' ),
393 'description' => 'CPT to store user design tokens',
394 'public' => false,
395 'show_ui' => false,
396 'show_in_rest' => true,
397 'rest_base' => '__experimental/global-styles',
398 'capabilities' => array(
399 'read' => 'edit_theme_options',
400 'create_posts' => 'edit_theme_options',
401 'edit_posts' => 'edit_theme_options',
402 'edit_published_posts' => 'edit_theme_options',
403 'delete_published_posts' => 'edit_theme_options',
404 'edit_others_posts' => 'edit_theme_options',
405 'delete_others_posts' => 'edit_theme_options',
406 ),
407 'map_meta_cap' => true,
408 'supports' => array(
409 'editor',
410 'revisions',
411 ),
412 );
413 register_post_type( 'wp_global_styles', $args );
414 }
415
416 /**
417 * Returns the ID of the custom post type
418 * that stores user data.
419 *
420 * @return integer
421 */
422 public static function get_user_custom_post_type_id() {
423 if ( null !== self::$user_custom_post_type_id ) {
424 return self::$user_custom_post_type_id;
425 }
426
427 $user_cpt = self::get_user_data_from_custom_post_type( true );
428 if ( array_key_exists( 'ID', $user_cpt ) ) {
429 self::$user_custom_post_type_id = $user_cpt['ID'];
430 }
431
432 return self::$user_custom_post_type_id;
433 }
434
435 }
436