| [ 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 * 674 * @param WP_Block_Template $item Template instance. 675 * @param WP_REST_Request $request Request object. 676 * @return WP_REST_Response Response object. 677 */ 678 public function prepare_item_for_response( $item, $request ) { 679 // Don't prepare the response body for HEAD requests. 680 if ( $request->is_method( 'HEAD' ) ) { 681 return new WP_REST_Response( array() ); 682 } 683 684 /* 685 * Resolve pattern blocks so they don't need to be resolved client-side 686 * in the editor, improving performance. 687 */ 688 $blocks = parse_blocks( $item->content ); 689 $blocks = resolve_pattern_blocks( $blocks ); 690 $item->content = serialize_blocks( $blocks ); 691 692 // Restores the more descriptive, specific name for use within this method. 693 $template = $item; 694 695 $fields = $this->get_fields_for_response( $request ); 696 697 // Base fields for every template. 698 $data = array(); 699 700 if ( rest_is_field_included( 'id', $fields ) ) { 701 $data['id'] = $template->id; 702 } 703 704 if ( rest_is_field_included( 'theme', $fields ) ) { 705 $data['theme'] = $template->theme; 706 } 707 708 if ( rest_is_field_included( 'content', $fields ) ) { 709 $data['content'] = array(); 710 } 711 if ( rest_is_field_included( 'content.raw', $fields ) ) { 712 $data['content']['raw'] = $template->content; 713 } 714 715 if ( rest_is_field_included( 'content.block_version', $fields ) ) { 716 $data['content']['block_version'] = block_version( $template->content ); 717 } 718 719 if ( rest_is_field_included( 'slug', $fields ) ) { 720 $data['slug'] = $template->slug; 721 } 722 723 if ( rest_is_field_included( 'source', $fields ) ) { 724 $data['source'] = $template->source; 725 } 726 727 if ( rest_is_field_included( 'origin', $fields ) ) { 728 $data['origin'] = $template->origin; 729 } 730 731 if ( rest_is_field_included( 'type', $fields ) ) { 732 $data['type'] = $template->type; 733 } 734 735 if ( rest_is_field_included( 'description', $fields ) ) { 736 $data['description'] = $template->description; 737 } 738 739 if ( rest_is_field_included( 'title', $fields ) ) { 740 $data['title'] = array(); 741 } 742 743 if ( rest_is_field_included( 'title.raw', $fields ) ) { 744 $data['title']['raw'] = $template->title; 745 } 746 747 if ( rest_is_field_included( 'title.rendered', $fields ) ) { 748 if ( $template->wp_id ) { 749 /** This filter is documented in wp-includes/post-template.php */ 750 $data['title']['rendered'] = apply_filters( 'the_title', $template->title, $template->wp_id ); 751 } else { 752 $data['title']['rendered'] = $template->title; 753 } 754 } 755 756 if ( rest_is_field_included( 'status', $fields ) ) { 757 $data['status'] = $template->status; 758 } 759 760 if ( rest_is_field_included( 'wp_id', $fields ) ) { 761 $data['wp_id'] = (int) $template->wp_id; 762 } 763 764 if ( rest_is_field_included( 'has_theme_file', $fields ) ) { 765 $data['has_theme_file'] = (bool) $template->has_theme_file; 766 } 767 768 if ( rest_is_field_included( 'is_custom', $fields ) && 'wp_template' === $template->type ) { 769 $data['is_custom'] = $template->is_custom; 770 } 771 772 if ( rest_is_field_included( 'author', $fields ) ) { 773 $data['author'] = (int) $template->author; 774 } 775 776 if ( rest_is_field_included( 'area', $fields ) && 'wp_template_part' === $template->type ) { 777 $data['area'] = $template->area; 778 } 779 780 if ( rest_is_field_included( 'modified', $fields ) ) { 781 /* 782 * File-backed templates have no modification date, and `mysql_to_rfc3339()` 783 * returns `false` for an empty or malformed value, which the schema does 784 * not allow. Return `null` in that case. 785 */ 786 $modified = mysql_to_rfc3339( $template->modified ); 787 $data['modified'] = false !== $modified ? $modified : null; 788 } 789 790 if ( rest_is_field_included( 'date', $fields ) ) { 791 /* 792 * File-backed templates have no date, and `mysql_to_rfc3339()` returns 793 * `false` for an empty or malformed value, which the schema does not 794 * allow. Return `null` in that case. 795 */ 796 $date = mysql_to_rfc3339( $template->date ); 797 $data['date'] = false !== $date ? $date : null; 798 } 799 800 if ( rest_is_field_included( 'author_text', $fields ) ) { 801 $data['author_text'] = self::get_wp_templates_author_text_field( $template ); 802 } 803 804 if ( rest_is_field_included( 'original_source', $fields ) ) { 805 $data['original_source'] = self::get_wp_templates_original_source_field( $template ); 806 } 807 808 if ( rest_is_field_included( 'plugin', $fields ) ) { 809 $registered_template = WP_Block_Templates_Registry::get_instance()->get_by_slug( $template->slug ); 810 if ( $registered_template ) { 811 $data['plugin'] = $registered_template->plugin; 812 } 813 } 814 815 $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; 816 $data = $this->add_additional_fields_to_object( $data, $request ); 817 $data = $this->filter_response_by_context( $data, $context ); 818 819 // Wrap the data in a response object. 820 $response = rest_ensure_response( $data ); 821 822 if ( rest_is_field_included( '_links', $fields ) || rest_is_field_included( '_embedded', $fields ) ) { 823 $links = $this->prepare_links( $template->id ); 824 $response->add_links( $links ); 825 if ( ! empty( $links['self']['href'] ) ) { 826 $actions = $this->get_available_actions(); 827 $self = $links['self']['href']; 828 foreach ( $actions as $rel ) { 829 $response->add_link( $rel, $self ); 830 } 831 } 832 } 833 834 return $response; 835 } 836 837 /** 838 * Returns the source from where the template originally comes from. 839 * 840 * @since 6.5.0 841 * 842 * @param WP_Block_Template $template_object Template instance. 843 * @return string Original source of the template one of theme, plugin, site, or user. 844 */ 845 private static function get_wp_templates_original_source_field( $template_object ) { 846 if ( 'wp_template' === $template_object->type || 'wp_template_part' === $template_object->type ) { 847 /* 848 * Added by theme. 849 * Template originally provided by a theme, but customized by a user. 850 * Templates originally didn't have the 'origin' field so identify 851 * older customized templates by checking for no origin and a 'theme' 852 * or 'custom' source. 853 */ 854 if ( $template_object->has_theme_file && 855 ( 'theme' === $template_object->origin || ( 856 empty( $template_object->origin ) && in_array( 857 $template_object->source, 858 array( 859 'theme', 860 'custom', 861 ), 862 true 863 ) ) 864 ) 865 ) { 866 return 'theme'; 867 } 868 869 // Added by plugin. 870 if ( 'plugin' === $template_object->origin ) { 871 return 'plugin'; 872 } 873 874 /* 875 * Added by site. 876 * Template was created from scratch, but has no author. Author support 877 * was only added to templates in WordPress 5.9. Fallback to showing the 878 * site logo and title. 879 */ 880 if ( empty( $template_object->has_theme_file ) && 'custom' === $template_object->source && empty( $template_object->author ) ) { 881 return 'site'; 882 } 883 } 884 885 // Added by user. 886 return 'user'; 887 } 888 889 /** 890 * Returns a human readable text for the author of the template. 891 * 892 * @since 6.5.0 893 * 894 * @param WP_Block_Template $template_object Template instance. 895 * @return string Human readable text for the author. 896 */ 897 private static function get_wp_templates_author_text_field( $template_object ) { 898 $original_source = self::get_wp_templates_original_source_field( $template_object ); 899 switch ( $original_source ) { 900 case 'theme': 901 $theme_name = wp_get_theme( $template_object->theme )->get( 'Name' ); 902 return empty( $theme_name ) ? $template_object->theme : $theme_name; 903 case 'plugin': 904 if ( ! function_exists( 'get_plugins' ) ) { 905 require_once ABSPATH . 'wp-admin/includes/plugin.php'; 906 } 907 if ( isset( $template_object->plugin ) ) { 908 $plugins = wp_get_active_and_valid_plugins(); 909 910 foreach ( $plugins as $plugin_file ) { 911 $plugin_basename = plugin_basename( $plugin_file ); 912 // Split basename by '/' to get the plugin slug. 913 list( $plugin_slug, ) = explode( '/', $plugin_basename ); 914 915 if ( $plugin_slug === $template_object->plugin ) { 916 $plugin_data = get_plugin_data( $plugin_file ); 917 918 if ( ! empty( $plugin_data['Name'] ) ) { 919 return $plugin_data['Name']; 920 } 921 922 break; 923 } 924 } 925 } 926 927 /* 928 * Fall back to the theme name if the plugin is not defined. That's needed to keep backwards 929 * compatibility with templates that were registered before the plugin attribute was added. 930 */ 931 $plugins = get_plugins(); 932 $plugin_basename = plugin_basename( sanitize_text_field( $template_object->theme . '.php' ) ); 933 if ( isset( $plugins[ $plugin_basename ] ) && isset( $plugins[ $plugin_basename ]['Name'] ) ) { 934 return $plugins[ $plugin_basename ]['Name']; 935 } 936 return $template_object->plugin ?? 937 $template_object->theme; 938 case 'site': 939 return get_bloginfo( 'name' ); 940 case 'user': 941 $author = get_user_by( 'id', $template_object->author ); 942 if ( ! $author ) { 943 return __( 'Unknown author' ); 944 } 945 return $author->get( 'display_name' ); 946 } 947 948 // Fail-safe to return a string should the original source ever fall through. 949 return ''; 950 } 951 952 953 /** 954 * Prepares links for the request. 955 * 956 * @since 5.8.0 957 * 958 * @param integer $id ID. 959 * @return array Links for the given post. 960 */ 961 protected function prepare_links( $id ) { 962 $links = array( 963 'self' => array( 964 'href' => rest_url( sprintf( '/%s/%s/%s', $this->namespace, $this->rest_base, $id ) ), 965 ), 966 'collection' => array( 967 'href' => rest_url( rest_get_route_for_post_type_items( $this->post_type ) ), 968 ), 969 'about' => array( 970 'href' => rest_url( 'wp/v2/types/' . $this->post_type ), 971 ), 972 ); 973 974 if ( post_type_supports( $this->post_type, 'revisions' ) ) { 975 $template = get_block_template( $id, $this->post_type ); 976 if ( $template instanceof WP_Block_Template && ! empty( $template->wp_id ) ) { 977 $revisions = wp_get_latest_revision_id_and_total_count( $template->wp_id ); 978 $revisions_count = ! is_wp_error( $revisions ) ? $revisions['count'] : 0; 979 $revisions_base = sprintf( '/%s/%s/%s/revisions', $this->namespace, $this->rest_base, $id ); 980 981 $links['version-history'] = array( 982 'href' => rest_url( $revisions_base ), 983 'count' => $revisions_count, 984 ); 985 986 if ( $revisions_count > 0 ) { 987 $links['predecessor-version'] = array( 988 'href' => rest_url( $revisions_base . '/' . $revisions['latest_id'] ), 989 'id' => $revisions['latest_id'], 990 ); 991 } 992 } 993 } 994 995 return $links; 996 } 997 998 /** 999 * Get the link relations available for the post and current user. 1000 * 1001 * @since 5.8.0 1002 * 1003 * @return string[] List of link relations. 1004 */ 1005 protected function get_available_actions() { 1006 $rels = array(); 1007 1008 $post_type = get_post_type_object( $this->post_type ); 1009 1010 if ( current_user_can( $post_type->cap->publish_posts ) ) { 1011 $rels[] = 'https://api.w.org/action-publish'; 1012 } 1013 1014 if ( current_user_can( 'unfiltered_html' ) ) { 1015 $rels[] = 'https://api.w.org/action-unfiltered-html'; 1016 } 1017 1018 return $rels; 1019 } 1020 1021 /** 1022 * Retrieves the query params for the posts collection. 1023 * 1024 * @since 5.8.0 1025 * @since 5.9.0 Added `'area'` and `'post_type'`. 1026 * 1027 * @return array Collection parameters. 1028 */ 1029 public function get_collection_params() { 1030 return array( 1031 'context' => $this->get_context_param( array( 'default' => 'view' ) ), 1032 'wp_id' => array( 1033 'description' => __( 'Limit to the specified post id.' ), 1034 'type' => 'integer', 1035 ), 1036 'area' => array( 1037 'description' => __( 'Limit to the specified template part area.' ), 1038 'type' => 'string', 1039 ), 1040 'post_type' => array( 1041 'description' => __( 'Post type to get the templates for.' ), 1042 'type' => 'string', 1043 ), 1044 ); 1045 } 1046 1047 /** 1048 * Retrieves the block type' schema, conforming to JSON Schema. 1049 * 1050 * @since 5.8.0 1051 * @since 5.9.0 Added `'area'`. 1052 * 1053 * @return array Item schema data. 1054 */ 1055 public function get_item_schema() { 1056 if ( $this->schema ) { 1057 return $this->add_additional_fields_schema( $this->schema ); 1058 } 1059 1060 $schema = array( 1061 '$schema' => 'http://json-schema.org/draft-04/schema#', 1062 'title' => $this->post_type, 1063 'type' => 'object', 1064 'properties' => array( 1065 'id' => array( 1066 'description' => __( 'ID of template.' ), 1067 'type' => 'string', 1068 'context' => array( 'embed', 'view', 'edit' ), 1069 'readonly' => true, 1070 ), 1071 'slug' => array( 1072 'description' => __( 'Unique slug identifying the template.' ), 1073 'type' => 'string', 1074 'context' => array( 'embed', 'view', 'edit' ), 1075 'required' => true, 1076 'minLength' => 1, 1077 'pattern' => '[a-zA-Z0-9_\%-]+', 1078 ), 1079 'theme' => array( 1080 'description' => __( 'Theme identifier for the template.' ), 1081 'type' => 'string', 1082 'context' => array( 'embed', 'view', 'edit' ), 1083 ), 1084 'type' => array( 1085 'description' => __( 'Type of template.' ), 1086 'type' => 'string', 1087 'context' => array( 'embed', 'view', 'edit' ), 1088 ), 1089 'source' => array( 1090 'description' => __( 'Source of template' ), 1091 'type' => 'string', 1092 'context' => array( 'embed', 'view', 'edit' ), 1093 'readonly' => true, 1094 ), 1095 'origin' => array( 1096 'description' => __( 'Source of a customized template' ), 1097 'type' => 'string', 1098 'context' => array( 'embed', 'view', 'edit' ), 1099 'readonly' => true, 1100 ), 1101 'content' => array( 1102 'description' => __( 'Content of template.' ), 1103 'type' => array( 'object', 'string' ), 1104 'default' => '', 1105 'context' => array( 'embed', 'view', 'edit' ), 1106 'properties' => array( 1107 'raw' => array( 1108 'description' => __( 'Content for the template, as it exists in the database.' ), 1109 'type' => 'string', 1110 'context' => array( 'view', 'edit' ), 1111 ), 1112 'block_version' => array( 1113 'description' => __( 'Version of the content block format used by the template.' ), 1114 'type' => 'integer', 1115 'context' => array( 'edit' ), 1116 'readonly' => true, 1117 ), 1118 ), 1119 ), 1120 'title' => array( 1121 'description' => __( 'Title of template.' ), 1122 'type' => array( 'object', 'string' ), 1123 'default' => '', 1124 'context' => array( 'embed', 'view', 'edit' ), 1125 'properties' => array( 1126 'raw' => array( 1127 'description' => __( 'Title for the template, as it exists in the database.' ), 1128 'type' => 'string', 1129 'context' => array( 'view', 'edit', 'embed' ), 1130 ), 1131 'rendered' => array( 1132 'description' => __( 'HTML title for the template, transformed for display.' ), 1133 'type' => 'string', 1134 'context' => array( 'view', 'edit', 'embed' ), 1135 'readonly' => true, 1136 ), 1137 ), 1138 ), 1139 'description' => array( 1140 'description' => __( 'Description of template.' ), 1141 'type' => 'string', 1142 'default' => '', 1143 'context' => array( 'embed', 'view', 'edit' ), 1144 ), 1145 'status' => array( 1146 'description' => __( 'Status of template.' ), 1147 'type' => 'string', 1148 'enum' => array_keys( get_post_stati( array( 'internal' => false ) ) ), 1149 'default' => 'publish', 1150 'context' => array( 'embed', 'view', 'edit' ), 1151 ), 1152 'wp_id' => array( 1153 'description' => __( 'Post ID.' ), 1154 'type' => 'integer', 1155 'context' => array( 'embed', 'view', 'edit' ), 1156 'readonly' => true, 1157 ), 1158 'has_theme_file' => array( 1159 'description' => __( 'Theme file exists.' ), 1160 'type' => 'bool', 1161 'context' => array( 'embed', 'view', 'edit' ), 1162 'readonly' => true, 1163 ), 1164 'author' => array( 1165 'description' => __( 'The ID for the author of the template.' ), 1166 'type' => 'integer', 1167 'context' => array( 'view', 'edit', 'embed' ), 1168 ), 1169 'modified' => array( 1170 'description' => __( "The date the template was last modified, in the site's timezone." ), 1171 'type' => array( 'string', 'null' ), 1172 'format' => 'date-time', 1173 'context' => array( 'view', 'edit' ), 1174 'readonly' => true, 1175 ), 1176 'author_text' => array( 1177 'type' => 'string', 1178 'description' => __( 'Human readable text for the author.' ), 1179 'readonly' => true, 1180 'context' => array( 'view', 'edit', 'embed' ), 1181 ), 1182 'original_source' => array( 1183 'description' => __( 'Where the template originally comes from e.g. \'theme\'' ), 1184 'type' => 'string', 1185 'readonly' => true, 1186 'context' => array( 'view', 'edit', 'embed' ), 1187 'enum' => array( 1188 'theme', 1189 'plugin', 1190 'site', 1191 'user', 1192 ), 1193 ), 1194 'date' => array( 1195 'description' => __( "The date the template was published, in the site's timezone." ), 1196 'type' => array( 'string', 'null' ), 1197 'format' => 'date-time', 1198 'context' => array( 'view', 'edit' ), 1199 'readonly' => true, 1200 ), 1201 ), 1202 ); 1203 1204 if ( 'wp_template' === $this->post_type ) { 1205 $schema['properties']['is_custom'] = array( 1206 'description' => __( 'Whether a template is a custom template.' ), 1207 'type' => 'bool', 1208 'context' => array( 'embed', 'view', 'edit' ), 1209 'readonly' => true, 1210 ); 1211 $schema['properties']['plugin'] = array( 1212 'type' => 'string', 1213 'description' => __( 'Plugin that registered the template.' ), 1214 'readonly' => true, 1215 'context' => array( 'view', 'edit', 'embed' ), 1216 ); 1217 } 1218 1219 if ( 'wp_template_part' === $this->post_type ) { 1220 $schema['properties']['area'] = array( 1221 'description' => __( 'Where the template part is intended for use (header, footer, etc.)' ), 1222 'type' => 'string', 1223 'context' => array( 'embed', 'view', 'edit' ), 1224 ); 1225 } 1226 1227 $this->schema = $schema; 1228 1229 return $this->add_additional_fields_schema( $this->schema ); 1230 } 1231 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
| Generated : Fri Jul 31 08:20:18 2026 | Cross-referenced by PHPXref |