[ Index ] |
PHP Cross Reference of WordPress Trunk (Updated Daily) |
[Summary view] [Print] [Text view]
1 <?php 2 /** 3 * REST API: WP_REST_Global_Styles_Controller class 4 * 5 * @package WordPress 6 * @subpackage REST_API 7 * @since 5.9.0 8 */ 9 10 /** 11 * Base Global Styles REST API Controller. 12 */ 13 class WP_REST_Global_Styles_Controller extends WP_REST_Posts_Controller { 14 /** 15 * Whether the controller supports batching. 16 * 17 * @since 6.6.0 18 * @var array 19 */ 20 protected $allow_batch = array( 'v1' => false ); 21 22 /** 23 * Constructor. 24 * 25 * @since 6.6.0 26 * 27 * @param string $post_type Post type. 28 */ 29 public function __construct( $post_type = 'wp_global_styles' ) { 30 parent::__construct( $post_type ); 31 } 32 33 /** 34 * Registers the controllers routes. 35 * 36 * @since 5.9.0 37 */ 38 public function register_routes() { 39 register_rest_route( 40 $this->namespace, 41 '/' . $this->rest_base . '/themes/(?P<stylesheet>[\/\s%\w\.\(\)\[\]\@_\-]+)/variations', 42 array( 43 array( 44 'methods' => WP_REST_Server::READABLE, 45 'callback' => array( $this, 'get_theme_items' ), 46 'permission_callback' => array( $this, 'get_theme_items_permissions_check' ), 47 'args' => array( 48 'stylesheet' => array( 49 'description' => __( 'The theme identifier' ), 50 'type' => 'string', 51 ), 52 ), 53 'allow_batch' => $this->allow_batch, 54 ), 55 ) 56 ); 57 58 // List themes global styles. 59 register_rest_route( 60 $this->namespace, 61 // The route. 62 sprintf( 63 '/%s/themes/(?P<stylesheet>%s)', 64 $this->rest_base, 65 /* 66 * Matches theme's directory: `/themes/<subdirectory>/<theme>/` or `/themes/<theme>/`. 67 * Excludes invalid directory name characters: `/:<>*?"|`. 68 */ 69 '[^\/:<>\*\?"\|]+(?:\/[^\/:<>\*\?"\|]+)?' 70 ), 71 array( 72 array( 73 'methods' => WP_REST_Server::READABLE, 74 'callback' => array( $this, 'get_theme_item' ), 75 'permission_callback' => array( $this, 'get_theme_item_permissions_check' ), 76 'args' => array( 77 'stylesheet' => array( 78 'description' => __( 'The theme identifier' ), 79 'type' => 'string', 80 'sanitize_callback' => array( $this, '_sanitize_global_styles_callback' ), 81 ), 82 ), 83 'allow_batch' => $this->allow_batch, 84 ), 85 ) 86 ); 87 88 // Lists/updates a single global style variation based on the given id. 89 register_rest_route( 90 $this->namespace, 91 '/' . $this->rest_base . '/(?P<id>[\/\w-]+)', 92 array( 93 array( 94 'methods' => WP_REST_Server::READABLE, 95 'callback' => array( $this, 'get_item' ), 96 'permission_callback' => array( $this, 'get_item_permissions_check' ), 97 'args' => array( 98 'id' => array( 99 'description' => __( 'The id of a template' ), 100 'type' => 'string', 101 'sanitize_callback' => array( $this, '_sanitize_global_styles_callback' ), 102 ), 103 ), 104 ), 105 array( 106 'methods' => WP_REST_Server::EDITABLE, 107 'callback' => array( $this, 'update_item' ), 108 'permission_callback' => array( $this, 'update_item_permissions_check' ), 109 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), 110 ), 111 'schema' => array( $this, 'get_public_item_schema' ), 112 'allow_batch' => $this->allow_batch, 113 ) 114 ); 115 } 116 117 /** 118 * Sanitize the global styles ID or stylesheet to decode endpoint. 119 * For example, `wp/v2/global-styles/twentytwentytwo%200.4.0` 120 * would be decoded to `twentytwentytwo 0.4.0`. 121 * 122 * @since 5.9.0 123 * 124 * @param string $id_or_stylesheet Global styles ID or stylesheet. 125 * @return string Sanitized global styles ID or stylesheet. 126 */ 127 public function _sanitize_global_styles_callback( $id_or_stylesheet ) { 128 return urldecode( $id_or_stylesheet ); 129 } 130 131 /** 132 * Get the post, if the ID is valid. 133 * 134 * @since 5.9.0 135 * 136 * @param int $id Supplied ID. 137 * @return WP_Post|WP_Error Post object if ID is valid, WP_Error otherwise. 138 */ 139 protected function get_post( $id ) { 140 $error = new WP_Error( 141 'rest_global_styles_not_found', 142 __( 'No global styles config exist with that id.' ), 143 array( 'status' => 404 ) 144 ); 145 146 $id = (int) $id; 147 if ( $id <= 0 ) { 148 return $error; 149 } 150 151 $post = get_post( $id ); 152 if ( empty( $post ) || empty( $post->ID ) || $this->post_type !== $post->post_type ) { 153 return $error; 154 } 155 156 return $post; 157 } 158 159 /** 160 * Checks if a given request has access to read a single global style. 161 * 162 * @since 5.9.0 163 * 164 * @param WP_REST_Request $request Full details about the request. 165 * @return true|WP_Error True if the request has read access, WP_Error object otherwise. 166 */ 167 public function get_item_permissions_check( $request ) { 168 $post = $this->get_post( $request['id'] ); 169 if ( is_wp_error( $post ) ) { 170 return $post; 171 } 172 173 if ( 'edit' === $request['context'] && $post && ! $this->check_update_permission( $post ) ) { 174 return new WP_Error( 175 'rest_forbidden_context', 176 __( 'Sorry, you are not allowed to edit this global style.' ), 177 array( 'status' => rest_authorization_required_code() ) 178 ); 179 } 180 181 if ( ! $this->check_read_permission( $post ) ) { 182 return new WP_Error( 183 'rest_cannot_view', 184 __( 'Sorry, you are not allowed to view this global style.' ), 185 array( 'status' => rest_authorization_required_code() ) 186 ); 187 } 188 189 return true; 190 } 191 192 /** 193 * Checks if a global style can be read. 194 * 195 * @since 5.9.0 196 * 197 * @param WP_Post $post Post object. 198 * @return bool Whether the post can be read. 199 */ 200 public function check_read_permission( $post ) { 201 return current_user_can( 'read_post', $post->ID ); 202 } 203 204 /** 205 * Checks if a given request has access to write a single global styles config. 206 * 207 * @since 5.9.0 208 * 209 * @param WP_REST_Request $request Full details about the request. 210 * @return true|WP_Error True if the request has write access for the item, WP_Error object otherwise. 211 */ 212 public function update_item_permissions_check( $request ) { 213 $post = $this->get_post( $request['id'] ); 214 if ( is_wp_error( $post ) ) { 215 return $post; 216 } 217 218 if ( $post && ! $this->check_update_permission( $post ) ) { 219 return new WP_Error( 220 'rest_cannot_edit', 221 __( 'Sorry, you are not allowed to edit this global style.' ), 222 array( 'status' => rest_authorization_required_code() ) 223 ); 224 } 225 226 return true; 227 } 228 229 /** 230 * Prepares a single global styles config for update. 231 * 232 * @since 5.9.0 233 * @since 6.2.0 Added validation of styles.css property. 234 * @since 6.6.0 Added registration of block style variations from theme.json sources (theme.json, user theme.json, partials). 235 * 236 * @param WP_REST_Request $request Request object. 237 * @return stdClass|WP_Error Prepared item on success. WP_Error on when the custom CSS is not valid. 238 */ 239 protected function prepare_item_for_database( $request ) { 240 $changes = new stdClass(); 241 $changes->ID = $request['id']; 242 243 $post = get_post( $request['id'] ); 244 $existing_config = array(); 245 if ( $post ) { 246 $existing_config = json_decode( $post->post_content, true ); 247 $json_decoding_error = json_last_error(); 248 if ( JSON_ERROR_NONE !== $json_decoding_error || ! isset( $existing_config['isGlobalStylesUserThemeJSON'] ) || 249 ! $existing_config['isGlobalStylesUserThemeJSON'] ) { 250 $existing_config = array(); 251 } 252 } 253 254 if ( isset( $request['styles'] ) || isset( $request['settings'] ) ) { 255 $config = array(); 256 if ( isset( $request['styles'] ) ) { 257 if ( isset( $request['styles']['css'] ) ) { 258 $css_validation_result = $this->validate_custom_css( $request['styles']['css'] ); 259 if ( is_wp_error( $css_validation_result ) ) { 260 return $css_validation_result; 261 } 262 } 263 $config['styles'] = $request['styles']; 264 } elseif ( isset( $existing_config['styles'] ) ) { 265 $config['styles'] = $existing_config['styles']; 266 } 267 268 // Register theme-defined variations e.g. from block style variation partials under `/styles`. 269 $variations = WP_Theme_JSON_Resolver::get_style_variations( 'block' ); 270 wp_register_block_style_variations_from_theme_json_partials( $variations ); 271 272 if ( isset( $request['settings'] ) ) { 273 $config['settings'] = $request['settings']; 274 } elseif ( isset( $existing_config['settings'] ) ) { 275 $config['settings'] = $existing_config['settings']; 276 } 277 $config['isGlobalStylesUserThemeJSON'] = true; 278 $config['version'] = WP_Theme_JSON::LATEST_SCHEMA; 279 $changes->post_content = wp_json_encode( $config ); 280 } 281 282 // Post title. 283 if ( isset( $request['title'] ) ) { 284 if ( is_string( $request['title'] ) ) { 285 $changes->post_title = $request['title']; 286 } elseif ( ! empty( $request['title']['raw'] ) ) { 287 $changes->post_title = $request['title']['raw']; 288 } 289 } 290 291 return $changes; 292 } 293 294 /** 295 * Prepare a global styles config output for response. 296 * 297 * @since 5.9.0 298 * @since 6.6.0 Added custom relative theme file URIs to `_links`. 299 * 300 * @param WP_Post $post Global Styles post object. 301 * @param WP_REST_Request $request Request object. 302 * @return WP_REST_Response Response object. 303 */ 304 public function prepare_item_for_response( $post, $request ) { 305 $raw_config = json_decode( $post->post_content, true ); 306 $is_global_styles_user_theme_json = isset( $raw_config['isGlobalStylesUserThemeJSON'] ) && true === $raw_config['isGlobalStylesUserThemeJSON']; 307 $config = array(); 308 $theme_json = null; 309 if ( $is_global_styles_user_theme_json ) { 310 $theme_json = new WP_Theme_JSON( $raw_config, 'custom' ); 311 $config = $theme_json->get_raw_data(); 312 } 313 314 // Base fields for every post. 315 $fields = $this->get_fields_for_response( $request ); 316 $data = array(); 317 318 if ( rest_is_field_included( 'id', $fields ) ) { 319 $data['id'] = $post->ID; 320 } 321 322 if ( rest_is_field_included( 'title', $fields ) ) { 323 $data['title'] = array(); 324 } 325 if ( rest_is_field_included( 'title.raw', $fields ) ) { 326 $data['title']['raw'] = $post->post_title; 327 } 328 if ( rest_is_field_included( 'title.rendered', $fields ) ) { 329 add_filter( 'protected_title_format', array( $this, 'protected_title_format' ) ); 330 add_filter( 'private_title_format', array( $this, 'protected_title_format' ) ); 331 332 $data['title']['rendered'] = get_the_title( $post->ID ); 333 334 remove_filter( 'protected_title_format', array( $this, 'protected_title_format' ) ); 335 remove_filter( 'private_title_format', array( $this, 'protected_title_format' ) ); 336 } 337 338 if ( rest_is_field_included( 'settings', $fields ) ) { 339 $data['settings'] = ! empty( $config['settings'] ) && $is_global_styles_user_theme_json ? $config['settings'] : new stdClass(); 340 } 341 342 if ( rest_is_field_included( 'styles', $fields ) ) { 343 $data['styles'] = ! empty( $config['styles'] ) && $is_global_styles_user_theme_json ? $config['styles'] : new stdClass(); 344 } 345 346 $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; 347 $data = $this->add_additional_fields_to_object( $data, $request ); 348 $data = $this->filter_response_by_context( $data, $context ); 349 350 // Wrap the data in a response object. 351 $response = rest_ensure_response( $data ); 352 353 if ( rest_is_field_included( '_links', $fields ) || rest_is_field_included( '_embedded', $fields ) ) { 354 $links = $this->prepare_links( $post->ID ); 355 356 // Only return resolved URIs for get requests to user theme JSON. 357 if ( $theme_json ) { 358 $resolved_theme_uris = WP_Theme_JSON_Resolver::get_resolved_theme_uris( $theme_json ); 359 if ( ! empty( $resolved_theme_uris ) ) { 360 $links['https://api.w.org/theme-file'] = $resolved_theme_uris; 361 } 362 } 363 364 $response->add_links( $links ); 365 if ( ! empty( $links['self']['href'] ) ) { 366 $actions = $this->get_available_actions( $post, $request ); 367 $self = $links['self']['href']; 368 foreach ( $actions as $rel ) { 369 $response->add_link( $rel, $self ); 370 } 371 } 372 } 373 374 return $response; 375 } 376 377 /** 378 * Prepares links for the request. 379 * 380 * @since 5.9.0 381 * @since 6.3.0 Adds revisions count and rest URL href to version-history. 382 * 383 * @param integer $id ID. 384 * @return array Links for the given post. 385 */ 386 protected function prepare_links( $id ) { 387 $base = sprintf( '%s/%s', $this->namespace, $this->rest_base ); 388 389 $links = array( 390 'self' => array( 391 'href' => rest_url( trailingslashit( $base ) . $id ), 392 ), 393 'about' => array( 394 'href' => rest_url( 'wp/v2/types/' . $this->post_type ), 395 ), 396 ); 397 398 if ( post_type_supports( $this->post_type, 'revisions' ) ) { 399 $revisions = wp_get_latest_revision_id_and_total_count( $id ); 400 $revisions_count = ! is_wp_error( $revisions ) ? $revisions['count'] : 0; 401 $revisions_base = sprintf( '/%s/%d/revisions', $base, $id ); 402 $links['version-history'] = array( 403 'href' => rest_url( $revisions_base ), 404 'count' => $revisions_count, 405 ); 406 } 407 408 return $links; 409 } 410 411 /** 412 * Get the link relations available for the post and current user. 413 * 414 * @since 5.9.0 415 * @since 6.2.0 Added 'edit-css' action. 416 * @since 6.6.0 Added $post and $request parameters. 417 * 418 * @param WP_Post $post Post object. 419 * @param WP_REST_Request $request Request object. 420 * @return array List of link relations. 421 */ 422 protected function get_available_actions( $post, $request ) { 423 $rels = array(); 424 425 $post_type = get_post_type_object( $post->post_type ); 426 if ( current_user_can( $post_type->cap->publish_posts ) ) { 427 $rels[] = 'https://api.w.org/action-publish'; 428 } 429 430 if ( current_user_can( 'edit_css' ) ) { 431 $rels[] = 'https://api.w.org/action-edit-css'; 432 } 433 434 return $rels; 435 } 436 437 /** 438 * Retrieves the query params for the global styles collection. 439 * 440 * @since 5.9.0 441 * 442 * @return array Collection parameters. 443 */ 444 public function get_collection_params() { 445 return array(); 446 } 447 448 /** 449 * Retrieves the global styles type' schema, conforming to JSON Schema. 450 * 451 * @since 5.9.0 452 * 453 * @return array Item schema data. 454 */ 455 public function get_item_schema() { 456 if ( $this->schema ) { 457 return $this->add_additional_fields_schema( $this->schema ); 458 } 459 460 $schema = array( 461 '$schema' => 'http://json-schema.org/draft-04/schema#', 462 'title' => $this->post_type, 463 'type' => 'object', 464 'properties' => array( 465 'id' => array( 466 'description' => __( 'ID of global styles config.' ), 467 'type' => 'string', 468 'context' => array( 'embed', 'view', 'edit' ), 469 'readonly' => true, 470 ), 471 'styles' => array( 472 'description' => __( 'Global styles.' ), 473 'type' => array( 'object' ), 474 'context' => array( 'view', 'edit' ), 475 ), 476 'settings' => array( 477 'description' => __( 'Global settings.' ), 478 'type' => array( 'object' ), 479 'context' => array( 'view', 'edit' ), 480 ), 481 'title' => array( 482 'description' => __( 'Title of the global styles variation.' ), 483 'type' => array( 'object', 'string' ), 484 'default' => '', 485 'context' => array( 'embed', 'view', 'edit' ), 486 'properties' => array( 487 'raw' => array( 488 'description' => __( 'Title for the global styles variation, as it exists in the database.' ), 489 'type' => 'string', 490 'context' => array( 'view', 'edit', 'embed' ), 491 ), 492 'rendered' => array( 493 'description' => __( 'HTML title for the post, transformed for display.' ), 494 'type' => 'string', 495 'context' => array( 'view', 'edit', 'embed' ), 496 'readonly' => true, 497 ), 498 ), 499 ), 500 ), 501 ); 502 503 $this->schema = $schema; 504 505 return $this->add_additional_fields_schema( $this->schema ); 506 } 507 508 /** 509 * Checks if a given request has access to read a single theme global styles config. 510 * 511 * @since 5.9.0 512 * @since 6.7.0 Allow users with edit post capabilities to view theme global styles. 513 * 514 * @param WP_REST_Request $request Full details about the request. 515 * @return true|WP_Error True if the request has read access for the item, WP_Error object otherwise. 516 */ 517 public function get_theme_item_permissions_check( $request ) { 518 /* 519 * Verify if the current user has edit_posts capability. 520 * This capability is required to view global styles. 521 */ 522 if ( current_user_can( 'edit_posts' ) ) { 523 return true; 524 } 525 526 foreach ( get_post_types( array( 'show_in_rest' => true ), 'objects' ) as $post_type ) { 527 if ( current_user_can( $post_type->cap->edit_posts ) ) { 528 return true; 529 } 530 } 531 532 /* 533 * Verify if the current user has edit_theme_options capability. 534 */ 535 if ( current_user_can( 'edit_theme_options' ) ) { 536 return true; 537 } 538 539 return new WP_Error( 540 'rest_cannot_read_global_styles', 541 __( 'Sorry, you are not allowed to access the global styles on this site.' ), 542 array( 543 'status' => rest_authorization_required_code(), 544 ) 545 ); 546 } 547 548 /** 549 * Returns the given theme global styles config. 550 * 551 * @since 5.9.0 552 * @since 6.6.0 Added custom relative theme file URIs to `_links`. 553 * 554 * @param WP_REST_Request $request The request instance. 555 * @return WP_REST_Response|WP_Error 556 */ 557 public function get_theme_item( $request ) { 558 if ( get_stylesheet() !== $request['stylesheet'] ) { 559 // This endpoint only supports the active theme for now. 560 return new WP_Error( 561 'rest_theme_not_found', 562 __( 'Theme not found.' ), 563 array( 'status' => 404 ) 564 ); 565 } 566 567 $theme = WP_Theme_JSON_Resolver::get_merged_data( 'theme' ); 568 $fields = $this->get_fields_for_response( $request ); 569 $data = array(); 570 571 if ( rest_is_field_included( 'settings', $fields ) ) { 572 $data['settings'] = $theme->get_settings(); 573 } 574 575 if ( rest_is_field_included( 'styles', $fields ) ) { 576 $raw_data = $theme->get_raw_data(); 577 $data['styles'] = isset( $raw_data['styles'] ) ? $raw_data['styles'] : array(); 578 } 579 580 $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; 581 $data = $this->add_additional_fields_to_object( $data, $request ); 582 $data = $this->filter_response_by_context( $data, $context ); 583 584 $response = rest_ensure_response( $data ); 585 586 if ( rest_is_field_included( '_links', $fields ) || rest_is_field_included( '_embedded', $fields ) ) { 587 $links = array( 588 'self' => array( 589 'href' => rest_url( sprintf( '%s/%s/themes/%s', $this->namespace, $this->rest_base, $request['stylesheet'] ) ), 590 ), 591 ); 592 $resolved_theme_uris = WP_Theme_JSON_Resolver::get_resolved_theme_uris( $theme ); 593 if ( ! empty( $resolved_theme_uris ) ) { 594 $links['https://api.w.org/theme-file'] = $resolved_theme_uris; 595 } 596 $response->add_links( $links ); 597 } 598 599 return $response; 600 } 601 602 /** 603 * Checks if a given request has access to read a single theme global styles config. 604 * 605 * @since 6.0.0 606 * @since 6.7.0 Allow users with edit post capabilities to view theme global styles. 607 * 608 * @param WP_REST_Request $request Full details about the request. 609 * @return true|WP_Error True if the request has read access for the item, WP_Error object otherwise. 610 */ 611 public function get_theme_items_permissions_check( $request ) { 612 return $this->get_theme_item_permissions_check( $request ); 613 } 614 615 /** 616 * Returns the given theme global styles variations. 617 * 618 * @since 6.0.0 619 * @since 6.2.0 Returns parent theme variations, if they exist. 620 * @since 6.6.0 Added custom relative theme file URIs to `_links` for each item. 621 * 622 * @param WP_REST_Request $request The request instance. 623 * 624 * @return WP_REST_Response|WP_Error 625 */ 626 public function get_theme_items( $request ) { 627 if ( get_stylesheet() !== $request['stylesheet'] ) { 628 // This endpoint only supports the active theme for now. 629 return new WP_Error( 630 'rest_theme_not_found', 631 __( 'Theme not found.' ), 632 array( 'status' => 404 ) 633 ); 634 } 635 636 $response = array(); 637 638 // Register theme-defined variations e.g. from block style variation partials under `/styles`. 639 $partials = WP_Theme_JSON_Resolver::get_style_variations( 'block' ); 640 wp_register_block_style_variations_from_theme_json_partials( $partials ); 641 642 $variations = WP_Theme_JSON_Resolver::get_style_variations(); 643 foreach ( $variations as $variation ) { 644 $variation_theme_json = new WP_Theme_JSON( $variation ); 645 $resolved_theme_uris = WP_Theme_JSON_Resolver::get_resolved_theme_uris( $variation_theme_json ); 646 $data = rest_ensure_response( $variation ); 647 if ( ! empty( $resolved_theme_uris ) ) { 648 $data->add_links( 649 array( 650 'https://api.w.org/theme-file' => $resolved_theme_uris, 651 ) 652 ); 653 } 654 $response[] = $this->prepare_response_for_collection( $data ); 655 } 656 657 return rest_ensure_response( $response ); 658 } 659 660 /** 661 * Validate style.css as valid CSS. 662 * 663 * Currently just checks for invalid markup. 664 * 665 * @since 6.2.0 666 * @since 6.4.0 Changed method visibility to protected. 667 * 668 * @param string $css CSS to validate. 669 * @return true|WP_Error True if the input was validated, otherwise WP_Error. 670 */ 671 protected function validate_custom_css( $css ) { 672 if ( preg_match( '#</?\w+#', $css ) ) { 673 return new WP_Error( 674 'rest_custom_css_illegal_markup', 675 __( 'Markup is not allowed in CSS.' ), 676 array( 'status' => 400 ) 677 ); 678 } 679 return true; 680 } 681 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated : Thu Nov 21 08:20:01 2024 | Cross-referenced by PHPXref |