PluginProbe
Gutenberg / 12.6.0
Gutenberg v12.6.0
23.9.1 23.9.0 23.8.0 23.7.2 23.7.1 23.7.0 23.6.1 23.6.2 23.6.0 23.5.3 23.5.2 23.5.1 23.5.0 23.4.0 23.3.2 23.3.1 23.3.0 23.2.0 23.2.1 23.2.2 23.1.1 23.1.0 23.0.1 12.6.0 7.4.0 All 402 releases
gutenberg / lib / compat / wordpress-5.9 / class-gutenberg-rest-templates-controller.php

class-gutenberg-rest-templates-controller.php in Gutenberg 12.6.0, at lib/compat/wordpress-5.9/class-gutenberg-rest-templates-controller.php

688 lines 21.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * REST API: Gutenberg_REST_Templates_Controller class
4 *
5 * @package Gutenberg
6 * @subpackage REST_API
7 */
8
9 /**
10 * Base Templates REST API Controller.
11 */
12 class Gutenberg_REST_Templates_Controller extends WP_REST_Controller {
13 /**
14 * Post type.
15 *
16 * @var string
17 */
18 protected $post_type;
19
20 /**
21 * Constructor.
22 *
23 * @param string $post_type Post type.
24 */
25 public function __construct( $post_type ) {
26 $this->post_type = $post_type;
27 $this->namespace = 'wp/v2';
28 $obj = get_post_type_object( $post_type );
29 $this->rest_base = ! empty( $obj->rest_base ) ? $obj->rest_base : $obj->name;
30 }
31
32 /**
33 * Registers the controllers routes.
34 *
35 * @return void
36 */
37 public function register_routes() {
38 // Lists all templates.
39 register_rest_route(
40 $this->namespace,
41 '/' . $this->rest_base,
42 array(
43 array(
44 'methods' => WP_REST_Server::READABLE,
45 'callback' => array( $this, 'get_items' ),
46 'permission_callback' => array( $this, 'get_items_permissions_check' ),
47 'args' => $this->get_collection_params(),
48 ),
49 array(
50 'methods' => WP_REST_Server::CREATABLE,
51 'callback' => array( $this, 'create_item' ),
52 'permission_callback' => array( $this, 'create_item_permissions_check' ),
53 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ),
54 ),
55 'schema' => array( $this, 'get_public_item_schema' ),
56 )
57 );
58
59 // Lists/updates a single template based on the given id.
60 register_rest_route(
61 $this->namespace,
62 '/' . $this->rest_base . '/(?P<id>[\/\s%\w\.\(\)\[\]\@_\-]+)',
63 array(
64 array(
65 'methods' => WP_REST_Server::READABLE,
66 'callback' => array( $this, 'get_item' ),
67 'permission_callback' => array( $this, 'get_item_permissions_check' ),
68 'args' => array(
69 'id' => array(
70 'description' => __( 'The id of a template', 'gutenberg' ),
71 'type' => 'string',
72 'sanitize_callback' => array( $this, '_sanitize_template_id' ),
73 ),
74 ),
75 ),
76 array(
77 'methods' => WP_REST_Server::EDITABLE,
78 'callback' => array( $this, 'update_item' ),
79 'permission_callback' => array( $this, 'update_item_permissions_check' ),
80 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
81 ),
82 array(
83 'methods' => WP_REST_Server::DELETABLE,
84 'callback' => array( $this, 'delete_item' ),
85 'permission_callback' => array( $this, 'delete_item_permissions_check' ),
86 'args' => array(
87 'force' => array(
88 'type' => 'boolean',
89 'default' => false,
90 'description' => __( 'Whether to bypass Trash and force deletion.', 'gutenberg' ),
91 ),
92 ),
93 ),
94 'schema' => array( $this, 'get_public_item_schema' ),
95 )
96 );
97 }
98
99 /**
100 * Checks if the user has permissions to make the request.
101 *
102 * @return true|WP_Error True if the request has read access, WP_Error object otherwise.
103 */
104 protected function permissions_check() {
105 // Verify if the current user has edit_theme_options capability.
106 // This capability is required to edit/view/delete templates.
107 if ( ! current_user_can( 'edit_theme_options' ) ) {
108 return new WP_Error(
109 'rest_cannot_manage_templates',
110 __( 'Sorry, you are not allowed to access the templates on this site.', 'gutenberg' ),
111 array(
112 'status' => rest_authorization_required_code(),
113 )
114 );
115 }
116
117 return true;
118 }
119
120 /**
121 * Requesting this endpoint for a template like "twentytwentytwo//home" requires using
122 * a path like /wp/v2/templates/twentytwentytwo//home. There are special cases when
123 * WordPress routing corrects the name to contain only a single slash like "twentytwentytwo/home".
124 *
125 * This method doubles the last slash if it's not already doubled. It relies on the template
126 * ID format {theme_name}//{template_slug} and the fact that slugs cannot contain slashes.
127 *
128 * See https://core.trac.wordpress.org/ticket/54507 for more context
129 *
130 * @param string $id Template ID.
131 * @return string Sanitized template ID.
132 */
133 public function _sanitize_template_id( $id ) {
134 // Decode empty space.
135 $last_slash_pos = strrpos( $id, '/' );
136 $id = urldecode( $id );
137
138 if ( false === $last_slash_pos ) {
139 return $id;
140 }
141
142 $is_double_slashed = substr( $id, $last_slash_pos - 1, 1 ) === '/';
143 if ( $is_double_slashed ) {
144 return $id;
145 }
146 return (
147 substr( $id, 0, $last_slash_pos )
148 . '/'
149 . substr( $id, $last_slash_pos )
150 );
151 }
152
153 /**
154 * Checks if a given request has access to read templates.
155 *
156 * @param WP_REST_Request $request Full details about the request.
157 * @return true|WP_Error True if the request has read access, WP_Error object otherwise.
158 */
159 public function get_items_permissions_check( $request ) {
160 return $this->permissions_check( $request );
161 }
162
163 /**
164 * Returns a list of templates.
165 *
166 * @param WP_REST_Request $request The request instance.
167 *
168 * @return WP_REST_Response
169 */
170 public function get_items( $request ) {
171 $query = array();
172 if ( isset( $request['wp_id'] ) ) {
173 $query['wp_id'] = $request['wp_id'];
174 }
175 if ( isset( $request['area'] ) ) {
176 $query['area'] = $request['area'];
177 }
178 if ( isset( $request['post_type'] ) ) {
179 $query['post_type'] = $request['post_type'];
180 }
181
182 $templates = array();
183 foreach ( gutenberg_get_block_templates( $query, $this->post_type ) as $template ) {
184 $data = $this->prepare_item_for_response( $template, $request );
185 $templates[] = $this->prepare_response_for_collection( $data );
186 }
187
188 return rest_ensure_response( $templates );
189 }
190
191 /**
192 * Checks if a given request has access to read a single template.
193 *
194 * @param WP_REST_Request $request Full details about the request.
195 * @return true|WP_Error True if the request has read access for the item, WP_Error object otherwise.
196 */
197 public function get_item_permissions_check( $request ) {
198 return $this->permissions_check( $request );
199 }
200
201 /**
202 * Returns the given template
203 *
204 * @param WP_REST_Request $request The request instance.
205 *
206 * @return WP_REST_Response|WP_Error
207 */
208 public function get_item( $request ) {
209 if ( isset( $request['source'] ) && 'theme' === $request['source'] ) {
210 $template = get_block_file_template( $request['id'], $this->post_type );
211 } else {
212 $template = gutenberg_get_block_template( $request['id'], $this->post_type );
213 }
214
215 if ( ! $template ) {
216 return new WP_Error( 'rest_template_not_found', __( 'No templates exist with that id.', 'gutenberg' ), array( 'status' => 404 ) );
217 }
218
219 return $this->prepare_item_for_response( $template, $request );
220 }
221
222 /**
223 * Checks if a given request has access to write a single template.
224 *
225 * @param WP_REST_Request $request Full details about the request.
226 * @return true|WP_Error True if the request has write access for the item, WP_Error object otherwise.
227 */
228 public function update_item_permissions_check( $request ) {
229 return $this->permissions_check( $request );
230 }
231
232 /**
233 * Updates a single template.
234 *
235 * @param WP_REST_Request $request Full details about the request.
236 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
237 */
238 public function update_item( $request ) {
239 $template = gutenberg_get_block_template( $request['id'], $this->post_type );
240 if ( ! $template ) {
241 return new WP_Error( 'rest_template_not_found', __( 'No templates exist with that id.', 'gutenberg' ), array( 'status' => 404 ) );
242 }
243
244 if ( isset( $request['source'] ) && 'theme' === $request['source'] ) {
245 wp_delete_post( $template->wp_id, true );
246 return $this->prepare_item_for_response( get_block_file_template( $request['id'], $this->post_type ), $request );
247 }
248
249 $changes = $this->prepare_item_for_database( $request );
250
251 if ( is_wp_error( $changes ) ) {
252 return $changes;
253 }
254
255 if ( 'custom' === $template->source ) {
256 $result = wp_update_post( wp_slash( (array) $changes ), true );
257 } else {
258 $result = wp_insert_post( wp_slash( (array) $changes ), true );
259 }
260 if ( is_wp_error( $result ) ) {
261 return $result;
262 }
263
264 $template = gutenberg_get_block_template( $request['id'], $this->post_type );
265 $fields_update = $this->update_additional_fields_for_object( $template, $request );
266 if ( is_wp_error( $fields_update ) ) {
267 return $fields_update;
268 }
269
270 return $this->prepare_item_for_response(
271 gutenberg_get_block_template( $request['id'], $this->post_type ),
272 $request
273 );
274 }
275
276 /**
277 * Checks if a given request has access to create a template.
278 *
279 * @param WP_REST_Request $request Full details about the request.
280 * @return true|WP_Error True if the request has access to create items, WP_Error object otherwise.
281 */
282 public function create_item_permissions_check( $request ) {
283 return $this->permissions_check( $request );
284 }
285
286 /**
287 * Creates a single template.
288 *
289 * @param WP_REST_Request $request Full details about the request.
290 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
291 */
292 public function create_item( $request ) {
293 $changes = $this->prepare_item_for_database( $request );
294
295 if ( is_wp_error( $changes ) ) {
296 return $changes;
297 }
298
299 $changes->post_name = $request['slug'];
300 $result = wp_insert_post( wp_slash( (array) $changes ), true );
301 if ( is_wp_error( $result ) ) {
302 return $result;
303 }
304 $posts = gutenberg_get_block_templates( array( 'wp_id' => $result ), $this->post_type );
305 if ( ! count( $posts ) ) {
306 return new WP_Error( 'rest_template_insert_error', __( 'No templates exist with that id.', 'gutenberg' ) );
307 }
308 $id = $posts[0]->id;
309 $template = gutenberg_get_block_template( $id, $this->post_type );
310 $fields_update = $this->update_additional_fields_for_object( $template, $request );
311 if ( is_wp_error( $fields_update ) ) {
312 return $fields_update;
313 }
314
315 return $this->prepare_item_for_response(
316 gutenberg_get_block_template( $id, $this->post_type ),
317 $request
318 );
319 }
320
321 /**
322 * Checks if a given request has access to delete a single template.
323 *
324 * @param WP_REST_Request $request Full details about the request.
325 * @return true|WP_Error True if the request has delete access for the item, WP_Error object otherwise.
326 */
327 public function delete_item_permissions_check( $request ) {
328 return $this->permissions_check( $request );
329 }
330
331 /**
332 * Deletes a single template.
333 *
334 * @param WP_REST_Request $request Full details about the request.
335 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
336 */
337 public function delete_item( $request ) {
338 $template = gutenberg_get_block_template( $request['id'], $this->post_type );
339 if ( ! $template ) {
340 return new WP_Error( 'rest_template_not_found', __( 'No templates exist with that id.', 'gutenberg' ), array( 'status' => 404 ) );
341 }
342 if ( 'custom' !== $template->source ) {
343 return new WP_Error( 'rest_invalid_template', __( 'Templates based on theme files can\'t be removed.', 'gutenberg' ), array( 'status' => 400 ) );
344 }
345
346 $id = $template->wp_id;
347 $force = (bool) $request['force'];
348
349 // If we're forcing, then delete permanently.
350 if ( $force ) {
351 $previous = $this->prepare_item_for_response( $template, $request );
352 wp_delete_post( $id, true );
353 $response = new WP_REST_Response();
354 $response->set_data(
355 array(
356 'deleted' => true,
357 'previous' => $previous->get_data(),
358 )
359 );
360
361 return $response;
362 }
363
364 // Otherwise, only trash if we haven't already.
365 if ( 'trash' === $template->status ) {
366 return new WP_Error(
367 'rest_template_already_trashed',
368 __( 'The template has already been deleted.', 'gutenberg' ),
369 array( 'status' => 410 )
370 );
371 }
372
373 wp_trash_post( $id );
374 $template->status = 'trash';
375 return $this->prepare_item_for_response( $template, $request );
376 }
377
378 /**
379 * Prepares a single template for create or update.
380 *
381 * @param WP_REST_Request $request Request object.
382 * @return stdClass Changes to pass to wp_update_post.
383 */
384 protected function prepare_item_for_database( $request ) {
385 $template = $request['id'] ? gutenberg_get_block_template( $request['id'], $this->post_type ) : null;
386 $changes = new stdClass();
387 if ( null === $template ) {
388 $changes->post_type = $this->post_type;
389 $changes->post_status = 'publish';
390 $changes->tax_input = array(
391 'wp_theme' => isset( $request['theme'] ) ? $request['theme'] : wp_get_theme()->get_stylesheet(),
392 );
393 } elseif ( 'custom' !== $template->source ) {
394 $changes->post_name = $template->slug;
395 $changes->post_type = $this->post_type;
396 $changes->post_status = 'publish';
397 $changes->tax_input = array(
398 'wp_theme' => $template->theme,
399 );
400 $changes->meta_input = array(
401 'origin' => $template->source,
402 );
403 } else {
404 $changes->post_name = $template->slug;
405 $changes->ID = $template->wp_id;
406 $changes->post_status = 'publish';
407 }
408 if ( isset( $request['content'] ) ) {
409 $changes->post_content = $request['content'];
410 } elseif ( null !== $template && 'custom' !== $template->source ) {
411 $changes->post_content = $template->content;
412 }
413 if ( isset( $request['title'] ) ) {
414 $changes->post_title = $request['title'];
415 } elseif ( null !== $template && 'custom' !== $template->source ) {
416 $changes->post_title = $template->title;
417 }
418 if ( isset( $request['description'] ) ) {
419 $changes->post_excerpt = $request['description'];
420 } elseif ( null !== $template && 'custom' !== $template->source ) {
421 $changes->post_excerpt = $template->description;
422 }
423
424 if ( 'wp_template_part' === $this->post_type ) {
425 if ( isset( $request['area'] ) ) {
426 $changes->tax_input['wp_template_part_area'] = _filter_block_template_part_area( $request['area'] );
427 } elseif ( null !== $template && 'custom' !== $template->source && $template->area ) {
428 $changes->tax_input['wp_template_part_area'] = _filter_block_template_part_area( $template->area );
429 } elseif ( ! $template->area ) {
430 $changes->tax_input['wp_template_part_area'] = WP_TEMPLATE_PART_AREA_UNCATEGORIZED;
431 }
432 }
433
434 if ( ! empty( $request['author'] ) ) {
435 $post_author = (int) $request['author'];
436
437 if ( get_current_user_id() !== $post_author ) {
438 $user_obj = get_userdata( $post_author );
439
440 if ( ! $user_obj ) {
441 return new WP_Error(
442 'rest_invalid_author',
443 __( 'Invalid author ID.', 'gutenberg' ),
444 array( 'status' => 400 )
445 );
446 }
447 }
448
449 $changes->post_author = $post_author;
450 }
451
452 return $changes;
453 }
454
455 /**
456 * Prepare a single template output for response
457 *
458 * @param Gutenberg_Block_Template $template Template instance.
459 * @param WP_REST_Request $request Request object.
460 *
461 * @return WP_REST_Response $data
462 */
463 public function prepare_item_for_response( $template, $request ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
464 $result = array(
465 'id' => $template->id,
466 'theme' => $template->theme,
467 'content' => array( 'raw' => $template->content ),
468 'slug' => $template->slug,
469 'source' => $template->source,
470 'origin' => $template->origin,
471 'type' => $template->type,
472 'description' => $template->description,
473 'title' => array(
474 'raw' => $template->title,
475 'rendered' => $template->title,
476 ),
477 'status' => $template->status,
478 'wp_id' => $template->wp_id,
479 'has_theme_file' => $template->has_theme_file,
480 'author' => (int) $template->author,
481 );
482
483 if ( 'wp_template' === $template->type ) {
484 $result['is_custom'] = $template->is_custom;
485 }
486
487 if ( 'wp_template_part' === $template->type ) {
488 $result['area'] = $template->area;
489 }
490
491 $result = $this->add_additional_fields_to_object( $result, $request );
492
493 $response = rest_ensure_response( $result );
494 $links = $this->prepare_links( $template->id );
495 $response->add_links( $links );
496 if ( ! empty( $links['self']['href'] ) ) {
497 $actions = $this->get_available_actions();
498 $self = $links['self']['href'];
499 foreach ( $actions as $rel ) {
500 $response->add_link( $rel, $self );
501 }
502 }
503
504 return $response;
505 }
506
507
508 /**
509 * Prepares links for the request.
510 *
511 * @param integer $id ID.
512 * @return array Links for the given post.
513 */
514 protected function prepare_links( $id ) {
515 $base = sprintf( '%s/%s', $this->namespace, $this->rest_base );
516
517 $links = array(
518 'self' => array(
519 'href' => rest_url( trailingslashit( $base ) . $id ),
520 ),
521 'collection' => array(
522 'href' => rest_url( $base ),
523 ),
524 'about' => array(
525 'href' => rest_url( 'wp/v2/types/' . $this->post_type ),
526 ),
527 );
528
529 return $links;
530 }
531
532 /**
533 * Get the link relations available for the post and current user.
534 *
535 * @return array List of link relations.
536 */
537 protected function get_available_actions() {
538 $rels = array();
539
540 $post_type = get_post_type_object( $this->post_type );
541
542 if ( current_user_can( $post_type->cap->publish_posts ) ) {
543 $rels[] = 'https://api.w.org/action-publish';
544 }
545
546 if ( current_user_can( 'unfiltered_html' ) ) {
547 $rels[] = 'https://api.w.org/action-unfiltered-html';
548 }
549
550 return $rels;
551 }
552
553 /**
554 * Retrieves the query params for the posts collection.
555 *
556 * @return array Collection parameters.
557 */
558 public function get_collection_params() {
559 return array(
560 'context' => $this->get_context_param(),
561 'wp_id' => array(
562 'description' => __( 'Limit to the specified post id.', 'gutenberg' ),
563 'type' => 'integer',
564 ),
565 'area' => array(
566 'description' => __( 'Limit to the specified template part area.', 'gutenberg' ),
567 'type' => 'string',
568 ),
569 'post_type' => array(
570 'description' => __( 'Post type to get the templates for.', 'gutenberg' ),
571 'type' => 'string',
572 ),
573 );
574 }
575
576 /**
577 * Retrieves the block type' schema, conforming to JSON Schema.
578 *
579 * @return array Item schema data.
580 */
581 public function get_item_schema() {
582 if ( $this->schema ) {
583 return $this->add_additional_fields_schema( $this->schema );
584 }
585
586 $schema = array(
587 '$schema' => 'http://json-schema.org/draft-04/schema#',
588 'title' => $this->post_type,
589 'type' => 'object',
590 'properties' => array(
591 'id' => array(
592 'description' => __( 'ID of template.', 'gutenberg' ),
593 'type' => 'string',
594 'context' => array( 'embed', 'view', 'edit' ),
595 'readonly' => true,
596 ),
597 'slug' => array(
598 'description' => __( 'Unique slug identifying the template.', 'gutenberg' ),
599 'type' => 'string',
600 'context' => array( 'embed', 'view', 'edit' ),
601 'required' => true,
602 'minLength' => 1,
603 'pattern' => '[a-zA-Z0-9_\-]+',
604 ),
605 'theme' => array(
606 'description' => __( 'Theme identifier for the template.', 'gutenberg' ),
607 'type' => 'string',
608 'context' => array( 'embed', 'view', 'edit' ),
609 ),
610 'source' => array(
611 'description' => __( 'Source of template', 'gutenberg' ),
612 'type' => 'string',
613 'context' => array( 'embed', 'view', 'edit' ),
614 'readonly' => true,
615 ),
616 'origin' => array(
617 'description' => __( 'Source of customized template', 'gutenberg' ),
618 'type' => 'string',
619 'context' => array( 'embed', 'view', 'edit' ),
620 'readonly' => true,
621 ),
622 'content' => array(
623 'description' => __( 'Content of template.', 'gutenberg' ),
624 'type' => array( 'object', 'string' ),
625 'default' => '',
626 'context' => array( 'embed', 'view', 'edit' ),
627 ),
628 'title' => array(
629 'description' => __( 'Title of template.', 'gutenberg' ),
630 'type' => array( 'object', 'string' ),
631 'default' => '',
632 'context' => array( 'embed', 'view', 'edit' ),
633 ),
634 'description' => array(
635 'description' => __( 'Description of template.', 'gutenberg' ),
636 'type' => 'string',
637 'default' => '',
638 'context' => array( 'embed', 'view', 'edit' ),
639 ),
640 'status' => array(
641 'description' => __( 'Status of template.', 'gutenberg' ),
642 'type' => 'string',
643 'default' => 'publish',
644 'context' => array( 'embed', 'view', 'edit' ),
645 ),
646 'wp_id' => array(
647 'description' => __( 'Post ID.', 'gutenberg' ),
648 'type' => 'integer',
649 'context' => array( 'embed', 'view', 'edit' ),
650 'readonly' => true,
651 ),
652 'has_theme_file' => array(
653 'description' => __( 'Theme file exists.', 'gutenberg' ),
654 'type' => 'bool',
655 'context' => array( 'embed', 'view', 'edit' ),
656 'readonly' => true,
657 ),
658 'author' => array(
659 'description' => __( 'The ID for the author of the template.', 'gutenberg' ),
660 'type' => 'integer',
661 'context' => array( 'view', 'edit', 'embed' ),
662 ),
663 ),
664 );
665
666 if ( 'wp_template' === $this->post_type ) {
667 $schema['properties']['is_custom'] = array(
668 'description' => __( 'Whether a template is a custom template.', 'gutenberg' ),
669 'type' => 'bool',
670 'context' => array( 'embed', 'view', 'edit' ),
671 'readonly' => true,
672 );
673 }
674
675 if ( 'wp_template_part' === $this->post_type ) {
676 $schema['properties']['area'] = array(
677 'description' => __( 'Where the template part is intended for use (header, footer, etc.)', 'gutenberg' ),
678 'type' => 'string',
679 'context' => array( 'embed', 'view', 'edit' ),
680 );
681 }
682
683 $this->schema = $schema;
684
685 return $this->add_additional_fields_schema( $this->schema );
686 }
687 }
688