[ Index ] |
PHP Cross Reference of WordPress |
[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 * @return void 39 */ 40 public function register_routes() { 41 register_rest_route( 42 $this->namespace, 43 '/' . $this->rest_base . '/themes/(?P<stylesheet>[\/\s%\w\.\(\)\[\]\@_\-]+)/variations', 44 array( 45 array( 46 'methods' => WP_REST_Server::READABLE, 47 'callback' => array( $this, 'get_theme_items' ), 48 'permission_callback' => array( $this, 'get_theme_items_permissions_check' ), 49 'args' => array( 50 'stylesheet' => array( 51 'description' => __( 'The theme identifier' ), 52 'type' => 'string', 53 ), 54 ), 55 ), 56 ) 57 ); 58 59 // List themes global styles. 60 register_rest_route( 61 $this->namespace, 62 // The route. 63 sprintf( 64 '/%s/themes/(?P<stylesheet>%s)', 65 $this->rest_base, 66 // Matches theme's directory: `/themes/<subdirectory>/<theme>/` or `/themes/<theme>/`. 67 // Excludes invalid directory name characters: `/:<>*?"|`. 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 * Checks if a given request has access to read a single global style. 130 * 131 * @since 5.9.0 132 * 133 * @param WP_REST_Request $request Full details about the request. 134 * @return true|WP_Error True if the request has read access, WP_Error object otherwise. 135 */ 136 public function get_item_permissions_check( $request ) { 137 $post = $this->get_post( $request['id'] ); 138 if ( is_wp_error( $post ) ) { 139 return $post; 140 } 141 142 if ( 'edit' === $request['context'] && $post && ! $this->check_update_permission( $post ) ) { 143 return new WP_Error( 144 'rest_forbidden_context', 145 __( 'Sorry, you are not allowed to edit this global style.' ), 146 array( 'status' => rest_authorization_required_code() ) 147 ); 148 } 149 150 if ( ! $this->check_read_permission( $post ) ) { 151 return new WP_Error( 152 'rest_cannot_view', 153 __( 'Sorry, you are not allowed to view this global style.' ), 154 array( 'status' => rest_authorization_required_code() ) 155 ); 156 } 157 158 return true; 159 } 160 161 /** 162 * Checks if a global style can be read. 163 * 164 * @since 5.9.0 165 * 166 * @param WP_Post $post Post object. 167 * @return bool Whether the post can be read. 168 */ 169 protected function check_read_permission( $post ) { 170 return current_user_can( 'read_post', $post->ID ); 171 } 172 173 /** 174 * Returns the given global styles config. 175 * 176 * @since 5.9.0 177 * 178 * @param WP_REST_Request $request The request instance. 179 * 180 * @return WP_REST_Response|WP_Error 181 */ 182 public function get_item( $request ) { 183 $post = $this->get_post( $request['id'] ); 184 if ( is_wp_error( $post ) ) { 185 return $post; 186 } 187 188 return $this->prepare_item_for_response( $post, $request ); 189 } 190 191 /** 192 * Checks if a given request has access to write a single global styles config. 193 * 194 * @since 5.9.0 195 * 196 * @param WP_REST_Request $request Full details about the request. 197 * @return true|WP_Error True if the request has write access for the item, WP_Error object otherwise. 198 */ 199 public function update_item_permissions_check( $request ) { 200 $post = $this->get_post( $request['id'] ); 201 if ( is_wp_error( $post ) ) { 202 return $post; 203 } 204 205 if ( $post && ! $this->check_update_permission( $post ) ) { 206 return new WP_Error( 207 'rest_cannot_edit', 208 __( 'Sorry, you are not allowed to edit this global style.' ), 209 array( 'status' => rest_authorization_required_code() ) 210 ); 211 } 212 213 return true; 214 } 215 216 /** 217 * Checks if a global style can be edited. 218 * 219 * @since 5.9.0 220 * 221 * @param WP_Post $post Post object. 222 * @return bool Whether the post can be edited. 223 */ 224 protected function check_update_permission( $post ) { 225 return current_user_can( 'edit_post', $post->ID ); 226 } 227 228 /** 229 * Updates a single global style config. 230 * 231 * @since 5.9.0 232 * 233 * @param WP_REST_Request $request Full details about the request. 234 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. 235 */ 236 public function update_item( $request ) { 237 $post_before = $this->get_post( $request['id'] ); 238 if ( is_wp_error( $post_before ) ) { 239 return $post_before; 240 } 241 242 $changes = $this->prepare_item_for_database( $request ); 243 $result = wp_update_post( wp_slash( (array) $changes ), true, false ); 244 if ( is_wp_error( $result ) ) { 245 return $result; 246 } 247 248 $post = get_post( $request['id'] ); 249 $fields_update = $this->update_additional_fields_for_object( $post, $request ); 250 if ( is_wp_error( $fields_update ) ) { 251 return $fields_update; 252 } 253 254 wp_after_insert_post( $post, true, $post_before ); 255 256 $response = $this->prepare_item_for_response( $post, $request ); 257 258 return rest_ensure_response( $response ); 259 } 260 261 /** 262 * Prepares a single global styles config for update. 263 * 264 * @since 5.9.0 265 * 266 * @param WP_REST_Request $request Request object. 267 * @return stdClass Changes to pass to wp_update_post. 268 */ 269 protected function prepare_item_for_database( $request ) { 270 $changes = new stdClass(); 271 $changes->ID = $request['id']; 272 273 $post = get_post( $request['id'] ); 274 $existing_config = array(); 275 if ( $post ) { 276 $existing_config = json_decode( $post->post_content, true ); 277 $json_decoding_error = json_last_error(); 278 if ( JSON_ERROR_NONE !== $json_decoding_error || ! isset( $existing_config['isGlobalStylesUserThemeJSON'] ) || 279 ! $existing_config['isGlobalStylesUserThemeJSON'] ) { 280 $existing_config = array(); 281 } 282 } 283 284 if ( isset( $request['styles'] ) || isset( $request['settings'] ) ) { 285 $config = array(); 286 if ( isset( $request['styles'] ) ) { 287 $config['styles'] = $request['styles']; 288 } elseif ( isset( $existing_config['styles'] ) ) { 289 $config['styles'] = $existing_config['styles']; 290 } 291 if ( isset( $request['settings'] ) ) { 292 $config['settings'] = $request['settings']; 293 } elseif ( isset( $existing_config['settings'] ) ) { 294 $config['settings'] = $existing_config['settings']; 295 } 296 $config['isGlobalStylesUserThemeJSON'] = true; 297 $config['version'] = WP_Theme_JSON::LATEST_SCHEMA; 298 $changes->post_content = wp_json_encode( $config ); 299 } 300 301 // Post title. 302 if ( isset( $request['title'] ) ) { 303 if ( is_string( $request['title'] ) ) { 304 $changes->post_title = $request['title']; 305 } elseif ( ! empty( $request['title']['raw'] ) ) { 306 $changes->post_title = $request['title']['raw']; 307 } 308 } 309 310 return $changes; 311 } 312 313 /** 314 * Prepare a global styles config output for response. 315 * 316 * @since 5.9.0 317 * 318 * @param WP_Post $post Global Styles post object. 319 * @param WP_REST_Request $request Request object. 320 * @return WP_REST_Response Response object. 321 */ 322 public function prepare_item_for_response( $post, $request ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable 323 $raw_config = json_decode( $post->post_content, true ); 324 $is_global_styles_user_theme_json = isset( $raw_config['isGlobalStylesUserThemeJSON'] ) && true === $raw_config['isGlobalStylesUserThemeJSON']; 325 $config = array(); 326 if ( $is_global_styles_user_theme_json ) { 327 $config = ( new WP_Theme_JSON( $raw_config, 'custom' ) )->get_raw_data(); 328 } 329 330 // Base fields for every post. 331 $data = array(); 332 $fields = $this->get_fields_for_response( $request ); 333 334 if ( rest_is_field_included( 'id', $fields ) ) { 335 $data['id'] = $post->ID; 336 } 337 338 if ( rest_is_field_included( 'title', $fields ) ) { 339 $data['title'] = array(); 340 } 341 if ( rest_is_field_included( 'title.raw', $fields ) ) { 342 $data['title']['raw'] = $post->post_title; 343 } 344 if ( rest_is_field_included( 'title.rendered', $fields ) ) { 345 add_filter( 'protected_title_format', array( $this, 'protected_title_format' ) ); 346 347 $data['title']['rendered'] = get_the_title( $post->ID ); 348 349 remove_filter( 'protected_title_format', array( $this, 'protected_title_format' ) ); 350 } 351 352 if ( rest_is_field_included( 'settings', $fields ) ) { 353 $data['settings'] = ! empty( $config['settings'] ) && $is_global_styles_user_theme_json ? $config['settings'] : new stdClass(); 354 } 355 356 if ( rest_is_field_included( 'styles', $fields ) ) { 357 $data['styles'] = ! empty( $config['styles'] ) && $is_global_styles_user_theme_json ? $config['styles'] : new stdClass(); 358 } 359 360 $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; 361 $data = $this->add_additional_fields_to_object( $data, $request ); 362 $data = $this->filter_response_by_context( $data, $context ); 363 364 // Wrap the data in a response object. 365 $response = rest_ensure_response( $data ); 366 367 $links = $this->prepare_links( $post->ID ); 368 $response->add_links( $links ); 369 if ( ! empty( $links['self']['href'] ) ) { 370 $actions = $this->get_available_actions(); 371 $self = $links['self']['href']; 372 foreach ( $actions as $rel ) { 373 $response->add_link( $rel, $self ); 374 } 375 } 376 377 return $response; 378 } 379 380 /** 381 * Get the post, if the ID is valid. 382 * 383 * @since 5.9.0 384 * 385 * @param int $id Supplied ID. 386 * @return WP_Post|WP_Error Post object if ID is valid, WP_Error otherwise. 387 */ 388 protected function get_post( $id ) { 389 $error = new WP_Error( 390 'rest_global_styles_not_found', 391 __( 'No global styles config exist with that id.' ), 392 array( 'status' => 404 ) 393 ); 394 395 $id = (int) $id; 396 if ( $id <= 0 ) { 397 return $error; 398 } 399 400 $post = get_post( $id ); 401 if ( empty( $post ) || empty( $post->ID ) || $this->post_type !== $post->post_type ) { 402 return $error; 403 } 404 405 return $post; 406 } 407 408 409 /** 410 * Prepares links for the request. 411 * 412 * @since 5.9.0 413 * 414 * @param integer $id ID. 415 * @return array Links for the given post. 416 */ 417 protected function prepare_links( $id ) { 418 $base = sprintf( '%s/%s', $this->namespace, $this->rest_base ); 419 420 $links = array( 421 'self' => array( 422 'href' => rest_url( trailingslashit( $base ) . $id ), 423 ), 424 ); 425 426 return $links; 427 } 428 429 /** 430 * Get the link relations available for the post and current user. 431 * 432 * @since 5.9.0 433 * 434 * @return array List of link relations. 435 */ 436 protected function get_available_actions() { 437 $rels = array(); 438 439 $post_type = get_post_type_object( $this->post_type ); 440 if ( current_user_can( $post_type->cap->publish_posts ) ) { 441 $rels[] = 'https://api.w.org/action-publish'; 442 } 443 444 return $rels; 445 } 446 447 /** 448 * Overwrites the default protected title format. 449 * 450 * By default, WordPress will show password protected posts with a title of 451 * "Protected: %s", as the REST API communicates the protected status of a post 452 * in a machine readable format, we remove the "Protected: " prefix. 453 * 454 * @since 5.9.0 455 * 456 * @return string Protected title format. 457 */ 458 public function protected_title_format() { 459 return '%s'; 460 } 461 462 /** 463 * Retrieves the query params for the global styles collection. 464 * 465 * @since 5.9.0 466 * 467 * @return array Collection parameters. 468 */ 469 public function get_collection_params() { 470 return array(); 471 } 472 473 /** 474 * Retrieves the global styles type' schema, conforming to JSON Schema. 475 * 476 * @since 5.9.0 477 * 478 * @return array Item schema data. 479 */ 480 public function get_item_schema() { 481 if ( $this->schema ) { 482 return $this->add_additional_fields_schema( $this->schema ); 483 } 484 485 $schema = array( 486 '$schema' => 'http://json-schema.org/draft-04/schema#', 487 'title' => $this->post_type, 488 'type' => 'object', 489 'properties' => array( 490 'id' => array( 491 'description' => __( 'ID of global styles config.' ), 492 'type' => 'string', 493 'context' => array( 'embed', 'view', 'edit' ), 494 'readonly' => true, 495 ), 496 'styles' => array( 497 'description' => __( 'Global styles.' ), 498 'type' => array( 'object' ), 499 'context' => array( 'view', 'edit' ), 500 ), 501 'settings' => array( 502 'description' => __( 'Global settings.' ), 503 'type' => array( 'object' ), 504 'context' => array( 'view', 'edit' ), 505 ), 506 'title' => array( 507 'description' => __( 'Title of the global styles variation.' ), 508 'type' => array( 'object', 'string' ), 509 'default' => '', 510 'context' => array( 'embed', 'view', 'edit' ), 511 'properties' => array( 512 'raw' => array( 513 'description' => __( 'Title for the global styles variation, as it exists in the database.' ), 514 'type' => 'string', 515 'context' => array( 'view', 'edit', 'embed' ), 516 ), 517 'rendered' => array( 518 'description' => __( 'HTML title for the post, transformed for display.' ), 519 'type' => 'string', 520 'context' => array( 'view', 'edit', 'embed' ), 521 'readonly' => true, 522 ), 523 ), 524 ), 525 ), 526 ); 527 528 $this->schema = $schema; 529 530 return $this->add_additional_fields_schema( $this->schema ); 531 } 532 533 /** 534 * Checks if a given request has access to read a single theme global styles config. 535 * 536 * @since 5.9.0 537 * 538 * @param WP_REST_Request $request Full details about the request. 539 * @return true|WP_Error True if the request has read access for the item, WP_Error object otherwise. 540 */ 541 public function get_theme_item_permissions_check( $request ) { 542 // Verify if the current user has edit_theme_options capability. 543 // This capability is required to edit/view/delete templates. 544 if ( ! current_user_can( 'edit_theme_options' ) ) { 545 return new WP_Error( 546 'rest_cannot_manage_global_styles', 547 __( 'Sorry, you are not allowed to access the global styles on this site.' ), 548 array( 549 'status' => rest_authorization_required_code(), 550 ) 551 ); 552 } 553 554 return true; 555 } 556 557 /** 558 * Returns the given theme global styles config. 559 * 560 * @since 5.9.0 561 * 562 * @param WP_REST_Request $request The request instance. 563 * @return WP_REST_Response|WP_Error 564 */ 565 public function get_theme_item( $request ) { 566 if ( wp_get_theme()->get_stylesheet() !== $request['stylesheet'] ) { 567 // This endpoint only supports the active theme for now. 568 return new WP_Error( 569 'rest_theme_not_found', 570 __( 'Theme not found.' ), 571 array( 'status' => 404 ) 572 ); 573 } 574 575 $theme = WP_Theme_JSON_Resolver::get_merged_data( 'theme' ); 576 $data = array(); 577 $fields = $this->get_fields_for_response( $request ); 578 579 if ( rest_is_field_included( 'settings', $fields ) ) { 580 $data['settings'] = $theme->get_settings(); 581 } 582 583 if ( rest_is_field_included( 'styles', $fields ) ) { 584 $raw_data = $theme->get_raw_data(); 585 $data['styles'] = isset( $raw_data['styles'] ) ? $raw_data['styles'] : array(); 586 } 587 588 $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; 589 $data = $this->add_additional_fields_to_object( $data, $request ); 590 $data = $this->filter_response_by_context( $data, $context ); 591 592 $response = rest_ensure_response( $data ); 593 594 $links = array( 595 'self' => array( 596 'href' => rest_url( sprintf( '%s/%s/themes/%s', $this->namespace, $this->rest_base, $request['stylesheet'] ) ), 597 ), 598 ); 599 600 $response->add_links( $links ); 601 602 return $response; 603 } 604 605 /** 606 * Checks if a given request has access to read a single theme global styles config. 607 * 608 * @since 6.0.0 609 * 610 * @param WP_REST_Request $request Full details about the request. 611 * @return true|WP_Error True if the request has read access for the item, WP_Error object otherwise. 612 */ 613 public function get_theme_items_permissions_check( $request ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable 614 // Verify if the current user has edit_theme_options capability. 615 // This capability is required to edit/view/delete templates. 616 if ( ! current_user_can( 'edit_theme_options' ) ) { 617 return new WP_Error( 618 'rest_cannot_manage_global_styles', 619 __( 'Sorry, you are not allowed to access the global styles on this site.' ), 620 array( 621 'status' => rest_authorization_required_code(), 622 ) 623 ); 624 } 625 626 return true; 627 } 628 629 /** 630 * Returns the given theme global styles variations. 631 * 632 * @since 6.0.0 633 * 634 * @param WP_REST_Request $request The request instance. 635 * 636 * @return WP_REST_Response|WP_Error 637 */ 638 public function get_theme_items( $request ) { 639 if ( wp_get_theme()->get_stylesheet() !== $request['stylesheet'] ) { 640 // This endpoint only supports the active theme for now. 641 return new WP_Error( 642 'rest_theme_not_found', 643 __( 'Theme not found.' ), 644 array( 'status' => 404 ) 645 ); 646 } 647 648 $variations = WP_Theme_JSON_Resolver::get_style_variations(); 649 $response = rest_ensure_response( $variations ); 650 651 return $response; 652 } 653 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Tue Dec 24 01:00:02 2024 | Cross-referenced by PHPXref 0.7.1 |