PluginProbe
Gutenberg / 12.6.0
Gutenberg v12.6.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 7.4.0 All 402 releases
gutenberg / lib / compat / wordpress-5.9 / class-wp-rest-global-styles-controller.php

class-wp-rest-global-styles-controller.php in Gutenberg 12.6.0, at lib/compat/wordpress-5.9/class-wp-rest-global-styles-controller.php

589 lines 17.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * REST API: Gutenberg_REST_Global_Styles_Controller class
4 *
5 * @package Gutenberg
6 * @subpackage REST_API
7 */
8
9 if ( ! class_exists( 'WP_REST_Global_Styles_Controller' ) ) {
10 /**
11 * Base Global Styles REST API Controller.
12 */
13 class WP_REST_Global_Styles_Controller extends WP_REST_Controller {
14
15 /**
16 * Post type.
17 *
18 * @since 5.9.0
19 * @var string
20 */
21 protected $post_type;
22
23 /**
24 * Constructor.
25 *
26 * @since 5.9.0
27 */
28 public function __construct() {
29 $this->namespace = 'wp/v2';
30 $this->rest_base = 'global-styles';
31 $this->post_type = 'wp_global_styles';
32 }
33
34 /**
35 * Registers the controllers routes.
36 *
37 * @since 5.9.0
38 *
39 * @return void
40 */
41 public function register_routes() {
42 // List themes global styles.
43 register_rest_route(
44 $this->namespace,
45 // The route.
46 sprintf(
47 '/%s/themes/(?P<stylesheet>%s)',
48 $this->rest_base,
49 // Matches theme's directory: `/themes/<subdirectory>/<theme>/` or `/themes/<theme>/`.
50 // Excludes invalid directory name characters: `/:<>*?"|`.
51 '[^\/:<>\*\?"\|]+(?:\/[^\/:<>\*\?"\|]+)?'
52 ),
53 array(
54 array(
55 'methods' => WP_REST_Server::READABLE,
56 'callback' => array( $this, 'get_theme_item' ),
57 'permission_callback' => array( $this, 'get_theme_item_permissions_check' ),
58 'args' => array(
59 'stylesheet' => array(
60 'description' => __( 'The theme identifier', 'gutenberg' ),
61 'type' => 'string',
62 'sanitize_callback' => array( $this, '_sanitize_global_styles_callback' ),
63 ),
64 ),
65 ),
66 )
67 );
68
69 // Lists/updates a single global style variation based on the given id.
70 register_rest_route(
71 $this->namespace,
72 '/' . $this->rest_base . '/(?P<id>[\/\w-]+)',
73 array(
74 array(
75 'methods' => WP_REST_Server::READABLE,
76 'callback' => array( $this, 'get_item' ),
77 'permission_callback' => array( $this, 'get_item_permissions_check' ),
78 'args' => array(
79 'id' => array(
80 'description' => __( 'The id of a template', 'gutenberg' ),
81 'type' => 'string',
82 'sanitize_callback' => array( $this, '_sanitize_global_styles_callback' ),
83 ),
84 ),
85 ),
86 array(
87 'methods' => WP_REST_Server::EDITABLE,
88 'callback' => array( $this, 'update_item' ),
89 'permission_callback' => array( $this, 'update_item_permissions_check' ),
90 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
91 ),
92 'schema' => array( $this, 'get_public_item_schema' ),
93 )
94 );
95 }
96
97 /**
98 * Sanitize the global styles ID or stylesheet to decode endpoint.
99 * For example, `wp/v2/global-styles/twentytwentytwo%200.4.0`
100 * would be decoded to `twentytwentytwo 0.4.0`.
101 *
102 * @since 5.9.0
103 *
104 * @param string $id_or_stylesheet Global styles ID or stylesheet.
105 * @return string Sanitized global styles ID or stylesheet.
106 */
107 public function _sanitize_global_styles_callback( $id_or_stylesheet ) {
108 return urldecode( $id_or_stylesheet );
109 }
110
111 /**
112 * Checks if a given request has access to read a single global style.
113 *
114 * @since 5.9.0
115 *
116 * @param WP_REST_Request $request Full details about the request.
117 * @return true|WP_Error True if the request has read access, WP_Error object otherwise.
118 */
119 public function get_item_permissions_check( $request ) {
120 $post = $this->get_post( $request['id'] );
121 if ( is_wp_error( $post ) ) {
122 return $post;
123 }
124
125 if ( 'edit' === $request['context'] && $post && ! $this->check_update_permission( $post ) ) {
126 return new WP_Error(
127 'rest_forbidden_context',
128 __( 'Sorry, you are not allowed to edit this global style.', 'gutenberg' ),
129 array( 'status' => rest_authorization_required_code() )
130 );
131 }
132
133 if ( ! $this->check_read_permission( $post ) ) {
134 return new WP_Error(
135 'rest_cannot_view',
136 __( 'Sorry, you are not allowed to view this global style.', 'gutenberg' ),
137 array( 'status' => rest_authorization_required_code() )
138 );
139 }
140
141 return true;
142 }
143
144 /**
145 * Checks if a global style can be read.
146 *
147 * @since 5.9.0
148 *
149 * @param WP_Post $post Post object.
150 * @return bool Whether the post can be read.
151 */
152 protected function check_read_permission( $post ) {
153 return current_user_can( 'read_post', $post->ID );
154 }
155
156 /**
157 * Returns the given global styles config.
158 *
159 * @since 5.9.0
160 *
161 * @param WP_REST_Request $request The request instance.
162 *
163 * @return WP_REST_Response|WP_Error
164 */
165 public function get_item( $request ) {
166 $post = $this->get_post( $request['id'] );
167 if ( is_wp_error( $post ) ) {
168 return $post;
169 }
170
171 return $this->prepare_item_for_response( $post, $request );
172 }
173
174 /**
175 * Checks if a given request has access to write a single global styles config.
176 *
177 * @since 5.9.0
178 *
179 * @param WP_REST_Request $request Full details about the request.
180 * @return true|WP_Error True if the request has write access for the item, WP_Error object otherwise.
181 */
182 public function update_item_permissions_check( $request ) {
183 $post = $this->get_post( $request['id'] );
184 if ( is_wp_error( $post ) ) {
185 return $post;
186 }
187
188 if ( $post && ! $this->check_update_permission( $post ) ) {
189 return new WP_Error(
190 'rest_cannot_edit',
191 __( 'Sorry, you are not allowed to edit this global style.', 'gutenberg' ),
192 array( 'status' => rest_authorization_required_code() )
193 );
194 }
195
196 return true;
197 }
198
199 /**
200 * Checks if a global style can be edited.
201 *
202 * @since 5.9.0
203 *
204 * @param WP_Post $post Post object.
205 * @return bool Whether the post can be edited.
206 */
207 protected function check_update_permission( $post ) {
208 return current_user_can( 'edit_post', $post->ID );
209 }
210
211 /**
212 * Updates a single global style config.
213 *
214 * @since 5.9.0
215 *
216 * @param WP_REST_Request $request Full details about the request.
217 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
218 */
219 public function update_item( $request ) {
220 $post_before = $this->get_post( $request['id'] );
221 if ( is_wp_error( $post_before ) ) {
222 return $post_before;
223 }
224
225 $changes = $this->prepare_item_for_database( $request );
226 $result = wp_update_post( wp_slash( (array) $changes ), true, false );
227 if ( is_wp_error( $result ) ) {
228 return $result;
229 }
230
231 $post = get_post( $request['id'] );
232 $fields_update = $this->update_additional_fields_for_object( $post, $request );
233 if ( is_wp_error( $fields_update ) ) {
234 return $fields_update;
235 }
236
237 wp_after_insert_post( $post, true, $post_before );
238
239 $response = $this->prepare_item_for_response( $post, $request );
240
241 return rest_ensure_response( $response );
242 }
243
244 /**
245 * Prepares a single global styles config for update.
246 *
247 * @since 5.9.0
248 *
249 * @param WP_REST_Request $request Request object.
250 * @return stdClass Changes to pass to wp_update_post.
251 */
252 protected function prepare_item_for_database( $request ) {
253 $changes = new stdClass();
254 $changes->ID = $request['id'];
255
256 $post = get_post( $request['id'] );
257 $existing_config = array();
258 if ( $post ) {
259 $existing_config = json_decode( $post->post_content, true );
260 $json_decoding_error = json_last_error();
261 if ( JSON_ERROR_NONE !== $json_decoding_error || ! isset( $existing_config['isGlobalStylesUserThemeJSON'] ) ||
262 ! $existing_config['isGlobalStylesUserThemeJSON'] ) {
263 $existing_config = array();
264 }
265 }
266
267 if ( isset( $request['styles'] ) || isset( $request['settings'] ) ) {
268 $config = array();
269 if ( isset( $request['styles'] ) ) {
270 $config['styles'] = $request['styles'];
271 } elseif ( isset( $existing_config['styles'] ) ) {
272 $config['styles'] = $existing_config['styles'];
273 }
274 if ( isset( $request['settings'] ) ) {
275 $config['settings'] = $request['settings'];
276 } elseif ( isset( $existing_config['settings'] ) ) {
277 $config['settings'] = $existing_config['settings'];
278 }
279 $config['isGlobalStylesUserThemeJSON'] = true;
280 $config['version'] = WP_Theme_JSON_Gutenberg::LATEST_SCHEMA;
281 $changes->post_content = wp_json_encode( $config );
282 }
283
284 // Post title.
285 if ( isset( $request['title'] ) ) {
286 if ( is_string( $request['title'] ) ) {
287 $changes->post_title = $request['title'];
288 } elseif ( ! empty( $request['title']['raw'] ) ) {
289 $changes->post_title = $request['title']['raw'];
290 }
291 }
292
293 return $changes;
294 }
295
296 /**
297 * Prepare a global styles config output for response.
298 *
299 * @since 5.9.0
300 *
301 * @param WP_Post $post Global Styles post object.
302 * @param WP_REST_Request $request Request object.
303 * @return WP_REST_Response Response object.
304 */
305 public function prepare_item_for_response( $post, $request ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
306 $raw_config = json_decode( $post->post_content, true );
307 $is_global_styles_user_theme_json = isset( $raw_config['isGlobalStylesUserThemeJSON'] ) && true === $raw_config['isGlobalStylesUserThemeJSON'];
308 $config = array();
309 if ( $is_global_styles_user_theme_json ) {
310 $config = ( new WP_Theme_JSON_Gutenberg( $raw_config, 'custom' ) )->get_raw_data();
311 }
312
313 // Base fields for every post.
314 $data = array();
315 $fields = $this->get_fields_for_response( $request );
316
317 if ( rest_is_field_included( 'id', $fields ) ) {
318 $data['id'] = $post->ID;
319 }
320
321 if ( rest_is_field_included( 'title', $fields ) ) {
322 $data['title'] = array();
323 }
324 if ( rest_is_field_included( 'title.raw', $fields ) ) {
325 $data['title']['raw'] = $post->post_title;
326 }
327 if ( rest_is_field_included( 'title.rendered', $fields ) ) {
328 add_filter( 'protected_title_format', array( $this, 'protected_title_format' ) );
329
330 $data['title']['rendered'] = get_the_title( $post->ID );
331
332 remove_filter( 'protected_title_format', array( $this, 'protected_title_format' ) );
333 }
334
335 if ( rest_is_field_included( 'settings', $fields ) ) {
336 $data['settings'] = ! empty( $config['settings'] ) && $is_global_styles_user_theme_json ? $config['settings'] : new stdClass();
337 }
338
339 if ( rest_is_field_included( 'styles', $fields ) ) {
340 $data['styles'] = ! empty( $config['styles'] ) && $is_global_styles_user_theme_json ? $config['styles'] : new stdClass();
341 }
342
343 $context = ! empty( $request['context'] ) ? $request['context'] : 'view';
344 $data = $this->add_additional_fields_to_object( $data, $request );
345 $data = $this->filter_response_by_context( $data, $context );
346
347 // Wrap the data in a response object.
348 $response = rest_ensure_response( $data );
349
350 $links = $this->prepare_links( $post->ID );
351 $response->add_links( $links );
352 if ( ! empty( $links['self']['href'] ) ) {
353 $actions = $this->get_available_actions();
354 $self = $links['self']['href'];
355 foreach ( $actions as $rel ) {
356 $response->add_link( $rel, $self );
357 }
358 }
359
360 return $response;
361 }
362
363 /**
364 * Get the post, if the ID is valid.
365 *
366 * @since 5.9.0
367 *
368 * @param int $id Supplied ID.
369 * @return WP_Post|WP_Error Post object if ID is valid, WP_Error otherwise.
370 */
371 protected function get_post( $id ) {
372 $error = new WP_Error(
373 'rest_global_styles_not_found',
374 __( 'No global styles config exist with that id.', 'gutenberg' ),
375 array( 'status' => 404 )
376 );
377
378 $id = (int) $id;
379 if ( $id <= 0 ) {
380 return $error;
381 }
382
383 $post = get_post( $id );
384 if ( empty( $post ) || empty( $post->ID ) || $this->post_type !== $post->post_type ) {
385 return $error;
386 }
387
388 return $post;
389 }
390
391
392 /**
393 * Prepares links for the request.
394 *
395 * @since 5.9.0
396 *
397 * @param integer $id ID.
398 * @return array Links for the given post.
399 */
400 protected function prepare_links( $id ) {
401 $base = sprintf( '%s/%s', $this->namespace, $this->rest_base );
402
403 $links = array(
404 'self' => array(
405 'href' => rest_url( trailingslashit( $base ) . $id ),
406 ),
407 );
408
409 return $links;
410 }
411
412 /**
413 * Get the link relations available for the post and current user.
414 *
415 * @since 5.9.0
416 *
417 * @return array List of link relations.
418 */
419 protected function get_available_actions() {
420 $rels = array();
421
422 $post_type = get_post_type_object( $this->post_type );
423 if ( current_user_can( $post_type->cap->publish_posts ) ) {
424 $rels[] = 'https://api.w.org/action-publish';
425 }
426
427 return $rels;
428 }
429
430 /**
431 * Overwrites the default protected title format.
432 *
433 * By default, WordPress will show password protected posts with a title of
434 * "Protected: %s", as the REST API communicates the protected status of a post
435 * in a machine readable format, we remove the "Protected: " prefix.
436 *
437 * @since 5.9.0
438 *
439 * @return string Protected title format.
440 */
441 public function protected_title_format() {
442 return '%s';
443 }
444
445 /**
446 * Retrieves the query params for the global styles collection.
447 *
448 * @since 5.9.0
449 *
450 * @return array Collection parameters.
451 */
452 public function get_collection_params() {
453 return array();
454 }
455
456 /**
457 * Retrieves the global styles type' schema, conforming to JSON Schema.
458 *
459 * @since 5.9.0
460 *
461 * @return array Item schema data.
462 */
463 public function get_item_schema() {
464 if ( $this->schema ) {
465 return $this->add_additional_fields_schema( $this->schema );
466 }
467
468 $schema = array(
469 '$schema' => 'http://json-schema.org/draft-04/schema#',
470 'title' => $this->post_type,
471 'type' => 'object',
472 'properties' => array(
473 'id' => array(
474 'description' => __( 'ID of global styles config.', 'gutenberg' ),
475 'type' => 'string',
476 'context' => array( 'embed', 'view', 'edit' ),
477 'readonly' => true,
478 ),
479 'styles' => array(
480 'description' => __( 'Global styles.', 'gutenberg' ),
481 'type' => array( 'object' ),
482 'context' => array( 'view', 'edit' ),
483 ),
484 'settings' => array(
485 'description' => __( 'Global settings.', 'gutenberg' ),
486 'type' => array( 'object' ),
487 'context' => array( 'view', 'edit' ),
488 ),
489 'title' => array(
490 'description' => __( 'Title of the global styles variation.', 'gutenberg' ),
491 'type' => array( 'object', 'string' ),
492 'default' => '',
493 'context' => array( 'embed', 'view', 'edit' ),
494 'properties' => array(
495 'raw' => array(
496 'description' => __( 'Title for the global styles variation, as it exists in the database.', 'gutenberg' ),
497 'type' => 'string',
498 'context' => array( 'view', 'edit', 'embed' ),
499 ),
500 'rendered' => array(
501 'description' => __( 'HTML title for the post, transformed for display.', 'gutenberg' ),
502 'type' => 'string',
503 'context' => array( 'view', 'edit', 'embed' ),
504 'readonly' => true,
505 ),
506 ),
507 ),
508 ),
509 );
510
511 $this->schema = $schema;
512
513 return $this->add_additional_fields_schema( $this->schema );
514 }
515
516 /**
517 * Checks if a given request has access to read a single theme global styles config.
518 *
519 * @since 5.9.0
520 *
521 * @param WP_REST_Request $request Full details about the request.
522 * @return true|WP_Error True if the request has read access for the item, WP_Error object otherwise.
523 */
524 public function get_theme_item_permissions_check( $request ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
525 // Verify if the current user has edit_theme_options capability.
526 // This capability is required to edit/view/delete templates.
527 if ( ! current_user_can( 'edit_theme_options' ) ) {
528 return new WP_Error(
529 'rest_cannot_manage_global_styles',
530 __( 'Sorry, you are not allowed to access the global styles on this site.', 'gutenberg' ),
531 array(
532 'status' => rest_authorization_required_code(),
533 )
534 );
535 }
536
537 return true;
538 }
539
540 /**
541 * Returns the given theme global styles config.
542 *
543 * @since 5.9.0
544 *
545 * @param WP_REST_Request $request The request instance.
546 * @return WP_REST_Response|WP_Error
547 */
548 public function get_theme_item( $request ) {
549 if ( wp_get_theme()->get_stylesheet() !== $request['stylesheet'] ) {
550 // This endpoint only supports the active theme for now.
551 return new WP_Error(
552 'rest_theme_not_found',
553 __( 'Theme not found.', 'gutenberg' ),
554 array( 'status' => 404 )
555 );
556 }
557
558 $theme = WP_Theme_JSON_Resolver_Gutenberg::get_merged_data( 'theme' );
559 $data = array();
560 $fields = $this->get_fields_for_response( $request );
561
562 if ( rest_is_field_included( 'settings', $fields ) ) {
563 $data['settings'] = $theme->get_settings();
564 }
565
566 if ( rest_is_field_included( 'styles', $fields ) ) {
567 $raw_data = $theme->get_raw_data();
568 $data['styles'] = isset( $raw_data['styles'] ) ? $raw_data['styles'] : array();
569 }
570
571 $context = ! empty( $request['context'] ) ? $request['context'] : 'view';
572 $data = $this->add_additional_fields_to_object( $data, $request );
573 $data = $this->filter_response_by_context( $data, $context );
574
575 $response = rest_ensure_response( $data );
576
577 $links = array(
578 'self' => array(
579 'href' => rest_url( sprintf( '%s/%s/themes/%s', $this->namespace, $this->rest_base, $request['stylesheet'] ) ),
580 ),
581 );
582
583 $response->add_links( $links );
584
585 return $response;
586 }
587 }
588 }
589