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