| [ Index ] |
PHP Cross Reference of WordPress Trunk (Updated Daily) |
[Summary view] [Print] [Text view]
1 <?php 2 /** 3 * REST API: WP_REST_Templates_Controller class 4 * 5 * @package WordPress 6 * @subpackage REST_API 7 * @since 5.8.0 8 */ 9 10 /** 11 * Base Templates REST API Controller. 12 * 13 * @since 5.8.0 14 * 15 * @see WP_REST_Controller 16 */ 17 class WP_REST_Templates_Controller extends WP_REST_Controller { 18 19 /** 20 * Post type. 21 * 22 * @since 5.8.0 23 * @var string 24 */ 25 protected $post_type; 26 27 /** 28 * Constructor. 29 * 30 * @since 5.8.0 31 * 32 * @param string $post_type Post type. 33 */ 34 public function __construct( $post_type ) { 35 $this->post_type = $post_type; 36 $obj = get_post_type_object( $post_type ); 37 $this->rest_base = ! empty( $obj->rest_base ) ? $obj->rest_base : $obj->name; 38 $this->namespace = ! empty( $obj->rest_namespace ) ? $obj->rest_namespace : 'wp/v2'; 39 } 40 41 /** 42 * Registers the controllers routes. 43 * 44 * @since 5.8.0 45 * @since 6.1.0 Endpoint for fallback template content. 46 */ 47 public function register_routes() { 48 // Lists all templates. 49 register_rest_route( 50 $this->namespace, 51 '/' . $this->rest_base, 52 array( 53 array( 54 'methods' => WP_REST_Server::READABLE, 55 'callback' => array( $this, 'get_items' ), 56 'permission_callback' => array( $this, 'get_items_permissions_check' ), 57 'args' => $this->get_collection_params(), 58 ), 59 array( 60 'methods' => WP_REST_Server::CREATABLE, 61 'callback' => array( $this, 'create_item' ), 62 'permission_callback' => array( $this, 'create_item_permissions_check' ), 63 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), 64 ), 65 'schema' => array( $this, 'get_public_item_schema' ), 66 ) 67 ); 68 69 // Get fallback template content. 70 register_rest_route( 71 $this->namespace, 72 '/' . $this->rest_base . '/lookup', 73 array( 74 array( 75 'methods' => WP_REST_Server::READABLE, 76 'callback' => array( $this, 'get_template_fallback' ), 77 'permission_callback' => array( $this, 'get_item_permissions_check' ), 78 'args' => array( 79 'slug' => array( 80 'description' => __( 'The slug of the template to get the fallback for' ), 81 'type' => 'string', 82 'required' => true, 83 ), 84 'is_custom' => array( 85 'description' => __( 'Indicates if a template is custom or part of the template hierarchy' ), 86 'type' => 'boolean', 87 ), 88 'template_prefix' => array( 89 'description' => __( 'The template prefix for the created template. This is used to extract the main template type, e.g. in `taxonomy-books` extracts the `taxonomy`' ), 90 'type' => 'string', 91 ), 92 ), 93 ), 94 ) 95 ); 96 97 // Lists/updates a single template based on the given id. 98 register_rest_route( 99 $this->namespace, 100 // The route. 101 sprintf( 102 '/%s/(?P<id>%s%s)', 103 $this->rest_base, 104 /* 105 * Matches theme's directory: `/themes/<subdirectory>/<theme>/` or `/themes/<theme>/`. 106 * Excludes invalid directory name characters: `/:<>*?"|`. 107 */ 108 '([^\/:<>\*\?"\|]+(?:\/[^\/:<>\*\?"\|]+)?)', 109 // Matches the template name. 110 '[\/\w%-]+' 111 ), 112 array( 113 'args' => array( 114 'id' => array( 115 'description' => __( 'The id of a template' ), 116 'type' => 'string', 117 'sanitize_callback' => array( $this, '_sanitize_template_id' ), 118 ), 119 ), 120 array( 121 'methods' => WP_REST_Server::READABLE, 122 'callback' => array( $this, 'get_item' ), 123 'permission_callback' => array( $this, 'get_item_permissions_check' ), 124 'args' => array( 125 'context' => $this->get_context_param( array( 'default' => 'view' ) ), 126 ), 127 ), 128 array( 129 'methods' => WP_REST_Server::EDITABLE, 130 'callback' => array( $this, 'update_item' ), 131 'permission_callback' => array( $this, 'update_item_permissions_check' ), 132 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), 133 ), 134 array( 135 'methods' => WP_REST_Server::DELETABLE, 136 'callback' => array( $this, 'delete_item' ), 137 'permission_callback' => array( $this, 'delete_item_permissions_check' ), 138 'args' => array( 139 'force' => array( 140 'type' => 'boolean', 141 'default' => false, 142 'description' => __( 'Whether to bypass Trash and force deletion.' ), 143 ), 144 ), 145 ), 146 'schema' => array( $this, 'get_public_item_schema' ), 147 ) 148 ); 149 } 150 151 /** 152 * Returns the fallback template for the given slug. 153 * 154 * @since 6.1.0 155 * @since 6.3.0 Ignore empty templates. 156 * 157 * @param WP_REST_Request $request The request instance. 158 * @return WP_REST_Response|WP_Error 159 */ 160 public function get_template_fallback( $request ) { 161 $hierarchy = get_template_hierarchy( $request['slug'], $request['is_custom'], $request['template_prefix'] ); 162 163 do { 164 $fallback_template = resolve_block_template( $request['slug'], $hierarchy, '' ); 165 array_shift( $hierarchy ); 166 } while ( ! empty( $hierarchy ) && empty( $fallback_template->content ) ); 167 168 // To maintain original behavior, return an empty object rather than a 404 error when no template is found. 169 $response = $fallback_template ? $this->prepare_item_for_response( $fallback_template, $request ) : new stdClass(); 170 171 return rest_ensure_response( $response ); 172 } 173 174 /** 175 * Checks if the user has permissions to make the request. 176 * 177 * @since 5.8.0 178 * 179 * @param WP_REST_Request $request Full details about the request. 180 * @return true|WP_Error True if the request has read access, WP_Error object otherwise. 181 */ 182 protected function permissions_check( $request ) { 183 /* 184 * Verify if the current user has edit_theme_options capability. 185 * This capability is required to edit/view/delete templates. 186 */ 187 if ( ! current_user_can( 'edit_theme_options' ) ) { 188 return new WP_Error( 189 'rest_cannot_manage_templates', 190 __( 'Sorry, you are not allowed to access the templates on this site.' ), 191 array( 192 'status' => rest_authorization_required_code(), 193 ) 194 ); 195 } 196 197 return true; 198 } 199 200 /** 201 * Requesting this endpoint for a template like 'twentytwentytwo//home' 202 * requires using a path like /wp/v2/templates/twentytwentytwo//home. There 203 * are special cases when WordPress routing corrects the name to contain 204 * only a single slash like 'twentytwentytwo/home'. 205 * 206 * This method doubles the last slash if it's not already doubled. It relies 207 * on the template ID format {theme_name}//{template_slug} and the fact that 208 * slugs cannot contain slashes. 209 * 210 * @since 5.9.0 211 * @link https://core.trac.wordpress.org/ticket/54507 212 * 213 * @param string $id Template ID. 214 * @return string Sanitized template ID. 215 */ 216 public function _sanitize_template_id( $id ) { 217 $id = urldecode( $id ); 218 219 $last_slash_pos = strrpos( $id, '/' ); 220 if ( false === $last_slash_pos ) { 221 return $id; 222 } 223 224 $is_double_slashed = substr( $id, $last_slash_pos - 1, 1 ) === '/'; 225 if ( $is_double_slashed ) { 226 return $id; 227 } 228 return ( 229 substr( $id, 0, $last_slash_pos ) 230 . '/' 231 . substr( $id, $last_slash_pos ) 232 ); 233 } 234 235 /** 236 * Checks if a given request has access to read templates. 237 * 238 * @since 5.8.0 239 * @since 6.6.0 Allow users with edit_posts capability to read templates. 240 * 241 * @param WP_REST_Request $request Full details about the request. 242 * @return true|WP_Error True if the request has read access, WP_Error object otherwise. 243 */ 244 public function get_items_permissions_check( $request ) { 245 if ( current_user_can( 'edit_posts' ) ) { 246 return true; 247 } 248 foreach ( get_post_types( array( 'show_in_rest' => true ), 'objects' ) as $post_type ) { 249 if ( current_user_can( $post_type->cap->edit_posts ) ) { 250 return true; 251 } 252 } 253 254 return new WP_Error( 255 'rest_cannot_manage_templates', 256 __( 'Sorry, you are not allowed to access the templates on this site.' ), 257 array( 258 'status' => rest_authorization_required_code(), 259 ) 260 ); 261 } 262 263 /** 264 * Returns a list of templates. 265 * 266 * @since 5.8.0 267 * 268 * @param WP_REST_Request $request The request instance. 269 * @return WP_REST_Response 270 */ 271 public function get_items( $request ) { 272 if ( $request->is_method( 'HEAD' ) ) { 273 // Return early as this handler doesn't add any response headers. 274 return new WP_REST_Response( array() ); 275 } 276 277 $query = array(); 278 if ( isset( $request['wp_id'] ) ) { 279 $query['wp_id'] = $request['wp_id']; 280 } 281 if ( isset( $request['area'] ) ) { 282 $query['area'] = $request['area']; 283 } 284 if ( isset( $request['post_type'] ) ) { 285 $query['post_type'] = $request['post_type']; 286 } 287 288 $templates = array(); 289 foreach ( get_block_templates( $query, $this->post_type ) as $template ) { 290 $data = $this->prepare_item_for_response( $template, $request ); 291 $templates[] = $this->prepare_response_for_collection( $data ); 292 } 293 294 return rest_ensure_response( $templates ); 295 } 296 297 /** 298 * Checks if a given request has access to read a single template. 299 * 300 * @since 5.8.0 301 * @since 6.6.0 Allow users with edit_posts capability to read individual templates. 302 * 303 * @param WP_REST_Request $request Full details about the request. 304 * @return true|WP_Error True if the request has read access for the item, WP_Error object otherwise. 305 */ 306 public function get_item_permissions_check( $request ) { 307 if ( current_user_can( 'edit_posts' ) ) { 308 return true; 309 } 310 foreach ( get_post_types( array( 'show_in_rest' => true ), 'objects' ) as $post_type ) { 311 if ( current_user_can( $post_type->cap->edit_posts ) ) { 312 return true; 313 } 314 } 315 316 return new WP_Error( 317 'rest_cannot_manage_templates', 318 __( 'Sorry, you are not allowed to access the templates on this site.' ), 319 array( 320 'status' => rest_authorization_required_code(), 321 ) 322 ); 323 } 324 325 /** 326 * Returns the given template 327 * 328 * @since 5.8.0 329 * 330 * @param WP_REST_Request $request The request instance. 331 * @return WP_REST_Response|WP_Error 332 */ 333 public function get_item( $request ) { 334 if ( isset( $request['source'] ) && ( 'theme' === $request['source'] || 'plugin' === $request['source'] ) ) { 335 $template = get_block_file_template( $request['id'], $this->post_type ); 336 } else { 337 $template = get_block_template( $request['id'], $this->post_type ); 338 } 339 340 if ( ! $template ) { 341 return new WP_Error( 'rest_template_not_found', __( 'No templates exist with that id.' ), array( 'status' => 404 ) ); 342 } 343 344 return $this->prepare_item_for_response( $template, $request ); 345 } 346 347 /** 348 * Checks if a given request has access to write a single template. 349 * 350 * @since 5.8.0 351 * 352 * @param WP_REST_Request $request Full details about the request. 353 * @return true|WP_Error True if the request has write access for the item, WP_Error object otherwise. 354 */ 355 public function update_item_permissions_check( $request ) { 356 return $this->permissions_check( $request ); 357 } 358 359 /** 360 * Updates a single template. 361 * 362 * @since 5.8.0 363 * 364 * @param WP_REST_Request $request Full details about the request. 365 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. 366 */ 367 public function update_item( $request ) { 368 $template = get_block_template( $request['id'], $this->post_type ); 369 if ( ! $template ) { 370 return new WP_Error( 'rest_template_not_found', __( 'No templates exist with that id.' ), array( 'status' => 404 ) ); 371 } 372 373 $post_before = get_post( $template->wp_id ); 374 375 if ( isset( $request['source'] ) && 'theme' === $request['source'] ) { 376 wp_delete_post( $template->wp_id, true ); 377 $request->set_param( 'context', 'edit' ); 378 379 $template = get_block_template( $request['id'], $this->post_type ); 380 $response = $this->prepare_item_for_response( $template, $request ); 381 382 return rest_ensure_response( $response ); 383 } 384 385 $changes = $this->prepare_item_for_database( $request ); 386 387 if ( is_wp_error( $changes ) ) { 388 return $changes; 389 } 390 391 if ( 'custom' === $template->source ) { 392 $update = true; 393 $result = wp_update_post( wp_slash( (array) $changes ), false ); 394 } else { 395 $update = false; 396 $post_before = null; 397 $result = wp_insert_post( wp_slash( (array) $changes ), false ); 398 } 399 400 if ( is_wp_error( $result ) ) { 401 if ( 'db_update_error' === $result->get_error_code() ) { 402 $result->add_data( array( 'status' => 500 ) ); 403 } else { 404 $result->add_data( array( 'status' => 400 ) ); 405 } 406 return $result; 407 } 408 409 $template = get_block_template( $request['id'], $this->post_type ); 410 $fields_update = $this->update_additional_fields_for_object( $template, $request ); 411 if ( is_wp_error( $fields_update ) ) { 412 return $fields_update; 413 } 414 415 $request->set_param( 'context', 'edit' ); 416 417 $post = get_post( $template->wp_id ); 418 /** This action is documented in wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php */ 419 do_action( "rest_after_insert_{$this->post_type}", $post, $request, false ); 420 421 wp_after_insert_post( $post, $update, $post_before ); 422 423 $response = $this->prepare_item_for_response( $template, $request ); 424 425 return rest_ensure_response( $response ); 426 } 427 428 /** 429 * Checks if a given request has access to create a template. 430 * 431 * @since 5.8.0 432 * 433 * @param WP_REST_Request $request Full details about the request. 434 * @return true|WP_Error True if the request has access to create items, WP_Error object otherwise. 435 */ 436 public function create_item_permissions_check( $request ) { 437 return $this->permissions_check( $request ); 438 } 439 440 /** 441 * Creates a single template. 442 * 443 * @since 5.8.0 444 * 445 * @param WP_REST_Request $request Full details about the request. 446 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. 447 */ 448 public function create_item( $request ) { 449 $prepared_post = $this->prepare_item_for_database( $request ); 450 451 if ( is_wp_error( $prepared_post ) ) { 452 return $prepared_post; 453 } 454 455 $prepared_post->post_name = $request['slug']; 456 $post_id = wp_insert_post( wp_slash( (array) $prepared_post ), true ); 457 if ( is_wp_error( $post_id ) ) { 458 if ( 'db_insert_error' === $post_id->get_error_code() ) { 459 $post_id->add_data( array( 'status' => 500 ) ); 460 } else { 461 $post_id->add_data( array( 'status' => 400 ) ); 462 } 463 464 return $post_id; 465 } 466 $posts = get_block_templates( array( 'wp_id' => $post_id ), $this->post_type ); 467 if ( ! count( $posts ) ) { 468 return new WP_Error( 'rest_template_insert_error', __( 'No templates exist with that id.' ), array( 'status' => 400 ) ); 469 } 470 $id = $posts[0]->id; 471 $post = get_post( $post_id ); 472 $template = get_block_template( $id, $this->post_type ); 473 $fields_update = $this->update_additional_fields_for_object( $template, $request ); 474 if ( is_wp_error( $fields_update ) ) { 475 return $fields_update; 476 } 477 478 /** This action is documented in wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php */ 479 do_action( "rest_after_insert_{$this->post_type}", $post, $request, true ); 480 481 wp_after_insert_post( $post, false, null ); 482 483 $response = $this->prepare_item_for_response( $template, $request ); 484 $response = rest_ensure_response( $response ); 485 486 $response->set_status( 201 ); 487 $response->header( 'Location', rest_url( sprintf( '%s/%s/%s', $this->namespace, $this->rest_base, $template->id ) ) ); 488 489 return $response; 490 } 491 492 /** 493 * Checks if a given request has access to delete a single template. 494 * 495 * @since 5.8.0 496 * 497 * @param WP_REST_Request $request Full details about the request. 498 * @return true|WP_Error True if the request has delete access for the item, WP_Error object otherwise. 499 */ 500 public function delete_item_permissions_check( $request ) { 501 return $this->permissions_check( $request ); 502 } 503 504 /** 505 * Deletes a single template. 506 * 507 * @since 5.8.0 508 * 509 * @param WP_REST_Request $request Full details about the request. 510 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. 511 */ 512 public function delete_item( $request ) { 513 $template = get_block_template( $request['id'], $this->post_type ); 514 if ( ! $template ) { 515 return new WP_Error( 'rest_template_not_found', __( 'No templates exist with that id.' ), array( 'status' => 404 ) ); 516 } 517 if ( 'custom' !== $template->source ) { 518 return new WP_Error( 'rest_invalid_template', __( 'Templates based on theme files can\'t be removed.' ), array( 'status' => 400 ) ); 519 } 520 521 $id = $template->wp_id; 522 $force = (bool) $request['force']; 523 524 $request->set_param( 'context', 'edit' ); 525 526 // If we're forcing, then delete permanently. 527 if ( $force ) { 528 $previous = $this->prepare_item_for_response( $template, $request ); 529 $result = wp_delete_post( $id, true ); 530 $response = new WP_REST_Response(); 531 $response->set_data( 532 array( 533 'deleted' => true, 534 'previous' => $previous->get_data(), 535 ) 536 ); 537 } else { 538 // Otherwise, only trash if we haven't already. 539 if ( 'trash' === $template->status ) { 540 return new WP_Error( 541 'rest_template_already_trashed', 542 __( 'The template has already been deleted.' ), 543 array( 'status' => 410 ) 544 ); 545 } 546 547 /* 548 * (Note that internally this falls through to `wp_delete_post()` 549 * if the Trash is disabled.) 550 */ 551 $result = wp_trash_post( $id ); 552 $template->status = 'trash'; 553 $response = $this->prepare_item_for_response( $template, $request ); 554 } 555 556 if ( ! $result ) { 557 return new WP_Error( 558 'rest_cannot_delete', 559 __( 'The template cannot be deleted.' ), 560 array( 'status' => 500 ) 561 ); 562 } 563 564 return $response; 565 } 566 567 /** 568 * Prepares a single template for create or update. 569 * 570 * @since 5.8.0 571 * 572 * @param WP_REST_Request $request Request object. 573 * @return stdClass|WP_Error Changes to pass to wp_update_post. 574 */ 575 protected function prepare_item_for_database( $request ) { 576 $template = $request['id'] ? get_block_template( $request['id'], $this->post_type ) : null; 577 $changes = new stdClass(); 578 if ( null === $template ) { 579 $changes->post_type = $this->post_type; 580 $changes->post_status = 'publish'; 581 $changes->tax_input = array( 582 'wp_theme' => $request['theme'] ?? get_stylesheet(), 583 ); 584 } elseif ( 'custom' !== $template->source ) { 585 $changes->post_name = $template->slug; 586 $changes->post_type = $this->post_type; 587 $changes->post_status = 'publish'; 588 $changes->tax_input = array( 589 'wp_theme' => $template->theme, 590 ); 591 $changes->meta_input = array( 592 'origin' => $template->source, 593 ); 594 } else { 595 $changes->post_name = $template->slug; 596 $changes->ID = $template->wp_id; 597 $changes->post_status = 'publish'; 598 } 599 if ( isset( $request['content'] ) ) { 600 if ( is_string( $request['content'] ) ) { 601 $changes->post_content = $request['content']; 602 } elseif ( isset( $request['content']['raw'] ) ) { 603 $changes->post_content = $request['content']['raw']; 604 } 605 } elseif ( null !== $template && 'custom' !== $template->source ) { 606 $changes->post_content = $template->content; 607 } 608 if ( isset( $request['title'] ) ) { 609 if ( is_string( $request['title'] ) ) { 610 $changes->post_title = $request['title']; 611 } elseif ( ! empty( $request['title']['raw'] ) ) { 612 $changes->post_title = $request['title']['raw']; 613 } 614 } elseif ( null !== $template && 'custom' !== $template->source ) { 615 $changes->post_title = $template->title; 616 } 617 if ( isset( $request['description'] ) ) { 618 $changes->post_excerpt = $request['description']; 619 } elseif ( null !== $template && 'custom' !== $template->source ) { 620 $changes->post_excerpt = $template->description; 621 } 622 623 if ( 'wp_template' === $this->post_type && isset( $request['is_wp_suggestion'] ) ) { 624 $changes->meta_input = wp_parse_args( 625 array( 626 'is_wp_suggestion' => $request['is_wp_suggestion'], 627 ), 628 $changes->meta_input = array() 629 ); 630 } 631 632 if ( 'wp_template_part' === $this->post_type ) { 633 if ( isset( $request['area'] ) ) { 634 $changes->tax_input['wp_template_part_area'] = _filter_block_template_part_area( $request['area'] ); 635 } elseif ( null !== $template && 'custom' !== $template->source && $template->area ) { 636 $changes->tax_input['wp_template_part_area'] = _filter_block_template_part_area( $template->area ); 637 } elseif ( empty( $template->area ) ) { 638 $changes->tax_input['wp_template_part_area'] = WP_TEMPLATE_PART_AREA_UNCATEGORIZED; 639 } 640 } 641 642 if ( ! empty( $request['author'] ) ) { 643 $post_author = (int) $request['author']; 644 645 if ( get_current_user_id() !== $post_author ) { 646 $user_obj = get_userdata( $post_author ); 647 648 if ( ! $user_obj ) { 649 return new WP_Error( 650 'rest_invalid_author', 651 __( 'Invalid author ID.' ), 652 array( 'status' => 400 ) 653 ); 654 } 655 } 656 657 $changes->post_author = $post_author; 658 } 659 660 /** This filter is documented in wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php */ 661 return apply_filters( "rest_pre_insert_{$this->post_type}", $changes, $request ); 662 } 663 664 /** 665 * Prepare a single template output for response 666 * 667 * @since 5.8.0 668 * @since 5.9.0 Renamed `$template` to `$item` to match parent class for PHP 8 named parameter support. 669 * @since 6.3.0 Added `modified` property to the response. 670 * @since 7.1.0 Added `date` property to the response. 671 * @since 7.1.0 The `modified` property is `null` for templates that have no 672 * modification date. 673 * @since 7.2.0 Returns a `WP_Error` instead of causing a fatal error 674 * when the template is `null`. 675 * 676 * @param WP_Block_Template|null $item Template instance. 677 * @param WP_REST_Request $request Request object. 678 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error when the template is `null`. 679 */ 680 public function prepare_item_for_response( $item, $request ) { 681 /* 682 * `update_item()` passes its `get_block_template()` refetches here 683 * unchecked, both after writing an update and after deleting the 684 * template's post on its revert-to-theme path. Reading `$item->content` 685 * on `null` is a fatal error, so answer with an error response instead. 686 */ 687 if ( ! $item ) { 688 return new WP_Error( 'rest_template_not_found', __( 'No templates exist with that id.' ), array( 'status' => 404 ) ); 689 } 690 691 // Don't prepare the response body for HEAD requests. 692 if ( $request->is_method( 'HEAD' ) ) { 693 return new WP_REST_Response( array() ); 694 } 695 696 /* 697 * Resolve pattern blocks so they don't need to be resolved client-side 698 * in the editor, improving performance. 699 */ 700 $blocks = parse_blocks( $item->content ); 701 $blocks = resolve_pattern_blocks( $blocks ); 702 $item->content = serialize_blocks( $blocks ); 703 704 // Restores the more descriptive, specific name for use within this method. 705 $template = $item; 706 707 $fields = $this->get_fields_for_response( $request ); 708 709 // Base fields for every template. 710 $data = array(); 711 712 if ( rest_is_field_included( 'id', $fields ) ) { 713 $data['id'] = $template->id; 714 } 715 716 if ( rest_is_field_included( 'theme', $fields ) ) { 717 $data['theme'] = $template->theme; 718 } 719 720 if ( rest_is_field_included( 'content', $fields ) ) { 721 $data['content'] = array(); 722 } 723 if ( rest_is_field_included( 'content.raw', $fields ) ) { 724 $data['content']['raw'] = $template->content; 725 } 726 727 if ( rest_is_field_included( 'content.block_version', $fields ) ) { 728 $data['content']['block_version'] = block_version( $template->content ); 729 } 730 731 if ( rest_is_field_included( 'slug', $fields ) ) { 732 $data['slug'] = $template->slug; 733 } 734 735 if ( rest_is_field_included( 'source', $fields ) ) { 736 $data['source'] = $template->source; 737 } 738 739 if ( rest_is_field_included( 'origin', $fields ) ) { 740 $data['origin'] = $template->origin; 741 } 742 743 if ( rest_is_field_included( 'type', $fields ) ) { 744 $data['type'] = $template->type; 745 } 746 747 if ( rest_is_field_included( 'description', $fields ) ) { 748 $data['description'] = $template->description; 749 } 750 751 if ( rest_is_field_included( 'title', $fields ) ) { 752 $data['title'] = array(); 753 } 754 755 if ( rest_is_field_included( 'title.raw', $fields ) ) { 756 $data['title']['raw'] = $template->title; 757 } 758 759 if ( rest_is_field_included( 'title.rendered', $fields ) ) { 760 if ( $template->wp_id ) { 761 /** This filter is documented in wp-includes/post-template.php */ 762 $data['title']['rendered'] = apply_filters( 'the_title', $template->title, $template->wp_id ); 763 } else { 764 $data['title']['rendered'] = $template->title; 765 } 766 } 767 768 if ( rest_is_field_included( 'status', $fields ) ) { 769 $data['status'] = $template->status; 770 } 771 772 if ( rest_is_field_included( 'wp_id', $fields ) ) { 773 $data['wp_id'] = (int) $template->wp_id; 774 } 775 776 if ( rest_is_field_included( 'has_theme_file', $fields ) ) { 777 $data['has_theme_file'] = (bool) $template->has_theme_file; 778 } 779 780 if ( rest_is_field_included( 'is_custom', $fields ) && 'wp_template' === $template->type ) { 781 $data['is_custom'] = $template->is_custom; 782 } 783 784 if ( rest_is_field_included( 'author', $fields ) ) { 785 $data['author'] = (int) $template->author; 786 } 787 788 if ( rest_is_field_included( 'area', $fields ) && 'wp_template_part' === $template->type ) { 789 $data['area'] = $template->area; 790 } 791 792 if ( rest_is_field_included( 'modified', $fields ) ) { 793 /* 794 * File-backed templates have no modification date, and `mysql_to_rfc3339()` 795 * returns `false` for an empty or malformed value, which the schema does 796 * not allow. Return `null` in that case. 797 */ 798 $modified = mysql_to_rfc3339( $template->modified ); 799 $data['modified'] = false !== $modified ? $modified : null; 800 } 801 802 if ( rest_is_field_included( 'date', $fields ) ) { 803 /* 804 * File-backed templates have no date, and `mysql_to_rfc3339()` returns 805 * `false` for an empty or malformed value, which the schema does not 806 * allow. Return `null` in that case. 807 */ 808 $date = mysql_to_rfc3339( $template->date ); 809 $data['date'] = false !== $date ? $date : null; 810 } 811 812 if ( rest_is_field_included( 'author_text', $fields ) ) { 813 $data['author_text'] = self::get_wp_templates_author_text_field( $template ); 814 } 815 816 if ( rest_is_field_included( 'original_source', $fields ) ) { 817 $data['original_source'] = self::get_wp_templates_original_source_field( $template ); 818 } 819 820 if ( rest_is_field_included( 'plugin', $fields ) ) { 821 $registered_template = WP_Block_Templates_Registry::get_instance()->get_by_slug( $template->slug ); 822 if ( $registered_template ) { 823 $data['plugin'] = $registered_template->plugin; 824 } 825 } 826 827 $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; 828 $data = $this->add_additional_fields_to_object( $data, $request ); 829 $data = $this->filter_response_by_context( $data, $context ); 830 831 // Wrap the data in a response object. 832 $response = rest_ensure_response( $data ); 833 834 if ( rest_is_field_included( '_links', $fields ) || rest_is_field_included( '_embedded', $fields ) ) { 835 $links = $this->prepare_links( $template->id ); 836 $response->add_links( $links ); 837 if ( ! empty( $links['self']['href'] ) ) { 838 $actions = $this->get_available_actions(); 839 $self = $links['self']['href']; 840 foreach ( $actions as $rel ) { 841 $response->add_link( $rel, $self ); 842 } 843 } 844 } 845 846 return $response; 847 } 848 849 /** 850 * Returns the source from where the template originally comes from. 851 * 852 * @since 6.5.0 853 * 854 * @param WP_Block_Template $template_object Template instance. 855 * @return string Original source of the template one of theme, plugin, site, or user. 856 */ 857 private static function get_wp_templates_original_source_field( $template_object ) { 858 if ( 'wp_template' === $template_object->type || 'wp_template_part' === $template_object->type ) { 859 /* 860 * Added by theme. 861 * Template originally provided by a theme, but customized by a user. 862 * Templates originally didn't have the 'origin' field so identify 863 * older customized templates by checking for no origin and a 'theme' 864 * or 'custom' source. 865 */ 866 if ( $template_object->has_theme_file && 867 ( 'theme' === $template_object->origin || ( 868 empty( $template_object->origin ) && in_array( 869 $template_object->source, 870 array( 871 'theme', 872 'custom', 873 ), 874 true 875 ) ) 876 ) 877 ) { 878 return 'theme'; 879 } 880 881 // Added by plugin. 882 if ( 'plugin' === $template_object->origin ) { 883 return 'plugin'; 884 } 885 886 /* 887 * Added by site. 888 * Template was created from scratch, but has no author. Author support 889 * was only added to templates in WordPress 5.9. Fallback to showing the 890 * site logo and title. 891 */ 892 if ( empty( $template_object->has_theme_file ) && 'custom' === $template_object->source && empty( $template_object->author ) ) { 893 return 'site'; 894 } 895 } 896 897 // Added by user. 898 return 'user'; 899 } 900 901 /** 902 * Returns a human readable text for the author of the template. 903 * 904 * @since 6.5.0 905 * 906 * @param WP_Block_Template $template_object Template instance. 907 * @return string Human readable text for the author. 908 */ 909 private static function get_wp_templates_author_text_field( $template_object ) { 910 $original_source = self::get_wp_templates_original_source_field( $template_object ); 911 switch ( $original_source ) { 912 case 'theme': 913 $theme_name = wp_get_theme( $template_object->theme )->get( 'Name' ); 914 return empty( $theme_name ) ? $template_object->theme : $theme_name; 915 case 'plugin': 916 if ( ! function_exists( 'get_plugins' ) ) { 917 require_once ABSPATH . 'wp-admin/includes/plugin.php'; 918 } 919 if ( isset( $template_object->plugin ) ) { 920 $plugins = wp_get_active_and_valid_plugins(); 921 922 foreach ( $plugins as $plugin_file ) { 923 $plugin_basename = plugin_basename( $plugin_file ); 924 // Split basename by '/' to get the plugin slug. 925 list( $plugin_slug, ) = explode( '/', $plugin_basename ); 926 927 if ( $plugin_slug === $template_object->plugin ) { 928 $plugin_data = get_plugin_data( $plugin_file ); 929 930 if ( ! empty( $plugin_data['Name'] ) ) { 931 return $plugin_data['Name']; 932 } 933 934 break; 935 } 936 } 937 } 938 939 /* 940 * Fall back to the theme name if the plugin is not defined. That's needed to keep backwards 941 * compatibility with templates that were registered before the plugin attribute was added. 942 */ 943 $plugins = get_plugins(); 944 $plugin_basename = plugin_basename( sanitize_text_field( $template_object->theme . '.php' ) ); 945 if ( isset( $plugins[ $plugin_basename ] ) && isset( $plugins[ $plugin_basename ]['Name'] ) ) { 946 return $plugins[ $plugin_basename ]['Name']; 947 } 948 return $template_object->plugin ?? 949 $template_object->theme; 950 case 'site': 951 return get_bloginfo( 'name' ); 952 case 'user': 953 $author = get_user_by( 'id', $template_object->author ); 954 if ( ! $author ) { 955 return __( 'Unknown author' ); 956 } 957 return $author->get( 'display_name' ); 958 } 959 960 // Fail-safe to return a string should the original source ever fall through. 961 return ''; 962 } 963 964 965 /** 966 * Prepares links for the request. 967 * 968 * @since 5.8.0 969 * 970 * @param integer $id ID. 971 * @return array Links for the given post. 972 */ 973 protected function prepare_links( $id ) { 974 $links = array( 975 'self' => array( 976 'href' => rest_url( sprintf( '/%s/%s/%s', $this->namespace, $this->rest_base, $id ) ), 977 ), 978 'collection' => array( 979 'href' => rest_url( rest_get_route_for_post_type_items( $this->post_type ) ), 980 ), 981 'about' => array( 982 'href' => rest_url( 'wp/v2/types/' . $this->post_type ), 983 ), 984 ); 985 986 if ( post_type_supports( $this->post_type, 'revisions' ) ) { 987 $template = get_block_template( $id, $this->post_type ); 988 if ( $template instanceof WP_Block_Template && ! empty( $template->wp_id ) ) { 989 $revisions = wp_get_latest_revision_id_and_total_count( $template->wp_id ); 990 $revisions_count = ! is_wp_error( $revisions ) ? $revisions['count'] : 0; 991 $revisions_base = sprintf( '/%s/%s/%s/revisions', $this->namespace, $this->rest_base, $id ); 992 993 $links['version-history'] = array( 994 'href' => rest_url( $revisions_base ), 995 'count' => $revisions_count, 996 ); 997 998 if ( $revisions_count > 0 ) { 999 $links['predecessor-version'] = array( 1000 'href' => rest_url( $revisions_base . '/' . $revisions['latest_id'] ), 1001 'id' => $revisions['latest_id'], 1002 ); 1003 } 1004 } 1005 } 1006 1007 return $links; 1008 } 1009 1010 /** 1011 * Get the link relations available for the post and current user. 1012 * 1013 * @since 5.8.0 1014 * 1015 * @return string[] List of link relations. 1016 */ 1017 protected function get_available_actions() { 1018 $rels = array(); 1019 1020 $post_type = get_post_type_object( $this->post_type ); 1021 1022 if ( current_user_can( $post_type->cap->publish_posts ) ) { 1023 $rels[] = 'https://api.w.org/action-publish'; 1024 } 1025 1026 if ( current_user_can( 'unfiltered_html' ) ) { 1027 $rels[] = 'https://api.w.org/action-unfiltered-html'; 1028 } 1029 1030 return $rels; 1031 } 1032 1033 /** 1034 * Retrieves the query params for the posts collection. 1035 * 1036 * @since 5.8.0 1037 * @since 5.9.0 Added `'area'` and `'post_type'`. 1038 * 1039 * @return array Collection parameters. 1040 */ 1041 public function get_collection_params() { 1042 return array( 1043 'context' => $this->get_context_param( array( 'default' => 'view' ) ), 1044 'wp_id' => array( 1045 'description' => __( 'Limit to the specified post id.' ), 1046 'type' => 'integer', 1047 ), 1048 'area' => array( 1049 'description' => __( 'Limit to the specified template part area.' ), 1050 'type' => 'string', 1051 ), 1052 'post_type' => array( 1053 'description' => __( 'Post type to get the templates for.' ), 1054 'type' => 'string', 1055 ), 1056 ); 1057 } 1058 1059 /** 1060 * Retrieves the block type' schema, conforming to JSON Schema. 1061 * 1062 * @since 5.8.0 1063 * @since 5.9.0 Added `'area'`. 1064 * 1065 * @return array Item schema data. 1066 */ 1067 public function get_item_schema() { 1068 if ( $this->schema ) { 1069 return $this->add_additional_fields_schema( $this->schema ); 1070 } 1071 1072 $schema = array( 1073 '$schema' => 'http://json-schema.org/draft-04/schema#', 1074 'title' => $this->post_type, 1075 'type' => 'object', 1076 'properties' => array( 1077 'id' => array( 1078 'description' => __( 'ID of template.' ), 1079 'type' => 'string', 1080 'context' => array( 'embed', 'view', 'edit' ), 1081 'readonly' => true, 1082 ), 1083 'slug' => array( 1084 'description' => __( 'Unique slug identifying the template.' ), 1085 'type' => 'string', 1086 'context' => array( 'embed', 'view', 'edit' ), 1087 'required' => true, 1088 'minLength' => 1, 1089 'pattern' => '[a-zA-Z0-9_\%-]+', 1090 ), 1091 'theme' => array( 1092 'description' => __( 'Theme identifier for the template.' ), 1093 'type' => 'string', 1094 'context' => array( 'embed', 'view', 'edit' ), 1095 ), 1096 'type' => array( 1097 'description' => __( 'Type of template.' ), 1098 'type' => 'string', 1099 'context' => array( 'embed', 'view', 'edit' ), 1100 ), 1101 'source' => array( 1102 'description' => __( 'Source of template' ), 1103 'type' => 'string', 1104 'context' => array( 'embed', 'view', 'edit' ), 1105 'readonly' => true, 1106 ), 1107 'origin' => array( 1108 'description' => __( 'Source of a customized template' ), 1109 'type' => 'string', 1110 'context' => array( 'embed', 'view', 'edit' ), 1111 'readonly' => true, 1112 ), 1113 'content' => array( 1114 'description' => __( 'Content of template.' ), 1115 'type' => array( 'object', 'string' ), 1116 'default' => '', 1117 'context' => array( 'embed', 'view', 'edit' ), 1118 'properties' => array( 1119 'raw' => array( 1120 'description' => __( 'Content for the template, as it exists in the database.' ), 1121 'type' => 'string', 1122 'context' => array( 'view', 'edit' ), 1123 ), 1124 'block_version' => array( 1125 'description' => __( 'Version of the content block format used by the template.' ), 1126 'type' => 'integer', 1127 'context' => array( 'edit' ), 1128 'readonly' => true, 1129 ), 1130 ), 1131 ), 1132 'title' => array( 1133 'description' => __( 'Title of template.' ), 1134 'type' => array( 'object', 'string' ), 1135 'default' => '', 1136 'context' => array( 'embed', 'view', 'edit' ), 1137 'properties' => array( 1138 'raw' => array( 1139 'description' => __( 'Title for the template, as it exists in the database.' ), 1140 'type' => 'string', 1141 'context' => array( 'view', 'edit', 'embed' ), 1142 ), 1143 'rendered' => array( 1144 'description' => __( 'HTML title for the template, transformed for display.' ), 1145 'type' => 'string', 1146 'context' => array( 'view', 'edit', 'embed' ), 1147 'readonly' => true, 1148 ), 1149 ), 1150 ), 1151 'description' => array( 1152 'description' => __( 'Description of template.' ), 1153 'type' => 'string', 1154 'default' => '', 1155 'context' => array( 'embed', 'view', 'edit' ), 1156 ), 1157 'status' => array( 1158 'description' => __( 'Status of template.' ), 1159 'type' => 'string', 1160 'enum' => array_keys( get_post_stati( array( 'internal' => false ) ) ), 1161 'default' => 'publish', 1162 'context' => array( 'embed', 'view', 'edit' ), 1163 ), 1164 'wp_id' => array( 1165 'description' => __( 'Post ID.' ), 1166 'type' => 'integer', 1167 'context' => array( 'embed', 'view', 'edit' ), 1168 'readonly' => true, 1169 ), 1170 'has_theme_file' => array( 1171 'description' => __( 'Theme file exists.' ), 1172 'type' => 'bool', 1173 'context' => array( 'embed', 'view', 'edit' ), 1174 'readonly' => true, 1175 ), 1176 'author' => array( 1177 'description' => __( 'The ID for the author of the template.' ), 1178 'type' => 'integer', 1179 'context' => array( 'view', 'edit', 'embed' ), 1180 ), 1181 'modified' => array( 1182 'description' => __( "The date the template was last modified, in the site's timezone." ), 1183 'type' => array( 'string', 'null' ), 1184 'format' => 'date-time', 1185 'context' => array( 'view', 'edit' ), 1186 'readonly' => true, 1187 ), 1188 'author_text' => array( 1189 'type' => 'string', 1190 'description' => __( 'Human readable text for the author.' ), 1191 'readonly' => true, 1192 'context' => array( 'view', 'edit', 'embed' ), 1193 ), 1194 'original_source' => array( 1195 'description' => __( 'Where the template originally comes from e.g. \'theme\'' ), 1196 'type' => 'string', 1197 'readonly' => true, 1198 'context' => array( 'view', 'edit', 'embed' ), 1199 'enum' => array( 1200 'theme', 1201 'plugin', 1202 'site', 1203 'user', 1204 ), 1205 ), 1206 'date' => array( 1207 'description' => __( "The date the template was published, in the site's timezone." ), 1208 'type' => array( 'string', 'null' ), 1209 'format' => 'date-time', 1210 'context' => array( 'view', 'edit' ), 1211 'readonly' => true, 1212 ), 1213 ), 1214 ); 1215 1216 if ( 'wp_template' === $this->post_type ) { 1217 $schema['properties']['is_custom'] = array( 1218 'description' => __( 'Whether a template is a custom template.' ), 1219 'type' => 'bool', 1220 'context' => array( 'embed', 'view', 'edit' ), 1221 'readonly' => true, 1222 ); 1223 $schema['properties']['plugin'] = array( 1224 'type' => 'string', 1225 'description' => __( 'Plugin that registered the template.' ), 1226 'readonly' => true, 1227 'context' => array( 'view', 'edit', 'embed' ), 1228 ); 1229 } 1230 1231 if ( 'wp_template_part' === $this->post_type ) { 1232 $schema['properties']['area'] = array( 1233 'description' => __( 'Where the template part is intended for use (header, footer, etc.)' ), 1234 'type' => 'string', 1235 'context' => array( 'embed', 'view', 'edit' ), 1236 ); 1237 } 1238 1239 $this->schema = $schema; 1240 1241 return $this->add_additional_fields_schema( $this->schema ); 1242 } 1243 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
| Generated : Thu Sep 10 08:20:30 2026 | Cross-referenced by PHPXref |