class-wp-rest-terms-controller.php 30 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015
  1. <?php
  2. /**
  3. * REST API: WP_REST_Terms_Controller class
  4. *
  5. * @package WordPress
  6. * @subpackage REST_API
  7. * @since 4.7.0
  8. */
  9. /**
  10. * Core class used to managed terms associated with a taxonomy via the REST API.
  11. *
  12. * @since 4.7.0
  13. *
  14. * @see WP_REST_Controller
  15. */
  16. class WP_REST_Terms_Controller extends WP_REST_Controller {
  17. /**
  18. * Taxonomy key.
  19. *
  20. * @since 4.7.0
  21. * @var string
  22. */
  23. protected $taxonomy;
  24. /**
  25. * Instance of a term meta fields object.
  26. *
  27. * @since 4.7.0
  28. * @var WP_REST_Term_Meta_Fields
  29. */
  30. protected $meta;
  31. /**
  32. * Column to have the terms be sorted by.
  33. *
  34. * @since 4.7.0
  35. * @var string
  36. */
  37. protected $sort_column;
  38. /**
  39. * Number of terms that were found.
  40. *
  41. * @since 4.7.0
  42. * @var int
  43. */
  44. protected $total_terms;
  45. /**
  46. * Constructor.
  47. *
  48. * @since 4.7.0
  49. *
  50. * @param string $taxonomy Taxonomy key.
  51. */
  52. public function __construct( $taxonomy ) {
  53. $this->taxonomy = $taxonomy;
  54. $this->namespace = 'wp/v2';
  55. $tax_obj = get_taxonomy( $taxonomy );
  56. $this->rest_base = ! empty( $tax_obj->rest_base ) ? $tax_obj->rest_base : $tax_obj->name;
  57. $this->meta = new WP_REST_Term_Meta_Fields( $taxonomy );
  58. }
  59. /**
  60. * Registers the routes for the objects of the controller.
  61. *
  62. * @since 4.7.0
  63. *
  64. * @see register_rest_route()
  65. */
  66. public function register_routes() {
  67. register_rest_route( $this->namespace, '/' . $this->rest_base, array(
  68. array(
  69. 'methods' => WP_REST_Server::READABLE,
  70. 'callback' => array( $this, 'get_items' ),
  71. 'permission_callback' => array( $this, 'get_items_permissions_check' ),
  72. 'args' => $this->get_collection_params(),
  73. ),
  74. array(
  75. 'methods' => WP_REST_Server::CREATABLE,
  76. 'callback' => array( $this, 'create_item' ),
  77. 'permission_callback' => array( $this, 'create_item_permissions_check' ),
  78. 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ),
  79. ),
  80. 'schema' => array( $this, 'get_public_item_schema' ),
  81. ) );
  82. register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<id>[\d]+)', array(
  83. 'args' => array(
  84. 'id' => array(
  85. 'description' => __( 'Unique identifier for the term.' ),
  86. 'type' => 'integer',
  87. ),
  88. ),
  89. array(
  90. 'methods' => WP_REST_Server::READABLE,
  91. 'callback' => array( $this, 'get_item' ),
  92. 'permission_callback' => array( $this, 'get_item_permissions_check' ),
  93. 'args' => array(
  94. 'context' => $this->get_context_param( array( 'default' => 'view' ) ),
  95. ),
  96. ),
  97. array(
  98. 'methods' => WP_REST_Server::EDITABLE,
  99. 'callback' => array( $this, 'update_item' ),
  100. 'permission_callback' => array( $this, 'update_item_permissions_check' ),
  101. 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
  102. ),
  103. array(
  104. 'methods' => WP_REST_Server::DELETABLE,
  105. 'callback' => array( $this, 'delete_item' ),
  106. 'permission_callback' => array( $this, 'delete_item_permissions_check' ),
  107. 'args' => array(
  108. 'force' => array(
  109. 'type' => 'boolean',
  110. 'default' => false,
  111. 'description' => __( 'Required to be true, as terms do not support trashing.' ),
  112. ),
  113. ),
  114. ),
  115. 'schema' => array( $this, 'get_public_item_schema' ),
  116. ) );
  117. }
  118. /**
  119. * Checks if a request has access to read terms in the specified taxonomy.
  120. *
  121. * @since 4.7.0
  122. *
  123. * @param WP_REST_Request $request Full details about the request.
  124. * @return bool|WP_Error True if the request has read access, otherwise false or WP_Error object.
  125. */
  126. public function get_items_permissions_check( $request ) {
  127. $tax_obj = get_taxonomy( $this->taxonomy );
  128. if ( ! $tax_obj || ! $this->check_is_taxonomy_allowed( $this->taxonomy ) ) {
  129. return false;
  130. }
  131. if ( 'edit' === $request['context'] && ! current_user_can( $tax_obj->cap->edit_terms ) ) {
  132. return new WP_Error( 'rest_forbidden_context', __( 'Sorry, you are not allowed to edit terms in this taxonomy.' ), array( 'status' => rest_authorization_required_code() ) );
  133. }
  134. return true;
  135. }
  136. /**
  137. * Retrieves terms associated with a taxonomy.
  138. *
  139. * @since 4.7.0
  140. *
  141. * @param WP_REST_Request $request Full details about the request.
  142. * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
  143. */
  144. public function get_items( $request ) {
  145. // Retrieve the list of registered collection query parameters.
  146. $registered = $this->get_collection_params();
  147. /*
  148. * This array defines mappings between public API query parameters whose
  149. * values are accepted as-passed, and their internal WP_Query parameter
  150. * name equivalents (some are the same). Only values which are also
  151. * present in $registered will be set.
  152. */
  153. $parameter_mappings = array(
  154. 'exclude' => 'exclude',
  155. 'include' => 'include',
  156. 'order' => 'order',
  157. 'orderby' => 'orderby',
  158. 'post' => 'post',
  159. 'hide_empty' => 'hide_empty',
  160. 'per_page' => 'number',
  161. 'search' => 'search',
  162. 'slug' => 'slug',
  163. );
  164. $prepared_args = array();
  165. /*
  166. * For each known parameter which is both registered and present in the request,
  167. * set the parameter's value on the query $prepared_args.
  168. */
  169. foreach ( $parameter_mappings as $api_param => $wp_param ) {
  170. if ( isset( $registered[ $api_param ], $request[ $api_param ] ) ) {
  171. $prepared_args[ $wp_param ] = $request[ $api_param ];
  172. }
  173. }
  174. if ( isset( $prepared_args['orderby'] ) && isset( $request['orderby'] ) ) {
  175. $orderby_mappings = array(
  176. 'include_slugs' => 'slug__in',
  177. );
  178. if ( isset( $orderby_mappings[ $request['orderby'] ] ) ) {
  179. $prepared_args['orderby'] = $orderby_mappings[ $request['orderby'] ];
  180. }
  181. }
  182. if ( isset( $registered['offset'] ) && ! empty( $request['offset'] ) ) {
  183. $prepared_args['offset'] = $request['offset'];
  184. } else {
  185. $prepared_args['offset'] = ( $request['page'] - 1 ) * $prepared_args['number'];
  186. }
  187. $taxonomy_obj = get_taxonomy( $this->taxonomy );
  188. if ( $taxonomy_obj->hierarchical && isset( $registered['parent'], $request['parent'] ) ) {
  189. if ( 0 === $request['parent'] ) {
  190. // Only query top-level terms.
  191. $prepared_args['parent'] = 0;
  192. } else {
  193. if ( $request['parent'] ) {
  194. $prepared_args['parent'] = $request['parent'];
  195. }
  196. }
  197. }
  198. /**
  199. * Filters the query arguments before passing them to get_terms().
  200. *
  201. * The dynamic portion of the hook name, `$this->taxonomy`, refers to the taxonomy slug.
  202. *
  203. * Enables adding extra arguments or setting defaults for a terms
  204. * collection request.
  205. *
  206. * @since 4.7.0
  207. *
  208. * @link https://developer.wordpress.org/reference/functions/get_terms/
  209. *
  210. * @param array $prepared_args Array of arguments to be
  211. * passed to get_terms().
  212. * @param WP_REST_Request $request The current request.
  213. */
  214. $prepared_args = apply_filters( "rest_{$this->taxonomy}_query", $prepared_args, $request );
  215. if ( ! empty( $prepared_args['post'] ) ) {
  216. $query_result = wp_get_object_terms( $prepared_args['post'], $this->taxonomy, $prepared_args );
  217. // Used when calling wp_count_terms() below.
  218. $prepared_args['object_ids'] = $prepared_args['post'];
  219. } else {
  220. $query_result = get_terms( $this->taxonomy, $prepared_args );
  221. }
  222. $count_args = $prepared_args;
  223. unset( $count_args['number'], $count_args['offset'] );
  224. $total_terms = wp_count_terms( $this->taxonomy, $count_args );
  225. // wp_count_terms can return a falsy value when the term has no children.
  226. if ( ! $total_terms ) {
  227. $total_terms = 0;
  228. }
  229. $response = array();
  230. foreach ( $query_result as $term ) {
  231. $data = $this->prepare_item_for_response( $term, $request );
  232. $response[] = $this->prepare_response_for_collection( $data );
  233. }
  234. $response = rest_ensure_response( $response );
  235. // Store pagination values for headers.
  236. $per_page = (int) $prepared_args['number'];
  237. $page = ceil( ( ( (int) $prepared_args['offset'] ) / $per_page ) + 1 );
  238. $response->header( 'X-WP-Total', (int) $total_terms );
  239. $max_pages = ceil( $total_terms / $per_page );
  240. $response->header( 'X-WP-TotalPages', (int) $max_pages );
  241. $base = add_query_arg( $request->get_query_params(), rest_url( $this->namespace . '/' . $this->rest_base ) );
  242. if ( $page > 1 ) {
  243. $prev_page = $page - 1;
  244. if ( $prev_page > $max_pages ) {
  245. $prev_page = $max_pages;
  246. }
  247. $prev_link = add_query_arg( 'page', $prev_page, $base );
  248. $response->link_header( 'prev', $prev_link );
  249. }
  250. if ( $max_pages > $page ) {
  251. $next_page = $page + 1;
  252. $next_link = add_query_arg( 'page', $next_page, $base );
  253. $response->link_header( 'next', $next_link );
  254. }
  255. return $response;
  256. }
  257. /**
  258. * Get the term, if the ID is valid.
  259. *
  260. * @since 4.7.2
  261. *
  262. * @param int $id Supplied ID.
  263. * @return WP_Term|WP_Error Term object if ID is valid, WP_Error otherwise.
  264. */
  265. protected function get_term( $id ) {
  266. $error = new WP_Error( 'rest_term_invalid', __( 'Term does not exist.' ), array( 'status' => 404 ) );
  267. if ( ! $this->check_is_taxonomy_allowed( $this->taxonomy ) ) {
  268. return $error;
  269. }
  270. if ( (int) $id <= 0 ) {
  271. return $error;
  272. }
  273. $term = get_term( (int) $id, $this->taxonomy );
  274. if ( empty( $term ) || $term->taxonomy !== $this->taxonomy ) {
  275. return $error;
  276. }
  277. return $term;
  278. }
  279. /**
  280. * Checks if a request has access to read or edit the specified term.
  281. *
  282. * @since 4.7.0
  283. *
  284. * @param WP_REST_Request $request Full details about the request.
  285. * @return bool|WP_Error True if the request has read access for the item, otherwise false or WP_Error object.
  286. */
  287. public function get_item_permissions_check( $request ) {
  288. $term = $this->get_term( $request['id'] );
  289. if ( is_wp_error( $term ) ) {
  290. return $term;
  291. }
  292. if ( 'edit' === $request['context'] && ! current_user_can( 'edit_term', $term->term_id ) ) {
  293. return new WP_Error( 'rest_forbidden_context', __( 'Sorry, you are not allowed to edit this term.' ), array( 'status' => rest_authorization_required_code() ) );
  294. }
  295. return true;
  296. }
  297. /**
  298. * Gets a single term from a taxonomy.
  299. *
  300. * @since 4.7.0
  301. *
  302. * @param WP_REST_Request $request Full details about the request.
  303. * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
  304. */
  305. public function get_item( $request ) {
  306. $term = $this->get_term( $request['id'] );
  307. if ( is_wp_error( $term ) ) {
  308. return $term;
  309. }
  310. $response = $this->prepare_item_for_response( $term, $request );
  311. return rest_ensure_response( $response );
  312. }
  313. /**
  314. * Checks if a request has access to create a term.
  315. *
  316. * @since 4.7.0
  317. *
  318. * @param WP_REST_Request $request Full details about the request.
  319. * @return bool|WP_Error True if the request has access to create items, false or WP_Error object otherwise.
  320. */
  321. public function create_item_permissions_check( $request ) {
  322. if ( ! $this->check_is_taxonomy_allowed( $this->taxonomy ) ) {
  323. return false;
  324. }
  325. $taxonomy_obj = get_taxonomy( $this->taxonomy );
  326. if ( ( is_taxonomy_hierarchical( $this->taxonomy )
  327. && ! current_user_can( $taxonomy_obj->cap->edit_terms ) )
  328. || ( ! is_taxonomy_hierarchical( $this->taxonomy )
  329. && ! current_user_can( $taxonomy_obj->cap->assign_terms ) ) ) {
  330. return new WP_Error( 'rest_cannot_create', __( 'Sorry, you are not allowed to create new terms.' ), array( 'status' => rest_authorization_required_code() ) );
  331. }
  332. return true;
  333. }
  334. /**
  335. * Creates a single term in a taxonomy.
  336. *
  337. * @since 4.7.0
  338. *
  339. * @param WP_REST_Request $request Full details about the request.
  340. * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
  341. */
  342. public function create_item( $request ) {
  343. if ( isset( $request['parent'] ) ) {
  344. if ( ! is_taxonomy_hierarchical( $this->taxonomy ) ) {
  345. return new WP_Error( 'rest_taxonomy_not_hierarchical', __( 'Cannot set parent term, taxonomy is not hierarchical.' ), array( 'status' => 400 ) );
  346. }
  347. $parent = get_term( (int) $request['parent'], $this->taxonomy );
  348. if ( ! $parent ) {
  349. return new WP_Error( 'rest_term_invalid', __( 'Parent term does not exist.' ), array( 'status' => 400 ) );
  350. }
  351. }
  352. $prepared_term = $this->prepare_item_for_database( $request );
  353. $term = wp_insert_term( wp_slash( $prepared_term->name ), $this->taxonomy, wp_slash( (array) $prepared_term ) );
  354. if ( is_wp_error( $term ) ) {
  355. /*
  356. * If we're going to inform the client that the term already exists,
  357. * give them the identifier for future use.
  358. */
  359. if ( $term_id = $term->get_error_data( 'term_exists' ) ) {
  360. $existing_term = get_term( $term_id, $this->taxonomy );
  361. $term->add_data( $existing_term->term_id, 'term_exists' );
  362. $term->add_data( array( 'status' => 409, 'term_id' => $term_id ) );
  363. }
  364. return $term;
  365. }
  366. $term = get_term( $term['term_id'], $this->taxonomy );
  367. /**
  368. * Fires after a single term is created or updated via the REST API.
  369. *
  370. * The dynamic portion of the hook name, `$this->taxonomy`, refers to the taxonomy slug.
  371. *
  372. * @since 4.7.0
  373. *
  374. * @param WP_Term $term Inserted or updated term object.
  375. * @param WP_REST_Request $request Request object.
  376. * @param bool $creating True when creating a term, false when updating.
  377. */
  378. do_action( "rest_insert_{$this->taxonomy}", $term, $request, true );
  379. $schema = $this->get_item_schema();
  380. if ( ! empty( $schema['properties']['meta'] ) && isset( $request['meta'] ) ) {
  381. $meta_update = $this->meta->update_value( $request['meta'], (int) $request['id'] );
  382. if ( is_wp_error( $meta_update ) ) {
  383. return $meta_update;
  384. }
  385. }
  386. $fields_update = $this->update_additional_fields_for_object( $term, $request );
  387. if ( is_wp_error( $fields_update ) ) {
  388. return $fields_update;
  389. }
  390. $request->set_param( 'context', 'view' );
  391. $response = $this->prepare_item_for_response( $term, $request );
  392. $response = rest_ensure_response( $response );
  393. $response->set_status( 201 );
  394. $response->header( 'Location', rest_url( $this->namespace . '/' . $this->rest_base . '/' . $term->term_id ) );
  395. return $response;
  396. }
  397. /**
  398. * Checks if a request has access to update the specified term.
  399. *
  400. * @since 4.7.0
  401. *
  402. * @param WP_REST_Request $request Full details about the request.
  403. * @return bool|WP_Error True if the request has access to update the item, false or WP_Error object otherwise.
  404. */
  405. public function update_item_permissions_check( $request ) {
  406. $term = $this->get_term( $request['id'] );
  407. if ( is_wp_error( $term ) ) {
  408. return $term;
  409. }
  410. if ( ! current_user_can( 'edit_term', $term->term_id ) ) {
  411. return new WP_Error( 'rest_cannot_update', __( 'Sorry, you are not allowed to edit this term.' ), array( 'status' => rest_authorization_required_code() ) );
  412. }
  413. return true;
  414. }
  415. /**
  416. * Updates a single term from a taxonomy.
  417. *
  418. * @since 4.7.0
  419. *
  420. * @param WP_REST_Request $request Full details about the request.
  421. * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
  422. */
  423. public function update_item( $request ) {
  424. $term = $this->get_term( $request['id'] );
  425. if ( is_wp_error( $term ) ) {
  426. return $term;
  427. }
  428. if ( isset( $request['parent'] ) ) {
  429. if ( ! is_taxonomy_hierarchical( $this->taxonomy ) ) {
  430. return new WP_Error( 'rest_taxonomy_not_hierarchical', __( 'Cannot set parent term, taxonomy is not hierarchical.' ), array( 'status' => 400 ) );
  431. }
  432. $parent = get_term( (int) $request['parent'], $this->taxonomy );
  433. if ( ! $parent ) {
  434. return new WP_Error( 'rest_term_invalid', __( 'Parent term does not exist.' ), array( 'status' => 400 ) );
  435. }
  436. }
  437. $prepared_term = $this->prepare_item_for_database( $request );
  438. // Only update the term if we haz something to update.
  439. if ( ! empty( $prepared_term ) ) {
  440. $update = wp_update_term( $term->term_id, $term->taxonomy, wp_slash( (array) $prepared_term ) );
  441. if ( is_wp_error( $update ) ) {
  442. return $update;
  443. }
  444. }
  445. $term = get_term( $term->term_id, $this->taxonomy );
  446. /** This action is documented in wp-includes/rest-api/endpoints/class-wp-rest-terms-controller.php */
  447. do_action( "rest_insert_{$this->taxonomy}", $term, $request, false );
  448. $schema = $this->get_item_schema();
  449. if ( ! empty( $schema['properties']['meta'] ) && isset( $request['meta'] ) ) {
  450. $meta_update = $this->meta->update_value( $request['meta'], $term->term_id );
  451. if ( is_wp_error( $meta_update ) ) {
  452. return $meta_update;
  453. }
  454. }
  455. $fields_update = $this->update_additional_fields_for_object( $term, $request );
  456. if ( is_wp_error( $fields_update ) ) {
  457. return $fields_update;
  458. }
  459. $request->set_param( 'context', 'view' );
  460. $response = $this->prepare_item_for_response( $term, $request );
  461. return rest_ensure_response( $response );
  462. }
  463. /**
  464. * Checks if a request has access to delete the specified term.
  465. *
  466. * @since 4.7.0
  467. *
  468. * @param WP_REST_Request $request Full details about the request.
  469. * @return bool|WP_Error True if the request has access to delete the item, otherwise false or WP_Error object.
  470. */
  471. public function delete_item_permissions_check( $request ) {
  472. $term = $this->get_term( $request['id'] );
  473. if ( is_wp_error( $term ) ) {
  474. return $term;
  475. }
  476. if ( ! current_user_can( 'delete_term', $term->term_id ) ) {
  477. return new WP_Error( 'rest_cannot_delete', __( 'Sorry, you are not allowed to delete this term.' ), array( 'status' => rest_authorization_required_code() ) );
  478. }
  479. return true;
  480. }
  481. /**
  482. * Deletes a single term from a taxonomy.
  483. *
  484. * @since 4.7.0
  485. *
  486. * @param WP_REST_Request $request Full details about the request.
  487. * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
  488. */
  489. public function delete_item( $request ) {
  490. $term = $this->get_term( $request['id'] );
  491. if ( is_wp_error( $term ) ) {
  492. return $term;
  493. }
  494. $force = isset( $request['force'] ) ? (bool) $request['force'] : false;
  495. // We don't support trashing for terms.
  496. if ( ! $force ) {
  497. /* translators: %s: force=true */
  498. return new WP_Error( 'rest_trash_not_supported', sprintf( __( "Terms do not support trashing. Set '%s' to delete." ), 'force=true' ), array( 'status' => 501 ) );
  499. }
  500. $request->set_param( 'context', 'view' );
  501. $previous = $this->prepare_item_for_response( $term, $request );
  502. $retval = wp_delete_term( $term->term_id, $term->taxonomy );
  503. if ( ! $retval ) {
  504. return new WP_Error( 'rest_cannot_delete', __( 'The term cannot be deleted.' ), array( 'status' => 500 ) );
  505. }
  506. $response = new WP_REST_Response();
  507. $response->set_data( array( 'deleted' => true, 'previous' => $previous->get_data() ) );
  508. /**
  509. * Fires after a single term is deleted via the REST API.
  510. *
  511. * The dynamic portion of the hook name, `$this->taxonomy`, refers to the taxonomy slug.
  512. *
  513. * @since 4.7.0
  514. *
  515. * @param WP_Term $term The deleted term.
  516. * @param WP_REST_Response $response The response data.
  517. * @param WP_REST_Request $request The request sent to the API.
  518. */
  519. do_action( "rest_delete_{$this->taxonomy}", $term, $response, $request );
  520. return $response;
  521. }
  522. /**
  523. * Prepares a single term for create or update.
  524. *
  525. * @since 4.7.0
  526. *
  527. * @param WP_REST_Request $request Request object.
  528. * @return object $prepared_term Term object.
  529. */
  530. public function prepare_item_for_database( $request ) {
  531. $prepared_term = new stdClass;
  532. $schema = $this->get_item_schema();
  533. if ( isset( $request['name'] ) && ! empty( $schema['properties']['name'] ) ) {
  534. $prepared_term->name = $request['name'];
  535. }
  536. if ( isset( $request['slug'] ) && ! empty( $schema['properties']['slug'] ) ) {
  537. $prepared_term->slug = $request['slug'];
  538. }
  539. if ( isset( $request['taxonomy'] ) && ! empty( $schema['properties']['taxonomy'] ) ) {
  540. $prepared_term->taxonomy = $request['taxonomy'];
  541. }
  542. if ( isset( $request['description'] ) && ! empty( $schema['properties']['description'] ) ) {
  543. $prepared_term->description = $request['description'];
  544. }
  545. if ( isset( $request['parent'] ) && ! empty( $schema['properties']['parent'] ) ) {
  546. $parent_term_id = 0;
  547. $parent_term = get_term( (int) $request['parent'], $this->taxonomy );
  548. if ( $parent_term ) {
  549. $parent_term_id = $parent_term->term_id;
  550. }
  551. $prepared_term->parent = $parent_term_id;
  552. }
  553. /**
  554. * Filters term data before inserting term via the REST API.
  555. *
  556. * The dynamic portion of the hook name, `$this->taxonomy`, refers to the taxonomy slug.
  557. *
  558. * @since 4.7.0
  559. *
  560. * @param object $prepared_term Term object.
  561. * @param WP_REST_Request $request Request object.
  562. */
  563. return apply_filters( "rest_pre_insert_{$this->taxonomy}", $prepared_term, $request );
  564. }
  565. /**
  566. * Prepares a single term output for response.
  567. *
  568. * @since 4.7.0
  569. *
  570. * @param obj $item Term object.
  571. * @param WP_REST_Request $request Request object.
  572. * @return WP_REST_Response $response Response object.
  573. */
  574. public function prepare_item_for_response( $item, $request ) {
  575. $fields = $this->get_fields_for_response( $request );
  576. $data = array();
  577. if ( in_array( 'id', $fields, true ) ) {
  578. $data['id'] = (int) $item->term_id;
  579. }
  580. if ( in_array( 'count', $fields, true ) ) {
  581. $data['count'] = (int) $item->count;
  582. }
  583. if ( in_array( 'description', $fields, true ) ) {
  584. $data['description'] = $item->description;
  585. }
  586. if ( in_array( 'link', $fields, true ) ) {
  587. $data['link'] = get_term_link( $item );
  588. }
  589. if ( in_array( 'name', $fields, true ) ) {
  590. $data['name'] = $item->name;
  591. }
  592. if ( in_array( 'slug', $fields, true ) ) {
  593. $data['slug'] = $item->slug;
  594. }
  595. if ( in_array( 'taxonomy', $fields, true ) ) {
  596. $data['taxonomy'] = $item->taxonomy;
  597. }
  598. if ( in_array( 'parent', $fields, true ) ) {
  599. $data['parent'] = (int) $item->parent;
  600. }
  601. if ( in_array( 'meta', $fields, true ) ) {
  602. $data['meta'] = $this->meta->get_value( $item->term_id, $request );
  603. }
  604. $context = ! empty( $request['context'] ) ? $request['context'] : 'view';
  605. $data = $this->add_additional_fields_to_object( $data, $request );
  606. $data = $this->filter_response_by_context( $data, $context );
  607. $response = rest_ensure_response( $data );
  608. $response->add_links( $this->prepare_links( $item ) );
  609. /**
  610. * Filters a term item returned from the API.
  611. *
  612. * The dynamic portion of the hook name, `$this->taxonomy`, refers to the taxonomy slug.
  613. *
  614. * Allows modification of the term data right before it is returned.
  615. *
  616. * @since 4.7.0
  617. *
  618. * @param WP_REST_Response $response The response object.
  619. * @param object $item The original term object.
  620. * @param WP_REST_Request $request Request used to generate the response.
  621. */
  622. return apply_filters( "rest_prepare_{$this->taxonomy}", $response, $item, $request );
  623. }
  624. /**
  625. * Prepares links for the request.
  626. *
  627. * @since 4.7.0
  628. *
  629. * @param object $term Term object.
  630. * @return array Links for the given term.
  631. */
  632. protected function prepare_links( $term ) {
  633. $base = $this->namespace . '/' . $this->rest_base;
  634. $links = array(
  635. 'self' => array(
  636. 'href' => rest_url( trailingslashit( $base ) . $term->term_id ),
  637. ),
  638. 'collection' => array(
  639. 'href' => rest_url( $base ),
  640. ),
  641. 'about' => array(
  642. 'href' => rest_url( sprintf( 'wp/v2/taxonomies/%s', $this->taxonomy ) ),
  643. ),
  644. );
  645. if ( $term->parent ) {
  646. $parent_term = get_term( (int) $term->parent, $term->taxonomy );
  647. if ( $parent_term ) {
  648. $links['up'] = array(
  649. 'href' => rest_url( trailingslashit( $base ) . $parent_term->term_id ),
  650. 'embeddable' => true,
  651. );
  652. }
  653. }
  654. $taxonomy_obj = get_taxonomy( $term->taxonomy );
  655. if ( empty( $taxonomy_obj->object_type ) ) {
  656. return $links;
  657. }
  658. $post_type_links = array();
  659. foreach ( $taxonomy_obj->object_type as $type ) {
  660. $post_type_object = get_post_type_object( $type );
  661. if ( empty( $post_type_object->show_in_rest ) ) {
  662. continue;
  663. }
  664. $rest_base = ! empty( $post_type_object->rest_base ) ? $post_type_object->rest_base : $post_type_object->name;
  665. $post_type_links[] = array(
  666. 'href' => add_query_arg( $this->rest_base, $term->term_id, rest_url( sprintf( 'wp/v2/%s', $rest_base ) ) ),
  667. );
  668. }
  669. if ( ! empty( $post_type_links ) ) {
  670. $links['https://api.w.org/post_type'] = $post_type_links;
  671. }
  672. return $links;
  673. }
  674. /**
  675. * Retrieves the term's schema, conforming to JSON Schema.
  676. *
  677. * @since 4.7.0
  678. *
  679. * @return array Item schema data.
  680. */
  681. public function get_item_schema() {
  682. $schema = array(
  683. '$schema' => 'http://json-schema.org/draft-04/schema#',
  684. 'title' => 'post_tag' === $this->taxonomy ? 'tag' : $this->taxonomy,
  685. 'type' => 'object',
  686. 'properties' => array(
  687. 'id' => array(
  688. 'description' => __( 'Unique identifier for the term.' ),
  689. 'type' => 'integer',
  690. 'context' => array( 'view', 'embed', 'edit' ),
  691. 'readonly' => true,
  692. ),
  693. 'count' => array(
  694. 'description' => __( 'Number of published posts for the term.' ),
  695. 'type' => 'integer',
  696. 'context' => array( 'view', 'edit' ),
  697. 'readonly' => true,
  698. ),
  699. 'description' => array(
  700. 'description' => __( 'HTML description of the term.' ),
  701. 'type' => 'string',
  702. 'context' => array( 'view', 'edit' ),
  703. ),
  704. 'link' => array(
  705. 'description' => __( 'URL of the term.' ),
  706. 'type' => 'string',
  707. 'format' => 'uri',
  708. 'context' => array( 'view', 'embed', 'edit' ),
  709. 'readonly' => true,
  710. ),
  711. 'name' => array(
  712. 'description' => __( 'HTML title for the term.' ),
  713. 'type' => 'string',
  714. 'context' => array( 'view', 'embed', 'edit' ),
  715. 'arg_options' => array(
  716. 'sanitize_callback' => 'sanitize_text_field',
  717. ),
  718. 'required' => true,
  719. ),
  720. 'slug' => array(
  721. 'description' => __( 'An alphanumeric identifier for the term unique to its type.' ),
  722. 'type' => 'string',
  723. 'context' => array( 'view', 'embed', 'edit' ),
  724. 'arg_options' => array(
  725. 'sanitize_callback' => array( $this, 'sanitize_slug' ),
  726. ),
  727. ),
  728. 'taxonomy' => array(
  729. 'description' => __( 'Type attribution for the term.' ),
  730. 'type' => 'string',
  731. 'enum' => array_keys( get_taxonomies() ),
  732. 'context' => array( 'view', 'embed', 'edit' ),
  733. 'readonly' => true,
  734. ),
  735. ),
  736. );
  737. $taxonomy = get_taxonomy( $this->taxonomy );
  738. if ( $taxonomy->hierarchical ) {
  739. $schema['properties']['parent'] = array(
  740. 'description' => __( 'The parent term ID.' ),
  741. 'type' => 'integer',
  742. 'context' => array( 'view', 'edit' ),
  743. );
  744. }
  745. $schema['properties']['meta'] = $this->meta->get_field_schema();
  746. return $this->add_additional_fields_schema( $schema );
  747. }
  748. /**
  749. * Retrieves the query params for collections.
  750. *
  751. * @since 4.7.0
  752. *
  753. * @return array Collection parameters.
  754. */
  755. public function get_collection_params() {
  756. $query_params = parent::get_collection_params();
  757. $taxonomy = get_taxonomy( $this->taxonomy );
  758. $query_params['context']['default'] = 'view';
  759. $query_params['exclude'] = array(
  760. 'description' => __( 'Ensure result set excludes specific IDs.' ),
  761. 'type' => 'array',
  762. 'items' => array(
  763. 'type' => 'integer',
  764. ),
  765. 'default' => array(),
  766. );
  767. $query_params['include'] = array(
  768. 'description' => __( 'Limit result set to specific IDs.' ),
  769. 'type' => 'array',
  770. 'items' => array(
  771. 'type' => 'integer',
  772. ),
  773. 'default' => array(),
  774. );
  775. if ( ! $taxonomy->hierarchical ) {
  776. $query_params['offset'] = array(
  777. 'description' => __( 'Offset the result set by a specific number of items.' ),
  778. 'type' => 'integer',
  779. );
  780. }
  781. $query_params['order'] = array(
  782. 'description' => __( 'Order sort attribute ascending or descending.' ),
  783. 'type' => 'string',
  784. 'default' => 'asc',
  785. 'enum' => array(
  786. 'asc',
  787. 'desc',
  788. ),
  789. );
  790. $query_params['orderby'] = array(
  791. 'description' => __( 'Sort collection by term attribute.' ),
  792. 'type' => 'string',
  793. 'default' => 'name',
  794. 'enum' => array(
  795. 'id',
  796. 'include',
  797. 'name',
  798. 'slug',
  799. 'include_slugs',
  800. 'term_group',
  801. 'description',
  802. 'count',
  803. ),
  804. );
  805. $query_params['hide_empty'] = array(
  806. 'description' => __( 'Whether to hide terms not assigned to any posts.' ),
  807. 'type' => 'boolean',
  808. 'default' => false,
  809. );
  810. if ( $taxonomy->hierarchical ) {
  811. $query_params['parent'] = array(
  812. 'description' => __( 'Limit result set to terms assigned to a specific parent.' ),
  813. 'type' => 'integer',
  814. );
  815. }
  816. $query_params['post'] = array(
  817. 'description' => __( 'Limit result set to terms assigned to a specific post.' ),
  818. 'type' => 'integer',
  819. 'default' => null,
  820. );
  821. $query_params['slug'] = array(
  822. 'description' => __( 'Limit result set to terms with one or more specific slugs.' ),
  823. 'type' => 'array',
  824. 'items' => array(
  825. 'type' => 'string'
  826. ),
  827. );
  828. /**
  829. * Filter collection parameters for the terms controller.
  830. *
  831. * The dynamic part of the filter `$this->taxonomy` refers to the taxonomy
  832. * slug for the controller.
  833. *
  834. * This filter registers the collection parameter, but does not map the
  835. * collection parameter to an internal WP_Term_Query parameter. Use the
  836. * `rest_{$this->taxonomy}_query` filter to set WP_Term_Query parameters.
  837. *
  838. * @since 4.7.0
  839. *
  840. * @param array $query_params JSON Schema-formatted collection parameters.
  841. * @param WP_Taxonomy $taxonomy Taxonomy object.
  842. */
  843. return apply_filters( "rest_{$this->taxonomy}_collection_params", $query_params, $taxonomy );
  844. }
  845. /**
  846. * Checks that the taxonomy is valid.
  847. *
  848. * @since 4.7.0
  849. *
  850. * @param string $taxonomy Taxonomy to check.
  851. * @return bool Whether the taxonomy is allowed for REST management.
  852. */
  853. protected function check_is_taxonomy_allowed( $taxonomy ) {
  854. $taxonomy_obj = get_taxonomy( $taxonomy );
  855. if ( $taxonomy_obj && ! empty( $taxonomy_obj->show_in_rest ) ) {
  856. return true;
  857. }
  858. return false;
  859. }
  860. }