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