WordPress 4.3.1
[autoinstalls/wordpress.git] / wp-includes / taxonomy.php
1 <?php
2 /**
3  * Taxonomy API
4  *
5  * @since 2.3.0
6  *
7  * @package WordPress
8  * @subpackage Taxonomy
9  */
10
11 //
12 // Taxonomy Registration
13 //
14
15 /**
16  * Creates the initial taxonomies.
17  *
18  * This function fires twice: in wp-settings.php before plugins are loaded (for
19  * backwards compatibility reasons), and again on the {@see 'init'} action. We must
20  * avoid registering rewrite rules before the {@see 'init'} action.
21  *
22  * @since 2.8.0
23  *
24  * @global WP_Rewrite $wp_rewrite The WordPress rewrite class.
25  */
26 function create_initial_taxonomies() {
27         global $wp_rewrite;
28
29         if ( ! did_action( 'init' ) ) {
30                 $rewrite = array( 'category' => false, 'post_tag' => false, 'post_format' => false );
31         } else {
32
33                 /**
34                  * Filter the post formats rewrite base.
35                  *
36                  * @since 3.1.0
37                  *
38                  * @param string $context Context of the rewrite base. Default 'type'.
39                  */
40                 $post_format_base = apply_filters( 'post_format_rewrite_base', 'type' );
41                 $rewrite = array(
42                         'category' => array(
43                                 'hierarchical' => true,
44                                 'slug' => get_option('category_base') ? get_option('category_base') : 'category',
45                                 'with_front' => ! get_option('category_base') || $wp_rewrite->using_index_permalinks(),
46                                 'ep_mask' => EP_CATEGORIES,
47                         ),
48                         'post_tag' => array(
49                                 'hierarchical' => false,
50                                 'slug' => get_option('tag_base') ? get_option('tag_base') : 'tag',
51                                 'with_front' => ! get_option('tag_base') || $wp_rewrite->using_index_permalinks(),
52                                 'ep_mask' => EP_TAGS,
53                         ),
54                         'post_format' => $post_format_base ? array( 'slug' => $post_format_base ) : false,
55                 );
56         }
57
58         register_taxonomy( 'category', 'post', array(
59                 'hierarchical' => true,
60                 'query_var' => 'category_name',
61                 'rewrite' => $rewrite['category'],
62                 'public' => true,
63                 'show_ui' => true,
64                 'show_admin_column' => true,
65                 '_builtin' => true,
66         ) );
67
68         register_taxonomy( 'post_tag', 'post', array(
69                 'hierarchical' => false,
70                 'query_var' => 'tag',
71                 'rewrite' => $rewrite['post_tag'],
72                 'public' => true,
73                 'show_ui' => true,
74                 'show_admin_column' => true,
75                 '_builtin' => true,
76         ) );
77
78         register_taxonomy( 'nav_menu', 'nav_menu_item', array(
79                 'public' => false,
80                 'hierarchical' => false,
81                 'labels' => array(
82                         'name' => __( 'Navigation Menus' ),
83                         'singular_name' => __( 'Navigation Menu' ),
84                 ),
85                 'query_var' => false,
86                 'rewrite' => false,
87                 'show_ui' => false,
88                 '_builtin' => true,
89                 'show_in_nav_menus' => false,
90         ) );
91
92         register_taxonomy( 'link_category', 'link', array(
93                 'hierarchical' => false,
94                 'labels' => array(
95                         'name' => __( 'Link Categories' ),
96                         'singular_name' => __( 'Link Category' ),
97                         'search_items' => __( 'Search Link Categories' ),
98                         'popular_items' => null,
99                         'all_items' => __( 'All Link Categories' ),
100                         'edit_item' => __( 'Edit Link Category' ),
101                         'update_item' => __( 'Update Link Category' ),
102                         'add_new_item' => __( 'Add New Link Category' ),
103                         'new_item_name' => __( 'New Link Category Name' ),
104                         'separate_items_with_commas' => null,
105                         'add_or_remove_items' => null,
106                         'choose_from_most_used' => null,
107                 ),
108                 'capabilities' => array(
109                         'manage_terms' => 'manage_links',
110                         'edit_terms'   => 'manage_links',
111                         'delete_terms' => 'manage_links',
112                         'assign_terms' => 'manage_links',
113                 ),
114                 'query_var' => false,
115                 'rewrite' => false,
116                 'public' => false,
117                 'show_ui' => false,
118                 '_builtin' => true,
119         ) );
120
121         register_taxonomy( 'post_format', 'post', array(
122                 'public' => true,
123                 'hierarchical' => false,
124                 'labels' => array(
125                         'name' => _x( 'Format', 'post format' ),
126                         'singular_name' => _x( 'Format', 'post format' ),
127                 ),
128                 'query_var' => true,
129                 'rewrite' => $rewrite['post_format'],
130                 'show_ui' => false,
131                 '_builtin' => true,
132                 'show_in_nav_menus' => current_theme_supports( 'post-formats' ),
133         ) );
134 }
135
136 /**
137  * Retrieves a list of registered taxonomy names or objects.
138  *
139  * @since 3.0.0
140  *
141  * @global array $wp_taxonomies The registered taxonomies.
142  *
143  * @param array  $args     Optional. An array of `key => value` arguments to match against the taxonomy objects.
144  *                         Default empty array.
145  * @param string $output   Optional. The type of output to return in the array. Accepts either taxonomy 'names'
146  *                         or 'objects'. Default 'names'.
147  * @param string $operator Optional. The logical operation to perform. Accepts 'and' or 'or'. 'or' means only
148  *                         one element from the array needs to match; 'and' means all elements must match.
149  *                         Default 'and'.
150  * @return array A list of taxonomy names or objects.
151  */
152 function get_taxonomies( $args = array(), $output = 'names', $operator = 'and' ) {
153         global $wp_taxonomies;
154
155         $field = ('names' == $output) ? 'name' : false;
156
157         return wp_filter_object_list($wp_taxonomies, $args, $operator, $field);
158 }
159
160 /**
161  * Return all of the taxonomy names that are of $object_type.
162  *
163  * It appears that this function can be used to find all of the names inside of
164  * $wp_taxonomies global variable.
165  *
166  * `<?php $taxonomies = get_object_taxonomies('post'); ?>` Should
167  * result in `Array( 'category', 'post_tag' )`
168  *
169  * @since 2.3.0
170  *
171  * @global array $wp_taxonomies The registered taxonomies.
172  *
173  * @param array|string|WP_Post $object Name of the type of taxonomy object, or an object (row from posts)
174  * @param string               $output Optional. The type of output to return in the array. Accepts either
175  *                                     taxonomy 'names' or 'objects'. Default 'names'.
176  * @return array The names of all taxonomy of $object_type.
177  */
178 function get_object_taxonomies( $object, $output = 'names' ) {
179         global $wp_taxonomies;
180
181         if ( is_object($object) ) {
182                 if ( $object->post_type == 'attachment' )
183                         return get_attachment_taxonomies($object);
184                 $object = $object->post_type;
185         }
186
187         $object = (array) $object;
188
189         $taxonomies = array();
190         foreach ( (array) $wp_taxonomies as $tax_name => $tax_obj ) {
191                 if ( array_intersect($object, (array) $tax_obj->object_type) ) {
192                         if ( 'names' == $output )
193                                 $taxonomies[] = $tax_name;
194                         else
195                                 $taxonomies[ $tax_name ] = $tax_obj;
196                 }
197         }
198
199         return $taxonomies;
200 }
201
202 /**
203  * Retrieves the taxonomy object of $taxonomy.
204  *
205  * The get_taxonomy function will first check that the parameter string given
206  * is a taxonomy object and if it is, it will return it.
207  *
208  * @since 2.3.0
209  *
210  * @global array $wp_taxonomies The registered taxonomies.
211  *
212  * @param string $taxonomy Name of taxonomy object to return.
213  * @return object|false The Taxonomy Object or false if $taxonomy doesn't exist.
214  */
215 function get_taxonomy( $taxonomy ) {
216         global $wp_taxonomies;
217
218         if ( ! taxonomy_exists( $taxonomy ) )
219                 return false;
220
221         return $wp_taxonomies[$taxonomy];
222 }
223
224 /**
225  * Checks that the taxonomy name exists.
226  *
227  * Formerly is_taxonomy(), introduced in 2.3.0.
228  *
229  * @since 3.0.0
230  *
231  * @global array $wp_taxonomies The registered taxonomies.
232  *
233  * @param string $taxonomy Name of taxonomy object.
234  * @return bool Whether the taxonomy exists.
235  */
236 function taxonomy_exists( $taxonomy ) {
237         global $wp_taxonomies;
238
239         return isset( $wp_taxonomies[$taxonomy] );
240 }
241
242 /**
243  * Whether the taxonomy object is hierarchical.
244  *
245  * Checks to make sure that the taxonomy is an object first. Then Gets the
246  * object, and finally returns the hierarchical value in the object.
247  *
248  * A false return value might also mean that the taxonomy does not exist.
249  *
250  * @since 2.3.0
251  *
252  * @param string $taxonomy Name of taxonomy object.
253  * @return bool Whether the taxonomy is hierarchical.
254  */
255 function is_taxonomy_hierarchical($taxonomy) {
256         if ( ! taxonomy_exists($taxonomy) )
257                 return false;
258
259         $taxonomy = get_taxonomy($taxonomy);
260         return $taxonomy->hierarchical;
261 }
262
263 /**
264  * Create or modify a taxonomy object. Do not use before init.
265  *
266  * A simple function for creating or modifying a taxonomy object based on the
267  * parameters given. The function will accept an array (third optional
268  * parameter), along with strings for the taxonomy name and another string for
269  * the object type.
270  *
271  * Nothing is returned, so expect error maybe or use taxonomy_exists() to check
272  * whether taxonomy exists.
273  *
274  * Optional $args contents:
275  *
276  * - label - Name of the taxonomy shown in the menu. Usually plural. If not set, labels['name'] will be used.
277  * - labels - An array of labels for this taxonomy.
278  *     * By default tag labels are used for non-hierarchical types and category labels for hierarchical ones.
279  *     * You can see accepted values in {@link get_taxonomy_labels()}.
280  * - description - A short descriptive summary of what the taxonomy is for. Defaults to blank.
281  * - public - If the taxonomy should be publicly queryable; //@TODO not implemented.
282  *     * Defaults to true.
283  * - hierarchical - Whether the taxonomy is hierarchical (e.g. category). Defaults to false.
284  * - show_ui - Whether to generate a default UI for managing this taxonomy in the admin.
285  *     * If not set, the default is inherited from public.
286  * - show_in_menu - Whether to show the taxonomy in the admin menu.
287  *     * If true, the taxonomy is shown as a submenu of the object type menu.
288  *     * If false, no menu is shown.
289  *     * show_ui must be true.
290  *     * If not set, the default is inherited from show_ui.
291  * - show_in_nav_menus - Makes this taxonomy available for selection in navigation menus.
292  *     * If not set, the default is inherited from public.
293  * - show_tagcloud - Whether to list the taxonomy in the Tag Cloud Widget.
294  *     * If not set, the default is inherited from show_ui.
295  * - show_in_quick_edit - Whether to show the taxonomy in the quick/bulk edit panel.
296  *     * It not set, the default is inherited from show_ui.
297  * - show_admin_column - Whether to display a column for the taxonomy on its post type listing screens.
298  *     * Defaults to false.
299  * - meta_box_cb - Provide a callback function for the meta box display.
300  *     * If not set, defaults to post_categories_meta_box for hierarchical taxonomies
301  *     and post_tags_meta_box for non-hierarchical.
302  *     * If false, no meta box is shown.
303  * - capabilities - Array of capabilities for this taxonomy.
304  *     * You can see accepted values in this function.
305  * - rewrite - Triggers the handling of rewrites for this taxonomy. Defaults to true, using $taxonomy as slug.
306  *     * To prevent rewrite, set to false.
307  *     * To specify rewrite rules, an array can be passed with any of these keys
308  *         * 'slug' => string Customize the permastruct slug. Defaults to $taxonomy key
309  *         * 'with_front' => bool Should the permastruct be prepended with WP_Rewrite::$front. Defaults to true.
310  *         * 'hierarchical' => bool Either hierarchical rewrite tag or not. Defaults to false.
311  *         * 'ep_mask' => const Assign an endpoint mask.
312  *             * If not specified, defaults to EP_NONE.
313  * - query_var - Sets the query_var key for this taxonomy. Defaults to $taxonomy key
314  *     * If false, a taxonomy cannot be loaded at ?{query_var}={term_slug}
315  *     * If specified as a string, the query ?{query_var_string}={term_slug} will be valid.
316  * - update_count_callback - Works much like a hook, in that it will be called when the count is updated.
317  *     * Defaults to _update_post_term_count() for taxonomies attached to post types, which then confirms
318  *       that the objects are published before counting them.
319  *     * Defaults to _update_generic_term_count() for taxonomies attached to other object types, such as links.
320  * - _builtin - true if this taxonomy is a native or "built-in" taxonomy. THIS IS FOR INTERNAL USE ONLY!
321  *
322  * @todo Document $args as a hash notation.
323  *
324  * @since 2.3.0
325  * @since 4.2.0 Introduced `show_in_quick_edit` argument.
326  *
327  * @global array $wp_taxonomies Registered taxonomies.
328  * @global WP    $wp            WP instance.
329  *
330  * @param string       $taxonomy    Taxonomy key, must not exceed 32 characters.
331  * @param array|string $object_type Name of the object type for the taxonomy object.
332  * @param array|string $args        See optional args description above.
333  * @return WP_Error|void WP_Error, if errors.
334  */
335 function register_taxonomy( $taxonomy, $object_type, $args = array() ) {
336         global $wp_taxonomies, $wp;
337
338         if ( ! is_array( $wp_taxonomies ) )
339                 $wp_taxonomies = array();
340
341         $defaults = array(
342                 'labels'                => array(),
343                 'description'           => '',
344                 'public'                => true,
345                 'hierarchical'          => false,
346                 'show_ui'               => null,
347                 'show_in_menu'          => null,
348                 'show_in_nav_menus'     => null,
349                 'show_tagcloud'         => null,
350                 'show_in_quick_edit'    => null,
351                 'show_admin_column'     => false,
352                 'meta_box_cb'           => null,
353                 'capabilities'          => array(),
354                 'rewrite'               => true,
355                 'query_var'             => $taxonomy,
356                 'update_count_callback' => '',
357                 '_builtin'              => false,
358         );
359         $args = wp_parse_args( $args, $defaults );
360
361         if ( empty( $taxonomy ) || strlen( $taxonomy ) > 32 ) {
362                 _doing_it_wrong( __FUNCTION__, __( 'Taxonomy names must be between 1 and 32 characters in length.' ), '4.2' );
363                 return new WP_Error( 'taxonomy_length_invalid', __( 'Taxonomy names must be between 1 and 32 characters in length.' ) );
364         }
365
366         if ( false !== $args['query_var'] && ! empty( $wp ) ) {
367                 if ( true === $args['query_var'] )
368                         $args['query_var'] = $taxonomy;
369                 else
370                         $args['query_var'] = sanitize_title_with_dashes( $args['query_var'] );
371                 $wp->add_query_var( $args['query_var'] );
372         }
373
374         if ( false !== $args['rewrite'] && ( is_admin() || '' != get_option( 'permalink_structure' ) ) ) {
375                 $args['rewrite'] = wp_parse_args( $args['rewrite'], array(
376                         'with_front' => true,
377                         'hierarchical' => false,
378                         'ep_mask' => EP_NONE,
379                 ) );
380
381                 if ( empty( $args['rewrite']['slug'] ) )
382                         $args['rewrite']['slug'] = sanitize_title_with_dashes( $taxonomy );
383
384                 if ( $args['hierarchical'] && $args['rewrite']['hierarchical'] )
385                         $tag = '(.+?)';
386                 else
387                         $tag = '([^/]+)';
388
389                 add_rewrite_tag( "%$taxonomy%", $tag, $args['query_var'] ? "{$args['query_var']}=" : "taxonomy=$taxonomy&term=" );
390                 add_permastruct( $taxonomy, "{$args['rewrite']['slug']}/%$taxonomy%", $args['rewrite'] );
391         }
392
393         // If not set, default to the setting for public.
394         if ( null === $args['show_ui'] )
395                 $args['show_ui'] = $args['public'];
396
397         // If not set, default to the setting for show_ui.
398         if ( null === $args['show_in_menu' ] || ! $args['show_ui'] )
399                 $args['show_in_menu' ] = $args['show_ui'];
400
401         // If not set, default to the setting for public.
402         if ( null === $args['show_in_nav_menus'] )
403                 $args['show_in_nav_menus'] = $args['public'];
404
405         // If not set, default to the setting for show_ui.
406         if ( null === $args['show_tagcloud'] )
407                 $args['show_tagcloud'] = $args['show_ui'];
408
409         // If not set, default to the setting for show_ui.
410         if ( null === $args['show_in_quick_edit'] ) {
411                 $args['show_in_quick_edit'] = $args['show_ui'];
412         }
413
414         $default_caps = array(
415                 'manage_terms' => 'manage_categories',
416                 'edit_terms'   => 'manage_categories',
417                 'delete_terms' => 'manage_categories',
418                 'assign_terms' => 'edit_posts',
419         );
420         $args['cap'] = (object) array_merge( $default_caps, $args['capabilities'] );
421         unset( $args['capabilities'] );
422
423         $args['name'] = $taxonomy;
424         $args['object_type'] = array_unique( (array) $object_type );
425
426         $args['labels'] = get_taxonomy_labels( (object) $args );
427         $args['label'] = $args['labels']->name;
428
429         // If not set, use the default meta box
430         if ( null === $args['meta_box_cb'] ) {
431                 if ( $args['hierarchical'] )
432                         $args['meta_box_cb'] = 'post_categories_meta_box';
433                 else
434                         $args['meta_box_cb'] = 'post_tags_meta_box';
435         }
436
437         $wp_taxonomies[ $taxonomy ] = (object) $args;
438
439         // register callback handling for metabox
440         add_filter( 'wp_ajax_add-' . $taxonomy, '_wp_ajax_add_hierarchical_term' );
441
442         /**
443          * Fires after a taxonomy is registered.
444          *
445          * @since 3.3.0
446          *
447          * @param string       $taxonomy    Taxonomy slug.
448          * @param array|string $object_type Object type or array of object types.
449          * @param array        $args        Array of taxonomy registration arguments.
450          */
451         do_action( 'registered_taxonomy', $taxonomy, $object_type, $args );
452 }
453
454 /**
455  * Builds an object with all taxonomy labels out of a taxonomy object
456  *
457  * Accepted keys of the label array in the taxonomy object:
458  *
459  * - name - general name for the taxonomy, usually plural. The same as and overridden by $tax->label. Default is Tags/Categories
460  * - singular_name - name for one object of this taxonomy. Default is Tag/Category
461  * - search_items - Default is Search Tags/Search Categories
462  * - popular_items - This string isn't used on hierarchical taxonomies. Default is Popular Tags
463  * - all_items - Default is All Tags/All Categories
464  * - parent_item - This string isn't used on non-hierarchical taxonomies. In hierarchical ones the default is Parent Category
465  * - parent_item_colon - The same as `parent_item`, but with colon `:` in the end
466  * - edit_item - Default is Edit Tag/Edit Category
467  * - view_item - Default is View Tag/View Category
468  * - update_item - Default is Update Tag/Update Category
469  * - add_new_item - Default is Add New Tag/Add New Category
470  * - new_item_name - Default is New Tag Name/New Category Name
471  * - separate_items_with_commas - This string isn't used on hierarchical taxonomies. Default is "Separate tags with commas", used in the meta box.
472  * - add_or_remove_items - This string isn't used on hierarchical taxonomies. Default is "Add or remove tags", used in the meta box when JavaScript is disabled.
473  * - choose_from_most_used - This string isn't used on hierarchical taxonomies. Default is "Choose from the most used tags", used in the meta box.
474  * - not_found - Default is "No tags found"/"No categories found", used in the meta box and taxonomy list table.
475  * - no_terms - Default is "No tags"/"No categories", used in the posts and media list tables.
476  *
477  * Above, the first default value is for non-hierarchical taxonomies (like tags) and the second one is for hierarchical taxonomies (like categories).
478  *
479  * @todo Better documentation for the labels array.
480  *
481  * @since 3.0.0
482  * @since 4.3.0 Added the `no_terms` label.
483  *
484  * @param object $tax Taxonomy object.
485  * @return object object with all the labels as member variables.
486  */
487 function get_taxonomy_labels( $tax ) {
488         $tax->labels = (array) $tax->labels;
489
490         if ( isset( $tax->helps ) && empty( $tax->labels['separate_items_with_commas'] ) )
491                 $tax->labels['separate_items_with_commas'] = $tax->helps;
492
493         if ( isset( $tax->no_tagcloud ) && empty( $tax->labels['not_found'] ) )
494                 $tax->labels['not_found'] = $tax->no_tagcloud;
495
496         $nohier_vs_hier_defaults = array(
497                 'name' => array( _x( 'Tags', 'taxonomy general name' ), _x( 'Categories', 'taxonomy general name' ) ),
498                 'singular_name' => array( _x( 'Tag', 'taxonomy singular name' ), _x( 'Category', 'taxonomy singular name' ) ),
499                 'search_items' => array( __( 'Search Tags' ), __( 'Search Categories' ) ),
500                 'popular_items' => array( __( 'Popular Tags' ), null ),
501                 'all_items' => array( __( 'All Tags' ), __( 'All Categories' ) ),
502                 'parent_item' => array( null, __( 'Parent Category' ) ),
503                 'parent_item_colon' => array( null, __( 'Parent Category:' ) ),
504                 'edit_item' => array( __( 'Edit Tag' ), __( 'Edit Category' ) ),
505                 'view_item' => array( __( 'View Tag' ), __( 'View Category' ) ),
506                 'update_item' => array( __( 'Update Tag' ), __( 'Update Category' ) ),
507                 'add_new_item' => array( __( 'Add New Tag' ), __( 'Add New Category' ) ),
508                 'new_item_name' => array( __( 'New Tag Name' ), __( 'New Category Name' ) ),
509                 'separate_items_with_commas' => array( __( 'Separate tags with commas' ), null ),
510                 'add_or_remove_items' => array( __( 'Add or remove tags' ), null ),
511                 'choose_from_most_used' => array( __( 'Choose from the most used tags' ), null ),
512                 'not_found' => array( __( 'No tags found.' ), __( 'No categories found.' ) ),
513                 'no_terms' => array( __( 'No tags' ), __( 'No categories' ) ),
514         );
515         $nohier_vs_hier_defaults['menu_name'] = $nohier_vs_hier_defaults['name'];
516
517         return _get_custom_object_labels( $tax, $nohier_vs_hier_defaults );
518 }
519
520 /**
521  * Add an already registered taxonomy to an object type.
522  *
523  * @since 3.0.0
524  *
525  * @global array $wp_taxonomies The registered taxonomies.
526  *
527  * @param string $taxonomy    Name of taxonomy object.
528  * @param string $object_type Name of the object type.
529  * @return bool True if successful, false if not.
530  */
531 function register_taxonomy_for_object_type( $taxonomy, $object_type) {
532         global $wp_taxonomies;
533
534         if ( !isset($wp_taxonomies[$taxonomy]) )
535                 return false;
536
537         if ( ! get_post_type_object($object_type) )
538                 return false;
539
540         if ( ! in_array( $object_type, $wp_taxonomies[$taxonomy]->object_type ) )
541                 $wp_taxonomies[$taxonomy]->object_type[] = $object_type;
542
543         // Filter out empties.
544         $wp_taxonomies[ $taxonomy ]->object_type = array_filter( $wp_taxonomies[ $taxonomy ]->object_type );
545
546         return true;
547 }
548
549 /**
550  * Remove an already registered taxonomy from an object type.
551  *
552  * @since 3.7.0
553  *
554  * @global array $wp_taxonomies The registered taxonomies.
555  *
556  * @param string $taxonomy    Name of taxonomy object.
557  * @param string $object_type Name of the object type.
558  * @return bool True if successful, false if not.
559  */
560 function unregister_taxonomy_for_object_type( $taxonomy, $object_type ) {
561         global $wp_taxonomies;
562
563         if ( ! isset( $wp_taxonomies[ $taxonomy ] ) )
564                 return false;
565
566         if ( ! get_post_type_object( $object_type ) )
567                 return false;
568
569         $key = array_search( $object_type, $wp_taxonomies[ $taxonomy ]->object_type, true );
570         if ( false === $key )
571                 return false;
572
573         unset( $wp_taxonomies[ $taxonomy ]->object_type[ $key ] );
574         return true;
575 }
576
577 //
578 // Term API
579 //
580
581 /**
582  * Retrieve object_ids of valid taxonomy and term.
583  *
584  * The strings of $taxonomies must exist before this function will continue. On
585  * failure of finding a valid taxonomy, it will return an WP_Error class, kind
586  * of like Exceptions in PHP 5, except you can't catch them. Even so, you can
587  * still test for the WP_Error class and get the error message.
588  *
589  * The $terms aren't checked the same as $taxonomies, but still need to exist
590  * for $object_ids to be returned.
591  *
592  * It is possible to change the order that object_ids is returned by either
593  * using PHP sort family functions or using the database by using $args with
594  * either ASC or DESC array. The value should be in the key named 'order'.
595  *
596  * @since 2.3.0
597  *
598  * @global wpdb $wpdb WordPress database abstraction object.
599  *
600  * @param int|array    $term_ids   Term id or array of term ids of terms that will be used.
601  * @param string|array $taxonomies String of taxonomy name or Array of string values of taxonomy names.
602  * @param array|string $args       Change the order of the object_ids, either ASC or DESC.
603  * @return WP_Error|array If the taxonomy does not exist, then WP_Error will be returned. On success.
604  *      the array can be empty meaning that there are no $object_ids found or it will return the $object_ids found.
605  */
606 function get_objects_in_term( $term_ids, $taxonomies, $args = array() ) {
607         global $wpdb;
608
609         if ( ! is_array( $term_ids ) ) {
610                 $term_ids = array( $term_ids );
611         }
612         if ( ! is_array( $taxonomies ) ) {
613                 $taxonomies = array( $taxonomies );
614         }
615         foreach ( (array) $taxonomies as $taxonomy ) {
616                 if ( ! taxonomy_exists( $taxonomy ) ) {
617                         return new WP_Error( 'invalid_taxonomy', __( 'Invalid taxonomy' ) );
618                 }
619         }
620
621         $defaults = array( 'order' => 'ASC' );
622         $args = wp_parse_args( $args, $defaults );
623
624         $order = ( 'desc' == strtolower( $args['order'] ) ) ? 'DESC' : 'ASC';
625
626         $term_ids = array_map('intval', $term_ids );
627
628         $taxonomies = "'" . implode( "', '", $taxonomies ) . "'";
629         $term_ids = "'" . implode( "', '", $term_ids ) . "'";
630
631         $object_ids = $wpdb->get_col("SELECT tr.object_id FROM $wpdb->term_relationships AS tr INNER JOIN $wpdb->term_taxonomy AS tt ON tr.term_taxonomy_id = tt.term_taxonomy_id WHERE tt.taxonomy IN ($taxonomies) AND tt.term_id IN ($term_ids) ORDER BY tr.object_id $order");
632
633         if ( ! $object_ids ){
634                 return array();
635         }
636         return $object_ids;
637 }
638
639 /**
640  * Given a taxonomy query, generates SQL to be appended to a main query.
641  *
642  * @since 3.1.0
643  *
644  * @see WP_Tax_Query
645  *
646  * @param array  $tax_query         A compact tax query
647  * @param string $primary_table
648  * @param string $primary_id_column
649  * @return array
650  */
651 function get_tax_sql( $tax_query, $primary_table, $primary_id_column ) {
652         $tax_query_obj = new WP_Tax_Query( $tax_query );
653         return $tax_query_obj->get_sql( $primary_table, $primary_id_column );
654 }
655
656 /**
657  * Class for generating SQL clauses that filter a primary query according to object taxonomy terms.
658  *
659  * `WP_Tax_Query` is a helper that allows primary query classes, such as WP_Query, to filter
660  * their results by object metadata, by generating `JOIN` and `WHERE` subclauses to be attached
661  * to the primary SQL query string.
662  *
663  * @since 3.1.0
664  */
665 class WP_Tax_Query {
666
667         /**
668          * Array of taxonomy queries.
669          *
670          * See {@see WP_Tax_Query::__construct()} for information on tax query arguments.
671          *
672          * @since 3.1.0
673          * @access public
674          * @var array
675          */
676         public $queries = array();
677
678         /**
679          * The relation between the queries. Can be one of 'AND' or 'OR'.
680          *
681          * @since 3.1.0
682          * @access public
683          * @var string
684          */
685         public $relation;
686
687         /**
688          * Standard response when the query should not return any rows.
689          *
690          * @since 3.2.0
691          *
692          * @static
693          * @access private
694          * @var string
695          */
696         private static $no_results = array( 'join' => array( '' ), 'where' => array( '0 = 1' ) );
697
698         /**
699          * A flat list of table aliases used in the JOIN clauses.
700          *
701          * @since 4.1.0
702          * @access protected
703          * @var array
704          */
705         protected $table_aliases = array();
706
707         /**
708          * Terms and taxonomies fetched by this query.
709          *
710          * We store this data in a flat array because they are referenced in a
711          * number of places by WP_Query.
712          *
713          * @since 4.1.0
714          * @access public
715          * @var array
716          */
717         public $queried_terms = array();
718
719         /**
720          * Database table that where the metadata's objects are stored (eg $wpdb->users).
721          *
722          * @since 4.1.0
723          * @access public
724          * @var string
725          */
726         public $primary_table;
727
728         /**
729          * Column in 'primary_table' that represents the ID of the object.
730          *
731          * @since 4.1.0
732          * @access public
733          * @var string
734          */
735         public $primary_id_column;
736
737         /**
738          * Constructor.
739          *
740          * @since 3.1.0
741          * @since 4.1.0 Added support for `$operator` 'NOT EXISTS' and 'EXISTS' values.
742          * @access public
743          *
744          * @param array $tax_query {
745          *     Array of taxonomy query clauses.
746          *
747          *     @type string $relation Optional. The MySQL keyword used to join
748          *                            the clauses of the query. Accepts 'AND', or 'OR'. Default 'AND'.
749          *     @type array {
750          *         Optional. An array of first-order clause parameters, or another fully-formed tax query.
751          *
752          *         @type string           $taxonomy         Taxonomy being queried. Optional when field=term_taxonomy_id.
753          *         @type string|int|array $terms            Term or terms to filter by.
754          *         @type string           $field            Field to match $terms against. Accepts 'term_id', 'slug',
755          *                                                 'name', or 'term_taxonomy_id'. Default: 'term_id'.
756          *         @type string           $operator         MySQL operator to be used with $terms in the WHERE clause.
757          *                                                  Accepts 'AND', 'IN', 'NOT IN', 'EXISTS', 'NOT EXISTS'.
758          *                                                  Default: 'IN'.
759          *         @type bool             $include_children Optional. Whether to include child terms.
760          *                                                  Requires a $taxonomy. Default: true.
761          *     }
762          * }
763          */
764         public function __construct( $tax_query ) {
765                 if ( isset( $tax_query['relation'] ) ) {
766                         $this->relation = $this->sanitize_relation( $tax_query['relation'] );
767                 } else {
768                         $this->relation = 'AND';
769                 }
770
771                 $this->queries = $this->sanitize_query( $tax_query );
772         }
773
774         /**
775          * Ensure the 'tax_query' argument passed to the class constructor is well-formed.
776          *
777          * Ensures that each query-level clause has a 'relation' key, and that
778          * each first-order clause contains all the necessary keys from `$defaults`.
779          *
780          * @since 4.1.0
781          * @access public
782          *
783          * @param array $queries Array of queries clauses.
784          * @return array Sanitized array of query clauses.
785          */
786         public function sanitize_query( $queries ) {
787                 $cleaned_query = array();
788
789                 $defaults = array(
790                         'taxonomy' => '',
791                         'terms' => array(),
792                         'field' => 'term_id',
793                         'operator' => 'IN',
794                         'include_children' => true,
795                 );
796
797                 foreach ( $queries as $key => $query ) {
798                         if ( 'relation' === $key ) {
799                                 $cleaned_query['relation'] = $this->sanitize_relation( $query );
800
801                         // First-order clause.
802                         } elseif ( self::is_first_order_clause( $query ) ) {
803
804                                 $cleaned_clause = array_merge( $defaults, $query );
805                                 $cleaned_clause['terms'] = (array) $cleaned_clause['terms'];
806                                 $cleaned_query[] = $cleaned_clause;
807
808                                 /*
809                                  * Keep a copy of the clause in the flate
810                                  * $queried_terms array, for use in WP_Query.
811                                  */
812                                 if ( ! empty( $cleaned_clause['taxonomy'] ) && 'NOT IN' !== $cleaned_clause['operator'] ) {
813                                         $taxonomy = $cleaned_clause['taxonomy'];
814                                         if ( ! isset( $this->queried_terms[ $taxonomy ] ) ) {
815                                                 $this->queried_terms[ $taxonomy ] = array();
816                                         }
817
818                                         /*
819                                          * Backward compatibility: Only store the first
820                                          * 'terms' and 'field' found for a given taxonomy.
821                                          */
822                                         if ( ! empty( $cleaned_clause['terms'] ) && ! isset( $this->queried_terms[ $taxonomy ]['terms'] ) ) {
823                                                 $this->queried_terms[ $taxonomy ]['terms'] = $cleaned_clause['terms'];
824                                         }
825
826                                         if ( ! empty( $cleaned_clause['field'] ) && ! isset( $this->queried_terms[ $taxonomy ]['field'] ) ) {
827                                                 $this->queried_terms[ $taxonomy ]['field'] = $cleaned_clause['field'];
828                                         }
829                                 }
830
831                         // Otherwise, it's a nested query, so we recurse.
832                         } elseif ( is_array( $query ) ) {
833                                 $cleaned_subquery = $this->sanitize_query( $query );
834
835                                 if ( ! empty( $cleaned_subquery ) ) {
836                                         // All queries with children must have a relation.
837                                         if ( ! isset( $cleaned_subquery['relation'] ) ) {
838                                                 $cleaned_subquery['relation'] = 'AND';
839                                         }
840
841                                         $cleaned_query[] = $cleaned_subquery;
842                                 }
843                         }
844                 }
845
846                 return $cleaned_query;
847         }
848
849         /**
850          * Sanitize a 'relation' operator.
851          *
852          * @since 4.1.0
853          * @access public
854          *
855          * @param string $relation Raw relation key from the query argument.
856          * @return string Sanitized relation ('AND' or 'OR').
857          */
858         public function sanitize_relation( $relation ) {
859                 if ( 'OR' === strtoupper( $relation ) ) {
860                         return 'OR';
861                 } else {
862                         return 'AND';
863                 }
864         }
865
866         /**
867          * Determine whether a clause is first-order.
868          *
869          * A "first-order" clause is one that contains any of the first-order
870          * clause keys ('terms', 'taxonomy', 'include_children', 'field',
871          * 'operator'). An empty clause also counts as a first-order clause,
872          * for backward compatibility. Any clause that doesn't meet this is
873          * determined, by process of elimination, to be a higher-order query.
874          *
875          * @since 4.1.0
876          *
877          * @static
878          * @access protected
879          *
880          * @param array $query Tax query arguments.
881          * @return bool Whether the query clause is a first-order clause.
882          */
883         protected static function is_first_order_clause( $query ) {
884                 return is_array( $query ) && ( empty( $query ) || array_key_exists( 'terms', $query ) || array_key_exists( 'taxonomy', $query ) || array_key_exists( 'include_children', $query ) || array_key_exists( 'field', $query ) || array_key_exists( 'operator', $query ) );
885         }
886
887         /**
888          * Generates SQL clauses to be appended to a main query.
889          *
890          * @since 3.1.0
891          *
892          * @static
893          * @access public
894          *
895          * @param string $primary_table     Database table where the object being filtered is stored (eg wp_users).
896          * @param string $primary_id_column ID column for the filtered object in $primary_table.
897          * @return array {
898          *     Array containing JOIN and WHERE SQL clauses to append to the main query.
899          *
900          *     @type string $join  SQL fragment to append to the main JOIN clause.
901          *     @type string $where SQL fragment to append to the main WHERE clause.
902          * }
903          */
904         public function get_sql( $primary_table, $primary_id_column ) {
905                 $this->primary_table = $primary_table;
906                 $this->primary_id_column = $primary_id_column;
907
908                 return $this->get_sql_clauses();
909         }
910
911         /**
912          * Generate SQL clauses to be appended to a main query.
913          *
914          * Called by the public WP_Tax_Query::get_sql(), this method
915          * is abstracted out to maintain parity with the other Query classes.
916          *
917          * @since 4.1.0
918          * @access protected
919          *
920          * @return array {
921          *     Array containing JOIN and WHERE SQL clauses to append to the main query.
922          *
923          *     @type string $join  SQL fragment to append to the main JOIN clause.
924          *     @type string $where SQL fragment to append to the main WHERE clause.
925          * }
926          */
927         protected function get_sql_clauses() {
928                 /*
929                  * $queries are passed by reference to get_sql_for_query() for recursion.
930                  * To keep $this->queries unaltered, pass a copy.
931                  */
932                 $queries = $this->queries;
933                 $sql = $this->get_sql_for_query( $queries );
934
935                 if ( ! empty( $sql['where'] ) ) {
936                         $sql['where'] = ' AND ' . $sql['where'];
937                 }
938
939                 return $sql;
940         }
941
942         /**
943          * Generate SQL clauses for a single query array.
944          *
945          * If nested subqueries are found, this method recurses the tree to
946          * produce the properly nested SQL.
947          *
948          * @since 4.1.0
949          * @access protected
950          *
951          * @param array $query Query to parse, passed by reference.
952          * @param int   $depth Optional. Number of tree levels deep we currently are.
953          *                     Used to calculate indentation. Default 0.
954          * @return array {
955          *     Array containing JOIN and WHERE SQL clauses to append to a single query array.
956          *
957          *     @type string $join  SQL fragment to append to the main JOIN clause.
958          *     @type string $where SQL fragment to append to the main WHERE clause.
959          * }
960          */
961         protected function get_sql_for_query( &$query, $depth = 0 ) {
962                 $sql_chunks = array(
963                         'join'  => array(),
964                         'where' => array(),
965                 );
966
967                 $sql = array(
968                         'join'  => '',
969                         'where' => '',
970                 );
971
972                 $indent = '';
973                 for ( $i = 0; $i < $depth; $i++ ) {
974                         $indent .= "  ";
975                 }
976
977                 foreach ( $query as $key => &$clause ) {
978                         if ( 'relation' === $key ) {
979                                 $relation = $query['relation'];
980                         } elseif ( is_array( $clause ) ) {
981
982                                 // This is a first-order clause.
983                                 if ( $this->is_first_order_clause( $clause ) ) {
984                                         $clause_sql = $this->get_sql_for_clause( $clause, $query );
985
986                                         $where_count = count( $clause_sql['where'] );
987                                         if ( ! $where_count ) {
988                                                 $sql_chunks['where'][] = '';
989                                         } elseif ( 1 === $where_count ) {
990                                                 $sql_chunks['where'][] = $clause_sql['where'][0];
991                                         } else {
992                                                 $sql_chunks['where'][] = '( ' . implode( ' AND ', $clause_sql['where'] ) . ' )';
993                                         }
994
995                                         $sql_chunks['join'] = array_merge( $sql_chunks['join'], $clause_sql['join'] );
996                                 // This is a subquery, so we recurse.
997                                 } else {
998                                         $clause_sql = $this->get_sql_for_query( $clause, $depth + 1 );
999
1000                                         $sql_chunks['where'][] = $clause_sql['where'];
1001                                         $sql_chunks['join'][]  = $clause_sql['join'];
1002                                 }
1003                         }
1004                 }
1005
1006                 // Filter to remove empties.
1007                 $sql_chunks['join']  = array_filter( $sql_chunks['join'] );
1008                 $sql_chunks['where'] = array_filter( $sql_chunks['where'] );
1009
1010                 if ( empty( $relation ) ) {
1011                         $relation = 'AND';
1012                 }
1013
1014                 // Filter duplicate JOIN clauses and combine into a single string.
1015                 if ( ! empty( $sql_chunks['join'] ) ) {
1016                         $sql['join'] = implode( ' ', array_unique( $sql_chunks['join'] ) );
1017                 }
1018
1019                 // Generate a single WHERE clause with proper brackets and indentation.
1020                 if ( ! empty( $sql_chunks['where'] ) ) {
1021                         $sql['where'] = '( ' . "\n  " . $indent . implode( ' ' . "\n  " . $indent . $relation . ' ' . "\n  " . $indent, $sql_chunks['where'] ) . "\n" . $indent . ')';
1022                 }
1023
1024                 return $sql;
1025         }
1026
1027         /**
1028          * Generate SQL JOIN and WHERE clauses for a "first-order" query clause.
1029          *
1030          * @since 4.1.0
1031          * @access public
1032          *
1033          * @global wpdb $wpdb The WordPress database abstraction object.
1034          *
1035          * @param array $clause       Query clause, passed by reference.
1036          * @param array $parent_query Parent query array.
1037          * @return array {
1038          *     Array containing JOIN and WHERE SQL clauses to append to a first-order query.
1039          *
1040          *     @type string $join  SQL fragment to append to the main JOIN clause.
1041          *     @type string $where SQL fragment to append to the main WHERE clause.
1042          * }
1043          */
1044         public function get_sql_for_clause( &$clause, $parent_query ) {
1045                 global $wpdb;
1046
1047                 $sql = array(
1048                         'where' => array(),
1049                         'join'  => array(),
1050                 );
1051
1052                 $join = $where = '';
1053
1054                 $this->clean_query( $clause );
1055
1056                 if ( is_wp_error( $clause ) ) {
1057                         return self::$no_results;
1058                 }
1059
1060                 $terms = $clause['terms'];
1061                 $operator = strtoupper( $clause['operator'] );
1062
1063                 if ( 'IN' == $operator ) {
1064
1065                         if ( empty( $terms ) ) {
1066                                 return self::$no_results;
1067                         }
1068
1069                         $terms = implode( ',', $terms );
1070
1071                         /*
1072                          * Before creating another table join, see if this clause has a
1073                          * sibling with an existing join that can be shared.
1074                          */
1075                         $alias = $this->find_compatible_table_alias( $clause, $parent_query );
1076                         if ( false === $alias ) {
1077                                 $i = count( $this->table_aliases );
1078                                 $alias = $i ? 'tt' . $i : $wpdb->term_relationships;
1079
1080                                 // Store the alias as part of a flat array to build future iterators.
1081                                 $this->table_aliases[] = $alias;
1082
1083                                 // Store the alias with this clause, so later siblings can use it.
1084                                 $clause['alias'] = $alias;
1085
1086                                 $join .= " INNER JOIN $wpdb->term_relationships";
1087                                 $join .= $i ? " AS $alias" : '';
1088                                 $join .= " ON ($this->primary_table.$this->primary_id_column = $alias.object_id)";
1089                         }
1090
1091
1092                         $where = "$alias.term_taxonomy_id $operator ($terms)";
1093
1094                 } elseif ( 'NOT IN' == $operator ) {
1095
1096                         if ( empty( $terms ) ) {
1097                                 return $sql;
1098                         }
1099
1100                         $terms = implode( ',', $terms );
1101
1102                         $where = "$this->primary_table.$this->primary_id_column NOT IN (
1103                                 SELECT object_id
1104                                 FROM $wpdb->term_relationships
1105                                 WHERE term_taxonomy_id IN ($terms)
1106                         )";
1107
1108                 } elseif ( 'AND' == $operator ) {
1109
1110                         if ( empty( $terms ) ) {
1111                                 return $sql;
1112                         }
1113
1114                         $num_terms = count( $terms );
1115
1116                         $terms = implode( ',', $terms );
1117
1118                         $where = "(
1119                                 SELECT COUNT(1)
1120                                 FROM $wpdb->term_relationships
1121                                 WHERE term_taxonomy_id IN ($terms)
1122                                 AND object_id = $this->primary_table.$this->primary_id_column
1123                         ) = $num_terms";
1124
1125                 } elseif ( 'NOT EXISTS' === $operator || 'EXISTS' === $operator ) {
1126
1127                         $where = $wpdb->prepare( "$operator (
1128                                 SELECT 1
1129                                 FROM $wpdb->term_relationships
1130                                 INNER JOIN $wpdb->term_taxonomy
1131                                 ON $wpdb->term_taxonomy.term_taxonomy_id = $wpdb->term_relationships.term_taxonomy_id
1132                                 WHERE $wpdb->term_taxonomy.taxonomy = %s
1133                                 AND $wpdb->term_relationships.object_id = $this->primary_table.$this->primary_id_column
1134                         )", $clause['taxonomy'] );
1135
1136                 }
1137
1138                 $sql['join'][]  = $join;
1139                 $sql['where'][] = $where;
1140                 return $sql;
1141         }
1142
1143         /**
1144          * Identify an existing table alias that is compatible with the current query clause.
1145          *
1146          * We avoid unnecessary table joins by allowing each clause to look for
1147          * an existing table alias that is compatible with the query that it
1148          * needs to perform.
1149          *
1150          * An existing alias is compatible if (a) it is a sibling of `$clause`
1151          * (ie, it's under the scope of the same relation), and (b) the combination
1152          * of operator and relation between the clauses allows for a shared table
1153          * join. In the case of WP_Tax_Query, this only applies to 'IN'
1154          * clauses that are connected by the relation 'OR'.
1155          *
1156          * @since 4.1.0
1157          * @access protected
1158          *
1159          * @param array       $clause       Query clause.
1160          * @param array       $parent_query Parent query of $clause.
1161          * @return string|false Table alias if found, otherwise false.
1162          */
1163         protected function find_compatible_table_alias( $clause, $parent_query ) {
1164                 $alias = false;
1165
1166                 // Sanity check. Only IN queries use the JOIN syntax .
1167                 if ( ! isset( $clause['operator'] ) || 'IN' !== $clause['operator'] ) {
1168                         return $alias;
1169                 }
1170
1171                 // Since we're only checking IN queries, we're only concerned with OR relations.
1172                 if ( ! isset( $parent_query['relation'] ) || 'OR' !== $parent_query['relation'] ) {
1173                         return $alias;
1174                 }
1175
1176                 $compatible_operators = array( 'IN' );
1177
1178                 foreach ( $parent_query as $sibling ) {
1179                         if ( ! is_array( $sibling ) || ! $this->is_first_order_clause( $sibling ) ) {
1180                                 continue;
1181                         }
1182
1183                         if ( empty( $sibling['alias'] ) || empty( $sibling['operator'] ) ) {
1184                                 continue;
1185                         }
1186
1187                         // The sibling must both have compatible operator to share its alias.
1188                         if ( in_array( strtoupper( $sibling['operator'] ), $compatible_operators ) ) {
1189                                 $alias = $sibling['alias'];
1190                                 break;
1191                         }
1192                 }
1193
1194                 return $alias;
1195         }
1196
1197         /**
1198          * Validates a single query.
1199          *
1200          * @since 3.2.0
1201          * @access private
1202          *
1203          * @param array &$query The single query.
1204          */
1205         private function clean_query( &$query ) {
1206                 if ( empty( $query['taxonomy'] ) ) {
1207                         if ( 'term_taxonomy_id' !== $query['field'] ) {
1208                                 $query = new WP_Error( 'Invalid taxonomy' );
1209                                 return;
1210                         }
1211
1212                         // so long as there are shared terms, include_children requires that a taxonomy is set
1213                         $query['include_children'] = false;
1214                 } elseif ( ! taxonomy_exists( $query['taxonomy'] ) ) {
1215                         $query = new WP_Error( 'Invalid taxonomy' );
1216                         return;
1217                 }
1218
1219                 $query['terms'] = array_unique( (array) $query['terms'] );
1220
1221                 if ( is_taxonomy_hierarchical( $query['taxonomy'] ) && $query['include_children'] ) {
1222                         $this->transform_query( $query, 'term_id' );
1223
1224                         if ( is_wp_error( $query ) )
1225                                 return;
1226
1227                         $children = array();
1228                         foreach ( $query['terms'] as $term ) {
1229                                 $children = array_merge( $children, get_term_children( $term, $query['taxonomy'] ) );
1230                                 $children[] = $term;
1231                         }
1232                         $query['terms'] = $children;
1233                 }
1234
1235                 $this->transform_query( $query, 'term_taxonomy_id' );
1236         }
1237
1238         /**
1239          * Transforms a single query, from one field to another.
1240          *
1241          * @since 3.2.0
1242          *
1243          * @global wpdb $wpdb The WordPress database abstraction object.
1244          *
1245          * @param array  &$query          The single query.
1246          * @param string $resulting_field The resulting field. Accepts 'slug', 'name', 'term_taxonomy_id',
1247          *                                or 'term_id'. Default 'term_id'.
1248          */
1249         public function transform_query( &$query, $resulting_field ) {
1250                 global $wpdb;
1251
1252                 if ( empty( $query['terms'] ) )
1253                         return;
1254
1255                 if ( $query['field'] == $resulting_field )
1256                         return;
1257
1258                 $resulting_field = sanitize_key( $resulting_field );
1259
1260                 switch ( $query['field'] ) {
1261                         case 'slug':
1262                         case 'name':
1263                                 foreach ( $query['terms'] as &$term ) {
1264                                         /*
1265                                          * 0 is the $term_id parameter. We don't have a term ID yet, but it doesn't
1266                                          * matter because `sanitize_term_field()` ignores the $term_id param when the
1267                                          * context is 'db'.
1268                                          */
1269                                         $term = "'" . esc_sql( sanitize_term_field( $query['field'], $term, 0, $query['taxonomy'], 'db' ) ) . "'";
1270                                 }
1271
1272                                 $terms = implode( ",", $query['terms'] );
1273
1274                                 $terms = $wpdb->get_col( "
1275                                         SELECT $wpdb->term_taxonomy.$resulting_field
1276                                         FROM $wpdb->term_taxonomy
1277                                         INNER JOIN $wpdb->terms USING (term_id)
1278                                         WHERE taxonomy = '{$query['taxonomy']}'
1279                                         AND $wpdb->terms.{$query['field']} IN ($terms)
1280                                 " );
1281                                 break;
1282                         case 'term_taxonomy_id':
1283                                 $terms = implode( ',', array_map( 'intval', $query['terms'] ) );
1284                                 $terms = $wpdb->get_col( "
1285                                         SELECT $resulting_field
1286                                         FROM $wpdb->term_taxonomy
1287                                         WHERE term_taxonomy_id IN ($terms)
1288                                 " );
1289                                 break;
1290                         default:
1291                                 $terms = implode( ',', array_map( 'intval', $query['terms'] ) );
1292                                 $terms = $wpdb->get_col( "
1293                                         SELECT $resulting_field
1294                                         FROM $wpdb->term_taxonomy
1295                                         WHERE taxonomy = '{$query['taxonomy']}'
1296                                         AND term_id IN ($terms)
1297                                 " );
1298                 }
1299
1300                 if ( 'AND' == $query['operator'] && count( $terms ) < count( $query['terms'] ) ) {
1301                         $query = new WP_Error( 'Inexistent terms' );
1302                         return;
1303                 }
1304
1305                 $query['terms'] = $terms;
1306                 $query['field'] = $resulting_field;
1307         }
1308 }
1309
1310 /**
1311  * Get all Term data from database by Term ID.
1312  *
1313  * The usage of the get_term function is to apply filters to a term object. It
1314  * is possible to get a term object from the database before applying the
1315  * filters.
1316  *
1317  * $term ID must be part of $taxonomy, to get from the database. Failure, might
1318  * be able to be captured by the hooks. Failure would be the same value as $wpdb
1319  * returns for the get_row method.
1320  *
1321  * There are two hooks, one is specifically for each term, named 'get_term', and
1322  * the second is for the taxonomy name, 'term_$taxonomy'. Both hooks gets the
1323  * term object, and the taxonomy name as parameters. Both hooks are expected to
1324  * return a Term object.
1325  *
1326  * {@see 'get_term'} hook - Takes two parameters the term Object and the taxonomy name.
1327  * Must return term object. Used in get_term() as a catch-all filter for every
1328  * $term.
1329  *
1330  * {@see 'get_$taxonomy'} hook - Takes two parameters the term Object and the taxonomy
1331  * name. Must return term object. $taxonomy will be the taxonomy name, so for
1332  * example, if 'category', it would be 'get_category' as the filter name. Useful
1333  * for custom taxonomies or plugging into default taxonomies.
1334  *
1335  * @todo Better formatting for DocBlock
1336  *
1337  * @since 2.3.0
1338  *
1339  * @global wpdb $wpdb WordPress database abstraction object.
1340  * @see sanitize_term_field() The $context param lists the available values for get_term_by() $filter param.
1341  *
1342  * @param int|object $term     If integer, will get from database. If object will apply filters and return $term.
1343  * @param string     $taxonomy Taxonomy name that $term is part of.
1344  * @param string     $output   Constant OBJECT, ARRAY_A, or ARRAY_N
1345  * @param string     $filter   Optional, default is raw or no WordPress defined filter will applied.
1346  * @return object|array|null|WP_Error Term Row from database. Will return null if $term is empty. If taxonomy does not
1347  * exist then WP_Error will be returned.
1348  */
1349 function get_term($term, $taxonomy, $output = OBJECT, $filter = 'raw') {
1350         global $wpdb;
1351
1352         if ( empty( $term ) ) {
1353                 return new WP_Error( 'invalid_term', __( 'Empty Term' ) );
1354         }
1355
1356         if ( ! taxonomy_exists( $taxonomy ) ) {
1357                 return new WP_Error( 'invalid_taxonomy', __( 'Invalid taxonomy' ) );
1358         }
1359
1360         if ( is_object($term) && empty($term->filter) ) {
1361                 wp_cache_add( $term->term_id, $term, $taxonomy );
1362                 $_term = $term;
1363         } else {
1364                 if ( is_object($term) )
1365                         $term = $term->term_id;
1366                 if ( !$term = (int) $term )
1367                         return null;
1368                 if ( ! $_term = wp_cache_get( $term, $taxonomy ) ) {
1369                         $_term = $wpdb->get_row( $wpdb->prepare( "SELECT t.*, tt.* FROM $wpdb->terms AS t INNER JOIN $wpdb->term_taxonomy AS tt ON t.term_id = tt.term_id WHERE tt.taxonomy = %s AND t.term_id = %d LIMIT 1", $taxonomy, $term) );
1370                         if ( ! $_term )
1371                                 return null;
1372                         wp_cache_add( $term, $_term, $taxonomy );
1373                 }
1374         }
1375
1376         /**
1377          * Filter a term.
1378          *
1379          * @since 2.3.0
1380          *
1381          * @param int|object $_term    Term object or ID.
1382          * @param string     $taxonomy The taxonomy slug.
1383          */
1384         $_term = apply_filters( 'get_term', $_term, $taxonomy );
1385
1386         /**
1387          * Filter a taxonomy.
1388          *
1389          * The dynamic portion of the filter name, `$taxonomy`, refers
1390          * to the taxonomy slug.
1391          *
1392          * @since 2.3.0
1393          *
1394          * @param int|object $_term    Term object or ID.
1395          * @param string     $taxonomy The taxonomy slug.
1396          */
1397         $_term = apply_filters( "get_$taxonomy", $_term, $taxonomy );
1398         $_term = sanitize_term($_term, $taxonomy, $filter);
1399
1400         if ( $output == OBJECT ) {
1401                 return $_term;
1402         } elseif ( $output == ARRAY_A ) {
1403                 $__term = get_object_vars($_term);
1404                 return $__term;
1405         } elseif ( $output == ARRAY_N ) {
1406                 $__term = array_values(get_object_vars($_term));
1407                 return $__term;
1408         } else {
1409                 return $_term;
1410         }
1411 }
1412
1413 /**
1414  * Get all Term data from database by Term field and data.
1415  *
1416  * Warning: $value is not escaped for 'name' $field. You must do it yourself, if
1417  * required.
1418  *
1419  * The default $field is 'id', therefore it is possible to also use null for
1420  * field, but not recommended that you do so.
1421  *
1422  * If $value does not exist, the return value will be false. If $taxonomy exists
1423  * and $field and $value combinations exist, the Term will be returned.
1424  *
1425  * @todo Better formatting for DocBlock.
1426  *
1427  * @since 2.3.0
1428  *
1429  * @global wpdb $wpdb WordPress database abstraction object.
1430  * @see sanitize_term_field() The $context param lists the available values for get_term_by() $filter param.
1431  *
1432  * @param string     $field    Either 'slug', 'name', 'id' (term_id), or 'term_taxonomy_id'
1433  * @param string|int $value    Search for this term value
1434  * @param string     $taxonomy Taxonomy Name
1435  * @param string     $output   Constant OBJECT, ARRAY_A, or ARRAY_N
1436  * @param string     $filter   Optional, default is raw or no WordPress defined filter will applied.
1437  * @return object|array|null|WP_Error|false Term Row from database.
1438  *                                          Will return false if $taxonomy does not exist or $term was not found.
1439  */
1440 function get_term_by($field, $value, $taxonomy, $output = OBJECT, $filter = 'raw') {
1441         global $wpdb;
1442
1443         if ( ! taxonomy_exists($taxonomy) )
1444                 return false;
1445
1446         if ( 'slug' == $field ) {
1447                 $field = 't.slug';
1448                 $value = sanitize_title($value);
1449                 if ( empty($value) )
1450                         return false;
1451         } elseif ( 'name' == $field ) {
1452                 // Assume already escaped
1453                 $value = wp_unslash($value);
1454                 $field = 't.name';
1455         } elseif ( 'term_taxonomy_id' == $field ) {
1456                 $value = (int) $value;
1457                 $field = 'tt.term_taxonomy_id';
1458         } else {
1459                 $term = get_term( (int) $value, $taxonomy, $output, $filter );
1460                 if ( is_wp_error( $term ) )
1461                         $term = false;
1462                 return $term;
1463         }
1464
1465         $term = $wpdb->get_row( $wpdb->prepare( "SELECT t.*, tt.* FROM $wpdb->terms AS t INNER JOIN $wpdb->term_taxonomy AS tt ON t.term_id = tt.term_id WHERE tt.taxonomy = %s AND $field = %s LIMIT 1", $taxonomy, $value ) );
1466         if ( ! $term )
1467                 return false;
1468
1469         wp_cache_add( $term->term_id, $term, $taxonomy );
1470
1471         /** This filter is documented in wp-includes/taxonomy.php */
1472         $term = apply_filters( 'get_term', $term, $taxonomy );
1473
1474         /** This filter is documented in wp-includes/taxonomy.php */
1475         $term = apply_filters( "get_$taxonomy", $term, $taxonomy );
1476
1477         $term = sanitize_term($term, $taxonomy, $filter);
1478
1479         if ( $output == OBJECT ) {
1480                 return $term;
1481         } elseif ( $output == ARRAY_A ) {
1482                 return get_object_vars($term);
1483         } elseif ( $output == ARRAY_N ) {
1484                 return array_values(get_object_vars($term));
1485         } else {
1486                 return $term;
1487         }
1488 }
1489
1490 /**
1491  * Merge all term children into a single array of their IDs.
1492  *
1493  * This recursive function will merge all of the children of $term into the same
1494  * array of term IDs. Only useful for taxonomies which are hierarchical.
1495  *
1496  * Will return an empty array if $term does not exist in $taxonomy.
1497  *
1498  * @since 2.3.0
1499  *
1500  * @param string $term_id  ID of Term to get children.
1501  * @param string $taxonomy Taxonomy Name.
1502  * @return array|WP_Error List of Term IDs. WP_Error returned if `$taxonomy` does not exist.
1503  */
1504 function get_term_children( $term_id, $taxonomy ) {
1505         if ( ! taxonomy_exists($taxonomy) )
1506                 return new WP_Error('invalid_taxonomy', __('Invalid taxonomy'));
1507
1508         $term_id = intval( $term_id );
1509
1510         $terms = _get_term_hierarchy($taxonomy);
1511
1512         if ( ! isset($terms[$term_id]) )
1513                 return array();
1514
1515         $children = $terms[$term_id];
1516
1517         foreach ( (array) $terms[$term_id] as $child ) {
1518                 if ( $term_id == $child ) {
1519                         continue;
1520                 }
1521
1522                 if ( isset($terms[$child]) )
1523                         $children = array_merge($children, get_term_children($child, $taxonomy));
1524         }
1525
1526         return $children;
1527 }
1528
1529 /**
1530  * Get sanitized Term field.
1531  *
1532  * Does checks for $term, based on the $taxonomy. The function is for contextual
1533  * reasons and for simplicity of usage. See sanitize_term_field() for more
1534  * information.
1535  *
1536  * @since 2.3.0
1537  *
1538  * @param string $field    Term field to fetch.
1539  * @param int    $term     Term ID.
1540  * @param string $taxonomy Taxonomy Name.
1541  * @param string $context  Optional, default is display. Look at sanitize_term_field() for available options.
1542  * @return string|int|null|WP_Error Will return an empty string if $term is not an object or if $field is not set in $term.
1543  */
1544 function get_term_field( $field, $term, $taxonomy, $context = 'display' ) {
1545         $term = (int) $term;
1546         $term = get_term( $term, $taxonomy );
1547         if ( is_wp_error($term) )
1548                 return $term;
1549
1550         if ( !is_object($term) )
1551                 return '';
1552
1553         if ( !isset($term->$field) )
1554                 return '';
1555
1556         return sanitize_term_field($field, $term->$field, $term->term_id, $taxonomy, $context);
1557 }
1558
1559 /**
1560  * Sanitizes Term for editing.
1561  *
1562  * Return value is sanitize_term() and usage is for sanitizing the term for
1563  * editing. Function is for contextual and simplicity.
1564  *
1565  * @since 2.3.0
1566  *
1567  * @param int|object $id       Term ID or object.
1568  * @param string     $taxonomy Taxonomy name.
1569  * @return string|int|null|WP_Error Will return empty string if $term is not an object.
1570  */
1571 function get_term_to_edit( $id, $taxonomy ) {
1572         $term = get_term( $id, $taxonomy );
1573
1574         if ( is_wp_error($term) )
1575                 return $term;
1576
1577         if ( !is_object($term) )
1578                 return '';
1579
1580         return sanitize_term($term, $taxonomy, 'edit');
1581 }
1582
1583 /**
1584  * Retrieve the terms in a given taxonomy or list of taxonomies.
1585  *
1586  * You can fully inject any customizations to the query before it is sent, as
1587  * well as control the output with a filter.
1588  *
1589  * The {@see 'get_terms'} filter will be called when the cache has the term and will
1590  * pass the found term along with the array of $taxonomies and array of $args.
1591  * This filter is also called before the array of terms is passed and will pass
1592  * the array of terms, along with the $taxonomies and $args.
1593  *
1594  * The {@see 'list_terms_exclusions'} filter passes the compiled exclusions along with
1595  * the $args.
1596  *
1597  * The {@see 'get_terms_orderby'} filter passes the `ORDER BY` clause for the query
1598  * along with the $args array.
1599  *
1600  * @since 2.3.0
1601  * @since 4.2.0 Introduced 'name' and 'childless' parameters.
1602  *
1603  * @global wpdb  $wpdb WordPress database abstraction object.
1604  * @global array $wp_filter
1605  *
1606  * @param string|array $taxonomies Taxonomy name or list of Taxonomy names.
1607  * @param array|string $args {
1608  *     Optional. Array or string of arguments to get terms.
1609  *
1610  *     @type string       $orderby           Field(s) to order terms by. Accepts term fields ('name', 'slug',
1611  *                                           'term_group', 'term_id', 'id', 'description'), 'count' for term
1612  *                                           taxonomy count, 'include' to match the 'order' of the $include param,
1613  *                                           or 'none' to skip ORDER BY. Defaults to 'name'.
1614  *     @type string       $order             Whether to order terms in ascending or descending order.
1615  *                                           Accepts 'ASC' (ascending) or 'DESC' (descending).
1616  *                                           Default 'ASC'.
1617  *     @type bool|int     $hide_empty        Whether to hide terms not assigned to any posts. Accepts
1618  *                                           1|true or 0|false. Default 1|true.
1619  *     @type array|string $include           Array or comma/space-separated string of term ids to include.
1620  *                                           Default empty array.
1621  *     @type array|string $exclude           Array or comma/space-separated string of term ids to exclude.
1622  *                                           If $include is non-empty, $exclude is ignored.
1623  *                                           Default empty array.
1624  *     @type array|string $exclude_tree      Array or comma/space-separated string of term ids to exclude
1625  *                                           along with all of their descendant terms. If $include is
1626  *                                           non-empty, $exclude_tree is ignored. Default empty array.
1627  *     @type int|string   $number            Maximum number of terms to return. Accepts ''|0 (all) or any
1628  *                                           positive number. Default ''|0 (all).
1629  *     @type int          $offset            The number by which to offset the terms query. Default empty.
1630  *     @type string       $fields            Term fields to query for. Accepts 'all' (returns an array of
1631  *                                           term objects), 'ids' or 'names' (returns an array of integers
1632  *                                           or strings, respectively. Default 'all'.
1633  *     @type string|array $name              Optional. Name or array of names to return term(s) for. Default empty.
1634  *     @type string|array $slug              Optional. Slug or array of slugs to return term(s) for. Default empty.
1635  *     @type bool         $hierarchical      Whether to include terms that have non-empty descendants (even
1636  *                                           if $hide_empty is set to true). Default true.
1637  *     @type string       $search            Search criteria to match terms. Will be SQL-formatted with
1638  *                                           wildcards before and after. Default empty.
1639  *     @type string       $name__like        Retrieve terms with criteria by which a term is LIKE $name__like.
1640  *                                           Default empty.
1641  *     @type string       $description__like Retrieve terms where the description is LIKE $description__like.
1642  *                                           Default empty.
1643  *     @type bool         $pad_counts        Whether to pad the quantity of a term's children in the quantity
1644  *                                           of each term's "count" object variable. Default false.
1645  *     @type string       $get               Whether to return terms regardless of ancestry or whether the terms
1646  *                                           are empty. Accepts 'all' or empty (disabled). Default empty.
1647  *     @type int          $child_of          Term ID to retrieve child terms of. If multiple taxonomies
1648  *                                           are passed, $child_of is ignored. Default 0.
1649  *     @type int|string   $parent            Parent term ID to retrieve direct-child terms of. Default empty.
1650  *     @type bool         $childless         True to limit results to terms that have no children. This parameter has
1651  *                                           no effect on non-hierarchical taxonomies. Default false.
1652  *     @type string       $cache_domain      Unique cache key to be produced when this query is stored in an
1653  *                                           object cache. Default is 'core'.
1654  * }
1655  * @return array|int|WP_Error List of Term Objects and their children. Will return WP_Error, if any of $taxonomies
1656  *                        do not exist.
1657  */
1658 function get_terms( $taxonomies, $args = '' ) {
1659         global $wpdb;
1660         $empty_array = array();
1661
1662         $single_taxonomy = ! is_array( $taxonomies ) || 1 === count( $taxonomies );
1663         if ( ! is_array( $taxonomies ) ) {
1664                 $taxonomies = array( $taxonomies );
1665         }
1666
1667         foreach ( $taxonomies as $taxonomy ) {
1668                 if ( ! taxonomy_exists($taxonomy) ) {
1669                         return new WP_Error( 'invalid_taxonomy', __( 'Invalid taxonomy' ) );
1670                 }
1671         }
1672
1673         $defaults = array('orderby' => 'name', 'order' => 'ASC',
1674                 'hide_empty' => true, 'exclude' => array(), 'exclude_tree' => array(), 'include' => array(),
1675                 'number' => '', 'fields' => 'all', 'name' => '', 'slug' => '', 'parent' => '', 'childless' => false,
1676                 'hierarchical' => true, 'child_of' => 0, 'get' => '', 'name__like' => '', 'description__like' => '',
1677                 'pad_counts' => false, 'offset' => '', 'search' => '', 'cache_domain' => 'core' );
1678         $args = wp_parse_args( $args, $defaults );
1679         $args['number'] = absint( $args['number'] );
1680         $args['offset'] = absint( $args['offset'] );
1681
1682         // Save queries by not crawling the tree in the case of multiple taxes or a flat tax.
1683         $has_hierarchical_tax = false;
1684         foreach ( $taxonomies as $_tax ) {
1685                 if ( is_taxonomy_hierarchical( $_tax ) ) {
1686                         $has_hierarchical_tax = true;
1687                 }
1688         }
1689
1690         if ( ! $has_hierarchical_tax ) {
1691                 $args['hierarchical'] = false;
1692                 $args['pad_counts'] = false;
1693         }
1694
1695         // 'parent' overrides 'child_of'.
1696         if ( 0 < intval( $args['parent'] ) ) {
1697                 $args['child_of'] = false;
1698         }
1699
1700         if ( 'all' == $args['get'] ) {
1701                 $args['childless'] = false;
1702                 $args['child_of'] = 0;
1703                 $args['hide_empty'] = 0;
1704                 $args['hierarchical'] = false;
1705                 $args['pad_counts'] = false;
1706         }
1707
1708         /**
1709          * Filter the terms query arguments.
1710          *
1711          * @since 3.1.0
1712          *
1713          * @param array $args       An array of get_term() arguments.
1714          * @param array $taxonomies An array of taxonomies.
1715          */
1716         $args = apply_filters( 'get_terms_args', $args, $taxonomies );
1717
1718         // Avoid the query if the queried parent/child_of term has no descendants.
1719         $child_of = $args['child_of'];
1720         $parent   = $args['parent'];
1721
1722         if ( $child_of ) {
1723                 $_parent = $child_of;
1724         } elseif ( $parent ) {
1725                 $_parent = $parent;
1726         } else {
1727                 $_parent = false;
1728         }
1729
1730         if ( $_parent ) {
1731                 $in_hierarchy = false;
1732                 foreach ( $taxonomies as $_tax ) {
1733                         $hierarchy = _get_term_hierarchy( $_tax );
1734
1735                         if ( isset( $hierarchy[ $_parent ] ) ) {
1736                                 $in_hierarchy = true;
1737                         }
1738                 }
1739
1740                 if ( ! $in_hierarchy ) {
1741                         return $empty_array;
1742                 }
1743         }
1744
1745         // $args can be whatever, only use the args defined in defaults to compute the key.
1746         $filter_key = ( has_filter('list_terms_exclusions') ) ? serialize($GLOBALS['wp_filter']['list_terms_exclusions']) : '';
1747         $key = md5( serialize( wp_array_slice_assoc( $args, array_keys( $defaults ) ) ) . serialize( $taxonomies ) . $filter_key );
1748         $last_changed = wp_cache_get( 'last_changed', 'terms' );
1749         if ( ! $last_changed ) {
1750                 $last_changed = microtime();
1751                 wp_cache_set( 'last_changed', $last_changed, 'terms' );
1752         }
1753         $cache_key = "get_terms:$key:$last_changed";
1754         $cache = wp_cache_get( $cache_key, 'terms' );
1755         if ( false !== $cache ) {
1756
1757                 /**
1758                  * Filter the given taxonomy's terms cache.
1759                  *
1760                  * @since 2.3.0
1761                  *
1762                  * @param array $cache      Cached array of terms for the given taxonomy.
1763                  * @param array $taxonomies An array of taxonomies.
1764                  * @param array $args       An array of get_terms() arguments.
1765                  */
1766                 return apply_filters( 'get_terms', $cache, $taxonomies, $args );
1767         }
1768
1769         $_orderby = strtolower( $args['orderby'] );
1770         if ( 'count' == $_orderby ) {
1771                 $orderby = 'tt.count';
1772         } elseif ( 'name' == $_orderby ) {
1773                 $orderby = 't.name';
1774         } elseif ( 'slug' == $_orderby ) {
1775                 $orderby = 't.slug';
1776         } elseif ( 'include' == $_orderby && ! empty( $args['include'] ) ) {
1777                 $include = implode( ',', array_map( 'absint', $args['include'] ) );
1778                 $orderby = "FIELD( t.term_id, $include )";
1779         } elseif ( 'term_group' == $_orderby ) {
1780                 $orderby = 't.term_group';
1781         } elseif ( 'description' == $_orderby ) {
1782                 $orderby = 'tt.description';
1783         } elseif ( 'none' == $_orderby ) {
1784                 $orderby = '';
1785         } elseif ( empty($_orderby) || 'id' == $_orderby ) {
1786                 $orderby = 't.term_id';
1787         } else {
1788                 $orderby = 't.name';
1789         }
1790
1791         /**
1792          * Filter the ORDERBY clause of the terms query.
1793          *
1794          * @since 2.8.0
1795          *
1796          * @param string $orderby    `ORDERBY` clause of the terms query.
1797          * @param array  $args       An array of terms query arguments.
1798          * @param array  $taxonomies An array of taxonomies.
1799          */
1800         $orderby = apply_filters( 'get_terms_orderby', $orderby, $args, $taxonomies );
1801
1802         $order = strtoupper( $args['order'] );
1803         if ( ! empty( $orderby ) ) {
1804                 $orderby = "ORDER BY $orderby";
1805         } else {
1806                 $order = '';
1807         }
1808
1809         if ( '' !== $order && ! in_array( $order, array( 'ASC', 'DESC' ) ) ) {
1810                 $order = 'ASC';
1811         }
1812
1813         $where = "tt.taxonomy IN ('" . implode("', '", $taxonomies) . "')";
1814
1815         $exclude = $args['exclude'];
1816         $exclude_tree = $args['exclude_tree'];
1817         $include = $args['include'];
1818
1819         $inclusions = '';
1820         if ( ! empty( $include ) ) {
1821                 $exclude = '';
1822                 $exclude_tree = '';
1823                 $inclusions = implode( ',', wp_parse_id_list( $include ) );
1824         }
1825
1826         if ( ! empty( $inclusions ) ) {
1827                 $inclusions = ' AND t.term_id IN ( ' . $inclusions . ' )';
1828                 $where .= $inclusions;
1829         }
1830
1831         $exclusions = array();
1832         if ( ! empty( $exclude_tree ) ) {
1833                 $exclude_tree = wp_parse_id_list( $exclude_tree );
1834                 $excluded_children = $exclude_tree;
1835                 foreach ( $exclude_tree as $extrunk ) {
1836                         $excluded_children = array_merge(
1837                                 $excluded_children,
1838                                 (array) get_terms( $taxonomies[0], array( 'child_of' => intval( $extrunk ), 'fields' => 'ids', 'hide_empty' => 0 ) )
1839                         );
1840                 }
1841                 $exclusions = array_merge( $excluded_children, $exclusions );
1842         }
1843
1844         if ( ! empty( $exclude ) ) {
1845                 $exclusions = array_merge( wp_parse_id_list( $exclude ), $exclusions );
1846         }
1847
1848         // 'childless' terms are those without an entry in the flattened term hierarchy.
1849         $childless = (bool) $args['childless'];
1850         if ( $childless ) {
1851                 foreach ( $taxonomies as $_tax ) {
1852                         $term_hierarchy = _get_term_hierarchy( $_tax );
1853                         $exclusions = array_merge( array_keys( $term_hierarchy ), $exclusions );
1854                 }
1855         }
1856
1857         if ( ! empty( $exclusions ) ) {
1858                 $exclusions = ' AND t.term_id NOT IN (' . implode( ',', array_map( 'intval', $exclusions ) ) . ')';
1859         } else {
1860                 $exclusions = '';
1861         }
1862
1863         /**
1864          * Filter the terms to exclude from the terms query.
1865          *
1866          * @since 2.3.0
1867          *
1868          * @param string $exclusions `NOT IN` clause of the terms query.
1869          * @param array  $args       An array of terms query arguments.
1870          * @param array  $taxonomies An array of taxonomies.
1871          */
1872         $exclusions = apply_filters( 'list_terms_exclusions', $exclusions, $args, $taxonomies );
1873
1874         if ( ! empty( $exclusions ) ) {
1875                 $where .= $exclusions;
1876         }
1877
1878         if ( ! empty( $args['name'] ) ) {
1879                 $names = (array) $args['name'];
1880                 foreach ( $names as &$_name ) {
1881                         $_name = sanitize_term_field( 'name', $_name, 0, reset( $taxonomies ), 'db' );
1882                 }
1883
1884                 $where .= " AND t.name IN ('" . implode( "', '", array_map( 'esc_sql', $names ) ) . "')";
1885         }
1886
1887         if ( ! empty( $args['slug'] ) ) {
1888                 if ( is_array( $args['slug'] ) ) {
1889                         $slug = array_map( 'sanitize_title', $args['slug'] );
1890                         $where .= " AND t.slug IN ('" . implode( "', '", $slug ) . "')";
1891                 } else {
1892                         $slug = sanitize_title( $args['slug'] );
1893                         $where .= " AND t.slug = '$slug'";
1894                 }
1895         }
1896
1897         if ( ! empty( $args['name__like'] ) ) {
1898                 $where .= $wpdb->prepare( " AND t.name LIKE %s", '%' . $wpdb->esc_like( $args['name__like'] ) . '%' );
1899         }
1900
1901         if ( ! empty( $args['description__like'] ) ) {
1902                 $where .= $wpdb->prepare( " AND tt.description LIKE %s", '%' . $wpdb->esc_like( $args['description__like'] ) . '%' );
1903         }
1904
1905         if ( '' !== $parent ) {
1906                 $parent = (int) $parent;
1907                 $where .= " AND tt.parent = '$parent'";
1908         }
1909
1910         $hierarchical = $args['hierarchical'];
1911         if ( 'count' == $args['fields'] ) {
1912                 $hierarchical = false;
1913         }
1914         if ( $args['hide_empty'] && !$hierarchical ) {
1915                 $where .= ' AND tt.count > 0';
1916         }
1917
1918         $number = $args['number'];
1919         $offset = $args['offset'];
1920
1921         // Don't limit the query results when we have to descend the family tree.
1922         if ( $number && ! $hierarchical && ! $child_of && '' === $parent ) {
1923                 if ( $offset ) {
1924                         $limits = 'LIMIT ' . $offset . ',' . $number;
1925                 } else {
1926                         $limits = 'LIMIT ' . $number;
1927                 }
1928         } else {
1929                 $limits = '';
1930         }
1931
1932         if ( ! empty( $args['search'] ) ) {
1933                 $like = '%' . $wpdb->esc_like( $args['search'] ) . '%';
1934                 $where .= $wpdb->prepare( ' AND ((t.name LIKE %s) OR (t.slug LIKE %s))', $like, $like );
1935         }
1936
1937         $selects = array();
1938         switch ( $args['fields'] ) {
1939                 case 'all':
1940                         $selects = array( 't.*', 'tt.*' );
1941                         break;
1942                 case 'ids':
1943                 case 'id=>parent':
1944                         $selects = array( 't.term_id', 'tt.parent', 'tt.count', 'tt.taxonomy' );
1945                         break;
1946                 case 'names':
1947                         $selects = array( 't.term_id', 'tt.parent', 'tt.count', 't.name', 'tt.taxonomy' );
1948                         break;
1949                 case 'count':
1950                         $orderby = '';
1951                         $order = '';
1952                         $selects = array( 'COUNT(*)' );
1953                         break;
1954                 case 'id=>name':
1955                         $selects = array( 't.term_id', 't.name', 'tt.count', 'tt.taxonomy' );
1956                         break;
1957                 case 'id=>slug':
1958                         $selects = array( 't.term_id', 't.slug', 'tt.count', 'tt.taxonomy' );
1959                         break;
1960         }
1961
1962         $_fields = $args['fields'];
1963
1964         /**
1965          * Filter the fields to select in the terms query.
1966          *
1967          * Field lists modified using this filter will only modify the term fields returned
1968          * by the function when the `$fields` parameter set to 'count' or 'all'. In all other
1969          * cases, the term fields in the results array will be determined by the `$fields`
1970          * parameter alone.
1971          *
1972          * Use of this filter can result in unpredictable behavior, and is not recommended.
1973          *
1974          * @since 2.8.0
1975          *
1976          * @param array $selects    An array of fields to select for the terms query.
1977          * @param array $args       An array of term query arguments.
1978          * @param array $taxonomies An array of taxonomies.
1979          */
1980         $fields = implode( ', ', apply_filters( 'get_terms_fields', $selects, $args, $taxonomies ) );
1981
1982         $join = "INNER JOIN $wpdb->term_taxonomy AS tt ON t.term_id = tt.term_id";
1983
1984         $pieces = array( 'fields', 'join', 'where', 'orderby', 'order', 'limits' );
1985
1986         /**
1987          * Filter the terms query SQL clauses.
1988          *
1989          * @since 3.1.0
1990          *
1991          * @param array $pieces     Terms query SQL clauses.
1992          * @param array $taxonomies An array of taxonomies.
1993          * @param array $args       An array of terms query arguments.
1994          */
1995         $clauses = apply_filters( 'terms_clauses', compact( $pieces ), $taxonomies, $args );
1996
1997         $fields = isset( $clauses[ 'fields' ] ) ? $clauses[ 'fields' ] : '';
1998         $join = isset( $clauses[ 'join' ] ) ? $clauses[ 'join' ] : '';
1999         $where = isset( $clauses[ 'where' ] ) ? $clauses[ 'where' ] : '';
2000         $orderby = isset( $clauses[ 'orderby' ] ) ? $clauses[ 'orderby' ] : '';
2001         $order = isset( $clauses[ 'order' ] ) ? $clauses[ 'order' ] : '';
2002         $limits = isset( $clauses[ 'limits' ] ) ? $clauses[ 'limits' ] : '';
2003
2004         $query = "SELECT $fields FROM $wpdb->terms AS t $join WHERE $where $orderby $order $limits";
2005
2006         if ( 'count' == $_fields ) {
2007                 return $wpdb->get_var( $query );
2008         }
2009
2010         $terms = $wpdb->get_results($query);
2011         if ( 'all' == $_fields ) {
2012                 update_term_cache( $terms );
2013         }
2014
2015         if ( empty($terms) ) {
2016                 wp_cache_add( $cache_key, array(), 'terms', DAY_IN_SECONDS );
2017
2018                 /** This filter is documented in wp-includes/taxonomy.php */
2019                 return apply_filters( 'get_terms', array(), $taxonomies, $args );
2020         }
2021
2022         if ( $child_of ) {
2023                 foreach ( $taxonomies as $_tax ) {
2024                         $children = _get_term_hierarchy( $_tax );
2025                         if ( ! empty( $children ) ) {
2026                                 $terms = _get_term_children( $child_of, $terms, $_tax );
2027                         }
2028                 }
2029         }
2030
2031         // Update term counts to include children.
2032         if ( $args['pad_counts'] && 'all' == $_fields ) {
2033                 foreach ( $taxonomies as $_tax ) {
2034                         _pad_term_counts( $terms, $_tax );
2035                 }
2036         }
2037
2038         // Make sure we show empty categories that have children.
2039         if ( $hierarchical && $args['hide_empty'] && is_array( $terms ) ) {
2040                 foreach ( $terms as $k => $term ) {
2041                         if ( ! $term->count ) {
2042                                 $children = get_term_children( $term->term_id, $term->taxonomy );
2043                                 if ( is_array( $children ) ) {
2044                                         foreach ( $children as $child_id ) {
2045                                                 $child = get_term( $child_id, $term->taxonomy );
2046                                                 if ( $child->count ) {
2047                                                         continue 2;
2048                                                 }
2049                                         }
2050                                 }
2051
2052                                 // It really is empty.
2053                                 unset($terms[$k]);
2054                         }
2055                 }
2056         }
2057
2058         $_terms = array();
2059         if ( 'id=>parent' == $_fields ) {
2060                 foreach ( $terms as $term ) {
2061                         $_terms[ $term->term_id ] = $term->parent;
2062                 }
2063         } elseif ( 'ids' == $_fields ) {
2064                 foreach ( $terms as $term ) {
2065                         $_terms[] = $term->term_id;
2066                 }
2067         } elseif ( 'names' == $_fields ) {
2068                 foreach ( $terms as $term ) {
2069                         $_terms[] = $term->name;
2070                 }
2071         } elseif ( 'id=>name' == $_fields ) {
2072                 foreach ( $terms as $term ) {
2073                         $_terms[ $term->term_id ] = $term->name;
2074                 }
2075         } elseif ( 'id=>slug' == $_fields ) {
2076                 foreach ( $terms as $term ) {
2077                         $_terms[ $term->term_id ] = $term->slug;
2078                 }
2079         }
2080
2081         if ( ! empty( $_terms ) ) {
2082                 $terms = $_terms;
2083         }
2084
2085         if ( $number && is_array( $terms ) && count( $terms ) > $number ) {
2086                 $terms = array_slice( $terms, $offset, $number );
2087         }
2088
2089         wp_cache_add( $cache_key, $terms, 'terms', DAY_IN_SECONDS );
2090
2091         /** This filter is documented in wp-includes/taxonomy */
2092         return apply_filters( 'get_terms', $terms, $taxonomies, $args );
2093 }
2094
2095 /**
2096  * Check if Term exists.
2097  *
2098  * Formerly is_term(), introduced in 2.3.0.
2099  *
2100  * @since 3.0.0
2101  *
2102  * @global wpdb $wpdb WordPress database abstraction object.
2103  *
2104  * @param int|string $term     The term to check
2105  * @param string     $taxonomy The taxonomy name to use
2106  * @param int        $parent   Optional. ID of parent term under which to confine the exists search.
2107  * @return mixed Returns null if the term does not exist. Returns the term ID
2108  *               if no taxonomy is specified and the term ID exists. Returns
2109  *               an array of the term ID and the term taxonomy ID the taxonomy
2110  *               is specified and the pairing exists.
2111  */
2112 function term_exists( $term, $taxonomy = '', $parent = null ) {
2113         global $wpdb;
2114
2115         $select = "SELECT term_id FROM $wpdb->terms as t WHERE ";
2116         $tax_select = "SELECT tt.term_id, tt.term_taxonomy_id FROM $wpdb->terms AS t INNER JOIN $wpdb->term_taxonomy as tt ON tt.term_id = t.term_id WHERE ";
2117
2118         if ( is_int($term) ) {
2119                 if ( 0 == $term )
2120                         return 0;
2121                 $where = 't.term_id = %d';
2122                 if ( !empty($taxonomy) )
2123                         return $wpdb->get_row( $wpdb->prepare( $tax_select . $where . " AND tt.taxonomy = %s", $term, $taxonomy ), ARRAY_A );
2124                 else
2125                         return $wpdb->get_var( $wpdb->prepare( $select . $where, $term ) );
2126         }
2127
2128         $term = trim( wp_unslash( $term ) );
2129         $slug = sanitize_title( $term );
2130
2131         $where = 't.slug = %s';
2132         $else_where = 't.name = %s';
2133         $where_fields = array($slug);
2134         $else_where_fields = array($term);
2135         $orderby = 'ORDER BY t.term_id ASC';
2136         $limit = 'LIMIT 1';
2137         if ( !empty($taxonomy) ) {
2138                 if ( is_numeric( $parent ) ) {
2139                         $parent = (int) $parent;
2140                         $where_fields[] = $parent;
2141                         $else_where_fields[] = $parent;
2142                         $where .= ' AND tt.parent = %d';
2143                         $else_where .= ' AND tt.parent = %d';
2144                 }
2145
2146                 $where_fields[] = $taxonomy;
2147                 $else_where_fields[] = $taxonomy;
2148
2149                 if ( $result = $wpdb->get_row( $wpdb->prepare("SELECT tt.term_id, tt.term_taxonomy_id FROM $wpdb->terms AS t INNER JOIN $wpdb->term_taxonomy as tt ON tt.term_id = t.term_id WHERE $where AND tt.taxonomy = %s $orderby $limit", $where_fields), ARRAY_A) )
2150                         return $result;
2151
2152                 return $wpdb->get_row( $wpdb->prepare("SELECT tt.term_id, tt.term_taxonomy_id FROM $wpdb->terms AS t INNER JOIN $wpdb->term_taxonomy as tt ON tt.term_id = t.term_id WHERE $else_where AND tt.taxonomy = %s $orderby $limit", $else_where_fields), ARRAY_A);
2153         }
2154
2155         if ( $result = $wpdb->get_var( $wpdb->prepare("SELECT term_id FROM $wpdb->terms as t WHERE $where $orderby $limit", $where_fields) ) )
2156                 return $result;
2157
2158         return $wpdb->get_var( $wpdb->prepare("SELECT term_id FROM $wpdb->terms as t WHERE $else_where $orderby $limit", $else_where_fields) );
2159 }
2160
2161 /**
2162  * Check if a term is an ancestor of another term.
2163  *
2164  * You can use either an id or the term object for both parameters.
2165  *
2166  * @since 3.4.0
2167  *
2168  * @param int|object $term1    ID or object to check if this is the parent term.
2169  * @param int|object $term2    The child term.
2170  * @param string     $taxonomy Taxonomy name that $term1 and `$term2` belong to.
2171  * @return bool Whether `$term2` is a child of `$term1`.
2172  */
2173 function term_is_ancestor_of( $term1, $term2, $taxonomy ) {
2174         if ( ! isset( $term1->term_id ) )
2175                 $term1 = get_term( $term1, $taxonomy );
2176         if ( ! isset( $term2->parent ) )
2177                 $term2 = get_term( $term2, $taxonomy );
2178
2179         if ( empty( $term1->term_id ) || empty( $term2->parent ) )
2180                 return false;
2181         if ( $term2->parent == $term1->term_id )
2182                 return true;
2183
2184         return term_is_ancestor_of( $term1, get_term( $term2->parent, $taxonomy ), $taxonomy );
2185 }
2186
2187 /**
2188  * Sanitize Term all fields.
2189  *
2190  * Relies on sanitize_term_field() to sanitize the term. The difference is that
2191  * this function will sanitize <strong>all</strong> fields. The context is based
2192  * on sanitize_term_field().
2193  *
2194  * The $term is expected to be either an array or an object.
2195  *
2196  * @since 2.3.0
2197  *
2198  * @param array|object $term     The term to check.
2199  * @param string       $taxonomy The taxonomy name to use.
2200  * @param string       $context  Optional. Context in which to sanitize the term. Accepts 'edit', 'db',
2201  *                               'display', 'attribute', or 'js'. Default 'display'.
2202  * @return array|object Term with all fields sanitized.
2203  */
2204 function sanitize_term($term, $taxonomy, $context = 'display') {
2205         $fields = array( 'term_id', 'name', 'description', 'slug', 'count', 'parent', 'term_group', 'term_taxonomy_id', 'object_id' );
2206
2207         $do_object = is_object( $term );
2208
2209         $term_id = $do_object ? $term->term_id : (isset($term['term_id']) ? $term['term_id'] : 0);
2210
2211         foreach ( (array) $fields as $field ) {
2212                 if ( $do_object ) {
2213                         if ( isset($term->$field) )
2214                                 $term->$field = sanitize_term_field($field, $term->$field, $term_id, $taxonomy, $context);
2215                 } else {
2216                         if ( isset($term[$field]) )
2217                                 $term[$field] = sanitize_term_field($field, $term[$field], $term_id, $taxonomy, $context);
2218                 }
2219         }
2220
2221         if ( $do_object )
2222                 $term->filter = $context;
2223         else
2224                 $term['filter'] = $context;
2225
2226         return $term;
2227 }
2228
2229 /**
2230  * Cleanse the field value in the term based on the context.
2231  *
2232  * Passing a term field value through the function should be assumed to have
2233  * cleansed the value for whatever context the term field is going to be used.
2234  *
2235  * If no context or an unsupported context is given, then default filters will
2236  * be applied.
2237  *
2238  * There are enough filters for each context to support a custom filtering
2239  * without creating your own filter function. Simply create a function that
2240  * hooks into the filter you need.
2241  *
2242  * @since 2.3.0
2243  *
2244  * @param string $field    Term field to sanitize.
2245  * @param string $value    Search for this term value.
2246  * @param int    $term_id  Term ID.
2247  * @param string $taxonomy Taxonomy Name.
2248  * @param string $context  Context in which to sanitize the term field. Accepts 'edit', 'db', 'display',
2249  *                         'attribute', or 'js'.
2250  * @return mixed Sanitized field.
2251  */
2252 function sanitize_term_field($field, $value, $term_id, $taxonomy, $context) {
2253         $int_fields = array( 'parent', 'term_id', 'count', 'term_group', 'term_taxonomy_id', 'object_id' );
2254         if ( in_array( $field, $int_fields ) ) {
2255                 $value = (int) $value;
2256                 if ( $value < 0 )
2257                         $value = 0;
2258         }
2259
2260         if ( 'raw' == $context )
2261                 return $value;
2262
2263         if ( 'edit' == $context ) {
2264
2265                 /**
2266                  * Filter a term field to edit before it is sanitized.
2267                  *
2268                  * The dynamic portion of the filter name, `$field`, refers to the term field.
2269                  *
2270                  * @since 2.3.0
2271                  *
2272                  * @param mixed $value     Value of the term field.
2273                  * @param int   $term_id   Term ID.
2274                  * @param string $taxonomy Taxonomy slug.
2275                  */
2276                 $value = apply_filters( "edit_term_{$field}", $value, $term_id, $taxonomy );
2277
2278                 /**
2279                  * Filter the taxonomy field to edit before it is sanitized.
2280                  *
2281                  * The dynamic portions of the filter name, `$taxonomy` and `$field`, refer
2282                  * to the taxonomy slug and taxonomy field, respectively.
2283                  *
2284                  * @since 2.3.0
2285                  *
2286                  * @param mixed $value   Value of the taxonomy field to edit.
2287                  * @param int   $term_id Term ID.
2288                  */
2289                 $value = apply_filters( "edit_{$taxonomy}_{$field}", $value, $term_id );
2290
2291                 if ( 'description' == $field )
2292                         $value = esc_html($value); // textarea_escaped
2293                 else
2294                         $value = esc_attr($value);
2295         } elseif ( 'db' == $context ) {
2296
2297                 /**
2298                  * Filter a term field value before it is sanitized.
2299                  *
2300                  * The dynamic portion of the filter name, `$field`, refers to the term field.
2301                  *
2302                  * @since 2.3.0
2303                  *
2304                  * @param mixed  $value    Value of the term field.
2305                  * @param string $taxonomy Taxonomy slug.
2306                  */
2307                 $value = apply_filters( "pre_term_{$field}", $value, $taxonomy );
2308
2309                 /**
2310                  * Filter a taxonomy field before it is sanitized.
2311                  *
2312                  * The dynamic portions of the filter name, `$taxonomy` and `$field`, refer
2313                  * to the taxonomy slug and field name, respectively.
2314                  *
2315                  * @since 2.3.0
2316                  *
2317                  * @param mixed $value Value of the taxonomy field.
2318                  */
2319                 $value = apply_filters( "pre_{$taxonomy}_{$field}", $value );
2320
2321                 // Back compat filters
2322                 if ( 'slug' == $field ) {
2323                         /**
2324                          * Filter the category nicename before it is sanitized.
2325                          *
2326                          * Use the pre_{$taxonomy}_{$field} hook instead.
2327                          *
2328                          * @since 2.0.3
2329                          *
2330                          * @param string $value The category nicename.
2331                          */
2332                         $value = apply_filters( 'pre_category_nicename', $value );
2333                 }
2334
2335         } elseif ( 'rss' == $context ) {
2336
2337                 /**
2338                  * Filter the term field for use in RSS.
2339                  *
2340                  * The dynamic portion of the filter name, `$field`, refers to the term field.
2341                  *
2342                  * @since 2.3.0
2343                  *
2344                  * @param mixed  $value    Value of the term field.
2345                  * @param string $taxonomy Taxonomy slug.
2346                  */
2347                 $value = apply_filters( "term_{$field}_rss", $value, $taxonomy );
2348
2349                 /**
2350                  * Filter the taxonomy field for use in RSS.
2351                  *
2352                  * The dynamic portions of the hook name, `$taxonomy`, and `$field`, refer
2353                  * to the taxonomy slug and field name, respectively.
2354                  *
2355                  * @since 2.3.0
2356                  *
2357                  * @param mixed $value Value of the taxonomy field.
2358                  */
2359                 $value = apply_filters( "{$taxonomy}_{$field}_rss", $value );
2360         } else {
2361                 // Use display filters by default.
2362
2363                 /**
2364                  * Filter the term field sanitized for display.
2365                  *
2366                  * The dynamic portion of the filter name, `$field`, refers to the term field name.
2367                  *
2368                  * @since 2.3.0
2369                  *
2370                  * @param mixed  $value    Value of the term field.
2371                  * @param int    $term_id  Term ID.
2372                  * @param string $taxonomy Taxonomy slug.
2373                  * @param string $context  Context to retrieve the term field value.
2374                  */
2375                 $value = apply_filters( "term_{$field}", $value, $term_id, $taxonomy, $context );
2376
2377                 /**
2378                  * Filter the taxonomy field sanitized for display.
2379                  *
2380                  * The dynamic portions of the filter name, `$taxonomy`, and `$field`, refer
2381                  * to the taxonomy slug and taxonomy field, respectively.
2382                  *
2383                  * @since 2.3.0
2384                  *
2385                  * @param mixed  $value   Value of the taxonomy field.
2386                  * @param int    $term_id Term ID.
2387                  * @param string $context Context to retrieve the taxonomy field value.
2388                  */
2389                 $value = apply_filters( "{$taxonomy}_{$field}", $value, $term_id, $context );
2390         }
2391
2392         if ( 'attribute' == $context ) {
2393                 $value = esc_attr($value);
2394         } elseif ( 'js' == $context ) {
2395                 $value = esc_js($value);
2396         }
2397         return $value;
2398 }
2399
2400 /**
2401  * Count how many terms are in Taxonomy.
2402  *
2403  * Default $args is 'hide_empty' which can be 'hide_empty=true' or array('hide_empty' => true).
2404  *
2405  * @todo Document $args as a hash notation.
2406  *
2407  * @since 2.3.0
2408  *
2409  * @param string       $taxonomy Taxonomy name
2410  * @param array|string $args     Overwrite defaults. See get_terms()
2411  * @return array|int|WP_Error How many terms are in $taxonomy. WP_Error if $taxonomy does not exist.
2412  */
2413 function wp_count_terms( $taxonomy, $args = array() ) {
2414         $defaults = array('hide_empty' => false);
2415         $args = wp_parse_args($args, $defaults);
2416
2417         // backwards compatibility
2418         if ( isset($args['ignore_empty']) ) {
2419                 $args['hide_empty'] = $args['ignore_empty'];
2420                 unset($args['ignore_empty']);
2421         }
2422
2423         $args['fields'] = 'count';
2424
2425         return get_terms($taxonomy, $args);
2426 }
2427
2428 /**
2429  * Will unlink the object from the taxonomy or taxonomies.
2430  *
2431  * Will remove all relationships between the object and any terms in
2432  * a particular taxonomy or taxonomies. Does not remove the term or
2433  * taxonomy itself.
2434  *
2435  * @since 2.3.0
2436  *
2437  * @param int          $object_id  The term Object Id that refers to the term.
2438  * @param string|array $taxonomies List of Taxonomy Names or single Taxonomy name.
2439  */
2440 function wp_delete_object_term_relationships( $object_id, $taxonomies ) {
2441         $object_id = (int) $object_id;
2442
2443         if ( !is_array($taxonomies) )
2444                 $taxonomies = array($taxonomies);
2445
2446         foreach ( (array) $taxonomies as $taxonomy ) {
2447                 $term_ids = wp_get_object_terms( $object_id, $taxonomy, array( 'fields' => 'ids' ) );
2448                 $term_ids = array_map( 'intval', $term_ids );
2449                 wp_remove_object_terms( $object_id, $term_ids, $taxonomy );
2450         }
2451 }
2452
2453 /**
2454  * Removes a term from the database.
2455  *
2456  * If the term is a parent of other terms, then the children will be updated to
2457  * that term's parent.
2458  *
2459  * The `$args` 'default' will only override the terms found, if there is only one
2460  * term found. Any other and the found terms are used.
2461  *
2462  * The $args 'force_default' will force the term supplied as default to be
2463  * assigned even if the object was not going to be termless
2464  *
2465  * @todo Document $args as a hash notation.
2466  *
2467  * @since 2.3.0
2468  *
2469  * @global wpdb $wpdb WordPress database abstraction object.
2470  *
2471  * @param int          $term     Term ID.
2472  * @param string       $taxonomy Taxonomy Name.
2473  * @param array|string $args     Optional. Change 'default' term id and override found term ids.
2474  * @return bool|int|WP_Error Returns false if not term; true if completes delete action.
2475  */
2476 function wp_delete_term( $term, $taxonomy, $args = array() ) {
2477         global $wpdb;
2478
2479         $term = (int) $term;
2480
2481         if ( ! $ids = term_exists($term, $taxonomy) )
2482                 return false;
2483         if ( is_wp_error( $ids ) )
2484                 return $ids;
2485
2486         $tt_id = $ids['term_taxonomy_id'];
2487
2488         $defaults = array();
2489
2490         if ( 'category' == $taxonomy ) {
2491                 $defaults['default'] = get_option( 'default_category' );
2492                 if ( $defaults['default'] == $term )
2493                         return 0; // Don't delete the default category
2494         }
2495
2496         $args = wp_parse_args($args, $defaults);
2497
2498         if ( isset( $args['default'] ) ) {
2499                 $default = (int) $args['default'];
2500                 if ( ! term_exists( $default, $taxonomy ) ) {
2501                         unset( $default );
2502                 }
2503         }
2504
2505         if ( isset( $args['force_default'] ) ) {
2506                 $force_default = $args['force_default'];
2507         }
2508
2509         /**
2510          * Fires when deleting a term, before any modifications are made to posts or terms.
2511          *
2512          * @since 4.1.0
2513          *
2514          * @param int    $term     Term ID.
2515          * @param string $taxonomy Taxonomy Name.
2516          */
2517         do_action( 'pre_delete_term', $term, $taxonomy );
2518
2519         // Update children to point to new parent
2520         if ( is_taxonomy_hierarchical($taxonomy) ) {
2521                 $term_obj = get_term($term, $taxonomy);
2522                 if ( is_wp_error( $term_obj ) )
2523                         return $term_obj;
2524                 $parent = $term_obj->parent;
2525
2526                 $edit_ids = $wpdb->get_results( "SELECT term_id, term_taxonomy_id FROM $wpdb->term_taxonomy WHERE `parent` = " . (int)$term_obj->term_id );
2527                 $edit_tt_ids = wp_list_pluck( $edit_ids, 'term_taxonomy_id' );
2528
2529                 /**
2530                  * Fires immediately before a term to delete's children are reassigned a parent.
2531                  *
2532                  * @since 2.9.0
2533                  *
2534                  * @param array $edit_tt_ids An array of term taxonomy IDs for the given term.
2535                  */
2536                 do_action( 'edit_term_taxonomies', $edit_tt_ids );
2537
2538                 $wpdb->update( $wpdb->term_taxonomy, compact( 'parent' ), array( 'parent' => $term_obj->term_id) + compact( 'taxonomy' ) );
2539
2540                 // Clean the cache for all child terms.
2541                 $edit_term_ids = wp_list_pluck( $edit_ids, 'term_id' );
2542                 clean_term_cache( $edit_term_ids, $taxonomy );
2543
2544                 /**
2545                  * Fires immediately after a term to delete's children are reassigned a parent.
2546                  *
2547                  * @since 2.9.0
2548                  *
2549                  * @param array $edit_tt_ids An array of term taxonomy IDs for the given term.
2550                  */
2551                 do_action( 'edited_term_taxonomies', $edit_tt_ids );
2552         }
2553
2554         $objects = $wpdb->get_col( $wpdb->prepare( "SELECT object_id FROM $wpdb->term_relationships WHERE term_taxonomy_id = %d", $tt_id ) );
2555
2556         foreach ( (array) $objects as $object ) {
2557                 $terms = wp_get_object_terms($object, $taxonomy, array('fields' => 'ids', 'orderby' => 'none'));
2558                 if ( 1 == count($terms) && isset($default) ) {
2559                         $terms = array($default);
2560                 } else {
2561                         $terms = array_diff($terms, array($term));
2562                         if (isset($default) && isset($force_default) && $force_default)
2563                                 $terms = array_merge($terms, array($default));
2564                 }
2565                 $terms = array_map('intval', $terms);
2566                 wp_set_object_terms($object, $terms, $taxonomy);
2567         }
2568
2569         // Clean the relationship caches for all object types using this term.
2570         $tax_object = get_taxonomy( $taxonomy );
2571         foreach ( $tax_object->object_type as $object_type )
2572                 clean_object_term_cache( $objects, $object_type );
2573
2574         // Get the object before deletion so we can pass to actions below
2575         $deleted_term = get_term( $term, $taxonomy );
2576
2577         /**
2578          * Fires immediately before a term taxonomy ID is deleted.
2579          *
2580          * @since 2.9.0
2581          *
2582          * @param int $tt_id Term taxonomy ID.
2583          */
2584         do_action( 'delete_term_taxonomy', $tt_id );
2585         $wpdb->delete( $wpdb->term_taxonomy, array( 'term_taxonomy_id' => $tt_id ) );
2586
2587         /**
2588          * Fires immediately after a term taxonomy ID is deleted.
2589          *
2590          * @since 2.9.0
2591          *
2592          * @param int $tt_id Term taxonomy ID.
2593          */
2594         do_action( 'deleted_term_taxonomy', $tt_id );
2595
2596         // Delete the term if no taxonomies use it.
2597         if ( !$wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM $wpdb->term_taxonomy WHERE term_id = %d", $term) ) )
2598                 $wpdb->delete( $wpdb->terms, array( 'term_id' => $term ) );
2599
2600         clean_term_cache($term, $taxonomy);
2601
2602         /**
2603          * Fires after a term is deleted from the database and the cache is cleaned.
2604          *
2605          * @since 2.5.0
2606          *
2607          * @param int     $term         Term ID.
2608          * @param int     $tt_id        Term taxonomy ID.
2609          * @param string  $taxonomy     Taxonomy slug.
2610          * @param mixed   $deleted_term Copy of the already-deleted term, in the form specified
2611          *                              by the parent function. WP_Error otherwise.
2612          */
2613         do_action( 'delete_term', $term, $tt_id, $taxonomy, $deleted_term );
2614
2615         /**
2616          * Fires after a term in a specific taxonomy is deleted.
2617          *
2618          * The dynamic portion of the hook name, `$taxonomy`, refers to the specific
2619          * taxonomy the term belonged to.
2620          *
2621          * @since 2.3.0
2622          *
2623          * @param int     $term         Term ID.
2624          * @param int     $tt_id        Term taxonomy ID.
2625          * @param mixed   $deleted_term Copy of the already-deleted term, in the form specified
2626          *                              by the parent function. WP_Error otherwise.
2627          */
2628         do_action( "delete_$taxonomy", $term, $tt_id, $deleted_term );
2629
2630         return true;
2631 }
2632
2633 /**
2634  * Deletes one existing category.
2635  *
2636  * @since 2.0.0
2637  *
2638  * @param int $cat_ID
2639  * @return bool|int|WP_Error Returns true if completes delete action; false if term doesn't exist;
2640  *      Zero on attempted deletion of default Category; WP_Error object is also a possibility.
2641  */
2642 function wp_delete_category( $cat_ID ) {
2643         return wp_delete_term( $cat_ID, 'category' );
2644 }
2645
2646 /**
2647  * Retrieves the terms associated with the given object(s), in the supplied taxonomies.
2648  *
2649  * @since 2.3.0
2650  * @since 4.2.0 Added support for 'taxonomy', 'parent', and 'term_taxonomy_id' values of `$orderby`.
2651  *              Introduced `$parent` argument.
2652  *
2653  * @global wpdb $wpdb WordPress database abstraction object.
2654  *
2655  * @param int|array    $object_ids The ID(s) of the object(s) to retrieve.
2656  * @param string|array $taxonomies The taxonomies to retrieve terms from.
2657  * @param array|string $args {
2658  *     Array of arguments.
2659  *     @type string $orderby Field by which results should be sorted. Accepts 'name', 'count', 'slug', 'term_group',
2660  *                           'term_order', 'taxonomy', 'parent', or 'term_taxonomy_id'. Default 'name'.
2661  *     @type string $order   Sort order. Accepts 'ASC' or 'DESC'. Default 'ASC'.
2662  *     @type string $fields  Fields to return for matched terms. Accepts 'all', 'ids', 'names', and
2663  *                           'all_with_object_id'. Note that 'all' or 'all_with_object_id' will result in an array of
2664  *                           term objects being returned, 'ids' will return an array of integers, and 'names' an array
2665  *                           of strings.
2666  *     @type int    $parent  Optional. Limit results to the direct children of a given term ID.
2667  * }
2668  * @return array|WP_Error The requested term data or empty array if no terms found.
2669  *                        WP_Error if any of the $taxonomies don't exist.
2670  */
2671 function wp_get_object_terms($object_ids, $taxonomies, $args = array()) {
2672         global $wpdb;
2673
2674         if ( empty( $object_ids ) || empty( $taxonomies ) )
2675                 return array();
2676
2677         if ( !is_array($taxonomies) )
2678                 $taxonomies = array($taxonomies);
2679
2680         foreach ( $taxonomies as $taxonomy ) {
2681                 if ( ! taxonomy_exists($taxonomy) )
2682                         return new WP_Error('invalid_taxonomy', __('Invalid taxonomy'));
2683         }
2684
2685         if ( !is_array($object_ids) )
2686                 $object_ids = array($object_ids);
2687         $object_ids = array_map('intval', $object_ids);
2688
2689         $defaults = array(
2690                 'orderby' => 'name',
2691                 'order'   => 'ASC',
2692                 'fields'  => 'all',
2693                 'parent'  => '',
2694         );
2695         $args = wp_parse_args( $args, $defaults );
2696
2697         $terms = array();
2698         if ( count($taxonomies) > 1 ) {
2699                 foreach ( $taxonomies as $index => $taxonomy ) {
2700                         $t = get_taxonomy($taxonomy);
2701                         if ( isset($t->args) && is_array($t->args) && $args != array_merge($args, $t->args) ) {
2702                                 unset($taxonomies[$index]);
2703                                 $terms = array_merge($terms, wp_get_object_terms($object_ids, $taxonomy, array_merge($args, $t->args)));
2704                         }
2705                 }
2706         } else {
2707                 $t = get_taxonomy($taxonomies[0]);
2708                 if ( isset($t->args) && is_array($t->args) )
2709                         $args = array_merge($args, $t->args);
2710         }
2711
2712         $orderby = $args['orderby'];
2713         $order = $args['order'];
2714         $fields = $args['fields'];
2715
2716         if ( in_array( $orderby, array( 'term_id', 'name', 'slug', 'term_group' ) ) ) {
2717                 $orderby = "t.$orderby";
2718         } elseif ( in_array( $orderby, array( 'count', 'parent', 'taxonomy', 'term_taxonomy_id' ) ) ) {
2719                 $orderby = "tt.$orderby";
2720         } elseif ( 'term_order' === $orderby ) {
2721                 $orderby = 'tr.term_order';
2722         } elseif ( 'none' === $orderby ) {
2723                 $orderby = '';
2724                 $order = '';
2725         } else {
2726                 $orderby = 't.term_id';
2727         }
2728
2729         // tt_ids queries can only be none or tr.term_taxonomy_id
2730         if ( ('tt_ids' == $fields) && !empty($orderby) )
2731                 $orderby = 'tr.term_taxonomy_id';
2732
2733         if ( !empty($orderby) )
2734                 $orderby = "ORDER BY $orderby";
2735
2736         $order = strtoupper( $order );
2737         if ( '' !== $order && ! in_array( $order, array( 'ASC', 'DESC' ) ) )
2738                 $order = 'ASC';
2739
2740         $taxonomy_array = $taxonomies;
2741         $object_id_array = $object_ids;
2742         $taxonomies = "'" . implode("', '", $taxonomies) . "'";
2743         $object_ids = implode(', ', $object_ids);
2744
2745         $select_this = '';
2746         if ( 'all' == $fields ) {
2747                 $select_this = 't.*, tt.*';
2748         } elseif ( 'ids' == $fields ) {
2749                 $select_this = 't.term_id';
2750         } elseif ( 'names' == $fields ) {
2751                 $select_this = 't.name';
2752         } elseif ( 'slugs' == $fields ) {
2753                 $select_this = 't.slug';
2754         } elseif ( 'all_with_object_id' == $fields ) {
2755                 $select_this = 't.*, tt.*, tr.object_id';
2756         }
2757
2758         $where = array(
2759                 "tt.taxonomy IN ($taxonomies)",
2760                 "tr.object_id IN ($object_ids)",
2761         );
2762
2763         if ( '' !== $args['parent'] ) {
2764                 $where[] = $wpdb->prepare( 'tt.parent = %d', $args['parent'] );
2765         }
2766
2767         $where = implode( ' AND ', $where );
2768
2769         $query = "SELECT $select_this FROM $wpdb->terms AS t INNER JOIN $wpdb->term_taxonomy AS tt ON tt.term_id = t.term_id INNER JOIN $wpdb->term_relationships AS tr ON tr.term_taxonomy_id = tt.term_taxonomy_id WHERE $where $orderby $order";
2770
2771         $objects = false;
2772         if ( 'all' == $fields || 'all_with_object_id' == $fields ) {
2773                 $_terms = $wpdb->get_results( $query );
2774                 foreach ( $_terms as $key => $term ) {
2775                         $_terms[$key] = sanitize_term( $term, $taxonomy, 'raw' );
2776                 }
2777                 $terms = array_merge( $terms, $_terms );
2778                 update_term_cache( $terms );
2779                 $objects = true;
2780         } elseif ( 'ids' == $fields || 'names' == $fields || 'slugs' == $fields ) {
2781                 $_terms = $wpdb->get_col( $query );
2782                 $_field = ( 'ids' == $fields ) ? 'term_id' : 'name';
2783                 foreach ( $_terms as $key => $term ) {
2784                         $_terms[$key] = sanitize_term_field( $_field, $term, $term, $taxonomy, 'raw' );
2785                 }
2786                 $terms = array_merge( $terms, $_terms );
2787         } elseif ( 'tt_ids' == $fields ) {
2788                 $terms = $wpdb->get_col("SELECT tr.term_taxonomy_id FROM $wpdb->term_relationships AS tr INNER JOIN $wpdb->term_taxonomy AS tt ON tr.term_taxonomy_id = tt.term_taxonomy_id WHERE tr.object_id IN ($object_ids) AND tt.taxonomy IN ($taxonomies) $orderby $order");
2789                 foreach ( $terms as $key => $tt_id ) {
2790                         $terms[$key] = sanitize_term_field( 'term_taxonomy_id', $tt_id, 0, $taxonomy, 'raw' ); // 0 should be the term id, however is not needed when using raw context.
2791                 }
2792         }
2793
2794         if ( ! $terms ) {
2795                 $terms = array();
2796         } elseif ( $objects && 'all_with_object_id' !== $fields ) {
2797                 $_tt_ids = array();
2798                 $_terms = array();
2799                 foreach ( $terms as $term ) {
2800                         if ( in_array( $term->term_taxonomy_id, $_tt_ids ) ) {
2801                                 continue;
2802                         }
2803
2804                         $_tt_ids[] = $term->term_taxonomy_id;
2805                         $_terms[] = $term;
2806                 }
2807                 $terms = $_terms;
2808         } elseif ( ! $objects ) {
2809                 $terms = array_values( array_unique( $terms ) );
2810         }
2811
2812         /**
2813          * Filter the terms for a given object or objects.
2814          *
2815          * @since 4.2.0
2816          *
2817          * @param array $terms           An array of terms for the given object or objects.
2818          * @param array $object_id_array Array of object IDs for which `$terms` were retrieved.
2819          * @param array $taxonomy_array  Array of taxonomies from which `$terms` were retrieved.
2820          * @param array $args            An array of arguments for retrieving terms for the given
2821          *                               object(s). See wp_get_object_terms() for details.
2822          */
2823         $terms = apply_filters( 'get_object_terms', $terms, $object_id_array, $taxonomy_array, $args );
2824
2825         /**
2826          * Filter the terms for a given object or objects.
2827          *
2828          * The `$taxonomies` parameter passed to this filter is formatted as a SQL fragment. The
2829          * {@see 'get_object_terms'} filter is recommended as an alternative.
2830          *
2831          * @since 2.8.0
2832          *
2833          * @param array     $terms      An array of terms for the given object or objects.
2834          * @param int|array $object_ids Object ID or array of IDs.
2835          * @param string    $taxonomies SQL-formatted (comma-separated and quoted) list of taxonomy names.
2836          * @param array     $args       An array of arguments for retrieving terms for the given object(s).
2837          *                              See {@see wp_get_object_terms()} for details.
2838          */
2839         return apply_filters( 'wp_get_object_terms', $terms, $object_ids, $taxonomies, $args );
2840 }
2841
2842 /**
2843  * Add a new term to the database.
2844  *
2845  * A non-existent term is inserted in the following sequence:
2846  * 1. The term is added to the term table, then related to the taxonomy.
2847  * 2. If everything is correct, several actions are fired.
2848  * 3. The 'term_id_filter' is evaluated.
2849  * 4. The term cache is cleaned.
2850  * 5. Several more actions are fired.
2851  * 6. An array is returned containing the term_id and term_taxonomy_id.
2852  *
2853  * If the 'slug' argument is not empty, then it is checked to see if the term
2854  * is invalid. If it is not a valid, existing term, it is added and the term_id
2855  * is given.
2856  *
2857  * If the taxonomy is hierarchical, and the 'parent' argument is not empty,
2858  * the term is inserted and the term_id will be given.
2859
2860  * Error handling:
2861  * If $taxonomy does not exist or $term is empty,
2862  * a WP_Error object will be returned.
2863  *
2864  * If the term already exists on the same hierarchical level,
2865  * or the term slug and name are not unique, a WP_Error object will be returned.
2866  *
2867  * @global wpdb $wpdb WordPress database abstraction object.
2868
2869  * @since 2.3.0
2870  *
2871  * @param string       $term     The term to add or update.
2872  * @param string       $taxonomy The taxonomy to which to add the term.
2873  * @param array|string $args {
2874  *     Optional. Array or string of arguments for inserting a term.
2875  *
2876  *     @type string $alias_of    Slug of the term to make this term an alias of.
2877  *                               Default empty string. Accepts a term slug.
2878  *     @type string $description The term description. Default empty string.
2879  *     @type int    $parent      The id of the parent term. Default 0.
2880  *     @type string $slug        The term slug to use. Default empty string.
2881  * }
2882  * @return array|WP_Error An array containing the `term_id` and `term_taxonomy_id`,
2883  *                        {@see WP_Error} otherwise.
2884  */
2885 function wp_insert_term( $term, $taxonomy, $args = array() ) {
2886         global $wpdb;
2887
2888         if ( ! taxonomy_exists($taxonomy) ) {
2889                 return new WP_Error('invalid_taxonomy', __('Invalid taxonomy'));
2890         }
2891         /**
2892          * Filter a term before it is sanitized and inserted into the database.
2893          *
2894          * @since 3.0.0
2895          *
2896          * @param string $term     The term to add or update.
2897          * @param string $taxonomy Taxonomy slug.
2898          */
2899         $term = apply_filters( 'pre_insert_term', $term, $taxonomy );
2900         if ( is_wp_error( $term ) ) {
2901                 return $term;
2902         }
2903         if ( is_int($term) && 0 == $term ) {
2904                 return new WP_Error('invalid_term_id', __('Invalid term ID'));
2905         }
2906         if ( '' == trim($term) ) {
2907                 return new WP_Error('empty_term_name', __('A name is required for this term'));
2908         }
2909         $defaults = array( 'alias_of' => '', 'description' => '', 'parent' => 0, 'slug' => '');
2910         $args = wp_parse_args( $args, $defaults );
2911
2912         if ( $args['parent'] > 0 && ! term_exists( (int) $args['parent'] ) ) {
2913                 return new WP_Error( 'missing_parent', __( 'Parent term does not exist.' ) );
2914         }
2915         $args['name'] = $term;
2916         $args['taxonomy'] = $taxonomy;
2917         $args = sanitize_term($args, $taxonomy, 'db');
2918
2919         // expected_slashed ($name)
2920         $name = wp_unslash( $args['name'] );
2921         $description = wp_unslash( $args['description'] );
2922         $parent = (int) $args['parent'];
2923
2924         $slug_provided = ! empty( $args['slug'] );
2925         if ( ! $slug_provided ) {
2926                 $slug = sanitize_title( $name );
2927         } else {
2928                 $slug = $args['slug'];
2929         }
2930
2931         $term_group = 0;
2932         if ( $args['alias_of'] ) {
2933                 $alias = get_term_by( 'slug', $args['alias_of'], $taxonomy );
2934                 if ( ! empty( $alias->term_group ) ) {
2935                         // The alias we want is already in a group, so let's use that one.
2936                         $term_group = $alias->term_group;
2937                 } elseif ( ! empty( $alias->term_id ) ) {
2938                         /*
2939                          * The alias is not in a group, so we create a new one
2940                          * and add the alias to it.
2941                          */
2942                         $term_group = $wpdb->get_var("SELECT MAX(term_group) FROM $wpdb->terms") + 1;
2943
2944                         wp_update_term( $alias->term_id, $taxonomy, array(
2945                                 'term_group' => $term_group,
2946                         ) );
2947                 }
2948         }
2949
2950         /*
2951          * Prevent the creation of terms with duplicate names at the same level of a taxonomy hierarchy,
2952          * unless a unique slug has been explicitly provided.
2953          */
2954         if ( $name_match = get_term_by( 'name', $name, $taxonomy ) ) {
2955                 $slug_match = get_term_by( 'slug', $slug, $taxonomy );
2956                 if ( ! $slug_provided || $name_match->slug === $slug || $slug_match ) {
2957                         if ( is_taxonomy_hierarchical( $taxonomy ) ) {
2958                                 $siblings = get_terms( $taxonomy, array( 'get' => 'all', 'parent' => $parent ) );
2959
2960                                 $existing_term = null;
2961                                 if ( $name_match->slug === $slug && in_array( $name, wp_list_pluck( $siblings, 'name' ) ) ) {
2962                                         $existing_term = $name_match;
2963                                 } elseif ( $slug_match && in_array( $slug, wp_list_pluck( $siblings, 'slug' ) ) ) {
2964                                         $existing_term = $slug_match;
2965                                 }
2966
2967                                 if ( $existing_term ) {
2968                                         return new WP_Error( 'term_exists', __( 'A term with the name provided already exists with this parent.' ), $existing_term->term_id );
2969                                 }
2970                         } else {
2971                                 return new WP_Error( 'term_exists', __( 'A term with the name provided already exists in this taxonomy.' ), $name_match->term_id );
2972                         }
2973                 }
2974         }
2975
2976         $slug = wp_unique_term_slug( $slug, (object) $args );
2977
2978         if ( false === $wpdb->insert( $wpdb->terms, compact( 'name', 'slug', 'term_group' ) ) ) {
2979                 return new WP_Error( 'db_insert_error', __( 'Could not insert term into the database' ), $wpdb->last_error );
2980         }
2981
2982         $term_id = (int) $wpdb->insert_id;
2983
2984         // Seems unreachable, However, Is used in the case that a term name is provided, which sanitizes to an empty string.
2985         if ( empty($slug) ) {
2986                 $slug = sanitize_title($slug, $term_id);
2987
2988                 /** This action is documented in wp-includes/taxonomy.php */
2989                 do_action( 'edit_terms', $term_id, $taxonomy );
2990                 $wpdb->update( $wpdb->terms, compact( 'slug' ), compact( 'term_id' ) );
2991
2992                 /** This action is documented in wp-includes/taxonomy.php */
2993                 do_action( 'edited_terms', $term_id, $taxonomy );
2994         }
2995
2996         $tt_id = $wpdb->get_var( $wpdb->prepare( "SELECT tt.term_taxonomy_id FROM $wpdb->term_taxonomy AS tt INNER JOIN $wpdb->terms AS t ON tt.term_id = t.term_id WHERE tt.taxonomy = %s AND t.term_id = %d", $taxonomy, $term_id ) );
2997
2998         if ( !empty($tt_id) ) {
2999                 return array('term_id' => $term_id, 'term_taxonomy_id' => $tt_id);
3000         }
3001         $wpdb->insert( $wpdb->term_taxonomy, compact( 'term_id', 'taxonomy', 'description', 'parent') + array( 'count' => 0 ) );
3002         $tt_id = (int) $wpdb->insert_id;
3003
3004         /*
3005          * Sanity check: if we just created a term with the same parent + taxonomy + slug but a higher term_id than
3006          * an existing term, then we have unwittingly created a duplicate term. Delete the dupe, and use the term_id
3007          * and term_taxonomy_id of the older term instead. Then return out of the function so that the "create" hooks
3008          * are not fired.
3009          */
3010         $duplicate_term = $wpdb->get_row( $wpdb->prepare( "SELECT t.term_id, tt.term_taxonomy_id FROM $wpdb->terms t INNER JOIN $wpdb->term_taxonomy tt ON ( tt.term_id = t.term_id ) WHERE t.slug = %s AND tt.parent = %d AND tt.taxonomy = %s AND t.term_id < %d AND tt.term_taxonomy_id != %d", $slug, $parent, $taxonomy, $term_id, $tt_id ) );
3011         if ( $duplicate_term ) {
3012                 $wpdb->delete( $wpdb->terms, array( 'term_id' => $term_id ) );
3013                 $wpdb->delete( $wpdb->term_taxonomy, array( 'term_taxonomy_id' => $tt_id ) );
3014
3015                 $term_id = (int) $duplicate_term->term_id;
3016                 $tt_id   = (int) $duplicate_term->term_taxonomy_id;
3017
3018                 clean_term_cache( $term_id, $taxonomy );
3019                 return array( 'term_id' => $term_id, 'term_taxonomy_id' => $tt_id );
3020         }
3021
3022         /**
3023          * Fires immediately after a new term is created, before the term cache is cleaned.
3024          *
3025          * @since 2.3.0
3026          *
3027          * @param int    $term_id  Term ID.
3028          * @param int    $tt_id    Term taxonomy ID.
3029          * @param string $taxonomy Taxonomy slug.
3030          */
3031         do_action( "create_term", $term_id, $tt_id, $taxonomy );
3032
3033         /**
3034          * Fires after a new term is created for a specific taxonomy.
3035          *
3036          * The dynamic portion of the hook name, `$taxonomy`, refers
3037          * to the slug of the taxonomy the term was created for.
3038          *
3039          * @since 2.3.0
3040          *
3041          * @param int $term_id Term ID.
3042          * @param int $tt_id   Term taxonomy ID.
3043          */
3044         do_action( "create_$taxonomy", $term_id, $tt_id );
3045
3046         /**
3047          * Filter the term ID after a new term is created.
3048          *
3049          * @since 2.3.0
3050          *
3051          * @param int $term_id Term ID.
3052          * @param int $tt_id   Taxonomy term ID.
3053          */
3054         $term_id = apply_filters( 'term_id_filter', $term_id, $tt_id );
3055
3056         clean_term_cache($term_id, $taxonomy);
3057
3058         /**
3059          * Fires after a new term is created, and after the term cache has been cleaned.
3060          *
3061          * @since 2.3.0
3062          *
3063          * @param int    $term_id  Term ID.
3064          * @param int    $tt_id    Term taxonomy ID.
3065          * @param string $taxonomy Taxonomy slug.
3066          */
3067         do_action( 'created_term', $term_id, $tt_id, $taxonomy );
3068
3069         /**
3070          * Fires after a new term in a specific taxonomy is created, and after the term
3071          * cache has been cleaned.
3072          *
3073          * The dynamic portion of the hook name, `$taxonomy`, refers to the taxonomy slug.
3074          *
3075          * @since 2.3.0
3076          *
3077          * @param int $term_id Term ID.
3078          * @param int $tt_id   Term taxonomy ID.
3079          */
3080         do_action( "created_$taxonomy", $term_id, $tt_id );
3081
3082         return array('term_id' => $term_id, 'term_taxonomy_id' => $tt_id);
3083 }
3084
3085 /**
3086  * Create Term and Taxonomy Relationships.
3087  *
3088  * Relates an object (post, link etc) to a term and taxonomy type. Creates the
3089  * term and taxonomy relationship if it doesn't already exist. Creates a term if
3090  * it doesn't exist (using the slug).
3091  *
3092  * A relationship means that the term is grouped in or belongs to the taxonomy.
3093  * A term has no meaning until it is given context by defining which taxonomy it
3094  * exists under.
3095  *
3096  * @since 2.3.0
3097  *
3098  * @global wpdb $wpdb The WordPress database abstraction object.
3099  *
3100  * @param int              $object_id The object to relate to.
3101  * @param array|int|string $terms     A single term slug, single term id, or array of either term slugs or ids.
3102  *                                    Will replace all existing related terms in this taxonomy.
3103  * @param string           $taxonomy  The context in which to relate the term to the object.
3104  * @param bool             $append    Optional. If false will delete difference of terms. Default false.
3105  * @return array|WP_Error Affected Term IDs.
3106  */
3107 function wp_set_object_terms( $object_id, $terms, $taxonomy, $append = false ) {
3108         global $wpdb;
3109
3110         $object_id = (int) $object_id;
3111
3112         if ( ! taxonomy_exists($taxonomy) )
3113                 return new WP_Error('invalid_taxonomy', __('Invalid taxonomy'));
3114
3115         if ( !is_array($terms) )
3116                 $terms = array($terms);
3117
3118         if ( ! $append )
3119                 $old_tt_ids =  wp_get_object_terms($object_id, $taxonomy, array('fields' => 'tt_ids', 'orderby' => 'none'));
3120         else
3121                 $old_tt_ids = array();
3122
3123         $tt_ids = array();
3124         $term_ids = array();
3125         $new_tt_ids = array();
3126
3127         foreach ( (array) $terms as $term) {
3128                 if ( !strlen(trim($term)) )
3129                         continue;
3130
3131                 if ( !$term_info = term_exists($term, $taxonomy) ) {
3132                         // Skip if a non-existent term ID is passed.
3133                         if ( is_int($term) )
3134                                 continue;
3135                         $term_info = wp_insert_term($term, $taxonomy);
3136                 }
3137                 if ( is_wp_error($term_info) )
3138                         return $term_info;
3139                 $term_ids[] = $term_info['term_id'];
3140                 $tt_id = $term_info['term_taxonomy_id'];
3141                 $tt_ids[] = $tt_id;
3142
3143                 if ( $wpdb->get_var( $wpdb->prepare( "SELECT term_taxonomy_id FROM $wpdb->term_relationships WHERE object_id = %d AND term_taxonomy_id = %d", $object_id, $tt_id ) ) )
3144                         continue;
3145
3146                 /**
3147                  * Fires immediately before an object-term relationship is added.
3148                  *
3149                  * @since 2.9.0
3150                  *
3151                  * @param int $object_id Object ID.
3152                  * @param int $tt_id     Term taxonomy ID.
3153                  */
3154                 do_action( 'add_term_relationship', $object_id, $tt_id );
3155                 $wpdb->insert( $wpdb->term_relationships, array( 'object_id' => $object_id, 'term_taxonomy_id' => $tt_id ) );
3156
3157                 /**
3158                  * Fires immediately after an object-term relationship is added.
3159                  *
3160                  * @since 2.9.0
3161                  *
3162                  * @param int $object_id Object ID.
3163                  * @param int $tt_id     Term taxonomy ID.
3164                  */
3165                 do_action( 'added_term_relationship', $object_id, $tt_id );
3166                 $new_tt_ids[] = $tt_id;
3167         }
3168
3169         if ( $new_tt_ids )
3170                 wp_update_term_count( $new_tt_ids, $taxonomy );
3171
3172         if ( ! $append ) {
3173                 $delete_tt_ids = array_diff( $old_tt_ids, $tt_ids );
3174
3175                 if ( $delete_tt_ids ) {
3176                         $in_delete_tt_ids = "'" . implode( "', '", $delete_tt_ids ) . "'";
3177                         $delete_term_ids = $wpdb->get_col( $wpdb->prepare( "SELECT tt.term_id FROM $wpdb->term_taxonomy AS tt WHERE tt.taxonomy = %s AND tt.term_taxonomy_id IN ($in_delete_tt_ids)", $taxonomy ) );
3178                         $delete_term_ids = array_map( 'intval', $delete_term_ids );
3179
3180                         $remove = wp_remove_object_terms( $object_id, $delete_term_ids, $taxonomy );
3181                         if ( is_wp_error( $remove ) ) {
3182                                 return $remove;
3183                         }
3184                 }
3185         }
3186
3187         $t = get_taxonomy($taxonomy);
3188         if ( ! $append && isset($t->sort) && $t->sort ) {
3189                 $values = array();
3190                 $term_order = 0;
3191                 $final_tt_ids = wp_get_object_terms($object_id, $taxonomy, array('fields' => 'tt_ids'));
3192                 foreach ( $tt_ids as $tt_id )
3193                         if ( in_array($tt_id, $final_tt_ids) )
3194                                 $values[] = $wpdb->prepare( "(%d, %d, %d)", $object_id, $tt_id, ++$term_order);
3195                 if ( $values )
3196                         if ( false === $wpdb->query( "INSERT INTO $wpdb->term_relationships (object_id, term_taxonomy_id, term_order) VALUES " . join( ',', $values ) . " ON DUPLICATE KEY UPDATE term_order = VALUES(term_order)" ) )
3197                                 return new WP_Error( 'db_insert_error', __( 'Could not insert term relationship into the database' ), $wpdb->last_error );
3198         }
3199
3200         wp_cache_delete( $object_id, $taxonomy . '_relationships' );
3201
3202         /**
3203          * Fires after an object's terms have been set.
3204          *
3205          * @since 2.8.0
3206          *
3207          * @param int    $object_id  Object ID.
3208          * @param array  $terms      An array of object terms.
3209          * @param array  $tt_ids     An array of term taxonomy IDs.
3210          * @param string $taxonomy   Taxonomy slug.
3211          * @param bool   $append     Whether to append new terms to the old terms.
3212          * @param array  $old_tt_ids Old array of term taxonomy IDs.
3213          */
3214         do_action( 'set_object_terms', $object_id, $terms, $tt_ids, $taxonomy, $append, $old_tt_ids );
3215         return $tt_ids;
3216 }
3217
3218 /**
3219  * Add term(s) associated with a given object.
3220  *
3221  * @since 3.6.0
3222  *
3223  * @param int              $object_id The ID of the object to which the terms will be added.
3224  * @param array|int|string $terms     The slug(s) or ID(s) of the term(s) to add.
3225  * @param array|string     $taxonomy  Taxonomy name.
3226  * @return array|WP_Error Affected Term IDs
3227  */
3228 function wp_add_object_terms( $object_id, $terms, $taxonomy ) {
3229         return wp_set_object_terms( $object_id, $terms, $taxonomy, true );
3230 }
3231
3232 /**
3233  * Remove term(s) associated with a given object.
3234  *
3235  * @since 3.6.0
3236  *
3237  * @global wpdb $wpdb WordPress database abstraction object.
3238  *
3239  * @param int              $object_id The ID of the object from which the terms will be removed.
3240  * @param array|int|string $terms     The slug(s) or ID(s) of the term(s) to remove.
3241  * @param array|string     $taxonomy  Taxonomy name.
3242  * @return bool|WP_Error True on success, false or WP_Error on failure.
3243  */
3244 function wp_remove_object_terms( $object_id, $terms, $taxonomy ) {
3245         global $wpdb;
3246
3247         $object_id = (int) $object_id;
3248
3249         if ( ! taxonomy_exists( $taxonomy ) ) {
3250                 return new WP_Error( 'invalid_taxonomy', __( 'Invalid Taxonomy' ) );
3251         }
3252
3253         if ( ! is_array( $terms ) ) {
3254                 $terms = array( $terms );
3255         }
3256
3257         $tt_ids = array();
3258
3259         foreach ( (array) $terms as $term ) {
3260                 if ( ! strlen( trim( $term ) ) ) {
3261                         continue;
3262                 }
3263
3264                 if ( ! $term_info = term_exists( $term, $taxonomy ) ) {
3265                         // Skip if a non-existent term ID is passed.
3266                         if ( is_int( $term ) ) {
3267                                 continue;
3268                         }
3269                 }
3270
3271                 if ( is_wp_error( $term_info ) ) {
3272                         return $term_info;
3273                 }
3274
3275                 $tt_ids[] = $term_info['term_taxonomy_id'];
3276         }
3277
3278         if ( $tt_ids ) {
3279                 $in_tt_ids = "'" . implode( "', '", $tt_ids ) . "'";
3280
3281                 /**
3282                  * Fires immediately before an object-term relationship is deleted.
3283                  *
3284                  * @since 2.9.0
3285                  *
3286                  * @param int   $object_id Object ID.
3287                  * @param array $tt_ids    An array of term taxonomy IDs.
3288                  */
3289                 do_action( 'delete_term_relationships', $object_id, $tt_ids );
3290                 $deleted = $wpdb->query( $wpdb->prepare( "DELETE FROM $wpdb->term_relationships WHERE object_id = %d AND term_taxonomy_id IN ($in_tt_ids)", $object_id ) );
3291
3292                 /**
3293                  * Fires immediately after an object-term relationship is deleted.
3294                  *
3295                  * @since 2.9.0
3296                  *
3297                  * @param int   $object_id Object ID.
3298                  * @param array $tt_ids    An array of term taxonomy IDs.
3299                  */
3300                 do_action( 'deleted_term_relationships', $object_id, $tt_ids );
3301
3302                 wp_update_term_count( $tt_ids, $taxonomy );
3303
3304                 return (bool) $deleted;
3305         }
3306
3307         return false;
3308 }
3309
3310 /**
3311  * Will make slug unique, if it isn't already.
3312  *
3313  * The `$slug` has to be unique global to every taxonomy, meaning that one
3314  * taxonomy term can't have a matching slug with another taxonomy term. Each
3315  * slug has to be globally unique for every taxonomy.
3316  *
3317  * The way this works is that if the taxonomy that the term belongs to is
3318  * hierarchical and has a parent, it will append that parent to the $slug.
3319  *
3320  * If that still doesn't return an unique slug, then it try to append a number
3321  * until it finds a number that is truly unique.
3322  *
3323  * The only purpose for `$term` is for appending a parent, if one exists.
3324  *
3325  * @since 2.3.0
3326  *
3327  * @global wpdb $wpdb WordPress database abstraction object.
3328  *
3329  * @param string $slug The string that will be tried for a unique slug.
3330  * @param object $term The term object that the `$slug` will belong to.
3331  * @return string Will return a true unique slug.
3332  */
3333 function wp_unique_term_slug( $slug, $term ) {
3334         global $wpdb;
3335
3336         $needs_suffix = true;
3337         $original_slug = $slug;
3338
3339         // As of 4.1, duplicate slugs are allowed as long as they're in different taxonomies.
3340         if ( ! term_exists( $slug ) || get_option( 'db_version' ) >= 30133 && ! get_term_by( 'slug', $slug, $term->taxonomy ) ) {
3341                 $needs_suffix = false;
3342         }
3343
3344         /*
3345          * If the taxonomy supports hierarchy and the term has a parent, make the slug unique
3346          * by incorporating parent slugs.
3347          */
3348         $parent_suffix = '';
3349         if ( $needs_suffix && is_taxonomy_hierarchical( $term->taxonomy ) && ! empty( $term->parent ) ) {
3350                 $the_parent = $term->parent;
3351                 while ( ! empty($the_parent) ) {
3352                         $parent_term = get_term($the_parent, $term->taxonomy);
3353                         if ( is_wp_error($parent_term) || empty($parent_term) )
3354                                 break;
3355                         $parent_suffix .= '-' . $parent_term->slug;
3356                         if ( ! term_exists( $slug . $parent_suffix ) ) {
3357                                 break;
3358                         }
3359
3360                         if ( empty($parent_term->parent) )
3361                                 break;
3362                         $the_parent = $parent_term->parent;
3363                 }
3364         }
3365
3366         // If we didn't get a unique slug, try appending a number to make it unique.
3367
3368         /**
3369          * Filter whether the proposed unique term slug is bad.
3370          *
3371          * @since 4.3.0
3372          *
3373          * @param bool   $needs_suffix Whether the slug needs to be made unique with a suffix.
3374          * @param string $slug         The slug.
3375          * @param object $term         Term object.
3376          */
3377         if ( apply_filters( 'wp_unique_term_slug_is_bad_slug', $needs_suffix, $slug, $term ) ) {
3378                 if ( $parent_suffix ) {
3379                         $slug .= $parent_suffix;
3380                 } else {
3381                         if ( ! empty( $term->term_id ) )
3382                                 $query = $wpdb->prepare( "SELECT slug FROM $wpdb->terms WHERE slug = %s AND term_id != %d", $slug, $term->term_id );
3383                         else
3384                                 $query = $wpdb->prepare( "SELECT slug FROM $wpdb->terms WHERE slug = %s", $slug );
3385
3386                         if ( $wpdb->get_var( $query ) ) {
3387                                 $num = 2;
3388                                 do {
3389                                         $alt_slug = $slug . "-$num";
3390                                         $num++;
3391                                         $slug_check = $wpdb->get_var( $wpdb->prepare( "SELECT slug FROM $wpdb->terms WHERE slug = %s", $alt_slug ) );
3392                                 } while ( $slug_check );
3393                                 $slug = $alt_slug;
3394                         }
3395                 }
3396         }
3397
3398         /**
3399          * Filter the unique term slug.
3400          *
3401          * @since 4.3.0
3402          *
3403          * @param string $slug          Unique term slug.
3404          * @param object $term          Term object.
3405          * @param string $original_slug Slug originally passed to the function for testing.
3406          */
3407         return apply_filters( 'wp_unique_term_slug', $slug, $term, $original_slug );
3408 }
3409
3410 /**
3411  * Update term based on arguments provided.
3412  *
3413  * The $args will indiscriminately override all values with the same field name.
3414  * Care must be taken to not override important information need to update or
3415  * update will fail (or perhaps create a new term, neither would be acceptable).
3416  *
3417  * Defaults will set 'alias_of', 'description', 'parent', and 'slug' if not
3418  * defined in $args already.
3419  *
3420  * 'alias_of' will create a term group, if it doesn't already exist, and update
3421  * it for the $term.
3422  *
3423  * If the 'slug' argument in $args is missing, then the 'name' in $args will be
3424  * used. It should also be noted that if you set 'slug' and it isn't unique then
3425  * a WP_Error will be passed back. If you don't pass any slug, then a unique one
3426  * will be created for you.
3427  *
3428  * For what can be overrode in `$args`, check the term scheme can contain and stay
3429  * away from the term keys.
3430  *
3431  * @since 2.3.0
3432  *
3433  * @global wpdb $wpdb WordPress database abstraction object.
3434  *
3435  * @param int          $term_id  The ID of the term
3436  * @param string       $taxonomy The context in which to relate the term to the object.
3437  * @param array|string $args     Optional. Array of get_terms() arguments. Default empty array.
3438  * @return array|WP_Error Returns Term ID and Taxonomy Term ID
3439  */
3440 function wp_update_term( $term_id, $taxonomy, $args = array() ) {
3441         global $wpdb;
3442
3443         if ( ! taxonomy_exists( $taxonomy ) ) {
3444                 return new WP_Error( 'invalid_taxonomy', __( 'Invalid taxonomy' ) );
3445         }
3446
3447         $term_id = (int) $term_id;
3448
3449         // First, get all of the original args
3450         $term = get_term( $term_id, $taxonomy, ARRAY_A );
3451
3452         if ( is_wp_error( $term ) ) {
3453                 return $term;
3454         }
3455
3456         if ( ! $term ) {
3457                 return new WP_Error( 'invalid_term', __( 'Empty Term' ) );
3458         }
3459
3460         // Escape data pulled from DB.
3461         $term = wp_slash($term);
3462
3463         // Merge old and new args with new args overwriting old ones.
3464         $args = array_merge($term, $args);
3465
3466         $defaults = array( 'alias_of' => '', 'description' => '', 'parent' => 0, 'slug' => '');
3467         $args = wp_parse_args($args, $defaults);
3468         $args = sanitize_term($args, $taxonomy, 'db');
3469         $parsed_args = $args;
3470
3471         // expected_slashed ($name)
3472         $name = wp_unslash( $args['name'] );
3473         $description = wp_unslash( $args['description'] );
3474
3475         $parsed_args['name'] = $name;
3476         $parsed_args['description'] = $description;
3477
3478         if ( '' == trim($name) )
3479                 return new WP_Error('empty_term_name', __('A name is required for this term'));
3480
3481         if ( $parsed_args['parent'] > 0 && ! term_exists( (int) $parsed_args['parent'] ) ) {
3482                 return new WP_Error( 'missing_parent', __( 'Parent term does not exist.' ) );
3483         }
3484
3485         $empty_slug = false;
3486         if ( empty( $args['slug'] ) ) {
3487                 $empty_slug = true;
3488                 $slug = sanitize_title($name);
3489         } else {
3490                 $slug = $args['slug'];
3491         }
3492
3493         $parsed_args['slug'] = $slug;
3494
3495         $term_group = isset( $parsed_args['term_group'] ) ? $parsed_args['term_group'] : 0;
3496         if ( $args['alias_of'] ) {
3497                 $alias = get_term_by( 'slug', $args['alias_of'], $taxonomy );
3498                 if ( ! empty( $alias->term_group ) ) {
3499                         // The alias we want is already in a group, so let's use that one.
3500                         $term_group = $alias->term_group;
3501                 } elseif ( ! empty( $alias->term_id ) ) {
3502                         /*
3503                          * The alias is not in a group, so we create a new one
3504                          * and add the alias to it.
3505                          */
3506                         $term_group = $wpdb->get_var("SELECT MAX(term_group) FROM $wpdb->terms") + 1;