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