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