Brak opisu

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

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