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 */ 17class 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 $this->namespace = 'wp/v2'; 37 $obj = get_post_type_object( $post_type ); 38 $this->rest_base = ! empty( $obj->rest_base ) ? $obj->rest_base : $obj->name; 39 } 40 41 /** 42 * Registers the controllers routes. 43 * 44 * @since 5.8.0 45 */ 46 public function register_routes() { 47 // Lists all templates. 48 register_rest_route( 49 $this->namespace, 50 '/' . $this->rest_base, 51 array( 52 array( 53 'methods' => WP_REST_Server::READABLE, 54 'callback' => array( $this, 'get_items' ), 55 'permission_callback' => array( $this, 'get_items_permissions_check' ), 56 'args' => $this->get_collection_params(), 57 ), 58 array( 59 'methods' => WP_REST_Server::CREATABLE, 60 'callback' => array( $this, 'create_item' ), 61 'permission_callback' => array( $this, 'create_item_permissions_check' ), 62 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), 63 ), 64 'schema' => array( $this, 'get_public_item_schema' ), 65 ) 66 ); 67 68 // Lists/updates a single template based on the given id. 69 register_rest_route( 70 $this->namespace, 71 '/' . $this->rest_base . '/(?P<id>[\/\w-]+)', 72 array( 73 array( 74 'methods' => WP_REST_Server::READABLE, 75 'callback' => array( $this, 'get_item' ), 76 'permission_callback' => array( $this, 'get_item_permissions_check' ), 77 'args' => array( 78 'id' => array( 79 'description' => __( 'The id of a template' ), 80 'type' => 'string', 81 ), 82 ), 83 ), 84 array( 85 'methods' => WP_REST_Server::EDITABLE, 86 'callback' => array( $this, 'update_item' ), 87 'permission_callback' => array( $this, 'update_item_permissions_check' ), 88 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), 89 ), 90 array( 91 'methods' => WP_REST_Server::DELETABLE, 92 'callback' => array( $this, 'delete_item' ), 93 'permission_callback' => array( $this, 'delete_item_permissions_check' ), 94 'args' => array( 95 'force' => array( 96 'type' => 'boolean', 97 'default' => false, 98 'description' => __( 'Whether to bypass Trash and force deletion.' ), 99 ), 100 ), 101 ), 102 'schema' => array( $this, 'get_public_item_schema' ), 103 ) 104 ); 105 } 106 107 /** 108 * Checks if the user has permissions to make the request. 109 * 110 * @since 5.8.0 111 * 112 * @param WP_REST_Request $request Full details about the request. 113 * @return true|WP_Error True if the request has read access, WP_Error object otherwise. 114 */ 115 protected function permissions_check( $request ) { 116 // Verify if the current user has edit_theme_options capability. 117 // This capability is required to edit/view/delete templates. 118 if ( ! current_user_can( 'edit_theme_options' ) ) { 119 return new WP_Error( 120 'rest_cannot_manage_templates', 121 __( 'Sorry, you are not allowed to access the templates on this site.' ), 122 array( 123 'status' => rest_authorization_required_code(), 124 ) 125 ); 126 } 127 128 return true; 129 } 130 131 /** 132 * Checks if a given request has access to read templates. 133 * 134 * @since 5.8.0 135 * 136 * @param WP_REST_Request $request Full details about the request. 137 * @return true|WP_Error True if the request has read access, WP_Error object otherwise. 138 */ 139 public function get_items_permissions_check( $request ) { 140 return $this->permissions_check( $request ); 141 } 142 143 /** 144 * Returns a list of templates. 145 * 146 * @since 5.8.0 147 * 148 * @param WP_REST_Request $request The request instance. 149 * @return WP_REST_Response 150 */ 151 public function get_items( $request ) { 152 $query = array(); 153 if ( isset( $request['wp_id'] ) ) { 154 $query['wp_id'] = $request['wp_id']; 155 } 156 if ( isset( $request['area'] ) ) { 157 $query['area'] = $request['area']; 158 } 159 160 $templates = array(); 161 foreach ( get_block_templates( $query, $this->post_type ) as $template ) { 162 $data = $this->prepare_item_for_response( $template, $request ); 163 $templates[] = $this->prepare_response_for_collection( $data ); 164 } 165 166 return rest_ensure_response( $templates ); 167 } 168 169 /** 170 * Checks if a given request has access to read a single template. 171 * 172 * @since 5.8.0 173 * 174 * @param WP_REST_Request $request Full details about the request. 175 * @return true|WP_Error True if the request has read access for the item, WP_Error object otherwise. 176 */ 177 public function get_item_permissions_check( $request ) { 178 return $this->permissions_check( $request ); 179 } 180 181 /** 182 * Returns the given template 183 * 184 * @since 5.8.0 185 * 186 * @param WP_REST_Request $request The request instance. 187 * @return WP_REST_Response|WP_Error 188 */ 189 public function get_item( $request ) { 190 $template = get_block_template( $request['id'], $this->post_type ); 191 192 if ( ! $template ) { 193 return new WP_Error( 'rest_template_not_found', __( 'No templates exist with that id.' ), array( 'status' => 404 ) ); 194 } 195 196 return $this->prepare_item_for_response( $template, $request ); 197 } 198 199 /** 200 * Checks if a given request has access to write a single template. 201 * 202 * @since 5.8.0 203 * 204 * @param WP_REST_Request $request Full details about the request. 205 * @return true|WP_Error True if the request has write access for the item, WP_Error object otherwise. 206 */ 207 public function update_item_permissions_check( $request ) { 208 return $this->permissions_check( $request ); 209 } 210 211 /** 212 * Updates a single template. 213 * 214 * @since 5.8.0 215 * 216 * @param WP_REST_Request $request Full details about the request. 217 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. 218 */ 219 public function update_item( $request ) { 220 $template = get_block_template( $request['id'], $this->post_type ); 221 if ( ! $template ) { 222 return new WP_Error( 'rest_template_not_found', __( 'No templates exist with that id.' ), array( 'status' => 404 ) ); 223 } 224 225 $changes = $this->prepare_item_for_database( $request ); 226 227 if ( 'custom' === $template->source ) { 228 $result = wp_update_post( wp_slash( (array) $changes ), true ); 229 } else { 230 $result = wp_insert_post( wp_slash( (array) $changes ), true ); 231 } 232 if ( is_wp_error( $result ) ) { 233 return $result; 234 } 235 236 $template = get_block_template( $request['id'], $this->post_type ); 237 $fields_update = $this->update_additional_fields_for_object( $template, $request ); 238 if ( is_wp_error( $fields_update ) ) { 239 return $fields_update; 240 } 241 242 return $this->prepare_item_for_response( 243 get_block_template( $request['id'], $this->post_type ), 244 $request 245 ); 246 } 247 248 /** 249 * Checks if a given request has access to create a template. 250 * 251 * @since 5.8.0 252 * 253 * @param WP_REST_Request $request Full details about the request. 254 * @return true|WP_Error True if the request has access to create items, WP_Error object otherwise. 255 */ 256 public function create_item_permissions_check( $request ) { 257 return $this->permissions_check( $request ); 258 } 259 260 /** 261 * Creates a single template. 262 * 263 * @since 5.8.0 264 * 265 * @param WP_REST_Request $request Full details about the request. 266 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. 267 */ 268 public function create_item( $request ) { 269 $changes = $this->prepare_item_for_database( $request ); 270 $changes->post_name = $request['slug']; 271 $result = wp_insert_post( wp_slash( (array) $changes ), true ); 272 if ( is_wp_error( $result ) ) { 273 return $result; 274 } 275 $posts = get_block_templates( array( 'wp_id' => $result ), $this->post_type ); 276 if ( ! count( $posts ) ) { 277 return new WP_Error( 'rest_template_insert_error', __( 'No templates exist with that id.' ) ); 278 } 279 $id = $posts[0]->id; 280 $template = get_block_template( $id, $this->post_type ); 281 $fields_update = $this->update_additional_fields_for_object( $template, $request ); 282 if ( is_wp_error( $fields_update ) ) { 283 return $fields_update; 284 } 285 286 return $this->prepare_item_for_response( 287 get_block_template( $id, $this->post_type ), 288 $request 289 ); 290 } 291 292 /** 293 * Checks if a given request has access to delete a single template. 294 * 295 * @since 5.8.0 296 * 297 * @param WP_REST_Request $request Full details about the request. 298 * @return true|WP_Error True if the request has delete access for the item, WP_Error object otherwise. 299 */ 300 public function delete_item_permissions_check( $request ) { 301 return $this->permissions_check( $request ); 302 } 303 304 /** 305 * Deletes a single template. 306 * 307 * @since 5.8.0 308 * 309 * @param WP_REST_Request $request Full details about the request. 310 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. 311 */ 312 public function delete_item( $request ) { 313 $template = get_block_template( $request['id'], $this->post_type ); 314 if ( ! $template ) { 315 return new WP_Error( 'rest_template_not_found', __( 'No templates exist with that id.' ), array( 'status' => 404 ) ); 316 } 317 if ( 'custom' !== $template->source ) { 318 return new WP_Error( 'rest_invalid_template', __( 'Templates based on theme files can\'t be removed.' ), array( 'status' => 400 ) ); 319 } 320 321 $id = $template->wp_id; 322 $force = (bool) $request['force']; 323 324 // If we're forcing, then delete permanently. 325 if ( $force ) { 326 $previous = $this->prepare_item_for_response( $template, $request ); 327 wp_delete_post( $id, true ); 328 $response = new WP_REST_Response(); 329 $response->set_data( 330 array( 331 'deleted' => true, 332 'previous' => $previous->get_data(), 333 ) 334 ); 335 336 return $response; 337 } 338 339 // Otherwise, only trash if we haven't already. 340 if ( 'trash' === $template->status ) { 341 return new WP_Error( 342 'rest_template_already_trashed', 343 __( 'The template has already been deleted.' ), 344 array( 'status' => 410 ) 345 ); 346 } 347 348 wp_trash_post( $id ); 349 $template->status = 'trash'; 350 return $this->prepare_item_for_response( $template, $request ); 351 } 352 353 /** 354 * Prepares a single template for create or update. 355 * 356 * @since 5.8.0 357 * 358 * @param WP_REST_Request $request Request object. 359 * @return stdClass Changes to pass to wp_update_post. 360 */ 361 protected function prepare_item_for_database( $request ) { 362 $template = $request['id'] ? get_block_template( $request['id'], $this->post_type ) : null; 363 $changes = new stdClass(); 364 if ( null === $template ) { 365 $changes->post_type = $this->post_type; 366 $changes->post_status = 'publish'; 367 $changes->tax_input = array( 368 'wp_theme' => isset( $request['theme'] ) ? $request['theme'] : wp_get_theme()->get_stylesheet(), 369 ); 370 } elseif ( 'custom' !== $template->source ) { 371 $changes->post_name = $template->slug; 372 $changes->post_type = $this->post_type; 373 $changes->post_status = 'publish'; 374 $changes->tax_input = array( 375 'wp_theme' => $template->theme, 376 ); 377 } else { 378 $changes->post_name = $template->slug; 379 $changes->ID = $template->wp_id; 380 $changes->post_status = 'publish'; 381 } 382 if ( isset( $request['content'] ) ) { 383 $changes->post_content = $request['content']; 384 } elseif ( null !== $template && 'custom' !== $template->source ) { 385 $changes->post_content = $template->content; 386 } 387 if ( isset( $request['title'] ) ) { 388 $changes->post_title = $request['title']; 389 } elseif ( null !== $template && 'custom' !== $template->source ) { 390 $changes->post_title = $template->title; 391 } 392 if ( isset( $request['description'] ) ) { 393 $changes->post_excerpt = $request['description']; 394 } elseif ( null !== $template && 'custom' !== $template->source ) { 395 $changes->post_excerpt = $template->description; 396 } 397 398 return $changes; 399 } 400 401 /** 402 * Prepare a single template output for response 403 * 404 * @since 5.8.0 405 * 406 * @param WP_Block_Template $template Template instance. 407 * @param WP_REST_Request $request Request object. 408 * @return WP_REST_Response $data 409 */ 410 public function prepare_item_for_response( $template, $request ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable 411 $result = array( 412 'id' => $template->id, 413 'theme' => $template->theme, 414 'content' => array( 'raw' => $template->content ), 415 'slug' => $template->slug, 416 'source' => $template->source, 417 'type' => $template->type, 418 'description' => $template->description, 419 'title' => array( 420 'raw' => $template->title, 421 'rendered' => $template->title, 422 ), 423 'status' => $template->status, 424 'wp_id' => $template->wp_id, 425 'has_theme_file' => $template->has_theme_file, 426 ); 427 428 if ( 'wp_template_part' === $template->type ) { 429 $result['area'] = $template->area; 430 } 431 432 $result = $this->add_additional_fields_to_object( $result, $request ); 433 434 $response = rest_ensure_response( $result ); 435 $links = $this->prepare_links( $template->id ); 436 $response->add_links( $links ); 437 if ( ! empty( $links['self']['href'] ) ) { 438 $actions = $this->get_available_actions(); 439 $self = $links['self']['href']; 440 foreach ( $actions as $rel ) { 441 $response->add_link( $rel, $self ); 442 } 443 } 444 445 return $response; 446 } 447 448 449 /** 450 * Prepares links for the request. 451 * 452 * @since 5.8.0 453 * 454 * @param integer $id ID. 455 * @return array Links for the given post. 456 */ 457 protected function prepare_links( $id ) { 458 $base = sprintf( '%s/%s', $this->namespace, $this->rest_base ); 459 460 $links = array( 461 'self' => array( 462 'href' => rest_url( trailingslashit( $base ) . $id ), 463 ), 464 'collection' => array( 465 'href' => rest_url( $base ), 466 ), 467 'about' => array( 468 'href' => rest_url( 'wp/v2/types/' . $this->post_type ), 469 ), 470 ); 471 472 return $links; 473 } 474 475 /** 476 * Get the link relations available for the post and current user. 477 * 478 * @since 5.8.0 479 * 480 * @return array List of link relations. 481 */ 482 protected function get_available_actions() { 483 $rels = array(); 484 485 $post_type = get_post_type_object( $this->post_type ); 486 487 if ( current_user_can( $post_type->cap->publish_posts ) ) { 488 $rels[] = 'https://api.w.org/action-publish'; 489 } 490 491 if ( current_user_can( 'unfiltered_html' ) ) { 492 $rels[] = 'https://api.w.org/action-unfiltered-html'; 493 } 494 495 return $rels; 496 } 497 498 /** 499 * Retrieves the query params for the posts collection. 500 * 501 * @since 5.8.0 502 * 503 * @return array Collection parameters. 504 */ 505 public function get_collection_params() { 506 return array( 507 'context' => $this->get_context_param(), 508 'wp_id' => array( 509 'description' => __( 'Limit to the specified post id.' ), 510 'type' => 'integer', 511 ), 512 ); 513 } 514 515 /** 516 * Retrieves the block type' schema, conforming to JSON Schema. 517 * 518 * @since 5.8.0 519 * 520 * @return array Item schema data. 521 */ 522 public function get_item_schema() { 523 if ( $this->schema ) { 524 return $this->add_additional_fields_schema( $this->schema ); 525 } 526 527 $schema = array( 528 '$schema' => 'http://json-schema.org/draft-04/schema#', 529 'title' => $this->post_type, 530 'type' => 'object', 531 'properties' => array( 532 'id' => array( 533 'description' => __( 'ID of template.' ), 534 'type' => 'string', 535 'context' => array( 'embed', 'view', 'edit' ), 536 'readonly' => true, 537 ), 538 'slug' => array( 539 'description' => __( 'Unique slug identifying the template.' ), 540 'type' => 'string', 541 'context' => array( 'embed', 'view', 'edit' ), 542 'required' => true, 543 'minLength' => 1, 544 'pattern' => '[a-zA-Z_\-]+', 545 ), 546 'theme' => array( 547 'description' => __( 'Theme identifier for the template.' ), 548 'type' => 'string', 549 'context' => array( 'embed', 'view', 'edit' ), 550 ), 551 'source' => array( 552 'description' => __( 'Source of template' ), 553 'type' => 'string', 554 'context' => array( 'embed', 'view', 'edit' ), 555 'readonly' => true, 556 ), 557 'content' => array( 558 'description' => __( 'Content of template.' ), 559 'type' => array( 'object', 'string' ), 560 'default' => '', 561 'context' => array( 'embed', 'view', 'edit' ), 562 ), 563 'title' => array( 564 'description' => __( 'Title of template.' ), 565 'type' => array( 'object', 'string' ), 566 'default' => '', 567 'context' => array( 'embed', 'view', 'edit' ), 568 ), 569 'description' => array( 570 'description' => __( 'Description of template.' ), 571 'type' => 'string', 572 'default' => '', 573 'context' => array( 'embed', 'view', 'edit' ), 574 ), 575 'status' => array( 576 'description' => __( 'Status of template.' ), 577 'type' => 'string', 578 'default' => 'publish', 579 'context' => array( 'embed', 'view', 'edit' ), 580 ), 581 'wp_id' => array( 582 'description' => __( 'Post ID.' ), 583 'type' => 'integer', 584 'context' => array( 'embed', 'view', 'edit' ), 585 'readonly' => true, 586 ), 587 'has_theme_file' => array( 588 'description' => __( 'Theme file exists.' ), 589 'type' => 'bool', 590 'context' => array( 'embed', 'view', 'edit' ), 591 'readonly' => true, 592 ), 593 ), 594 ); 595 596 $this->schema = $schema; 597 598 return $this->add_additional_fields_schema( $this->schema ); 599 } 600} 601