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