Diff: STRATO-apps/wordpress_03/app/wp-includes/rest-api/endpoints/class-wp-rest-templates-controller.php

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