class-wp-rest-templates-controller.php 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953
  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. * Base Templates REST API Controller.
  11. *
  12. * @since 5.8.0
  13. *
  14. * @see WP_REST_Controller
  15. */
  16. class WP_REST_Templates_Controller extends WP_REST_Controller {
  17. /**
  18. * Post type.
  19. *
  20. * @since 5.8.0
  21. * @var string
  22. */
  23. protected $post_type;
  24. /**
  25. * Constructor.
  26. *
  27. * @since 5.8.0
  28. *
  29. * @param string $post_type Post type.
  30. */
  31. public function __construct( $post_type ) {
  32. $this->post_type = $post_type;
  33. $obj = get_post_type_object( $post_type );
  34. $this->rest_base = ! empty( $obj->rest_base ) ? $obj->rest_base : $obj->name;
  35. $this->namespace = ! empty( $obj->rest_namespace ) ? $obj->rest_namespace : 'wp/v2';
  36. }
  37. /**
  38. * Registers the controllers routes.
  39. *
  40. * @since 5.8.0
  41. * @since 6.1.0 Endpoint for fallback template content.
  42. */
  43. public function register_routes() {
  44. // Lists all templates.
  45. register_rest_route(
  46. $this->namespace,
  47. '/' . $this->rest_base,
  48. array(
  49. array(
  50. 'methods' => WP_REST_Server::READABLE,
  51. 'callback' => array( $this, 'get_items' ),
  52. 'permission_callback' => array( $this, 'get_items_permissions_check' ),
  53. 'args' => $this->get_collection_params(),
  54. ),
  55. array(
  56. 'methods' => WP_REST_Server::CREATABLE,
  57. 'callback' => array( $this, 'create_item' ),
  58. 'permission_callback' => array( $this, 'create_item_permissions_check' ),
  59. 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ),
  60. ),
  61. 'schema' => array( $this, 'get_public_item_schema' ),
  62. )
  63. );
  64. // Get fallback template content.
  65. register_rest_route(
  66. $this->namespace,
  67. '/' . $this->rest_base . '/lookup',
  68. array(
  69. array(
  70. 'methods' => WP_REST_Server::READABLE,
  71. 'callback' => array( $this, 'get_template_fallback' ),
  72. 'permission_callback' => array( $this, 'get_item_permissions_check' ),
  73. 'args' => array(
  74. 'slug' => array(
  75. 'description' => __( 'The slug of the template to get the fallback for' ),
  76. 'type' => 'string',
  77. 'required' => true,
  78. ),
  79. 'is_custom' => array(
  80. 'description' => __( 'Indicates if a template is custom or part of the template hierarchy' ),
  81. 'type' => 'boolean',
  82. ),
  83. 'template_prefix' => array(
  84. '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`' ),
  85. 'type' => 'string',
  86. ),
  87. ),
  88. ),
  89. )
  90. );
  91. // Lists/updates a single template based on the given id.
  92. register_rest_route(
  93. $this->namespace,
  94. // The route.
  95. sprintf(
  96. '/%s/(?P<id>%s%s)',
  97. $this->rest_base,
  98. // Matches theme's directory: `/themes/<subdirectory>/<theme>/` or `/themes/<theme>/`.
  99. // Excludes invalid directory name characters: `/:<>*?"|`.
  100. '([^\/:<>\*\?"\|]+(?:\/[^\/:<>\*\?"\|]+)?)',
  101. // Matches the template name.
  102. '[\/\w-]+'
  103. ),
  104. array(
  105. 'args' => array(
  106. 'id' => array(
  107. 'description' => __( 'The id of a template' ),
  108. 'type' => 'string',
  109. 'sanitize_callback' => array( $this, '_sanitize_template_id' ),
  110. ),
  111. ),
  112. array(
  113. 'methods' => WP_REST_Server::READABLE,
  114. 'callback' => array( $this, 'get_item' ),
  115. 'permission_callback' => array( $this, 'get_item_permissions_check' ),
  116. 'args' => array(
  117. 'context' => $this->get_context_param( array( 'default' => 'view' ) ),
  118. ),
  119. ),
  120. array(
  121. 'methods' => WP_REST_Server::EDITABLE,
  122. 'callback' => array( $this, 'update_item' ),
  123. 'permission_callback' => array( $this, 'update_item_permissions_check' ),
  124. 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
  125. ),
  126. array(
  127. 'methods' => WP_REST_Server::DELETABLE,
  128. 'callback' => array( $this, 'delete_item' ),
  129. 'permission_callback' => array( $this, 'delete_item_permissions_check' ),
  130. 'args' => array(
  131. 'force' => array(
  132. 'type' => 'boolean',
  133. 'default' => false,
  134. 'description' => __( 'Whether to bypass Trash and force deletion.' ),
  135. ),
  136. ),
  137. ),
  138. 'schema' => array( $this, 'get_public_item_schema' ),
  139. )
  140. );
  141. }
  142. /**
  143. * Returns the fallback template for the given slug.
  144. *
  145. * @since 6.1.0
  146. *
  147. * @param WP_REST_Request $request The request instance.
  148. * @return WP_REST_Response|WP_Error
  149. */
  150. public function get_template_fallback( $request ) {
  151. $hierarchy = get_template_hierarchy( $request['slug'], $request['is_custom'], $request['template_prefix'] );
  152. $fallback_template = resolve_block_template( $request['slug'], $hierarchy, '' );
  153. $response = $this->prepare_item_for_response( $fallback_template, $request );
  154. return rest_ensure_response( $response );
  155. }
  156. /**
  157. * Checks if the user has permissions to make the request.
  158. *
  159. * @since 5.8.0
  160. *
  161. * @param WP_REST_Request $request Full details about the request.
  162. * @return true|WP_Error True if the request has read access, WP_Error object otherwise.
  163. */
  164. protected function permissions_check( $request ) {
  165. // Verify if the current user has edit_theme_options capability.
  166. // This capability is required to edit/view/delete templates.
  167. if ( ! current_user_can( 'edit_theme_options' ) ) {
  168. return new WP_Error(
  169. 'rest_cannot_manage_templates',
  170. __( 'Sorry, you are not allowed to access the templates on this site.' ),
  171. array(
  172. 'status' => rest_authorization_required_code(),
  173. )
  174. );
  175. }
  176. return true;
  177. }
  178. /**
  179. * Requesting this endpoint for a template like 'twentytwentytwo//home'
  180. * requires using a path like /wp/v2/templates/twentytwentytwo//home. There
  181. * are special cases when WordPress routing corrects the name to contain
  182. * only a single slash like 'twentytwentytwo/home'.
  183. *
  184. * This method doubles the last slash if it's not already doubled. It relies
  185. * on the template ID format {theme_name}//{template_slug} and the fact that
  186. * slugs cannot contain slashes.
  187. *
  188. * @since 5.9.0
  189. * @see https://core.trac.wordpress.org/ticket/54507
  190. *
  191. * @param string $id Template ID.
  192. * @return string Sanitized template ID.
  193. */
  194. public function _sanitize_template_id( $id ) {
  195. $id = urldecode( $id );
  196. $last_slash_pos = strrpos( $id, '/' );
  197. if ( false === $last_slash_pos ) {
  198. return $id;
  199. }
  200. $is_double_slashed = substr( $id, $last_slash_pos - 1, 1 ) === '/';
  201. if ( $is_double_slashed ) {
  202. return $id;
  203. }
  204. return (
  205. substr( $id, 0, $last_slash_pos )
  206. . '/'
  207. . substr( $id, $last_slash_pos )
  208. );
  209. }
  210. /**
  211. * Checks if a given request has access to read templates.
  212. *
  213. * @since 5.8.0
  214. *
  215. * @param WP_REST_Request $request Full details about the request.
  216. * @return true|WP_Error True if the request has read access, WP_Error object otherwise.
  217. */
  218. public function get_items_permissions_check( $request ) {
  219. return $this->permissions_check( $request );
  220. }
  221. /**
  222. * Returns a list of templates.
  223. *
  224. * @since 5.8.0
  225. *
  226. * @param WP_REST_Request $request The request instance.
  227. * @return WP_REST_Response
  228. */
  229. public function get_items( $request ) {
  230. $query = array();
  231. if ( isset( $request['wp_id'] ) ) {
  232. $query['wp_id'] = $request['wp_id'];
  233. }
  234. if ( isset( $request['area'] ) ) {
  235. $query['area'] = $request['area'];
  236. }
  237. if ( isset( $request['post_type'] ) ) {
  238. $query['post_type'] = $request['post_type'];
  239. }
  240. $templates = array();
  241. foreach ( get_block_templates( $query, $this->post_type ) as $template ) {
  242. $data = $this->prepare_item_for_response( $template, $request );
  243. $templates[] = $this->prepare_response_for_collection( $data );
  244. }
  245. return rest_ensure_response( $templates );
  246. }
  247. /**
  248. * Checks if a given request has access to read a single template.
  249. *
  250. * @since 5.8.0
  251. *
  252. * @param WP_REST_Request $request Full details about the request.
  253. * @return true|WP_Error True if the request has read access for the item, WP_Error object otherwise.
  254. */
  255. public function get_item_permissions_check( $request ) {
  256. return $this->permissions_check( $request );
  257. }
  258. /**
  259. * Returns the given template
  260. *
  261. * @since 5.8.0
  262. *
  263. * @param WP_REST_Request $request The request instance.
  264. * @return WP_REST_Response|WP_Error
  265. */
  266. public function get_item( $request ) {
  267. if ( isset( $request['source'] ) && 'theme' === $request['source'] ) {
  268. $template = get_block_file_template( $request['id'], $this->post_type );
  269. } else {
  270. $template = get_block_template( $request['id'], $this->post_type );
  271. }
  272. if ( ! $template ) {
  273. return new WP_Error( 'rest_template_not_found', __( 'No templates exist with that id.' ), array( 'status' => 404 ) );
  274. }
  275. return $this->prepare_item_for_response( $template, $request );
  276. }
  277. /**
  278. * Checks if a given request has access to write a single template.
  279. *
  280. * @since 5.8.0
  281. *
  282. * @param WP_REST_Request $request Full details about the request.
  283. * @return true|WP_Error True if the request has write access for the item, WP_Error object otherwise.
  284. */
  285. public function update_item_permissions_check( $request ) {
  286. return $this->permissions_check( $request );
  287. }
  288. /**
  289. * Updates a single template.
  290. *
  291. * @since 5.8.0
  292. *
  293. * @param WP_REST_Request $request Full details about the request.
  294. * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
  295. */
  296. public function update_item( $request ) {
  297. $template = get_block_template( $request['id'], $this->post_type );
  298. if ( ! $template ) {
  299. return new WP_Error( 'rest_template_not_found', __( 'No templates exist with that id.' ), array( 'status' => 404 ) );
  300. }
  301. $post_before = get_post( $template->wp_id );
  302. if ( isset( $request['source'] ) && 'theme' === $request['source'] ) {
  303. wp_delete_post( $template->wp_id, true );
  304. $request->set_param( 'context', 'edit' );
  305. $template = get_block_template( $request['id'], $this->post_type );
  306. $response = $this->prepare_item_for_response( $template, $request );
  307. return rest_ensure_response( $response );
  308. }
  309. $changes = $this->prepare_item_for_database( $request );
  310. if ( is_wp_error( $changes ) ) {
  311. return $changes;
  312. }
  313. if ( 'custom' === $template->source ) {
  314. $update = true;
  315. $result = wp_update_post( wp_slash( (array) $changes ), false );
  316. } else {
  317. $update = false;
  318. $post_before = null;
  319. $result = wp_insert_post( wp_slash( (array) $changes ), false );
  320. }
  321. if ( is_wp_error( $result ) ) {
  322. if ( 'db_update_error' === $result->get_error_code() ) {
  323. $result->add_data( array( 'status' => 500 ) );
  324. } else {
  325. $result->add_data( array( 'status' => 400 ) );
  326. }
  327. return $result;
  328. }
  329. $template = get_block_template( $request['id'], $this->post_type );
  330. $fields_update = $this->update_additional_fields_for_object( $template, $request );
  331. if ( is_wp_error( $fields_update ) ) {
  332. return $fields_update;
  333. }
  334. $request->set_param( 'context', 'edit' );
  335. $post = get_post( $template->wp_id );
  336. /** This action is documented in wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php */
  337. do_action( "rest_after_insert_{$this->post_type}", $post, $request, false );
  338. wp_after_insert_post( $post, $update, $post_before );
  339. $response = $this->prepare_item_for_response( $template, $request );
  340. return rest_ensure_response( $response );
  341. }
  342. /**
  343. * Checks if a given request has access to create a template.
  344. *
  345. * @since 5.8.0
  346. *
  347. * @param WP_REST_Request $request Full details about the request.
  348. * @return true|WP_Error True if the request has access to create items, WP_Error object otherwise.
  349. */
  350. public function create_item_permissions_check( $request ) {
  351. return $this->permissions_check( $request );
  352. }
  353. /**
  354. * Creates a single template.
  355. *
  356. * @since 5.8.0
  357. *
  358. * @param WP_REST_Request $request Full details about the request.
  359. * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
  360. */
  361. public function create_item( $request ) {
  362. $prepared_post = $this->prepare_item_for_database( $request );
  363. if ( is_wp_error( $prepared_post ) ) {
  364. return $prepared_post;
  365. }
  366. $prepared_post->post_name = $request['slug'];
  367. $post_id = wp_insert_post( wp_slash( (array) $prepared_post ), true );
  368. if ( is_wp_error( $post_id ) ) {
  369. if ( 'db_insert_error' === $post_id->get_error_code() ) {
  370. $post_id->add_data( array( 'status' => 500 ) );
  371. } else {
  372. $post_id->add_data( array( 'status' => 400 ) );
  373. }
  374. return $post_id;
  375. }
  376. $posts = get_block_templates( array( 'wp_id' => $post_id ), $this->post_type );
  377. if ( ! count( $posts ) ) {
  378. return new WP_Error( 'rest_template_insert_error', __( 'No templates exist with that id.' ), array( 'status' => 400 ) );
  379. }
  380. $id = $posts[0]->id;
  381. $post = get_post( $post_id );
  382. $template = get_block_template( $id, $this->post_type );
  383. $fields_update = $this->update_additional_fields_for_object( $template, $request );
  384. if ( is_wp_error( $fields_update ) ) {
  385. return $fields_update;
  386. }
  387. /** This action is documented in wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php */
  388. do_action( "rest_after_insert_{$this->post_type}", $post, $request, true );
  389. wp_after_insert_post( $post, false, null );
  390. $response = $this->prepare_item_for_response( $template, $request );
  391. $response = rest_ensure_response( $response );
  392. $response->set_status( 201 );
  393. $response->header( 'Location', rest_url( sprintf( '%s/%s/%s', $this->namespace, $this->rest_base, $template->id ) ) );
  394. return $response;
  395. }
  396. /**
  397. * Checks if a given request has access to delete a single template.
  398. *
  399. * @since 5.8.0
  400. *
  401. * @param WP_REST_Request $request Full details about the request.
  402. * @return true|WP_Error True if the request has delete access for the item, WP_Error object otherwise.
  403. */
  404. public function delete_item_permissions_check( $request ) {
  405. return $this->permissions_check( $request );
  406. }
  407. /**
  408. * Deletes a single template.
  409. *
  410. * @since 5.8.0
  411. *
  412. * @param WP_REST_Request $request Full details about the request.
  413. * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
  414. */
  415. public function delete_item( $request ) {
  416. $template = get_block_template( $request['id'], $this->post_type );
  417. if ( ! $template ) {
  418. return new WP_Error( 'rest_template_not_found', __( 'No templates exist with that id.' ), array( 'status' => 404 ) );
  419. }
  420. if ( 'custom' !== $template->source ) {
  421. return new WP_Error( 'rest_invalid_template', __( 'Templates based on theme files can\'t be removed.' ), array( 'status' => 400 ) );
  422. }
  423. $id = $template->wp_id;
  424. $force = (bool) $request['force'];
  425. $request->set_param( 'context', 'edit' );
  426. // If we're forcing, then delete permanently.
  427. if ( $force ) {
  428. $previous = $this->prepare_item_for_response( $template, $request );
  429. $result = wp_delete_post( $id, true );
  430. $response = new WP_REST_Response();
  431. $response->set_data(
  432. array(
  433. 'deleted' => true,
  434. 'previous' => $previous->get_data(),
  435. )
  436. );
  437. } else {
  438. // Otherwise, only trash if we haven't already.
  439. if ( 'trash' === $template->status ) {
  440. return new WP_Error(
  441. 'rest_template_already_trashed',
  442. __( 'The template has already been deleted.' ),
  443. array( 'status' => 410 )
  444. );
  445. }
  446. // (Note that internally this falls through to `wp_delete_post()`
  447. // if the Trash is disabled.)
  448. $result = wp_trash_post( $id );
  449. $template->status = 'trash';
  450. $response = $this->prepare_item_for_response( $template, $request );
  451. }
  452. if ( ! $result ) {
  453. return new WP_Error(
  454. 'rest_cannot_delete',
  455. __( 'The template cannot be deleted.' ),
  456. array( 'status' => 500 )
  457. );
  458. }
  459. return $response;
  460. }
  461. /**
  462. * Prepares a single template for create or update.
  463. *
  464. * @since 5.8.0
  465. *
  466. * @param WP_REST_Request $request Request object.
  467. * @return stdClass Changes to pass to wp_update_post.
  468. */
  469. protected function prepare_item_for_database( $request ) {
  470. $template = $request['id'] ? get_block_template( $request['id'], $this->post_type ) : null;
  471. $changes = new stdClass();
  472. if ( null === $template ) {
  473. $changes->post_type = $this->post_type;
  474. $changes->post_status = 'publish';
  475. $changes->tax_input = array(
  476. 'wp_theme' => isset( $request['theme'] ) ? $request['theme'] : get_stylesheet(),
  477. );
  478. } elseif ( 'custom' !== $template->source ) {
  479. $changes->post_name = $template->slug;
  480. $changes->post_type = $this->post_type;
  481. $changes->post_status = 'publish';
  482. $changes->tax_input = array(
  483. 'wp_theme' => $template->theme,
  484. );
  485. $changes->meta_input = array(
  486. 'origin' => $template->source,
  487. );
  488. } else {
  489. $changes->post_name = $template->slug;
  490. $changes->ID = $template->wp_id;
  491. $changes->post_status = 'publish';
  492. }
  493. if ( isset( $request['content'] ) ) {
  494. if ( is_string( $request['content'] ) ) {
  495. $changes->post_content = $request['content'];
  496. } elseif ( isset( $request['content']['raw'] ) ) {
  497. $changes->post_content = $request['content']['raw'];
  498. }
  499. } elseif ( null !== $template && 'custom' !== $template->source ) {
  500. $changes->post_content = $template->content;
  501. }
  502. if ( isset( $request['title'] ) ) {
  503. if ( is_string( $request['title'] ) ) {
  504. $changes->post_title = $request['title'];
  505. } elseif ( ! empty( $request['title']['raw'] ) ) {
  506. $changes->post_title = $request['title']['raw'];
  507. }
  508. } elseif ( null !== $template && 'custom' !== $template->source ) {
  509. $changes->post_title = $template->title;
  510. }
  511. if ( isset( $request['description'] ) ) {
  512. $changes->post_excerpt = $request['description'];
  513. } elseif ( null !== $template && 'custom' !== $template->source ) {
  514. $changes->post_excerpt = $template->description;
  515. }
  516. if ( 'wp_template' === $this->post_type && isset( $request['is_wp_suggestion'] ) ) {
  517. $changes->meta_input = wp_parse_args(
  518. array(
  519. 'is_wp_suggestion' => $request['is_wp_suggestion'],
  520. ),
  521. $changes->meta_input = array()
  522. );
  523. }
  524. if ( 'wp_template_part' === $this->post_type ) {
  525. if ( isset( $request['area'] ) ) {
  526. $changes->tax_input['wp_template_part_area'] = _filter_block_template_part_area( $request['area'] );
  527. } elseif ( null !== $template && 'custom' !== $template->source && $template->area ) {
  528. $changes->tax_input['wp_template_part_area'] = _filter_block_template_part_area( $template->area );
  529. } elseif ( ! $template->area ) {
  530. $changes->tax_input['wp_template_part_area'] = WP_TEMPLATE_PART_AREA_UNCATEGORIZED;
  531. }
  532. }
  533. if ( ! empty( $request['author'] ) ) {
  534. $post_author = (int) $request['author'];
  535. if ( get_current_user_id() !== $post_author ) {
  536. $user_obj = get_userdata( $post_author );
  537. if ( ! $user_obj ) {
  538. return new WP_Error(
  539. 'rest_invalid_author',
  540. __( 'Invalid author ID.' ),
  541. array( 'status' => 400 )
  542. );
  543. }
  544. }
  545. $changes->post_author = $post_author;
  546. }
  547. return $changes;
  548. }
  549. /**
  550. * Prepare a single template output for response
  551. *
  552. * @since 5.8.0
  553. * @since 5.9.0 Renamed `$template` to `$item` to match parent class for PHP 8 named parameter support.
  554. *
  555. * @param WP_Block_Template $item Template instance.
  556. * @param WP_REST_Request $request Request object.
  557. * @return WP_REST_Response Response object.
  558. */
  559. public function prepare_item_for_response( $item, $request ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
  560. // Restores the more descriptive, specific name for use within this method.
  561. $template = $item;
  562. $fields = $this->get_fields_for_response( $request );
  563. // Base fields for every template.
  564. $data = array();
  565. if ( rest_is_field_included( 'id', $fields ) ) {
  566. $data['id'] = $template->id;
  567. }
  568. if ( rest_is_field_included( 'theme', $fields ) ) {
  569. $data['theme'] = $template->theme;
  570. }
  571. if ( rest_is_field_included( 'content', $fields ) ) {
  572. $data['content'] = array();
  573. }
  574. if ( rest_is_field_included( 'content.raw', $fields ) ) {
  575. $data['content']['raw'] = $template->content;
  576. }
  577. if ( rest_is_field_included( 'content.block_version', $fields ) ) {
  578. $data['content']['block_version'] = block_version( $template->content );
  579. }
  580. if ( rest_is_field_included( 'slug', $fields ) ) {
  581. $data['slug'] = $template->slug;
  582. }
  583. if ( rest_is_field_included( 'source', $fields ) ) {
  584. $data['source'] = $template->source;
  585. }
  586. if ( rest_is_field_included( 'origin', $fields ) ) {
  587. $data['origin'] = $template->origin;
  588. }
  589. if ( rest_is_field_included( 'type', $fields ) ) {
  590. $data['type'] = $template->type;
  591. }
  592. if ( rest_is_field_included( 'description', $fields ) ) {
  593. $data['description'] = $template->description;
  594. }
  595. if ( rest_is_field_included( 'title', $fields ) ) {
  596. $data['title'] = array();
  597. }
  598. if ( rest_is_field_included( 'title.raw', $fields ) ) {
  599. $data['title']['raw'] = $template->title;
  600. }
  601. if ( rest_is_field_included( 'title.rendered', $fields ) ) {
  602. if ( $template->wp_id ) {
  603. /** This filter is documented in wp-includes/post-template.php */
  604. $data['title']['rendered'] = apply_filters( 'the_title', $template->title, $template->wp_id );
  605. } else {
  606. $data['title']['rendered'] = $template->title;
  607. }
  608. }
  609. if ( rest_is_field_included( 'status', $fields ) ) {
  610. $data['status'] = $template->status;
  611. }
  612. if ( rest_is_field_included( 'wp_id', $fields ) ) {
  613. $data['wp_id'] = (int) $template->wp_id;
  614. }
  615. if ( rest_is_field_included( 'has_theme_file', $fields ) ) {
  616. $data['has_theme_file'] = (bool) $template->has_theme_file;
  617. }
  618. if ( rest_is_field_included( 'is_custom', $fields ) && 'wp_template' === $template->type ) {
  619. $data['is_custom'] = $template->is_custom;
  620. }
  621. if ( rest_is_field_included( 'author', $fields ) ) {
  622. $data['author'] = (int) $template->author;
  623. }
  624. if ( rest_is_field_included( 'area', $fields ) && 'wp_template_part' === $template->type ) {
  625. $data['area'] = $template->area;
  626. }
  627. $context = ! empty( $request['context'] ) ? $request['context'] : 'view';
  628. $data = $this->add_additional_fields_to_object( $data, $request );
  629. $data = $this->filter_response_by_context( $data, $context );
  630. // Wrap the data in a response object.
  631. $response = rest_ensure_response( $data );
  632. if ( rest_is_field_included( '_links', $fields ) || rest_is_field_included( '_embedded', $fields ) ) {
  633. $links = $this->prepare_links( $template->id );
  634. $response->add_links( $links );
  635. if ( ! empty( $links['self']['href'] ) ) {
  636. $actions = $this->get_available_actions();
  637. $self = $links['self']['href'];
  638. foreach ( $actions as $rel ) {
  639. $response->add_link( $rel, $self );
  640. }
  641. }
  642. }
  643. return $response;
  644. }
  645. /**
  646. * Prepares links for the request.
  647. *
  648. * @since 5.8.0
  649. *
  650. * @param integer $id ID.
  651. * @return array Links for the given post.
  652. */
  653. protected function prepare_links( $id ) {
  654. $links = array(
  655. 'self' => array(
  656. 'href' => rest_url( rest_get_route_for_post( $id ) ),
  657. ),
  658. 'collection' => array(
  659. 'href' => rest_url( rest_get_route_for_post_type_items( $this->post_type ) ),
  660. ),
  661. 'about' => array(
  662. 'href' => rest_url( 'wp/v2/types/' . $this->post_type ),
  663. ),
  664. );
  665. return $links;
  666. }
  667. /**
  668. * Get the link relations available for the post and current user.
  669. *
  670. * @since 5.8.0
  671. *
  672. * @return string[] List of link relations.
  673. */
  674. protected function get_available_actions() {
  675. $rels = array();
  676. $post_type = get_post_type_object( $this->post_type );
  677. if ( current_user_can( $post_type->cap->publish_posts ) ) {
  678. $rels[] = 'https://api.w.org/action-publish';
  679. }
  680. if ( current_user_can( 'unfiltered_html' ) ) {
  681. $rels[] = 'https://api.w.org/action-unfiltered-html';
  682. }
  683. return $rels;
  684. }
  685. /**
  686. * Retrieves the query params for the posts collection.
  687. *
  688. * @since 5.8.0
  689. * @since 5.9.0 Added `'area'` and `'post_type'`.
  690. *
  691. * @return array Collection parameters.
  692. */
  693. public function get_collection_params() {
  694. return array(
  695. 'context' => $this->get_context_param( array( 'default' => 'view' ) ),
  696. 'wp_id' => array(
  697. 'description' => __( 'Limit to the specified post id.' ),
  698. 'type' => 'integer',
  699. ),
  700. 'area' => array(
  701. 'description' => __( 'Limit to the specified template part area.' ),
  702. 'type' => 'string',
  703. ),
  704. 'post_type' => array(
  705. 'description' => __( 'Post type to get the templates for.' ),
  706. 'type' => 'string',
  707. ),
  708. );
  709. }
  710. /**
  711. * Retrieves the block type' schema, conforming to JSON Schema.
  712. *
  713. * @since 5.8.0
  714. * @since 5.9.0 Added `'area'`.
  715. *
  716. * @return array Item schema data.
  717. */
  718. public function get_item_schema() {
  719. if ( $this->schema ) {
  720. return $this->add_additional_fields_schema( $this->schema );
  721. }
  722. $schema = array(
  723. '$schema' => 'http://json-schema.org/draft-04/schema#',
  724. 'title' => $this->post_type,
  725. 'type' => 'object',
  726. 'properties' => array(
  727. 'id' => array(
  728. 'description' => __( 'ID of template.' ),
  729. 'type' => 'string',
  730. 'context' => array( 'embed', 'view', 'edit' ),
  731. 'readonly' => true,
  732. ),
  733. 'slug' => array(
  734. 'description' => __( 'Unique slug identifying the template.' ),
  735. 'type' => 'string',
  736. 'context' => array( 'embed', 'view', 'edit' ),
  737. 'required' => true,
  738. 'minLength' => 1,
  739. 'pattern' => '[a-zA-Z0-9_\-]+',
  740. ),
  741. 'theme' => array(
  742. 'description' => __( 'Theme identifier for the template.' ),
  743. 'type' => 'string',
  744. 'context' => array( 'embed', 'view', 'edit' ),
  745. ),
  746. 'type' => array(
  747. 'description' => __( 'Type of template.' ),
  748. 'type' => 'string',
  749. 'context' => array( 'embed', 'view', 'edit' ),
  750. ),
  751. 'source' => array(
  752. 'description' => __( 'Source of template' ),
  753. 'type' => 'string',
  754. 'context' => array( 'embed', 'view', 'edit' ),
  755. 'readonly' => true,
  756. ),
  757. 'origin' => array(
  758. 'description' => __( 'Source of a customized template' ),
  759. 'type' => 'string',
  760. 'context' => array( 'embed', 'view', 'edit' ),
  761. 'readonly' => true,
  762. ),
  763. 'content' => array(
  764. 'description' => __( 'Content of template.' ),
  765. 'type' => array( 'object', 'string' ),
  766. 'default' => '',
  767. 'context' => array( 'embed', 'view', 'edit' ),
  768. 'properties' => array(
  769. 'raw' => array(
  770. 'description' => __( 'Content for the template, as it exists in the database.' ),
  771. 'type' => 'string',
  772. 'context' => array( 'view', 'edit' ),
  773. ),
  774. 'block_version' => array(
  775. 'description' => __( 'Version of the content block format used by the template.' ),
  776. 'type' => 'integer',
  777. 'context' => array( 'edit' ),
  778. 'readonly' => true,
  779. ),
  780. ),
  781. ),
  782. 'title' => array(
  783. 'description' => __( 'Title of template.' ),
  784. 'type' => array( 'object', 'string' ),
  785. 'default' => '',
  786. 'context' => array( 'embed', 'view', 'edit' ),
  787. 'properties' => array(
  788. 'raw' => array(
  789. 'description' => __( 'Title for the template, as it exists in the database.' ),
  790. 'type' => 'string',
  791. 'context' => array( 'view', 'edit', 'embed' ),
  792. ),
  793. 'rendered' => array(
  794. 'description' => __( 'HTML title for the template, transformed for display.' ),
  795. 'type' => 'string',
  796. 'context' => array( 'view', 'edit', 'embed' ),
  797. 'readonly' => true,
  798. ),
  799. ),
  800. ),
  801. 'description' => array(
  802. 'description' => __( 'Description of template.' ),
  803. 'type' => 'string',
  804. 'default' => '',
  805. 'context' => array( 'embed', 'view', 'edit' ),
  806. ),
  807. 'status' => array(
  808. 'description' => __( 'Status of template.' ),
  809. 'type' => 'string',
  810. 'enum' => array_keys( get_post_stati( array( 'internal' => false ) ) ),
  811. 'default' => 'publish',
  812. 'context' => array( 'embed', 'view', 'edit' ),
  813. ),
  814. 'wp_id' => array(
  815. 'description' => __( 'Post ID.' ),
  816. 'type' => 'integer',
  817. 'context' => array( 'embed', 'view', 'edit' ),
  818. 'readonly' => true,
  819. ),
  820. 'has_theme_file' => array(
  821. 'description' => __( 'Theme file exists.' ),
  822. 'type' => 'bool',
  823. 'context' => array( 'embed', 'view', 'edit' ),
  824. 'readonly' => true,
  825. ),
  826. 'author' => array(
  827. 'description' => __( 'The ID for the author of the template.' ),
  828. 'type' => 'integer',
  829. 'context' => array( 'view', 'edit', 'embed' ),
  830. ),
  831. ),
  832. );
  833. if ( 'wp_template' === $this->post_type ) {
  834. $schema['properties']['is_custom'] = array(
  835. 'description' => __( 'Whether a template is a custom template.' ),
  836. 'type' => 'bool',
  837. 'context' => array( 'embed', 'view', 'edit' ),
  838. 'readonly' => true,
  839. );
  840. }
  841. if ( 'wp_template_part' === $this->post_type ) {
  842. $schema['properties']['area'] = array(
  843. 'description' => __( 'Where the template part is intended for use (header, footer, etc.)' ),
  844. 'type' => 'string',
  845. 'context' => array( 'embed', 'view', 'edit' ),
  846. );
  847. }
  848. $this->schema = $schema;
  849. return $this->add_additional_fields_schema( $this->schema );
  850. }
  851. }