[ Index ]

PHP Cross Reference of WordPress Trunk (Updated Daily)

Search

title

Body

[close]

/wp-includes/ -> taxonomy.php (source)

   1  <?php
   2  /**
   3   * Core Taxonomy API
   4   *
   5   * @package WordPress
   6   * @subpackage Taxonomy
   7   */
   8  
   9  //
  10  // Taxonomy registration.
  11  //
  12  
  13  /**
  14   * Creates the initial taxonomies.
  15   *
  16   * This function fires twice: in wp-settings.php before plugins are loaded (for
  17   * backward compatibility reasons), and again on the {@see 'init'} action. We must
  18   * avoid registering rewrite rules before the {@see 'init'} action.
  19   *
  20   * @since 2.8.0
  21   * @since 5.9.0 Added `'wp_template_part_area'` taxonomy.
  22   *
  23   * @global WP_Rewrite $wp_rewrite WordPress rewrite component.
  24   */
  25  function create_initial_taxonomies() {
  26      global $wp_rewrite;
  27  
  28      WP_Taxonomy::reset_default_labels();
  29  
  30      if ( ! did_action( 'init' ) ) {
  31          $rewrite = array(
  32              'category'    => false,
  33              'post_tag'    => false,
  34              'post_format' => false,
  35          );
  36      } else {
  37  
  38          /**
  39           * Filters the post formats rewrite base.
  40           *
  41           * @since 3.1.0
  42           *
  43           * @param string $context Context of the rewrite base. Default 'type'.
  44           */
  45          $post_format_base = apply_filters( 'post_format_rewrite_base', 'type' );
  46          $rewrite          = array(
  47              'category'    => array(
  48                  'hierarchical' => true,
  49                  'slug'         => get_option( 'category_base' ) ? get_option( 'category_base' ) : 'category',
  50                  'with_front'   => ! get_option( 'category_base' ) || $wp_rewrite->using_index_permalinks(),
  51                  'ep_mask'      => EP_CATEGORIES,
  52              ),
  53              'post_tag'    => array(
  54                  'hierarchical' => false,
  55                  'slug'         => get_option( 'tag_base' ) ? get_option( 'tag_base' ) : 'tag',
  56                  'with_front'   => ! get_option( 'tag_base' ) || $wp_rewrite->using_index_permalinks(),
  57                  'ep_mask'      => EP_TAGS,
  58              ),
  59              'post_format' => $post_format_base ? array( 'slug' => $post_format_base ) : false,
  60          );
  61      }
  62  
  63      register_taxonomy(
  64          'category',
  65          'post',
  66          array(
  67              'hierarchical'          => true,
  68              'query_var'             => 'category_name',
  69              'rewrite'               => $rewrite['category'],
  70              'public'                => true,
  71              'show_ui'               => true,
  72              'show_admin_column'     => true,
  73              '_builtin'              => true,
  74              'capabilities'          => array(
  75                  'manage_terms' => 'manage_categories',
  76                  'edit_terms'   => 'edit_categories',
  77                  'delete_terms' => 'delete_categories',
  78                  'assign_terms' => 'assign_categories',
  79              ),
  80              'show_in_rest'          => true,
  81              'rest_base'             => 'categories',
  82              'rest_controller_class' => 'WP_REST_Terms_Controller',
  83          )
  84      );
  85  
  86      register_taxonomy(
  87          'post_tag',
  88          'post',
  89          array(
  90              'hierarchical'          => false,
  91              'query_var'             => 'tag',
  92              'rewrite'               => $rewrite['post_tag'],
  93              'public'                => true,
  94              'show_ui'               => true,
  95              'show_admin_column'     => true,
  96              '_builtin'              => true,
  97              'capabilities'          => array(
  98                  'manage_terms' => 'manage_post_tags',
  99                  'edit_terms'   => 'edit_post_tags',
 100                  'delete_terms' => 'delete_post_tags',
 101                  'assign_terms' => 'assign_post_tags',
 102              ),
 103              'show_in_rest'          => true,
 104              'rest_base'             => 'tags',
 105              'rest_controller_class' => 'WP_REST_Terms_Controller',
 106          )
 107      );
 108  
 109      register_taxonomy(
 110          'nav_menu',
 111          'nav_menu_item',
 112          array(
 113              'public'                => false,
 114              'hierarchical'          => false,
 115              'labels'                => array(
 116                  'name'          => __( 'Navigation Menus' ),
 117                  'singular_name' => __( 'Navigation Menu' ),
 118              ),
 119              'query_var'             => false,
 120              'rewrite'               => false,
 121              'show_ui'               => false,
 122              '_builtin'              => true,
 123              'show_in_nav_menus'     => false,
 124              'capabilities'          => array(
 125                  'manage_terms' => 'edit_theme_options',
 126                  'edit_terms'   => 'edit_theme_options',
 127                  'delete_terms' => 'edit_theme_options',
 128                  'assign_terms' => 'edit_theme_options',
 129              ),
 130              'show_in_rest'          => true,
 131              'rest_base'             => 'menus',
 132              'rest_controller_class' => 'WP_REST_Menus_Controller',
 133          )
 134      );
 135  
 136      register_taxonomy(
 137          'link_category',
 138          'link',
 139          array(
 140              'hierarchical' => false,
 141              'labels'       => array(
 142                  'name'                       => __( 'Link Categories' ),
 143                  'singular_name'              => __( 'Link Category' ),
 144                  'search_items'               => __( 'Search Link Categories' ),
 145                  'popular_items'              => null,
 146                  'all_items'                  => __( 'All Link Categories' ),
 147                  'edit_item'                  => __( 'Edit Link Category' ),
 148                  'update_item'                => __( 'Update Link Category' ),
 149                  'add_new_item'               => __( 'Add Link Category' ),
 150                  'new_item_name'              => __( 'New Link Category Name' ),
 151                  'separate_items_with_commas' => null,
 152                  'add_or_remove_items'        => null,
 153                  'choose_from_most_used'      => null,
 154                  'back_to_items'              => __( '&larr; Go to Link Categories' ),
 155              ),
 156              'capabilities' => array(
 157                  'manage_terms' => 'manage_links',
 158                  'edit_terms'   => 'manage_links',
 159                  'delete_terms' => 'manage_links',
 160                  'assign_terms' => 'manage_links',
 161              ),
 162              'query_var'    => false,
 163              'rewrite'      => false,
 164              'public'       => false,
 165              'show_ui'      => true,
 166              '_builtin'     => true,
 167          )
 168      );
 169  
 170      register_taxonomy(
 171          'post_format',
 172          'post',
 173          array(
 174              'public'            => true,
 175              'hierarchical'      => false,
 176              'labels'            => array(
 177                  'name'          => _x( 'Formats', 'post format' ),
 178                  'singular_name' => _x( 'Format', 'post format' ),
 179              ),
 180              'query_var'         => true,
 181              'rewrite'           => $rewrite['post_format'],
 182              'show_ui'           => false,
 183              '_builtin'          => true,
 184              'show_in_nav_menus' => current_theme_supports( 'post-formats' ),
 185          )
 186      );
 187  
 188      register_taxonomy(
 189          'wp_theme',
 190          array( 'wp_template', 'wp_template_part', 'wp_global_styles' ),
 191          array(
 192              'public'            => false,
 193              'hierarchical'      => false,
 194              'labels'            => array(
 195                  'name'          => __( 'Themes' ),
 196                  'singular_name' => __( 'Theme' ),
 197              ),
 198              'query_var'         => false,
 199              'rewrite'           => false,
 200              'show_ui'           => false,
 201              '_builtin'          => true,
 202              'show_in_nav_menus' => false,
 203              'show_in_rest'      => false,
 204          )
 205      );
 206  
 207      register_taxonomy(
 208          'wp_template_part_area',
 209          array( 'wp_template_part' ),
 210          array(
 211              'public'            => false,
 212              'hierarchical'      => false,
 213              'labels'            => array(
 214                  'name'          => __( 'Template Part Areas' ),
 215                  'singular_name' => __( 'Template Part Area' ),
 216              ),
 217              'query_var'         => false,
 218              'rewrite'           => false,
 219              'show_ui'           => false,
 220              '_builtin'          => true,
 221              'show_in_nav_menus' => false,
 222              'show_in_rest'      => false,
 223          )
 224      );
 225  
 226      register_taxonomy(
 227          'wp_pattern_category',
 228          array( 'wp_block' ),
 229          array(
 230              'public'             => false,
 231              'publicly_queryable' => false,
 232              'hierarchical'       => false,
 233              'labels'             => array(
 234                  'name'                       => _x( 'Pattern Categories', 'taxonomy general name' ),
 235                  'singular_name'              => _x( 'Pattern Category', 'taxonomy singular name' ),
 236                  'add_new_item'               => __( 'Add Category' ),
 237                  'add_or_remove_items'        => __( 'Add or remove pattern categories' ),
 238                  'back_to_items'              => __( '&larr; Go to Pattern Categories' ),
 239                  'choose_from_most_used'      => __( 'Choose from the most used pattern categories' ),
 240                  'edit_item'                  => __( 'Edit Pattern Category' ),
 241                  'item_link'                  => __( 'Pattern Category Link' ),
 242                  'item_link_description'      => __( 'A link to a pattern category.' ),
 243                  'items_list'                 => __( 'Pattern Categories list' ),
 244                  'items_list_navigation'      => __( 'Pattern Categories list navigation' ),
 245                  'new_item_name'              => __( 'New Pattern Category Name' ),
 246                  'no_terms'                   => __( 'No pattern categories' ),
 247                  'not_found'                  => __( 'No pattern categories found.' ),
 248                  'popular_items'              => __( 'Popular Pattern Categories' ),
 249                  'search_items'               => __( 'Search Pattern Categories' ),
 250                  'separate_items_with_commas' => __( 'Separate pattern categories with commas' ),
 251                  'update_item'                => __( 'Update Pattern Category' ),
 252                  'view_item'                  => __( 'View Pattern Category' ),
 253              ),
 254              'query_var'          => false,
 255              'rewrite'            => false,
 256              'show_ui'            => true,
 257              '_builtin'           => true,
 258              'show_in_nav_menus'  => false,
 259              'show_in_rest'       => true,
 260              'show_admin_column'  => true,
 261              'show_tagcloud'      => false,
 262          )
 263      );
 264  }
 265  
 266  /**
 267   * Retrieves a list of registered taxonomy names or objects.
 268   *
 269   * @since 3.0.0
 270   *
 271   * @global WP_Taxonomy[] $wp_taxonomies The registered taxonomies.
 272   *
 273   * @param array  $args     Optional. An array of `key => value` arguments to match against the taxonomy objects.
 274   *                         Default empty array.
 275   * @param string $output   Optional. The type of output to return in the array. Either 'names'
 276   *                         or 'objects'. Default 'names'.
 277   * @param string $operator Optional. The logical operation to perform. Accepts 'and' or 'or'. 'or' means only
 278   *                         one element from the array needs to match; 'and' means all elements must match.
 279   *                         Default 'and'.
 280   * @return string[]|WP_Taxonomy[] An array of taxonomy names or objects.
 281   * @phpstan-return ( $output is 'names' ? array<non-falsy-string, non-falsy-string> : array<non-falsy-string, WP_Taxonomy> )
 282   */
 283  function get_taxonomies( $args = array(), $output = 'names', $operator = 'and' ) {
 284      global $wp_taxonomies;
 285  
 286      $field = ( 'names' === $output ) ? 'name' : false;
 287  
 288      return wp_filter_object_list( $wp_taxonomies, $args, $operator, $field );
 289  }
 290  
 291  /**
 292   * Returns the names or objects of the taxonomies which are registered for the requested object or object type,
 293   * such as a post object or post type name.
 294   *
 295   * Example:
 296   *
 297   *     $taxonomies = get_object_taxonomies( 'post' );
 298   *
 299   * This results in:
 300   *
 301   *     Array( 'category', 'post_tag' )
 302   *
 303   * @since 2.3.0
 304   *
 305   * @global array<non-falsy-string, WP_Taxonomy> $wp_taxonomies The registered taxonomies.
 306   *
 307   * @param string|string[]|WP_Post $object_type Name of the type of taxonomy object, or an object (row from posts).
 308   * @param string                  $output      Optional. The type of output to return in the array. Accepts either
 309   *                                             'names' or 'objects'. Default 'names'.
 310   * @return string[]|WP_Taxonomy[] The names or objects of all taxonomies of `$object_type`.
 311   * @phpstan-return ( $output is 'names' ? list<non-falsy-string> : array<non-falsy-string, WP_Taxonomy> )
 312   */
 313  function get_object_taxonomies( $object_type, $output = 'names' ) {
 314      global $wp_taxonomies;
 315  
 316      if ( is_object( $object_type ) ) {
 317          if ( 'attachment' === $object_type->post_type ) {
 318              return get_attachment_taxonomies( $object_type, $output );
 319          }
 320          $object_type = $object_type->post_type;
 321      }
 322  
 323      $object_type = (array) $object_type;
 324  
 325      $taxonomies = array();
 326      foreach ( (array) $wp_taxonomies as $tax_name => $tax_obj ) {
 327          if ( array_intersect( $object_type, (array) $tax_obj->object_type ) ) {
 328              if ( 'names' === $output ) {
 329                  $taxonomies[] = $tax_name;
 330              } else {
 331                  $taxonomies[ $tax_name ] = $tax_obj;
 332              }
 333          }
 334      }
 335  
 336      return $taxonomies;
 337  }
 338  
 339  /**
 340   * Retrieves the taxonomy object of $taxonomy.
 341   *
 342   * The get_taxonomy function will first check that the parameter string given
 343   * is a taxonomy object and if it is, it will return it.
 344   *
 345   * @since 2.3.0
 346   *
 347   * @global WP_Taxonomy[] $wp_taxonomies The registered taxonomies.
 348   *
 349   * @param string $taxonomy Name of taxonomy object to return.
 350   * @return WP_Taxonomy|false The taxonomy object or false if $taxonomy doesn't exist.
 351   */
 352  function get_taxonomy( $taxonomy ) {
 353      global $wp_taxonomies;
 354  
 355      if ( ! taxonomy_exists( $taxonomy ) ) {
 356          return false;
 357      }
 358  
 359      return $wp_taxonomies[ $taxonomy ];
 360  }
 361  
 362  /**
 363   * Determines whether the taxonomy name exists.
 364   *
 365   * Formerly is_taxonomy(), introduced in 2.3.0.
 366   *
 367   * For more information on this and similar theme functions, check out
 368   * the {@link https://developer.wordpress.org/themes/basics/conditional-tags/
 369   * Conditional Tags} article in the Theme Developer Handbook.
 370   *
 371   * @since 3.0.0
 372   *
 373   * @global WP_Taxonomy[] $wp_taxonomies The registered taxonomies.
 374   *
 375   * @param string $taxonomy Name of taxonomy object.
 376   * @return bool Whether the taxonomy exists.
 377   */
 378  function taxonomy_exists( $taxonomy ) {
 379      global $wp_taxonomies;
 380  
 381      return is_string( $taxonomy ) && isset( $wp_taxonomies[ $taxonomy ] );
 382  }
 383  
 384  /**
 385   * Determines whether the taxonomy object is hierarchical.
 386   *
 387   * Checks to make sure that the taxonomy is an object first. Then Gets the
 388   * object, and finally returns the hierarchical value in the object.
 389   *
 390   * A false return value might also mean that the taxonomy does not exist.
 391   *
 392   * For more information on this and similar theme functions, check out
 393   * the {@link https://developer.wordpress.org/themes/basics/conditional-tags/
 394   * Conditional Tags} article in the Theme Developer Handbook.
 395   *
 396   * @since 2.3.0
 397   *
 398   * @param string $taxonomy Name of taxonomy object.
 399   * @return bool Whether the taxonomy is hierarchical.
 400   */
 401  function is_taxonomy_hierarchical( $taxonomy ) {
 402      if ( ! taxonomy_exists( $taxonomy ) ) {
 403          return false;
 404      }
 405  
 406      $taxonomy = get_taxonomy( $taxonomy );
 407      return $taxonomy->hierarchical;
 408  }
 409  
 410  /**
 411   * Creates or modifies a taxonomy object.
 412   *
 413   * Note: Do not use before the {@see 'init'} hook.
 414   *
 415   * A simple function for creating or modifying a taxonomy object based on
 416   * the parameters given. If modifying an existing taxonomy object, note
 417   * that the `$object_type` value from the original registration will be
 418   * overwritten.
 419   *
 420   * @since 2.3.0
 421   * @since 4.2.0 Introduced `show_in_quick_edit` argument.
 422   * @since 4.4.0 The `show_ui` argument is now enforced on the term editing screen.
 423   * @since 4.4.0 The `public` argument now controls whether the taxonomy can be queried on the front end.
 424   * @since 4.5.0 Introduced `publicly_queryable` argument.
 425   * @since 4.7.0 Introduced `show_in_rest`, 'rest_base' and 'rest_controller_class'
 426   *              arguments to register the taxonomy in REST API.
 427   * @since 5.1.0 Introduced `meta_box_sanitize_cb` argument.
 428   * @since 5.4.0 Added the registered taxonomy object as a return value.
 429   * @since 5.5.0 Introduced `default_term` argument.
 430   * @since 5.9.0 Introduced `rest_namespace` argument.
 431   *
 432   * @global WP_Taxonomy[] $wp_taxonomies Registered taxonomies.
 433   *
 434   * @param string       $taxonomy    Taxonomy key. Must not exceed 32 characters and may only contain
 435   *                                  lowercase alphanumeric characters, dashes, and underscores. See sanitize_key().
 436   * @param array|string $object_type Object type or array of object types with which the taxonomy should be associated.
 437   * @param array|string $args        {
 438   *     Optional. Array or query string of arguments for registering a taxonomy.
 439   *
 440   *     @type string[]      $labels                An array of labels for this taxonomy. By default, Tag labels are
 441   *                                                used for non-hierarchical taxonomies, and Category labels are used
 442   *                                                for hierarchical taxonomies. See accepted values in
 443   *                                                get_taxonomy_labels(). Default empty array.
 444   *     @type string        $description           A short descriptive summary of what the taxonomy is for. Default empty.
 445   *     @type bool          $public                Whether a taxonomy is intended for use publicly either via
 446   *                                                the admin interface or by front-end users. The default settings
 447   *                                                of `$publicly_queryable`, `$show_ui`, and `$show_in_nav_menus`
 448   *                                                are inherited from `$public`.
 449   *     @type bool          $publicly_queryable    Whether the taxonomy is publicly queryable.
 450   *                                                If not set, the default is inherited from `$public`
 451   *     @type bool          $hierarchical          Whether the taxonomy is hierarchical. Default false.
 452   *     @type bool          $show_ui               Whether to generate and allow a UI for managing terms in this taxonomy in
 453   *                                                the admin. If not set, the default is inherited from `$public`
 454   *                                                (default true).
 455   *     @type bool          $show_in_menu          Whether to show the taxonomy in the admin menu. If true, the taxonomy is
 456   *                                                shown as a submenu of the object type menu. If false, no menu is shown.
 457   *                                                `$show_ui` must be true. If not set, default is inherited from `$show_ui`
 458   *                                                (default true).
 459   *     @type bool          $show_in_nav_menus     Makes this taxonomy available for selection in navigation menus. If not
 460   *                                                set, the default is inherited from `$public` (default true).
 461   *     @type bool          $show_in_rest          Whether to include the taxonomy in the REST API. Set this to true
 462   *                                                for the taxonomy to be available in the block editor.
 463   *     @type string        $rest_base             To change the base url of REST API route. Default is $taxonomy.
 464   *     @type string        $rest_namespace        To change the namespace URL of REST API route. Default is wp/v2.
 465   *     @type string        $rest_controller_class REST API Controller class name. Default is 'WP_REST_Terms_Controller'.
 466   *     @type bool          $show_tagcloud         Whether to list the taxonomy in the Tag Cloud Widget controls. If not set,
 467   *                                                the default is inherited from `$show_ui` (default true).
 468   *     @type bool          $show_in_quick_edit    Whether to show the taxonomy in the quick/bulk edit panel. It not set,
 469   *                                                the default is inherited from `$show_ui` (default true).
 470   *     @type bool          $show_admin_column     Whether to display a column for the taxonomy on its post type listing
 471   *                                                screens. Default false.
 472   *     @type bool|callable $meta_box_cb           Provide a callback function for the meta box display. If not set,
 473   *                                                post_categories_meta_box() is used for hierarchical taxonomies, and
 474   *                                                post_tags_meta_box() is used for non-hierarchical. If false, no meta
 475   *                                                box is shown.
 476   *     @type callable      $meta_box_sanitize_cb  Callback function for sanitizing taxonomy data saved from a meta
 477   *                                                box. If no callback is defined, an appropriate one is determined
 478   *                                                based on the value of `$meta_box_cb`.
 479   *     @type string[]      $capabilities {
 480   *         Array of capabilities for this taxonomy.
 481   *
 482   *         @type string $manage_terms Default 'manage_categories'.
 483   *         @type string $edit_terms   Default 'manage_categories'.
 484   *         @type string $delete_terms Default 'manage_categories'.
 485   *         @type string $assign_terms Default 'edit_posts'.
 486   *     }
 487   *     @type bool|array    $rewrite {
 488   *         Triggers the handling of rewrites for this taxonomy. Default true, using $taxonomy as slug. To prevent
 489   *         rewrite, set to false. To specify rewrite rules, an array can be passed with any of these keys:
 490   *
 491   *         @type string $slug         Customize the permastruct slug. Default `$taxonomy` key.
 492   *         @type bool   $with_front   Should the permastruct be prepended with WP_Rewrite::$front. Default true.
 493   *         @type bool   $hierarchical Either hierarchical rewrite tag or not. Default false.
 494   *         @type int    $ep_mask      Assign an endpoint mask. Default `EP_NONE`.
 495   *     }
 496   *     @type string|bool   $query_var             Sets the query var key for this taxonomy. Default `$taxonomy` key. If
 497   *                                                false, a taxonomy cannot be loaded at `?{query_var}={term_slug}`. If a
 498   *                                                string, the query `?{query_var}={term_slug}` will be valid.
 499   *     @type callable      $update_count_callback Works much like a hook, in that it will be called when the count is
 500   *                                                updated. Default _update_post_term_count() for taxonomies attached
 501   *                                                to post types, which confirms that the objects are published before
 502   *                                                counting them. Default _update_generic_term_count() for taxonomies
 503   *                                                attached to other object types, such as users.
 504   *     @type string|array  $default_term {
 505   *         Default term to be used for the taxonomy.
 506   *
 507   *         @type string $name         Name of default term.
 508   *         @type string $slug         Slug for default term. Default empty.
 509   *         @type string $description  Description for default term. Default empty.
 510   *     }
 511   *     @type bool          $sort                  Whether terms in this taxonomy should be sorted in the order they are
 512   *                                                provided to `wp_set_object_terms()`. Default null which equates to false.
 513   *     @type array         $args                  Array of arguments to automatically use inside `wp_get_object_terms()`
 514   *                                                for this taxonomy.
 515   *     @type bool          $_builtin              This taxonomy is a "built-in" taxonomy. INTERNAL USE ONLY!
 516   *                                                Default false.
 517   * }
 518   * @return WP_Taxonomy|WP_Error The registered taxonomy object on success, WP_Error object on failure.
 519   */
 520  function register_taxonomy( $taxonomy, $object_type, $args = array() ) {
 521      global $wp_taxonomies;
 522  
 523      if ( ! is_array( $wp_taxonomies ) ) {
 524          $wp_taxonomies = array();
 525      }
 526  
 527      $args = wp_parse_args( $args );
 528  
 529      if ( empty( $taxonomy ) || strlen( $taxonomy ) > 32 ) {
 530          _doing_it_wrong( __FUNCTION__, __( 'Taxonomy names must be between 1 and 32 characters in length.' ), '4.2.0' );
 531          return new WP_Error( 'taxonomy_length_invalid', __( 'Taxonomy names must be between 1 and 32 characters in length.' ) );
 532      }
 533  
 534      $taxonomy_object = new WP_Taxonomy( $taxonomy, $object_type, $args );
 535      $taxonomy_object->add_rewrite_rules();
 536  
 537      $wp_taxonomies[ $taxonomy ] = $taxonomy_object;
 538  
 539      $taxonomy_object->add_hooks();
 540  
 541      // Add default term.
 542      if ( ! empty( $taxonomy_object->default_term ) ) {
 543          $term = term_exists( $taxonomy_object->default_term['name'], $taxonomy );
 544          if ( $term ) {
 545              update_option( 'default_term_' . $taxonomy_object->name, $term['term_id'] );
 546          } else {
 547              $term = wp_insert_term(
 548                  $taxonomy_object->default_term['name'],
 549                  $taxonomy,
 550                  array(
 551                      'slug'        => sanitize_title( $taxonomy_object->default_term['slug'] ),
 552                      'description' => $taxonomy_object->default_term['description'],
 553                  )
 554              );
 555  
 556              // Update `term_id` in options.
 557              if ( ! is_wp_error( $term ) ) {
 558                  update_option( 'default_term_' . $taxonomy_object->name, $term['term_id'] );
 559              }
 560          }
 561      }
 562  
 563      /**
 564       * Fires after a taxonomy is registered.
 565       *
 566       * @since 3.3.0
 567       *
 568       * @param string       $taxonomy    Taxonomy slug.
 569       * @param array|string $object_type Object type or array of object types.
 570       * @param array        $args        Array of taxonomy registration arguments.
 571       */
 572      do_action( 'registered_taxonomy', $taxonomy, $object_type, (array) $taxonomy_object );
 573  
 574      /**
 575       * Fires after a specific taxonomy is registered.
 576       *
 577       * The dynamic portion of the filter name, `$taxonomy`, refers to the taxonomy key.
 578       *
 579       * Possible hook names include:
 580       *
 581       *  - `registered_taxonomy_category`
 582       *  - `registered_taxonomy_post_tag`
 583       *
 584       * @since 6.0.0
 585       *
 586       * @param string       $taxonomy    Taxonomy slug.
 587       * @param array|string $object_type Object type or array of object types.
 588       * @param array        $args        Array of taxonomy registration arguments.
 589       */
 590      do_action( "registered_taxonomy_{$taxonomy}", $taxonomy, $object_type, (array) $taxonomy_object );
 591  
 592      return $taxonomy_object;
 593  }
 594  
 595  /**
 596   * Unregisters a taxonomy.
 597   *
 598   * Can not be used to unregister built-in taxonomies.
 599   *
 600   * @since 4.5.0
 601   *
 602   * @global WP_Taxonomy[] $wp_taxonomies List of taxonomies.
 603   *
 604   * @param string $taxonomy Taxonomy name.
 605   * @return true|WP_Error True on success, WP_Error on failure or if the taxonomy doesn't exist.
 606   */
 607  function unregister_taxonomy( $taxonomy ) {
 608      global $wp_taxonomies;
 609  
 610      if ( ! taxonomy_exists( $taxonomy ) ) {
 611          return new WP_Error( 'invalid_taxonomy', __( 'Invalid taxonomy.' ) );
 612      }
 613  
 614      $taxonomy_object = get_taxonomy( $taxonomy );
 615  
 616      // Do not allow unregistering internal taxonomies.
 617      if ( $taxonomy_object->_builtin ) {
 618          return new WP_Error( 'invalid_taxonomy', __( 'Unregistering a built-in taxonomy is not allowed.' ) );
 619      }
 620  
 621      $taxonomy_object->remove_rewrite_rules();
 622      $taxonomy_object->remove_hooks();
 623  
 624      // Remove the taxonomy.
 625      unset( $wp_taxonomies[ $taxonomy ] );
 626  
 627      /**
 628       * Fires after a taxonomy is unregistered.
 629       *
 630       * @since 4.5.0
 631       *
 632       * @param string $taxonomy Taxonomy name.
 633       */
 634      do_action( 'unregistered_taxonomy', $taxonomy );
 635  
 636      return true;
 637  }
 638  
 639  /**
 640   * Builds an object with all taxonomy labels out of a taxonomy object.
 641   *
 642   * @since 3.0.0
 643   * @since 4.3.0 Added the `no_terms` label.
 644   * @since 4.4.0 Added the `items_list_navigation` and `items_list` labels.
 645   * @since 4.9.0 Added the `most_used` and `back_to_items` labels.
 646   * @since 5.7.0 Added the `filter_by_item` label.
 647   * @since 5.8.0 Added the `item_link` and `item_link_description` labels.
 648   * @since 5.9.0 Added the `name_field_description`, `slug_field_description`,
 649   *              `parent_field_description`, and `desc_field_description` labels.
 650   * @since 6.6.0 Added the `template_name` label.
 651   *
 652   * @param WP_Taxonomy $tax Taxonomy object.
 653   * @return stdClass {
 654   *     Taxonomy labels object. The first default value is for non-hierarchical taxonomies
 655   *     (like tags) and the second one is for hierarchical taxonomies (like categories).
 656   *
 657   *     @type string      $name                       General name for the taxonomy, usually plural. The same
 658   *                                                   as and overridden by `$tax->label`. Default 'Tags'/'Categories'.
 659   *     @type string      $singular_name              Name for one object of this taxonomy. Default 'Tag'/'Category'.
 660   *     @type string      $menu_name                  Label for the menu name. Default 'Tags'/'Categories'.
 661   *     @type string      $name_admin_bar             Label for the object name in the admin bar. Default is the value of
 662   *                                                   `singular_name` in the given labels, or the taxonomy key.
 663   *     @type string      $search_items               Default 'Search Tags'/'Search Categories'.
 664   *     @type string|null $popular_items              This label is only used for non-hierarchical taxonomies.
 665   *                                                   Default 'Popular Tags'.
 666   *     @type string      $all_items                  Default 'All Tags'/'All Categories'.
 667   *     @type string|null $parent_item                This label is only used for hierarchical taxonomies. Default
 668   *                                                   'Parent Category'.
 669   *     @type string|null $parent_item_colon          This label is only used for hierarchical taxonomies. The same as
 670   *                                                   `parent_item`, but with colon `:` in the end.
 671   *     @type string      $name_field_description     Description for the Name field on Edit Tags screen.
 672   *                                                   Default 'The name is how it appears on your site'.
 673   *     @type string      $slug_field_description     Description for the Slug field on Edit Tags screen.
 674   *                                                   Default 'The &#8220;slug&#8221; is the URL-friendly version
 675   *                                                   of the name. It is usually all lowercase and contains
 676   *                                                   only letters, numbers, and hyphens'.
 677   *     @type string|null $parent_field_description   Description for the Parent field on Edit Tags screen.
 678   *                                                   Default 'Assign a parent term to create a hierarchy.
 679   *                                                   The term Jazz, for example, would be the parent
 680   *                                                   of Bebop and Big Band'.
 681   *     @type string      $desc_field_description     Description for the Description field on Edit Tags screen.
 682   *                                                   Default 'The description is not prominent by default;
 683   *                                                   however, some themes may show it'.
 684   *     @type string      $edit_item                  Default 'Edit Tag'/'Edit Category'.
 685   *     @type string      $view_item                  Default 'View Tag'/'View Category'.
 686   *     @type string      $update_item                Default 'Update Tag'/'Update Category'.
 687   *     @type string      $add_new_item               Default 'Add Tag'/'Add Category'.
 688   *     @type string      $new_item_name              Default 'New Tag Name'/'New Category Name'.
 689   *     @type string      $template_name              Default 'Tag Archives'/'Category Archives'.
 690   *     @type string|null $separate_items_with_commas This label is only used for non-hierarchical taxonomies. Default
 691   *                                                   'Separate tags with commas', used in the meta box.
 692   *     @type string|null $add_or_remove_items        This label is only used for non-hierarchical taxonomies. Default
 693   *                                                   'Add or remove tags', used in the meta box when JavaScript
 694   *                                                   is disabled.
 695   *     @type string|null $choose_from_most_used      This label is only used on non-hierarchical taxonomies. Default
 696   *                                                   'Choose from the most used tags', used in the meta box.
 697   *     @type string      $not_found                  Default 'No tags found'/'No categories found', used in
 698   *                                                   the meta box and taxonomy list table.
 699   *     @type string      $no_terms                   Default 'No tags'/'No categories', used in the posts and media
 700   *                                                   list tables.
 701   *     @type string|null $filter_by_item             This label is only used for hierarchical taxonomies. Default
 702   *                                                   'Filter by category', used in the posts list table.
 703   *     @type string      $items_list_navigation      Label for the table pagination hidden heading.
 704   *     @type string      $items_list                 Label for the table hidden heading.
 705   *     @type string      $most_used                  Title for the Most Used tab. Default 'Most Used'.
 706   *     @type string      $back_to_items              Label displayed after a term has been updated.
 707   *     @type string      $item_link                  Used in the block editor. Title for a navigation link block
 708   *                                                   variation. Default 'Tag Link'/'Category Link'.
 709   *     @type string      $item_link_description      Used in the block editor. Description for a navigation link block
 710   *                                                   variation. Default 'A link to a tag'/'A link to a category'.
 711   * }
 712   */
 713  function get_taxonomy_labels( $tax ) {
 714      $tax->labels = (array) $tax->labels;
 715  
 716      if ( isset( $tax->helps ) && empty( $tax->labels['separate_items_with_commas'] ) ) {
 717          $tax->labels['separate_items_with_commas'] = $tax->helps;
 718      }
 719  
 720      if ( isset( $tax->no_tagcloud ) && empty( $tax->labels['not_found'] ) ) {
 721          $tax->labels['not_found'] = $tax->no_tagcloud;
 722      }
 723  
 724      $nohier_vs_hier_defaults = WP_Taxonomy::get_default_labels();
 725  
 726      $nohier_vs_hier_defaults['menu_name'] = $nohier_vs_hier_defaults['name'];
 727  
 728      $labels = _get_custom_object_labels( $tax, $nohier_vs_hier_defaults );
 729  
 730      if ( ! isset( $tax->labels->template_name ) && isset( $labels->singular_name ) ) {
 731          /* translators: %s: Taxonomy name. */
 732          $labels->template_name = sprintf( _x( '%s Archives', 'taxonomy template name' ), $labels->singular_name );
 733      }
 734  
 735      $taxonomy = $tax->name;
 736  
 737      $default_labels = clone $labels;
 738  
 739      /**
 740       * Filters the labels of a specific taxonomy.
 741       *
 742       * The dynamic portion of the hook name, `$taxonomy`, refers to the taxonomy slug.
 743       *
 744       * Possible hook names include:
 745       *
 746       *  - `taxonomy_labels_category`
 747       *  - `taxonomy_labels_post_tag`
 748       *
 749       * @since 4.4.0
 750       *
 751       * @see get_taxonomy_labels() for the full list of taxonomy labels.
 752       *
 753       * @param object $labels Object with labels for the taxonomy as member variables.
 754       */
 755      $labels = apply_filters( "taxonomy_labels_{$taxonomy}", $labels );
 756  
 757      // Ensure that the filtered labels contain all required default values.
 758      $labels = (object) array_merge( (array) $default_labels, (array) $labels );
 759  
 760      return $labels;
 761  }
 762  
 763  /**
 764   * Adds an already registered taxonomy to an object type.
 765   *
 766   * @since 3.0.0
 767   *
 768   * @global WP_Taxonomy[] $wp_taxonomies The registered taxonomies.
 769   *
 770   * @param string $taxonomy    Name of taxonomy object.
 771   * @param string $object_type Name of the object type.
 772   * @return bool True if successful, false if not.
 773   */
 774  function register_taxonomy_for_object_type( $taxonomy, $object_type ) {
 775      global $wp_taxonomies;
 776  
 777      if ( ! isset( $wp_taxonomies[ $taxonomy ] ) ) {
 778          return false;
 779      }
 780  
 781      if ( ! get_post_type_object( $object_type ) ) {
 782          return false;
 783      }
 784  
 785      if ( ! in_array( $object_type, $wp_taxonomies[ $taxonomy ]->object_type, true ) ) {
 786          $wp_taxonomies[ $taxonomy ]->object_type[] = $object_type;
 787      }
 788  
 789      // Filter out empties.
 790      $wp_taxonomies[ $taxonomy ]->object_type = array_filter( $wp_taxonomies[ $taxonomy ]->object_type );
 791  
 792      /**
 793       * Fires after a taxonomy is registered for an object type.
 794       *
 795       * @since 5.1.0
 796       *
 797       * @param string $taxonomy    Taxonomy name.
 798       * @param string $object_type Name of the object type.
 799       */
 800      do_action( 'registered_taxonomy_for_object_type', $taxonomy, $object_type );
 801  
 802      return true;
 803  }
 804  
 805  /**
 806   * Removes an already registered taxonomy from an object type.
 807   *
 808   * @since 3.7.0
 809   *
 810   * @global WP_Taxonomy[] $wp_taxonomies The registered taxonomies.
 811   *
 812   * @param string $taxonomy    Name of taxonomy object.
 813   * @param string $object_type Name of the object type.
 814   * @return bool True if successful, false if not.
 815   */
 816  function unregister_taxonomy_for_object_type( $taxonomy, $object_type ) {
 817      global $wp_taxonomies;
 818  
 819      if ( ! isset( $wp_taxonomies[ $taxonomy ] ) ) {
 820          return false;
 821      }
 822  
 823      if ( ! get_post_type_object( $object_type ) ) {
 824          return false;
 825      }
 826  
 827      $key = array_search( $object_type, $wp_taxonomies[ $taxonomy ]->object_type, true );
 828      if ( false === $key ) {
 829          return false;
 830      }
 831  
 832      unset( $wp_taxonomies[ $taxonomy ]->object_type[ $key ] );
 833  
 834      /**
 835       * Fires after a taxonomy is unregistered for an object type.
 836       *
 837       * @since 5.1.0
 838       *
 839       * @param string $taxonomy    Taxonomy name.
 840       * @param string $object_type Name of the object type.
 841       */
 842      do_action( 'unregistered_taxonomy_for_object_type', $taxonomy, $object_type );
 843  
 844      return true;
 845  }
 846  
 847  //
 848  // Term API.
 849  //
 850  
 851  /**
 852   * Retrieves object IDs of valid taxonomy and term.
 853   *
 854   * The strings of `$taxonomies` must exist before this function will continue.
 855   * On failure of finding a valid taxonomy, it will return a WP_Error.
 856   *
 857   * The `$terms` aren't checked the same as `$taxonomies`, but still need to exist
 858   * for object IDs to be returned.
 859   *
 860   * It is possible to change the order that object IDs are returned by using `$args`
 861   * with either ASC or DESC array. The value should be in the key named 'order'.
 862   *
 863   * @since 2.3.0
 864   *
 865   * @global wpdb $wpdb WordPress database abstraction object.
 866   *
 867   * @param int|int[]       $term_ids   Term ID or array of term IDs of terms that will be used.
 868   * @param string|string[] $taxonomies String of taxonomy name or Array of string values of taxonomy names.
 869   * @param array|string    $args       {
 870   *     Change the order of the object IDs.
 871   *
 872   *     @type string $order Order to retrieve terms. Accepts 'ASC' or 'DESC'. Default 'ASC'.
 873   * }
 874   * @return string[]|WP_Error An array of object IDs as numeric strings on success,
 875   *                           WP_Error if the taxonomy does not exist.
 876   */
 877  function get_objects_in_term( $term_ids, $taxonomies, $args = array() ) {
 878      global $wpdb;
 879  
 880      if ( ! is_array( $term_ids ) ) {
 881          $term_ids = array( $term_ids );
 882      }
 883      if ( ! is_array( $taxonomies ) ) {
 884          $taxonomies = array( $taxonomies );
 885      }
 886      foreach ( (array) $taxonomies as $taxonomy ) {
 887          if ( ! taxonomy_exists( $taxonomy ) ) {
 888              return new WP_Error( 'invalid_taxonomy', __( 'Invalid taxonomy.' ) );
 889          }
 890      }
 891  
 892      $defaults = array( 'order' => 'ASC' );
 893      $args     = wp_parse_args( $args, $defaults );
 894  
 895      $order = ( 'desc' === strtolower( $args['order'] ) ) ? 'DESC' : 'ASC';
 896  
 897      $term_ids = array_map( 'intval', $term_ids );
 898  
 899      $taxonomies = "'" . implode( "', '", array_map( 'esc_sql', $taxonomies ) ) . "'";
 900      $term_ids   = "'" . implode( "', '", $term_ids ) . "'";
 901  
 902      $sql = "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";
 903  
 904      $last_changed = wp_cache_get_last_changed( 'terms' );
 905      $cache_key    = 'get_objects_in_term:' . md5( $sql );
 906      $cache        = wp_cache_get_salted( $cache_key, 'term-queries', $last_changed );
 907      if ( false === $cache ) {
 908          $object_ids = $wpdb->get_col( $sql );
 909          wp_cache_set_salted( $cache_key, $object_ids, 'term-queries', $last_changed );
 910      } else {
 911          $object_ids = (array) $cache;
 912      }
 913  
 914      if ( ! $object_ids ) {
 915          return array();
 916      }
 917      return $object_ids;
 918  }
 919  
 920  /**
 921   * Given a taxonomy query, generates SQL to be appended to a main query.
 922   *
 923   * @since 3.1.0
 924   *
 925   * @see WP_Tax_Query
 926   *
 927   * @param array  $tax_query         A compact tax query
 928   * @param string $primary_table
 929   * @param string $primary_id_column
 930   * @return string[]
 931   */
 932  function get_tax_sql( $tax_query, $primary_table, $primary_id_column ) {
 933      $tax_query_obj = new WP_Tax_Query( $tax_query );
 934      return $tax_query_obj->get_sql( $primary_table, $primary_id_column );
 935  }
 936  
 937  /**
 938   * Gets all term data from database by term ID.
 939   *
 940   * The usage of the get_term function is to apply filters to a term object. It
 941   * is possible to get a term object from the database before applying the
 942   * filters.
 943   *
 944   * $term ID must be part of $taxonomy, to get from the database. Failure, might
 945   * be able to be captured by the hooks. Failure would be the same value as $wpdb
 946   * returns for the get_row method.
 947   *
 948   * There are two hooks, one is specifically for each term, named 'get_term', and
 949   * the second is for the taxonomy name, 'term_$taxonomy'. Both hooks gets the
 950   * term object, and the taxonomy name as parameters. Both hooks are expected to
 951   * return a term object.
 952   *
 953   * {@see 'get_term'} hook - Takes two parameters the term Object and the taxonomy name.
 954   * Must return term object. Used in get_term() as a catch-all filter for every
 955   * $term.
 956   *
 957   * {@see 'get_$taxonomy'} hook - Takes two parameters the term Object and the taxonomy
 958   * name. Must return term object. $taxonomy will be the taxonomy name, so for
 959   * example, if 'category', it would be 'get_category' as the filter name. Useful
 960   * for custom taxonomies or plugging into default taxonomies.
 961   *
 962   * @todo Better formatting for DocBlock
 963   *
 964   * @since 2.3.0
 965   * @since 4.4.0 Converted to return a WP_Term object if `$output` is `OBJECT`.
 966   *              The `$taxonomy` parameter was made optional.
 967   *
 968   * @see sanitize_term_field() The $context param lists the available values for get_term_by() $filter param.
 969   *
 970   * @param int|WP_Term|object $term     If integer, term data will be fetched from the database,
 971   *                                     or from the cache if available.
 972   *                                     If stdClass object (as in the results of a database query),
 973   *                                     will apply filters and return a `WP_Term` object with the `$term` data.
 974   *                                     If `WP_Term`, will return `$term`.
 975   * @param string             $taxonomy Optional. Taxonomy name that `$term` is part of.
 976   * @param string             $output   Optional. The required return type. One of OBJECT, ARRAY_A, or ARRAY_N, which
 977   *                                     correspond to a WP_Term object, an associative array, or a numeric array,
 978   *                                     respectively. Default OBJECT.
 979   * @param string             $filter   Optional. How to sanitize term fields. Default 'raw'.
 980   * @return WP_Term|array|WP_Error|null WP_Term instance (or array) on success, depending on the `$output` value.
 981   *                                     WP_Error if `$taxonomy` does not exist. Null for miscellaneous failure.
 982   */
 983  function get_term( $term, $taxonomy = '', $output = OBJECT, $filter = 'raw' ) {
 984      if ( empty( $term ) ) {
 985          return new WP_Error( 'invalid_term', __( 'Empty Term.' ) );
 986      }
 987  
 988      if ( $taxonomy && ! taxonomy_exists( $taxonomy ) ) {
 989          return new WP_Error( 'invalid_taxonomy', __( 'Invalid taxonomy.' ) );
 990      }
 991  
 992      if ( $term instanceof WP_Term ) {
 993          $_term = $term;
 994      } elseif ( is_object( $term ) ) {
 995          if ( empty( $term->filter ) || 'raw' === $term->filter ) {
 996              $_term = sanitize_term( $term, $taxonomy, 'raw' );
 997              $_term = new WP_Term( $_term );
 998          } else {
 999              $_term = WP_Term::get_instance( $term->term_id );
1000          }
1001      } else {
1002          $_term = WP_Term::get_instance( $term, $taxonomy );
1003      }
1004  
1005      if ( is_wp_error( $_term ) ) {
1006          return $_term;
1007      } elseif ( ! $_term ) {
1008          return null;
1009      }
1010  
1011      // Ensure for filters that this is not empty.
1012      $taxonomy = $_term->taxonomy;
1013  
1014      $old_term = $_term;
1015      /**
1016       * Filters a taxonomy term object.
1017       *
1018       * The {@see 'get_$taxonomy'} hook is also available for targeting a specific
1019       * taxonomy.
1020       *
1021       * @since 2.3.0
1022       * @since 4.4.0 `$_term` is now a WP_Term object.
1023       *
1024       * @param WP_Term $_term    Term object.
1025       * @param string  $taxonomy The taxonomy slug.
1026       */
1027      $_term = apply_filters( 'get_term', $_term, $taxonomy );
1028  
1029      /**
1030       * Filters a taxonomy term object.
1031       *
1032       * The dynamic portion of the hook name, `$taxonomy`, refers
1033       * to the slug of the term's taxonomy.
1034       *
1035       * Possible hook names include:
1036       *
1037       *  - `get_category`
1038       *  - `get_post_tag`
1039       *
1040       * @since 2.3.0
1041       * @since 4.4.0 `$_term` is now a WP_Term object.
1042       *
1043       * @param WP_Term $_term    Term object.
1044       * @param string  $taxonomy The taxonomy slug.
1045       */
1046      $_term = apply_filters( "get_{$taxonomy}", $_term, $taxonomy );
1047  
1048      // Bail if a filter callback has changed the type of the `$_term` object.
1049      if ( ! ( $_term instanceof WP_Term ) ) {
1050          return $_term;
1051      }
1052  
1053      // Sanitize term, according to the specified filter.
1054      if ( $_term !== $old_term || $_term->filter !== $filter ) {
1055          $_term->filter( $filter );
1056      }
1057  
1058      if ( ARRAY_A === $output ) {
1059          return $_term->to_array();
1060      } elseif ( ARRAY_N === $output ) {
1061          return array_values( $_term->to_array() );
1062      }
1063  
1064      return $_term;
1065  }
1066  
1067  /**
1068   * Gets all term data from database by term field and data.
1069   *
1070   * Warning: $value is not escaped for 'name' $field. You must do it yourself, if
1071   * required.
1072   *
1073   * The default $field is 'id', therefore it is possible to also use null for
1074   * field, but not recommended that you do so.
1075   *
1076   * If $value does not exist, the return value will be false. If $taxonomy exists
1077   * and $field and $value combinations exist, the term will be returned.
1078   *
1079   * This function will always return the first term that matches the `$field`-
1080   * `$value`-`$taxonomy` combination specified in the parameters. If your query
1081   * is likely to match more than one term (as is likely to be the case when
1082   * `$field` is 'name', for example), consider using get_terms() instead; that
1083   * way, you will get all matching terms, and can provide your own logic for
1084   * deciding which one was intended.
1085   *
1086   * @todo Better formatting for DocBlock.
1087   *
1088   * @since 2.3.0
1089   * @since 4.4.0 `$taxonomy` is optional if `$field` is 'term_taxonomy_id'. Converted to return
1090   *              a WP_Term object if `$output` is `OBJECT`.
1091   * @since 5.5.0 Added 'ID' as an alias of 'id' for the `$field` parameter.
1092   *
1093   * @see sanitize_term_field() The $context param lists the available values for get_term_by() $filter param.
1094   *
1095   * @param string     $field    Either 'slug', 'name', 'term_id' (or 'id', 'ID'), or 'term_taxonomy_id'.
1096   * @param string|int $value    Search for this term value.
1097   * @param string     $taxonomy Taxonomy name. Optional, if `$field` is 'term_taxonomy_id'.
1098   * @param string     $output   Optional. The required return type. One of OBJECT, ARRAY_A, or ARRAY_N, which
1099   *                             correspond to a WP_Term object, an associative array, or a numeric array,
1100   *                             respectively. Default OBJECT.
1101   * @param string     $filter   Optional. How to sanitize term fields. Default 'raw'.
1102   * @return WP_Term|array|false WP_Term instance (or array) on success, depending on the `$output` value.
1103   *                             False if `$taxonomy` does not exist or `$term` was not found.
1104   */
1105  function get_term_by( $field, $value, $taxonomy = '', $output = OBJECT, $filter = 'raw' ) {
1106  
1107      // 'term_taxonomy_id' lookups don't require taxonomy checks.
1108      if ( 'term_taxonomy_id' !== $field && ! taxonomy_exists( $taxonomy ) ) {
1109          return false;
1110      }
1111  
1112      // No need to perform a query for empty 'slug' or 'name'.
1113      if ( 'slug' === $field || 'name' === $field ) {
1114          $value = (string) $value;
1115  
1116          if ( 0 === strlen( $value ) ) {
1117              return false;
1118          }
1119      }
1120  
1121      if ( 'id' === $field || 'ID' === $field || 'term_id' === $field ) {
1122          $term = get_term( (int) $value, $taxonomy, $output, $filter );
1123          if ( is_wp_error( $term ) || null === $term ) {
1124              $term = false;
1125          }
1126          return $term;
1127      }
1128  
1129      $args = array(
1130          'get'                    => 'all',
1131          'number'                 => 1,
1132          'taxonomy'               => $taxonomy,
1133          'update_term_meta_cache' => false,
1134          'orderby'                => 'none',
1135          'suppress_filter'        => true,
1136      );
1137  
1138      switch ( $field ) {
1139          case 'slug':
1140              $args['slug'] = $value;
1141              break;
1142          case 'name':
1143              $args['name'] = $value;
1144              break;
1145          case 'term_taxonomy_id':
1146              $args['term_taxonomy_id'] = $value;
1147              unset( $args['taxonomy'] );
1148              break;
1149          default:
1150              return false;
1151      }
1152  
1153      $terms = get_terms( $args );
1154      if ( is_wp_error( $terms ) || empty( $terms ) ) {
1155          return false;
1156      }
1157  
1158      $term = array_shift( $terms );
1159  
1160      // In the case of 'term_taxonomy_id', override the provided `$taxonomy` with whatever we find in the DB.
1161      if ( 'term_taxonomy_id' === $field ) {
1162          $taxonomy = $term->taxonomy;
1163      }
1164  
1165      return get_term( $term, $taxonomy, $output, $filter );
1166  }
1167  
1168  /**
1169   * Merges all term children into a single array of their IDs.
1170   *
1171   * This recursive function will merge all of the children of $term into the same
1172   * array of term IDs. Only useful for taxonomies which are hierarchical.
1173   *
1174   * Will return an empty array if $term does not exist in $taxonomy.
1175   *
1176   * @since 2.3.0
1177   *
1178   * @param int    $term_id  ID of term to get children.
1179   * @param string $taxonomy Taxonomy name.
1180   * @return array|WP_Error List of term IDs. WP_Error returned if `$taxonomy` does not exist.
1181   */
1182  function get_term_children( $term_id, $taxonomy ) {
1183      if ( ! taxonomy_exists( $taxonomy ) ) {
1184          return new WP_Error( 'invalid_taxonomy', __( 'Invalid taxonomy.' ) );
1185      }
1186  
1187      $term_id = (int) $term_id;
1188  
1189      $terms = _get_term_hierarchy( $taxonomy );
1190  
1191      if ( ! isset( $terms[ $term_id ] ) ) {
1192          return array();
1193      }
1194  
1195      $children = $terms[ $term_id ];
1196  
1197      foreach ( (array) $terms[ $term_id ] as $child ) {
1198          if ( $term_id === $child ) {
1199              continue;
1200          }
1201  
1202          if ( isset( $terms[ $child ] ) ) {
1203              $children = array_merge( $children, get_term_children( $child, $taxonomy ) );
1204          }
1205      }
1206  
1207      return $children;
1208  }
1209  
1210  /**
1211   * Gets sanitized term field.
1212   *
1213   * The function is for contextual reasons and for simplicity of usage.
1214   *
1215   * @since 2.3.0
1216   * @since 4.4.0 The `$taxonomy` parameter was made optional. `$term` can also now accept a WP_Term object.
1217   *
1218   * @see sanitize_term_field()
1219   *
1220   * @param string      $field    Term field to fetch.
1221   * @param int|WP_Term $term     Term ID or object.
1222   * @param string      $taxonomy Optional. Taxonomy name. Default empty.
1223   * @param string      $context  Optional. How to sanitize term fields. Look at sanitize_term_field() for available options.
1224   *                              Default 'display'.
1225   * @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.
1226   */
1227  function get_term_field( $field, $term, $taxonomy = '', $context = 'display' ) {
1228      $term = get_term( $term, $taxonomy );
1229      if ( is_wp_error( $term ) ) {
1230          return $term;
1231      }
1232  
1233      if ( ! is_object( $term ) ) {
1234          return '';
1235      }
1236  
1237      if ( ! isset( $term->$field ) ) {
1238          return '';
1239      }
1240  
1241      return sanitize_term_field( $field, $term->$field, $term->term_id, $term->taxonomy, $context );
1242  }
1243  
1244  /**
1245   * Sanitizes term for editing.
1246   *
1247   * Return value is sanitize_term() and usage is for sanitizing the term for
1248   * editing. Function is for contextual and simplicity.
1249   *
1250   * @since 2.3.0
1251   *
1252   * @param int|object $id       Term ID or object.
1253   * @param string     $taxonomy Taxonomy name.
1254   * @return string|int|null|WP_Error Will return empty string if $term is not an object.
1255   */
1256  function get_term_to_edit( $id, $taxonomy ) {
1257      $term = get_term( $id, $taxonomy );
1258  
1259      if ( is_wp_error( $term ) ) {
1260          return $term;
1261      }
1262  
1263      if ( ! is_object( $term ) ) {
1264          return '';
1265      }
1266  
1267      return sanitize_term( $term, $taxonomy, 'edit' );
1268  }
1269  
1270  /**
1271   * Retrieves the terms in a given taxonomy or list of taxonomies.
1272   *
1273   * You can fully inject any customizations to the query before it is sent, as
1274   * well as control the output with a filter.
1275   *
1276   * The return type varies depending on the value passed to `$args['fields']`. See
1277   * WP_Term_Query::get_terms() for details. In all cases, a `WP_Error` object will
1278   * be returned if an invalid taxonomy is requested.
1279   *
1280   * The {@see 'get_terms'} filter will be called when the cache has the term and will
1281   * pass the found term along with the array of $taxonomies and array of $args.
1282   * This filter is also called before the array of terms is passed and will pass
1283   * the array of terms, along with the $taxonomies and $args.
1284   *
1285   * The {@see 'list_terms_exclusions'} filter passes the compiled exclusions along with
1286   * the $args.
1287   *
1288   * The {@see 'get_terms_orderby'} filter passes the `ORDER BY` clause for the query
1289   * along with the $args array.
1290   *
1291   * Taxonomy or an array of taxonomies should be passed via the 'taxonomy' argument
1292   * in the `$args` array:
1293   *
1294   *     $terms = get_terms( array(
1295   *         'taxonomy'   => 'post_tag',
1296   *         'hide_empty' => false,
1297   *     ) );
1298   *
1299   * Prior to 4.5.0, taxonomy was passed as the first parameter of `get_terms()`.
1300   *
1301   * {@internal The `$deprecated` parameter is parsed for backward compatibility only.}
1302   *
1303   * @since 2.3.0
1304   * @since 4.2.0 Introduced 'name' and 'childless' parameters.
1305   * @since 4.4.0 Introduced the ability to pass 'term_id' as an alias of 'id' for the `orderby` parameter.
1306   *              Introduced the 'meta_query' and 'update_term_meta_cache' parameters. Converted to return
1307   *              a list of WP_Term objects.
1308   * @since 4.5.0 Changed the function signature so that the `$args` array can be provided as the first parameter.
1309   *              Introduced 'meta_key' and 'meta_value' parameters. Introduced the ability to order results by metadata.
1310   * @since 4.8.0 Introduced 'suppress_filter' parameter.
1311   *
1312   * @param array|string $args       Optional. Array or string of arguments. See {@see WP_Term_Query::__construct()}
1313   *                                 for information on accepted arguments. Default empty array.
1314   * @param array|string $deprecated Optional. Argument array, when using the legacy function parameter format.
1315   *                                 If present, this parameter will be interpreted as `$args`, and the first
1316   *                                 function parameter will be parsed as a taxonomy or array of taxonomies.
1317   *                                 Default empty.
1318   * @return WP_Term[]|int[]|string[]|int|string|WP_Error Array of terms, a count thereof as a numeric string,
1319   *                                                      the integer 0 when the queried parent term is not in
1320   *                                                      the taxonomy hierarchy, or WP_Error if any of the
1321   *                                                      taxonomies do not exist. See the function description
1322   *                                                      for more information.
1323   *
1324   * @phpstan-return (
1325   *     $args is array{ fields: 'count', ... }
1326   *         ? 0|numeric-string|WP_Error
1327   *         : ( $args is array{ fields: 'ids'|'tt_ids', ... }
1328   *             ? int[]|WP_Error
1329   *             : ( $args is array{ fields: 'id=>parent', ... }
1330   *                 ? array<int, int>|WP_Error
1331   *                 : ( $args is array{ fields: 'names'|'slugs', ... }
1332   *                     ? string[]|WP_Error
1333   *                     : ( $args is array{ fields: 'id=>name'|'id=>slug', ... }
1334   *                         ? array<int, string>|WP_Error
1335   *                         : ( $deprecated is ''
1336   *                             ? ( $args is array
1337   *                                 ? WP_Term[]|WP_Error
1338   *                                 : WP_Term[]|int[]|string[]|int|string|WP_Error )
1339   *                             : WP_Term[]|int[]|string[]|int|string|WP_Error ) ) ) ) )
1340   * )
1341   */
1342  function get_terms( $args = array(), $deprecated = '' ) {
1343      $term_query = new WP_Term_Query();
1344  
1345      $defaults = array(
1346          'suppress_filter' => false,
1347      );
1348  
1349      /*
1350       * Legacy argument format ($taxonomy, $args) takes precedence.
1351       *
1352       * We detect legacy argument format by checking if
1353       * (a) a second non-empty parameter is passed, or
1354       * (b) the first parameter shares no keys with the default array (ie, it's a list of taxonomies)
1355       */
1356      $_args          = wp_parse_args( $args );
1357      $key_intersect  = array_intersect_key( $term_query->query_var_defaults, (array) $_args );
1358      $do_legacy_args = $deprecated || empty( $key_intersect );
1359  
1360      if ( $do_legacy_args ) {
1361          $taxonomies       = (array) $args;
1362          $args             = wp_parse_args( $deprecated, $defaults );
1363          $args['taxonomy'] = $taxonomies;
1364      } else {
1365          $args = wp_parse_args( $args, $defaults );
1366          if ( isset( $args['taxonomy'] ) ) {
1367              $args['taxonomy'] = (array) $args['taxonomy'];
1368          }
1369      }
1370  
1371      if ( ! empty( $args['taxonomy'] ) ) {
1372          foreach ( $args['taxonomy'] as $taxonomy ) {
1373              if ( ! taxonomy_exists( $taxonomy ) ) {
1374                  return new WP_Error( 'invalid_taxonomy', __( 'Invalid taxonomy.' ) );
1375              }
1376          }
1377      }
1378  
1379      // Don't pass suppress_filter to WP_Term_Query.
1380      $suppress_filter = $args['suppress_filter'];
1381      unset( $args['suppress_filter'] );
1382  
1383      $terms = $term_query->query( $args );
1384  
1385      // Count queries are not filtered, for legacy reasons.
1386      if ( ! is_array( $terms ) ) {
1387          return $terms;
1388      }
1389  
1390      if ( $suppress_filter ) {
1391          return $terms;
1392      }
1393  
1394      /**
1395       * Filters the found terms.
1396       *
1397       * @since 2.3.0
1398       * @since 4.6.0 Added the `$term_query` parameter.
1399       *
1400       * @param array         $terms      Array of found terms.
1401       * @param array|null    $taxonomies An array of taxonomies if known.
1402       * @param array         $args       An array of get_terms() arguments.
1403       * @param WP_Term_Query $term_query The WP_Term_Query object.
1404       */
1405      return apply_filters( 'get_terms', $terms, $term_query->query_vars['taxonomy'], $term_query->query_vars, $term_query );
1406  }
1407  
1408  /**
1409   * Adds metadata to a term.
1410   *
1411   * For historical reasons both the meta key and the meta value are expected to be "slashed" (slashes escaped) on input.
1412   *
1413   * @since 4.4.0
1414   *
1415   * @param int    $term_id    Term ID.
1416   * @param string $meta_key   Metadata name.
1417   * @param mixed  $meta_value Metadata value. Arrays and objects are stored as serialized data and
1418   *                           will be returned as the same type when retrieved. Other data types will
1419   *                           be stored as strings in the database:
1420   *                           - false is stored and retrieved as an empty string ('')
1421   *                           - true is stored and retrieved as '1'
1422   *                           - numbers (both integer and float) are stored and retrieved as strings
1423   *                           Must be serializable if non-scalar.
1424   * @param bool   $unique     Optional. Whether the same key should not be added.
1425   *                           Default false.
1426   * @return int|false|WP_Error Meta ID on success, false on failure.
1427   *                            WP_Error when term_id is ambiguous between taxonomies.
1428   */
1429  function add_term_meta( $term_id, $meta_key, $meta_value, $unique = false ) {
1430      if ( wp_term_is_shared( $term_id ) ) {
1431          return new WP_Error( 'ambiguous_term_id', __( 'Term meta cannot be added to terms that are shared between taxonomies.' ), $term_id );
1432      }
1433  
1434      return add_metadata( 'term', $term_id, $meta_key, $meta_value, $unique );
1435  }
1436  
1437  /**
1438   * Removes metadata matching criteria from a term.
1439   *
1440   * For historical reasons both the meta key and the meta value are expected to be "slashed" (slashes escaped) on input.
1441   *
1442   * @since 4.4.0
1443   *
1444   * @param int    $term_id    Term ID.
1445   * @param string $meta_key   Metadata name.
1446   * @param mixed  $meta_value Optional. Metadata value. If provided,
1447   *                           rows will only be removed that match the value.
1448   *                           Must be serializable if non-scalar. Default empty.
1449   * @return bool True on success, false on failure.
1450   *
1451   * @phpstan-param positive-int $term_id
1452   */
1453  function delete_term_meta( $term_id, $meta_key, $meta_value = '' ) {
1454      return delete_metadata( 'term', $term_id, $meta_key, $meta_value );
1455  }
1456  
1457  /**
1458   * Retrieves metadata for a term.
1459   *
1460   * @since 4.4.0
1461   *
1462   * @param int    $term_id Term ID.
1463   * @param string $key     Optional. The meta key to retrieve. By default,
1464   *                        returns data for all keys. Default empty.
1465   * @param bool   $single  Optional. Whether to return a single value.
1466   *                        This parameter has no effect if `$key` is not specified.
1467   *                        Default false.
1468   * @return mixed An array of values if `$single` is false.
1469   *               The value of the meta field if `$single` is true.
1470   *               False for an invalid `$term_id` (non-numeric, zero, or negative value).
1471   *               An empty array if a valid but non-existing term ID is passed and `$single` is false.
1472   *               An empty string if a valid but non-existing term ID is passed and `$single` is true.
1473   *               Note: Non-serialized values are returned as strings:
1474   *               - false values are returned as empty strings ('')
1475   *               - true values are returned as '1'
1476   *               - numbers are returned as strings
1477   *               Arrays and objects retain their original type.
1478   *               These conversions apply to stored values. A default value registered
1479   *               with {@see register_meta()} is never stored, so it is returned with
1480   *               the type it was registered with, which may be an integer, float, or
1481   *               boolean.
1482   *
1483   * @phpstan-return (
1484   *     $key is ''|'0'
1485   *         ? array<array-key, list<string>>|false
1486   *         : ( $single is true
1487   *             ? mixed
1488   *             : list<mixed>|false )
1489   * )
1490   */
1491  function get_term_meta( $term_id, $key = '', $single = false ) {
1492      return get_metadata( 'term', $term_id, $key, $single );
1493  }
1494  
1495  /**
1496   * Updates term metadata.
1497   *
1498   * Use the `$prev_value` parameter to differentiate between meta fields with the same key and term ID.
1499   *
1500   * If the meta field for the term does not exist, it will be added.
1501   *
1502   * For historical reasons both the meta key and the meta value are expected to be "slashed" (slashes escaped) on input.
1503   *
1504   * @since 4.4.0
1505   *
1506   * @param int    $term_id    Term ID.
1507   * @param string $meta_key   Metadata key.
1508   * @param mixed  $meta_value Metadata value. Must be serializable if non-scalar.
1509   * @param mixed  $prev_value Optional. Previous value to check before updating.
1510   *                           If specified, only update existing metadata entries with
1511   *                           this value. Otherwise, update all entries. Default empty.
1512   * @return int|bool|WP_Error Meta ID if the key didn't exist. true on successful update,
1513   *                           false on failure or if the value passed to the function
1514   *                           is the same as the one that is already in the database.
1515   *                           WP_Error when term_id is ambiguous between taxonomies.
1516   */
1517  function update_term_meta( $term_id, $meta_key, $meta_value, $prev_value = '' ) {
1518      if ( wp_term_is_shared( $term_id ) ) {
1519          return new WP_Error( 'ambiguous_term_id', __( 'Term meta cannot be added to terms that are shared between taxonomies.' ), $term_id );
1520      }
1521  
1522      return update_metadata( 'term', $term_id, $meta_key, $meta_value, $prev_value );
1523  }
1524  
1525  /**
1526   * Updates metadata cache for list of term IDs.
1527   *
1528   * Performs SQL query to retrieve all metadata for the terms matching `$term_ids` and stores them in the cache.
1529   * Subsequent calls to `get_term_meta()` will not need to query the database.
1530   *
1531   * @since 4.4.0
1532   *
1533   * @param array $term_ids List of term IDs.
1534   * @return array|false An array of metadata on success, false if there is nothing to update.
1535   */
1536  function update_termmeta_cache( $term_ids ) {
1537      return update_meta_cache( 'term', $term_ids );
1538  }
1539  
1540  
1541  /**
1542   * Queue term meta for lazy-loading.
1543   *
1544   * @since 6.3.0
1545   *
1546   * @param array $term_ids List of term IDs.
1547   */
1548  function wp_lazyload_term_meta( array $term_ids ) {
1549      if ( empty( $term_ids ) ) {
1550          return;
1551      }
1552      $lazyloader = wp_metadata_lazyloader();
1553      $lazyloader->queue_objects( 'term', $term_ids );
1554  }
1555  
1556  /**
1557   * Gets all meta data, including meta IDs, for the given term ID.
1558   *
1559   * @since 4.9.0
1560   *
1561   * @global wpdb $wpdb WordPress database abstraction object.
1562   *
1563   * @param int $term_id Term ID.
1564   * @return array|false Array with meta data, or false when the meta table is not installed.
1565   */
1566  function has_term_meta( $term_id ) {
1567      $check = wp_check_term_meta_support_prefilter( null );
1568      if ( null !== $check ) {
1569          return $check;
1570      }
1571  
1572      global $wpdb;
1573  
1574      return $wpdb->get_results( $wpdb->prepare( "SELECT meta_key, meta_value, meta_id, term_id FROM $wpdb->termmeta WHERE term_id = %d ORDER BY meta_key,meta_id", $term_id ), ARRAY_A );
1575  }
1576  
1577  /**
1578   * Registers a meta key for terms.
1579   *
1580   * @since 4.9.8
1581   *
1582   * @param string $taxonomy Taxonomy to register a meta key for. Pass an empty string
1583   *                         to register the meta key across all existing taxonomies.
1584   * @param string $meta_key The meta key to register.
1585   * @param array  $args     Data used to describe the meta key when registered. See
1586   *                         {@see register_meta()} for a list of supported arguments.
1587   * @return bool True if the meta key was successfully registered, false if not.
1588   */
1589  function register_term_meta( $taxonomy, $meta_key, array $args ) {
1590      $args['object_subtype'] = $taxonomy;
1591  
1592      return register_meta( 'term', $meta_key, $args );
1593  }
1594  
1595  /**
1596   * Unregisters a meta key for terms.
1597   *
1598   * @since 4.9.8
1599   *
1600   * @param string $taxonomy Taxonomy the meta key is currently registered for. Pass
1601   *                         an empty string if the meta key is registered across all
1602   *                         existing taxonomies.
1603   * @param string $meta_key The meta key to unregister.
1604   * @return bool True on success, false if the meta key was not previously registered.
1605   */
1606  function unregister_term_meta( $taxonomy, $meta_key ) {
1607      return unregister_meta_key( 'term', $meta_key, $taxonomy );
1608  }
1609  
1610  /**
1611   * Determines whether a taxonomy term exists.
1612   *
1613   * Formerly is_term(), introduced in 2.3.0.
1614   *
1615   * For more information on this and similar theme functions, check out
1616   * the {@link https://developer.wordpress.org/themes/basics/conditional-tags/
1617   * Conditional Tags} article in the Theme Developer Handbook.
1618   *
1619   * @since 3.0.0
1620   * @since 6.0.0 Converted to use `get_terms()`.
1621   *
1622   * @global bool $_wp_suspend_cache_invalidation
1623   *
1624   * @param int|string|null $term        The term to check. Accepts term ID, slug, or name.
1625   * @param string          $taxonomy    Optional. The taxonomy name to use.
1626   * @param int             $parent_term Optional. ID of parent term under which to confine the exists search.
1627   * @return mixed Returns null if the term does not exist.
1628   *               Returns the term ID if no taxonomy is specified and the term ID exists.
1629   *               Returns an array of the term ID and the term taxonomy ID if the taxonomy is specified and the pairing exists.
1630   *               Returns 0 if term ID 0 is passed to the function.
1631   *
1632   * @phpstan-return (
1633   *     $term is null ? null : (
1634   *         $term is 0 ? 0 : (
1635   *             $taxonomy is '' ? numeric-string|null : (
1636   *                 array{
1637   *                     term_id: numeric-string,
1638   *                     term_taxonomy_id: numeric-string,
1639   *                 }|null
1640   *             )
1641   *         )
1642   *     )
1643   * )
1644   */
1645  function term_exists( $term, $taxonomy = '', $parent_term = null ) {
1646      global $_wp_suspend_cache_invalidation;
1647  
1648      if ( null === $term ) {
1649          return null;
1650      }
1651  
1652      $defaults = array(
1653          'get'                    => 'all',
1654          'fields'                 => 'ids',
1655          'number'                 => 1,
1656          'update_term_meta_cache' => false,
1657          'order'                  => 'ASC',
1658          'orderby'                => 'term_id',
1659          'suppress_filter'        => true,
1660      );
1661  
1662      // Ensure that while importing, queries are not cached.
1663      if ( ! empty( $_wp_suspend_cache_invalidation ) ) {
1664          $defaults['cache_results'] = false;
1665      }
1666  
1667      if ( ! empty( $taxonomy ) ) {
1668          $defaults['taxonomy'] = $taxonomy;
1669          $defaults['fields']   = 'all';
1670      }
1671  
1672      /**
1673       * Filters default query arguments for checking if a term exists.
1674       *
1675       * @since 6.0.0
1676       *
1677       * @param array      $defaults    An array of arguments passed to get_terms().
1678       * @param int|string $term        The term to check. Accepts term ID, slug, or name.
1679       * @param string     $taxonomy    The taxonomy name to use. An empty string indicates
1680       *                                the search is against all taxonomies.
1681       * @param int|null   $parent_term ID of parent term under which to confine the exists search.
1682       *                                Null indicates the search is unconfined.
1683       */
1684      $defaults = apply_filters( 'term_exists_default_query_args', $defaults, $term, $taxonomy, $parent_term );
1685  
1686      if ( ! empty( $taxonomy ) && is_numeric( $parent_term ) ) {
1687          $defaults['parent'] = (int) $parent_term;
1688      }
1689  
1690      if ( is_int( $term ) ) {
1691          if ( 0 === $term ) {
1692              return 0;
1693          }
1694          $args  = wp_parse_args( array( 'include' => array( $term ) ), $defaults );
1695          $terms = get_terms( $args );
1696      } else {
1697          $term = trim( wp_unslash( $term ) );
1698          if ( '' === $term ) {
1699              return null;
1700          }
1701  
1702          $args  = wp_parse_args( array( 'slug' => sanitize_title( $term ) ), $defaults );
1703          $terms = get_terms( $args );
1704          if ( empty( $terms ) || is_wp_error( $terms ) ) {
1705              $args  = wp_parse_args( array( 'name' => $term ), $defaults );
1706              $terms = get_terms( $args );
1707          }
1708      }
1709  
1710      if ( empty( $terms ) || is_wp_error( $terms ) ) {
1711          return null;
1712      }
1713  
1714      $_term = array_shift( $terms );
1715  
1716      if ( ! empty( $taxonomy ) ) {
1717          return array(
1718              'term_id'          => (string) $_term->term_id,
1719              'term_taxonomy_id' => (string) $_term->term_taxonomy_id,
1720          );
1721      }
1722  
1723      return (string) $_term;
1724  }
1725  
1726  /**
1727   * Checks if a term is an ancestor of another term.
1728   *
1729   * You can use either an ID or the term object for both parameters.
1730   *
1731   * @since 3.4.0
1732   *
1733   * @param int|object $term1    ID or object to check if this is the parent term.
1734   * @param int|object $term2    The child term.
1735   * @param string     $taxonomy Taxonomy name that $term1 and `$term2` belong to.
1736   * @return bool Whether `$term2` is a child of `$term1`.
1737   */
1738  function term_is_ancestor_of( $term1, $term2, $taxonomy ) {
1739      if ( ! isset( $term1->term_id ) ) {
1740          $term1 = get_term( $term1, $taxonomy );
1741      }
1742      if ( ! isset( $term2->parent ) ) {
1743          $term2 = get_term( $term2, $taxonomy );
1744      }
1745  
1746      if ( empty( $term1->term_id ) || empty( $term2->parent ) ) {
1747          return false;
1748      }
1749      if ( $term2->parent === $term1->term_id ) {
1750          return true;
1751      }
1752  
1753      return term_is_ancestor_of( $term1, get_term( $term2->parent, $taxonomy ), $taxonomy );
1754  }
1755  
1756  /**
1757   * Sanitizes all term fields.
1758   *
1759   * Relies on sanitize_term_field() to sanitize the term. The difference is that
1760   * this function will sanitize **all** fields. The context is based
1761   * on sanitize_term_field().
1762   *
1763   * The `$term` is expected to be either an array or an object.
1764   *
1765   * @since 2.3.0
1766   *
1767   * @param array|object $term     The term to check.
1768   * @param string       $taxonomy The taxonomy name to use.
1769   * @param string       $context  Optional. Context in which to sanitize the term.
1770   *                               Accepts 'raw', 'edit', 'db', 'display', 'rss',
1771   *                               'attribute', or 'js'. Default 'display'.
1772   * @return array|object Term with all fields sanitized.
1773   */
1774  function sanitize_term( $term, $taxonomy, $context = 'display' ) {
1775      $fields = array( 'term_id', 'name', 'description', 'slug', 'count', 'parent', 'term_group', 'term_taxonomy_id', 'object_id' );
1776  
1777      $do_object = is_object( $term );
1778  
1779      $term_id = $do_object ? ( $term->term_id ?? 0 ) : ( $term['term_id'] ?? 0 );
1780  
1781      foreach ( (array) $fields as $field ) {
1782          if ( $do_object ) {
1783              if ( isset( $term->$field ) ) {
1784                  $term->$field = sanitize_term_field( $field, $term->$field, $term_id, $taxonomy, $context );
1785              }
1786          } else {
1787              if ( isset( $term[ $field ] ) ) {
1788                  $term[ $field ] = sanitize_term_field( $field, $term[ $field ], $term_id, $taxonomy, $context );
1789              }
1790          }
1791      }
1792  
1793      if ( $do_object ) {
1794          $term->filter = $context;
1795      } else {
1796          $term['filter'] = $context;
1797      }
1798  
1799      return $term;
1800  }
1801  
1802  /**
1803   * Sanitizes the field value in the term based on the context.
1804   *
1805   * Passing a term field value through the function should be assumed to have
1806   * cleansed the value for whatever context the term field is going to be used.
1807   *
1808   * If no context or an unsupported context is given, then default filters will
1809   * be applied.
1810   *
1811   * There are enough filters for each context to support a custom filtering
1812   * without creating your own filter function. Simply create a function that
1813   * hooks into the filter you need.
1814   *
1815   * @since 2.3.0
1816   *
1817   * @param string $field    Term field to sanitize.
1818   * @param string $value    Search for this term value.
1819   * @param int    $term_id  Term ID.
1820   * @param string $taxonomy Taxonomy name.
1821   * @param string $context  Context in which to sanitize the term field.
1822   *                         Accepts 'raw', 'edit', 'db', 'display', 'rss',
1823   *                         'attribute', or 'js'.
1824   * @return mixed Sanitized field.
1825   */
1826  function sanitize_term_field( $field, $value, $term_id, $taxonomy, $context ) {
1827      $int_fields = array( 'parent', 'term_id', 'count', 'term_group', 'term_taxonomy_id', 'object_id' );
1828      if ( in_array( $field, $int_fields, true ) ) {
1829          $value = (int) $value;
1830          if ( $value < 0 ) {
1831              $value = 0;
1832          }
1833      }
1834  
1835      $context = strtolower( $context );
1836  
1837      if ( 'raw' === $context ) {
1838          return $value;
1839      }
1840  
1841      if ( 'edit' === $context ) {
1842  
1843          /**
1844           * Filters a term field to edit before it is sanitized.
1845           *
1846           * The dynamic portion of the hook name, `$field`, refers to the term field.
1847           *
1848           * @since 2.3.0
1849           *
1850           * @param mixed  $value    Value of the term field.
1851           * @param int    $term_id  Term ID.
1852           * @param string $taxonomy Taxonomy slug.
1853           */
1854          $value = apply_filters( "edit_term_{$field}", $value, $term_id, $taxonomy );
1855  
1856          /**
1857           * Filters the taxonomy field to edit before it is sanitized.
1858           *
1859           * The dynamic portions of the filter name, `$taxonomy` and `$field`, refer
1860           * to the taxonomy slug and taxonomy field, respectively.
1861           *
1862           * @since 2.3.0
1863           *
1864           * @param mixed $value   Value of the taxonomy field to edit.
1865           * @param int   $term_id Term ID.
1866           */
1867          $value = apply_filters( "edit_{$taxonomy}_{$field}", $value, $term_id );
1868  
1869          if ( 'description' === $field ) {
1870              $value = esc_html( $value ); // textarea_escaped
1871          } else {
1872              $value = esc_attr( $value );
1873          }
1874      } elseif ( 'db' === $context ) {
1875  
1876          /**
1877           * Filters a term field value before it is sanitized.
1878           *
1879           * The dynamic portion of the hook name, `$field`, refers to the term field.
1880           *
1881           * @since 2.3.0
1882           *
1883           * @param mixed  $value    Value of the term field.
1884           * @param string $taxonomy Taxonomy slug.
1885           */
1886          $value = apply_filters( "pre_term_{$field}", $value, $taxonomy );
1887  
1888          /**
1889           * Filters a taxonomy field before it is sanitized.
1890           *
1891           * The dynamic portions of the filter name, `$taxonomy` and `$field`, refer
1892           * to the taxonomy slug and field name, respectively.
1893           *
1894           * @since 2.3.0
1895           *
1896           * @param mixed $value Value of the taxonomy field.
1897           */
1898          $value = apply_filters( "pre_{$taxonomy}_{$field}", $value );
1899  
1900          // Back compat filters.
1901          if ( 'slug' === $field ) {
1902              /**
1903               * Filters the category nicename before it is sanitized.
1904               *
1905               * Use the {@see 'pre_$taxonomy_$field'} hook instead.
1906               *
1907               * @since 2.0.3
1908               *
1909               * @param string $value The category nicename.
1910               */
1911              $value = apply_filters( 'pre_category_nicename', $value );
1912          }
1913      } elseif ( 'rss' === $context ) {
1914  
1915          /**
1916           * Filters the term field for use in RSS.
1917           *
1918           * The dynamic portion of the hook name, `$field`, refers to the term field.
1919           *
1920           * @since 2.3.0
1921           *
1922           * @param mixed  $value    Value of the term field.
1923           * @param string $taxonomy Taxonomy slug.
1924           */
1925          $value = apply_filters( "term_{$field}_rss", $value, $taxonomy );
1926  
1927          /**
1928           * Filters the taxonomy field for use in RSS.
1929           *
1930           * The dynamic portions of the hook name, `$taxonomy`, and `$field`, refer
1931           * to the taxonomy slug and field name, respectively.
1932           *
1933           * @since 2.3.0
1934           *
1935           * @param mixed $value Value of the taxonomy field.
1936           */
1937          $value = apply_filters( "{$taxonomy}_{$field}_rss", $value );
1938      } else {
1939          // Use display filters by default.
1940  
1941          /**
1942           * Filters the term field sanitized for display.
1943           *
1944           * The dynamic portion of the hook name, `$field`, refers to the term field name.
1945           *
1946           * @since 2.3.0
1947           *
1948           * @param mixed  $value    Value of the term field.
1949           * @param int    $term_id  Term ID.
1950           * @param string $taxonomy Taxonomy slug.
1951           * @param string $context  Context to retrieve the term field value.
1952           */
1953          $value = apply_filters( "term_{$field}", $value, $term_id, $taxonomy, $context );
1954  
1955          /**
1956           * Filters the taxonomy field sanitized for display.
1957           *
1958           * The dynamic portions of the filter name, `$taxonomy`, and `$field`, refer
1959           * to the taxonomy slug and taxonomy field, respectively.
1960           *
1961           * @since 2.3.0
1962           *
1963           * @param mixed  $value   Value of the taxonomy field.
1964           * @param int    $term_id Term ID.
1965           * @param string $context Context to retrieve the taxonomy field value.
1966           */
1967          $value = apply_filters( "{$taxonomy}_{$field}", $value, $term_id, $context );
1968      }
1969  
1970      if ( 'attribute' === $context ) {
1971          $value = esc_attr( $value );
1972      } elseif ( 'js' === $context ) {
1973          $value = esc_js( $value );
1974      }
1975  
1976      // Restore the type for integer fields after esc_attr().
1977      if ( in_array( $field, $int_fields, true ) ) {
1978          $value = (int) $value;
1979      }
1980  
1981      return $value;
1982  }
1983  
1984  /**
1985   * Counts how many terms are in taxonomy.
1986   *
1987   * Default $args is 'hide_empty' which can be 'hide_empty=true' or array('hide_empty' => true).
1988   *
1989   * {@internal The `$deprecated` parameter is parsed for backward compatibility only.}
1990   *
1991   * @since 2.3.0
1992   * @since 5.6.0 Changed the function signature so that the `$args` array can be provided as the first parameter.
1993   *
1994   * @param array|string $args       Optional. Array or string of arguments. See {@see WP_Term_Query::__construct()}
1995   *                                 for information on accepted arguments. Default empty array.
1996   * @param array|string $deprecated Optional. Argument array, when using the legacy function parameter format.
1997   *                                 If present, this parameter will be interpreted as `$args`, and the first
1998   *                                 function parameter will be parsed as a taxonomy or array of taxonomies.
1999   *                                 Default empty.
2000   * @return string|int|WP_Error Numeric string containing the number of terms in that taxonomy,
2001   *                             the integer 0 when the queried parent term is not in the taxonomy
2002   *                             hierarchy, or WP_Error if the taxonomy does not exist.
2003   * @phpstan-return numeric-string|0|WP_Error
2004   */
2005  function wp_count_terms( $args = array(), $deprecated = '' ) {
2006      $use_legacy_args = false;
2007  
2008      // Check whether function is used with legacy signature: `$taxonomy` and `$args`.
2009      if ( $args
2010          && ( is_string( $args ) && taxonomy_exists( $args )
2011              || is_array( $args ) && wp_is_numeric_array( $args ) )
2012      ) {
2013          $use_legacy_args = true;
2014      }
2015  
2016      $defaults = array( 'hide_empty' => false );
2017  
2018      if ( $use_legacy_args ) {
2019          $defaults['taxonomy'] = $args;
2020          $args                 = $deprecated;
2021      }
2022  
2023      $args = wp_parse_args( $args, $defaults );
2024  
2025      // Backward compatibility.
2026      if ( isset( $args['ignore_empty'] ) ) {
2027          $args['hide_empty'] = $args['ignore_empty'];
2028          unset( $args['ignore_empty'] );
2029      }
2030  
2031      $args = array_merge(
2032          $args,
2033          array( 'fields' => 'count' )
2034      );
2035  
2036      return get_terms( $args );
2037  }
2038  
2039  /**
2040   * Unlinks the object from the taxonomy or taxonomies.
2041   *
2042   * Will remove all relationships between the object and any terms in
2043   * a particular taxonomy or taxonomies. Does not remove the term or
2044   * taxonomy itself.
2045   *
2046   * @since 2.3.0
2047   *
2048   * @param int          $object_id  The term object ID that refers to the term.
2049   * @param string|array $taxonomies List of taxonomy names or single taxonomy name.
2050   */
2051  function wp_delete_object_term_relationships( $object_id, $taxonomies ) {
2052      $object_id = (int) $object_id;
2053  
2054      if ( ! is_array( $taxonomies ) ) {
2055          $taxonomies = array( $taxonomies );
2056      }
2057  
2058      foreach ( (array) $taxonomies as $taxonomy ) {
2059          $term_ids = wp_get_object_terms( $object_id, $taxonomy, array( 'fields' => 'ids' ) );
2060          if ( ! is_array( $term_ids ) ) {
2061              // Skip return value in the case of an error or the 'wp_get_object_terms' filter returning an invalid value.
2062              continue;
2063          }
2064          $term_ids = array_map( 'intval', $term_ids );
2065          wp_remove_object_terms( $object_id, $term_ids, $taxonomy );
2066      }
2067  }
2068  
2069  /**
2070   * Removes a term from the database.
2071   *
2072   * If the term is a parent of other terms, then the children will be updated to
2073   * that term's parent.
2074   *
2075   * Metadata associated with the term will be deleted.
2076   *
2077   * @since 2.3.0
2078   *
2079   * @global wpdb $wpdb WordPress database abstraction object.
2080   *
2081   * @param int          $term     Term ID.
2082   * @param string       $taxonomy Taxonomy name.
2083   * @param array|string $args {
2084   *     Optional. Array of arguments to override the default term ID. Default empty array.
2085   *
2086   *     @type int  $default       The term ID to make the default term. This will only override
2087   *                               the terms found if there is only one term found. Any other and
2088   *                               the found terms are used.
2089   *     @type bool $force_default Optional. Whether to force the supplied term as default to be
2090   *                               assigned even if the object was not going to be term-less.
2091   *                               Default false.
2092   * }
2093   * @return bool|int|WP_Error True on success, false if term does not exist. Zero on attempted
2094   *                           deletion of default Category. WP_Error if the taxonomy does not exist.
2095   * @phpstan-param non-empty-string $taxonomy
2096   * @phpstan-param string|array{
2097   *     default?: positive-int,
2098   *     force_default?: bool,
2099   * } $args
2100   * @phpstan-return bool|WP_Error|0
2101   */
2102  function wp_delete_term( $term, $taxonomy, $args = array() ) {
2103      global $wpdb;
2104  
2105      $term = (int) $term;
2106  
2107      $ids = term_exists( $term, $taxonomy );
2108      if ( ! $ids ) {
2109          return false;
2110      }
2111      if ( is_wp_error( $ids ) ) {
2112          return $ids;
2113      }
2114  
2115      $tt_id = $ids['term_taxonomy_id'];
2116  
2117      $defaults = array();
2118  
2119      if ( 'category' === $taxonomy ) {
2120          $defaults['default'] = (int) get_option( 'default_category' );
2121          if ( $defaults['default'] === $term ) {
2122              return 0; // Don't delete the default category.
2123          }
2124      }
2125  
2126      // Don't delete the default custom taxonomy term.
2127      $taxonomy_object = get_taxonomy( $taxonomy );
2128      if ( ! empty( $taxonomy_object->default_term ) ) {
2129          $defaults['default'] = (int) get_option( 'default_term_' . $taxonomy );
2130          if ( $defaults['default'] === $term ) {
2131              return 0;
2132          }
2133      }
2134  
2135      $args = wp_parse_args( $args, $defaults );
2136  
2137      if ( isset( $args['default'] ) ) {
2138          $default = (int) $args['default'];
2139          if ( ! term_exists( $default, $taxonomy ) ) {
2140              unset( $default );
2141          }
2142      }
2143  
2144      if ( isset( $args['force_default'] ) ) {
2145          $force_default = $args['force_default'];
2146      }
2147  
2148      /**
2149       * Fires when deleting a term, before any modifications are made to posts or terms.
2150       *
2151       * @since 4.1.0
2152       *
2153       * @param int    $term     Term ID.
2154       * @param string $taxonomy Taxonomy name.
2155       */
2156      do_action( 'pre_delete_term', $term, $taxonomy );
2157  
2158      // Update children to point to new parent.
2159      if ( is_taxonomy_hierarchical( $taxonomy ) ) {
2160          $term_obj = get_term( $term, $taxonomy );
2161          if ( is_wp_error( $term_obj ) ) {
2162              return $term_obj;
2163          }
2164          $parent = $term_obj->parent;
2165  
2166          $edit_ids    = $wpdb->get_results( "SELECT term_id, term_taxonomy_id FROM $wpdb->term_taxonomy WHERE `parent` = " . (int) $term_obj->term_id );
2167          $edit_tt_ids = wp_list_pluck( $edit_ids, 'term_taxonomy_id' );
2168  
2169          /**
2170           * Fires immediately before a term to delete's children are reassigned a parent.
2171           *
2172           * @since 2.9.0
2173           *
2174           * @param array $edit_tt_ids An array of term taxonomy IDs for the given term.
2175           */
2176          do_action( 'edit_term_taxonomies', $edit_tt_ids );
2177  
2178          $wpdb->update( $wpdb->term_taxonomy, compact( 'parent' ), array( 'parent' => $term_obj->term_id ) + compact( 'taxonomy' ) );
2179  
2180          // Clean the cache for all child terms.
2181          $edit_term_ids = wp_list_pluck( $edit_ids, 'term_id' );
2182          clean_term_cache( $edit_term_ids, $taxonomy );
2183  
2184          /**
2185           * Fires immediately after a term to delete's children are reassigned a parent.
2186           *
2187           * @since 2.9.0
2188           *
2189           * @param array $edit_tt_ids An array of term taxonomy IDs for the given term.
2190           */
2191          do_action( 'edited_term_taxonomies', $edit_tt_ids );
2192      }
2193  
2194      // Get the term before deleting it or its term relationships so we can pass to actions below.
2195      $deleted_term = get_term( $term, $taxonomy );
2196  
2197      $object_ids = (array) $wpdb->get_col( $wpdb->prepare( "SELECT object_id FROM $wpdb->term_relationships WHERE term_taxonomy_id = %d", $tt_id ) );
2198  
2199      foreach ( $object_ids as $object_id ) {
2200          if ( ! isset( $default ) ) {
2201              wp_remove_object_terms( $object_id, $term, $taxonomy );
2202              continue;
2203          }
2204  
2205          $terms = wp_get_object_terms(
2206              $object_id,
2207              $taxonomy,
2208              array(
2209                  'fields'  => 'ids',
2210                  'orderby' => 'none',
2211              )
2212          );
2213  
2214          if ( 1 === count( $terms ) ) {
2215              $terms = array( $default );
2216          } else {
2217              $terms = array_diff( $terms, array( $term ) );
2218              if ( isset( $force_default ) && $force_default ) {
2219                  $terms = array_merge( $terms, array( $default ) );
2220              }
2221          }
2222  
2223          $terms = array_map( 'intval', $terms );
2224          wp_set_object_terms( $object_id, $terms, $taxonomy );
2225      }
2226  
2227      // Clean the relationship caches for all object types using this term.
2228      $tax_object = get_taxonomy( $taxonomy );
2229      foreach ( $tax_object->object_type as $object_type ) {
2230          clean_object_term_cache( $object_ids, $object_type );
2231      }
2232  
2233      $term_meta_ids = $wpdb->get_col( $wpdb->prepare( "SELECT meta_id FROM $wpdb->termmeta WHERE term_id = %d ", $term ) );
2234      foreach ( $term_meta_ids as $mid ) {
2235          delete_metadata_by_mid( 'term', $mid );
2236      }
2237  
2238      /**
2239       * Fires immediately before a term taxonomy ID is deleted.
2240       *
2241       * @since 2.9.0
2242       *
2243       * @param int $tt_id Term taxonomy ID.
2244       */
2245      do_action( 'delete_term_taxonomy', $tt_id );
2246  
2247      $wpdb->delete( $wpdb->term_taxonomy, array( 'term_taxonomy_id' => $tt_id ) );
2248  
2249      /**
2250       * Fires immediately after a term taxonomy ID is deleted.
2251       *
2252       * @since 2.9.0
2253       *
2254       * @param int $tt_id Term taxonomy ID.
2255       */
2256      do_action( 'deleted_term_taxonomy', $tt_id );
2257  
2258      // Delete the term if no taxonomies use it.
2259      if ( ! $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM $wpdb->term_taxonomy WHERE term_id = %d", $term ) ) ) {
2260          $wpdb->delete( $wpdb->terms, array( 'term_id' => $term ) );
2261      }
2262  
2263      clean_term_cache( $term, $taxonomy );
2264  
2265      /**
2266       * Fires after a term is deleted from the database and the cache is cleaned.
2267       *
2268       * The {@see 'delete_$taxonomy'} hook is also available for targeting a specific
2269       * taxonomy.
2270       *
2271       * @since 2.5.0
2272       * @since 4.5.0 Introduced the `$object_ids` argument.
2273       *
2274       * @param int     $term         Term ID.
2275       * @param int     $tt_id        Term taxonomy ID.
2276       * @param string  $taxonomy     Taxonomy slug.
2277       * @param WP_Term $deleted_term Copy of the already-deleted term.
2278       * @param array   $object_ids   List of term object IDs.
2279       */
2280      do_action( 'delete_term', $term, $tt_id, $taxonomy, $deleted_term, $object_ids );
2281  
2282      /**
2283       * Fires after a term in a specific taxonomy is deleted.
2284       *
2285       * The dynamic portion of the hook name, `$taxonomy`, refers to the specific
2286       * taxonomy the term belonged to.
2287       *
2288       * Possible hook names include:
2289       *
2290       *  - `delete_category`
2291       *  - `delete_post_tag`
2292       *
2293       * @since 2.3.0
2294       * @since 4.5.0 Introduced the `$object_ids` argument.
2295       *
2296       * @param int     $term         Term ID.
2297       * @param int     $tt_id        Term taxonomy ID.
2298       * @param WP_Term $deleted_term Copy of the already-deleted term.
2299       * @param array   $object_ids   List of term object IDs.
2300       */
2301      do_action( "delete_{$taxonomy}", $term, $tt_id, $deleted_term, $object_ids );
2302  
2303      return true;
2304  }
2305  
2306  /**
2307   * Deletes one existing category.
2308   *
2309   * @since 2.0.0
2310   *
2311   * @param int $cat_id Category term ID.
2312   * @return bool|int|WP_Error Returns true if completes delete action; false if term doesn't exist;
2313   *                           Zero on attempted deletion of default Category; WP_Error object is
2314   *                           also a possibility.
2315   */
2316  function wp_delete_category( $cat_id ) {
2317      return wp_delete_term( $cat_id, 'category' );
2318  }
2319  
2320  /**
2321   * Retrieves the terms associated with the given object(s), in the supplied taxonomies.
2322   *
2323   * @since 2.3.0
2324   * @since 4.2.0 Added support for 'taxonomy', 'parent', and 'term_taxonomy_id' values of `$orderby`.
2325   *              Introduced `$parent` argument.
2326   * @since 4.4.0 Introduced `$meta_query` and `$update_term_meta_cache` arguments. When `$fields` is 'all' or
2327   *              'all_with_object_id', an array of `WP_Term` objects will be returned.
2328   * @since 4.7.0 Refactored to use WP_Term_Query, and to support any WP_Term_Query arguments.
2329   * @since 6.3.0 Passing `update_term_meta_cache` argument value false by default resulting in get_terms() to not
2330   *              prime the term meta cache.
2331   * @since 7.2.0 A count is returned as a numeric string when `$fields` is 'count'. Previously requesting
2332   *              a count resulted in an error.
2333   *
2334   * @param int|int[]       $object_ids The ID(s) of the object(s) to retrieve.
2335   * @param string|string[] $taxonomies The taxonomy names to retrieve terms from.
2336   * @param array|string    $args       See {@see WP_Term_Query::__construct()} for supported arguments.
2337   * @return WP_Term[]|int[]|string[]|string|WP_Error Array of terms, a count thereof as a numeric string,
2338   *                                                  or WP_Error if any of the taxonomies do not exist.
2339   *                                                  See {@see WP_Term_Query::get_terms()} for more information.
2340   *
2341   * @phpstan-return (
2342   *     $args is array{ fields: 'count', ... }
2343   *         ? numeric-string|WP_Error
2344   *         : ( $args is array{ fields: 'ids'|'tt_ids', ... }
2345   *             ? int[]|WP_Error
2346   *             : ( $args is array{ fields: 'id=>parent', ... }
2347   *                 ? array<int, int>|WP_Error
2348   *                 : ( $args is array{ fields: 'names'|'slugs', ... }
2349   *                     ? string[]|WP_Error
2350   *                     : ( $args is array{ fields: 'id=>name'|'id=>slug', ... }
2351   *                         ? array<int, string>|WP_Error
2352   *                         : ( $args is array
2353   *                             ? WP_Term[]|WP_Error
2354   *                             : WP_Term[]|int[]|string[]|string|WP_Error ) ) ) ) )
2355   * )
2356   */
2357  function wp_get_object_terms( $object_ids, $taxonomies, $args = array() ) {
2358      $args = wp_parse_args( $args );
2359  
2360      if ( empty( $object_ids ) || empty( $taxonomies ) ) {
2361          if ( isset( $args['fields'] ) && 'count' === $args['fields'] ) {
2362              return '0';
2363          }
2364  
2365          return array();
2366      }
2367  
2368      if ( ! is_array( $taxonomies ) ) {
2369          $taxonomies = array( $taxonomies );
2370      }
2371  
2372      foreach ( $taxonomies as $taxonomy ) {
2373          if ( ! taxonomy_exists( $taxonomy ) ) {
2374              return new WP_Error( 'invalid_taxonomy', __( 'Invalid taxonomy.' ) );
2375          }
2376      }
2377  
2378      if ( ! is_array( $object_ids ) ) {
2379          $object_ids = array( $object_ids );
2380      }
2381      $object_ids = array_map( 'intval', $object_ids );
2382  
2383      $defaults = array(
2384          'update_term_meta_cache' => false,
2385      );
2386  
2387      $args = wp_parse_args( $args, $defaults );
2388  
2389      /**
2390       * Filters arguments for retrieving object terms.
2391       *
2392       * @since 4.9.0
2393       *
2394       * @param array    $args       An array of arguments for retrieving terms for the given object(s).
2395       *                             See {@see wp_get_object_terms()} for details.
2396       * @param int[]    $object_ids Array of object IDs.
2397       * @param string[] $taxonomies Array of taxonomy names to retrieve terms from.
2398       */
2399      $args = apply_filters( 'wp_get_object_terms_args', $args, $object_ids, $taxonomies );
2400  
2401      /*
2402       * When one or more queried taxonomies is registered with an 'args' array,
2403       * those params override the `$args` passed to this function.
2404       *
2405       * That array is for query modifiers such as 'orderby'. Overriding 'fields'
2406       * there does not work, because callers depend on the shape they asked for:
2407       * get_the_taxonomies() reads `$term->name` off the result, and
2408       * wp_set_object_terms() needs the 'tt_ids' it requested. A taxonomy that
2409       * overrides it also returns a different shape than the taxonomies queried
2410       * below, leaving the two merged into one another here.
2411       */
2412      $terms = array();
2413      if ( count( $taxonomies ) > 1 ) {
2414          foreach ( $taxonomies as $index => $taxonomy ) {
2415              $t = get_taxonomy( $taxonomy );
2416              if ( isset( $t->args ) && is_array( $t->args ) && array_merge( $args, $t->args ) != $args ) {
2417                  unset( $taxonomies[ $index ] );
2418  
2419                  // Cast because a count is returned as a numeric string. The counts are summed below.
2420                  $terms_from_taxonomy = (array) wp_get_object_terms( $object_ids, $taxonomy, array_merge( $args, $t->args ) );
2421  
2422                  // Array keys should be preserved for values of $fields that use term_id for keys.
2423                  if ( ! empty( $args['fields'] ) && str_starts_with( $args['fields'], 'id=>' ) ) {
2424                      $terms = $terms + $terms_from_taxonomy;
2425                  } else {
2426                      $terms = array_merge( $terms, $terms_from_taxonomy );
2427                  }
2428              }
2429          }
2430      } else {
2431          $t = get_taxonomy( $taxonomies[0] );
2432          if ( isset( $t->args ) && is_array( $t->args ) ) {
2433              $args = array_merge( $args, $t->args );
2434          }
2435      }
2436  
2437      $args['taxonomy']   = $taxonomies;
2438      $args['object_ids'] = $object_ids;
2439  
2440      // Taxonomies registered without an 'args' param are handled here.
2441      if ( ! empty( $taxonomies ) ) {
2442          // Cast because a count is returned as a numeric string. The counts are summed below.
2443          $terms_from_remaining_taxonomies = (array) get_terms( $args );
2444  
2445          // Array keys should be preserved for values of $fields that use term_id for keys.
2446          if ( ! empty( $args['fields'] ) && str_starts_with( $args['fields'], 'id=>' ) ) {
2447              $terms = $terms + $terms_from_remaining_taxonomies;
2448          } else {
2449              $terms = array_merge( $terms, $terms_from_remaining_taxonomies );
2450          }
2451      }
2452  
2453      if ( isset( $args['fields'] ) && 'count' === $args['fields'] ) {
2454          $terms = (string) array_sum( $terms );
2455      }
2456  
2457      /**
2458       * Filters the terms for a given object or objects.
2459       *
2460       * @since 4.2.0
2461       *
2462       * @param WP_Term[]|int[]|string[]|string $terms      Array of terms or a count thereof as a numeric string.
2463       * @param int[]                           $object_ids Array of object IDs for which terms were retrieved.
2464       * @param string[]                        $taxonomies Array of taxonomy names from which terms were retrieved.
2465       * @param array                           $args       Array of arguments for retrieving terms for the given
2466       *                                                    object(s). See {@see wp_get_object_terms()} for details.
2467       */
2468      $terms = apply_filters( 'get_object_terms', $terms, $object_ids, $taxonomies, $args );
2469  
2470      $object_ids = implode( ',', $object_ids );
2471      $taxonomies = "'" . implode( "', '", array_map( 'esc_sql', $taxonomies ) ) . "'";
2472  
2473      /**
2474       * Filters the terms for a given object or objects.
2475       *
2476       * The `$taxonomies` parameter passed to this filter is formatted as a SQL fragment. The
2477       * {@see 'get_object_terms'} filter is recommended as an alternative.
2478       *
2479       * @since 2.8.0
2480       *
2481       * @param WP_Term[]|int[]|string[]|string $terms      Array of terms or a count thereof as a numeric string.
2482       * @param string                          $object_ids Comma separated list of object IDs for which terms were retrieved.
2483       * @param string                          $taxonomies SQL fragment of taxonomy names from which terms were retrieved.
2484       * @param array                           $args       Array of arguments for retrieving terms for the given
2485       *                                                    object(s). See {@see wp_get_object_terms()} for details.
2486       */
2487      return apply_filters( 'wp_get_object_terms', $terms, $object_ids, $taxonomies, $args );
2488  }
2489  
2490  /**
2491   * Adds a new term to the database.
2492   *
2493   * A non-existent term is inserted in the following sequence:
2494   * 1. The term is added to the term table, then related to the taxonomy.
2495   * 2. If everything is correct, several actions are fired.
2496   * 3. The 'term_id_filter' is evaluated.
2497   * 4. The term cache is cleaned.
2498   * 5. Several more actions are fired.
2499   * 6. An array is returned containing the `term_id` and `term_taxonomy_id`.
2500   *
2501   * If the 'slug' argument is not empty, then it is checked to see if the term
2502   * is invalid. If it is not a valid, existing term, it is added and the term_id
2503   * is given.
2504   *
2505   * If the taxonomy is hierarchical, and the 'parent' argument is not empty,
2506   * the term is inserted and the term_id will be given.
2507   *
2508   * Error handling:
2509   * If `$taxonomy` does not exist or `$term` is empty,
2510   * a WP_Error object will be returned.
2511   *
2512   * If the term already exists on the same hierarchical level,
2513   * or the term slug and name are not unique, a WP_Error object will be returned.
2514   *
2515   * @global wpdb $wpdb WordPress database abstraction object.
2516   *
2517   * @since 2.3.0
2518   *
2519   * @param string       $term     The term name to add.
2520   * @param string       $taxonomy The taxonomy to which to add the term.
2521   * @param array|string $args {
2522   *     Optional. Array or query string of arguments for inserting a term.
2523   *
2524   *     @type string $alias_of    Slug of the term to make this term an alias of.
2525   *                               Default empty string. Accepts a term slug.
2526   *     @type string $description The term description. Default empty string.
2527   *     @type int    $parent      The id of the parent term. Default 0.
2528   *     @type string $slug        The term slug to use. Default empty string.
2529   * }
2530   * @return array|WP_Error {
2531   *     An array of the new term data, WP_Error otherwise.
2532   *
2533   *     @type int        $term_id          The new term ID.
2534   *     @type int|string $term_taxonomy_id The new term taxonomy ID. Can be a numeric string.
2535   * }
2536   * @phpstan-param string|array{
2537   *     alias_of?: string,
2538   *     description?: string|null,
2539   *     parent?: non-negative-int,
2540   *     slug?: string|null,
2541   *     ...
2542   * } $args
2543   * @phpstan-return array{
2544   *     term_id: int,
2545   *     term_taxonomy_id: int|numeric-string,
2546   * }|WP_Error
2547   */
2548  function wp_insert_term( $term, $taxonomy, $args = array() ) {
2549      global $wpdb;
2550  
2551      if ( ! taxonomy_exists( $taxonomy ) ) {
2552          return new WP_Error( 'invalid_taxonomy', __( 'Invalid taxonomy.' ) );
2553      }
2554  
2555      /**
2556       * Filters a term before it is sanitized and inserted into the database.
2557       *
2558       * @since 3.0.0
2559       * @since 6.1.0 The `$args` parameter was added.
2560       *
2561       * @param string|WP_Error $term     The term name to add, or a WP_Error object if there's an error.
2562       * @param string          $taxonomy Taxonomy slug.
2563       * @param array|string    $args     Array or query string of arguments passed to wp_insert_term().
2564       */
2565      $term = apply_filters( 'pre_insert_term', $term, $taxonomy, $args );
2566  
2567      if ( is_wp_error( $term ) ) {
2568          return $term;
2569      }
2570  
2571      if ( is_int( $term ) && 0 === $term ) {
2572          return new WP_Error( 'invalid_term_id', __( 'Invalid term ID.' ) );
2573      }
2574  
2575      if ( '' === trim( $term ) ) {
2576          return new WP_Error( 'empty_term_name', __( 'A name is required for this term.' ) );
2577      }
2578  
2579      $defaults = array(
2580          'alias_of'    => '',
2581          'description' => '',
2582          'parent'      => 0,
2583          'slug'        => '',
2584      );
2585      $args     = wp_parse_args( $args, $defaults );
2586  
2587      if ( (int) $args['parent'] > 0 && ! term_exists( (int) $args['parent'] ) ) {
2588          return new WP_Error( 'missing_parent', __( 'Parent term does not exist.' ) );
2589      }
2590  
2591      $args['name']     = $term;
2592      $args['taxonomy'] = $taxonomy;
2593  
2594      // Coerce null description to strings, to avoid database errors.
2595      $args['description'] = (string) $args['description'];
2596  
2597      $args = sanitize_term( $args, $taxonomy, 'db' );
2598  
2599      // expected_slashed ($name)
2600      $name        = wp_unslash( $args['name'] );
2601      $description = wp_unslash( $args['description'] );
2602      $parent      = (int) $args['parent'];
2603  
2604      // Sanitization could clean the name to an empty string that must be checked again.
2605      if ( '' === $name ) {
2606          return new WP_Error( 'invalid_term_name', __( 'Invalid term name.' ) );
2607      }
2608  
2609      $slug_provided = ! empty( $args['slug'] );
2610      if ( ! $slug_provided ) {
2611          $slug = sanitize_title( $name );
2612      } else {
2613          $slug = $args['slug'];
2614      }
2615  
2616      $term_group = 0;
2617      if ( $args['alias_of'] ) {
2618          $alias = get_term_by( 'slug', $args['alias_of'], $taxonomy );
2619          if ( ! empty( $alias->term_group ) ) {
2620              // The alias we want is already in a group, so let's use that one.
2621              $term_group = $alias->term_group;
2622          } elseif ( ! empty( $alias->term_id ) ) {
2623              /*
2624               * The alias is not in a group, so we create a new one
2625               * and add the alias to it.
2626               */
2627              $term_group = $wpdb->get_var( "SELECT MAX(term_group) FROM $wpdb->terms" ) + 1;
2628  
2629              wp_update_term(
2630                  $alias->term_id,
2631                  $taxonomy,
2632                  array(
2633                      'term_group' => $term_group,
2634                  )
2635              );
2636          }
2637      }
2638  
2639      /*
2640       * Prevent the creation of terms with duplicate names at the same level of a taxonomy hierarchy,
2641       * unless a unique slug has been explicitly provided.
2642       */
2643      $name_matches = get_terms(
2644          array(
2645              'taxonomy'               => $taxonomy,
2646              'name'                   => $name,
2647              'hide_empty'             => false,
2648              'parent'                 => $args['parent'],
2649              'update_term_meta_cache' => false,
2650          )
2651      );
2652  
2653      /*
2654       * The `name` match in `get_terms()` doesn't differentiate accented characters,
2655       * so we do a stricter comparison here.
2656       */
2657      $name_match = null;
2658      if ( $name_matches ) {
2659          foreach ( $name_matches as $_match ) {
2660              if ( strtolower( $name ) === strtolower( $_match->name ) ) {
2661                  $name_match = $_match;
2662                  break;
2663              }
2664          }
2665      }
2666  
2667      if ( $name_match ) {
2668          $slug_match = get_term_by( 'slug', $slug, $taxonomy );
2669          if ( ! $slug_provided || $name_match->slug === $slug || $slug_match ) {
2670              if ( is_taxonomy_hierarchical( $taxonomy ) ) {
2671                  $siblings = get_terms(
2672                      array(
2673                          'taxonomy'               => $taxonomy,
2674                          'get'                    => 'all',
2675                          'parent'                 => $parent,
2676                          'update_term_meta_cache' => false,
2677                      )
2678                  );
2679  
2680                  $existing_term = null;
2681                  $sibling_names = wp_list_pluck( $siblings, 'name' );
2682                  $sibling_slugs = wp_list_pluck( $siblings, 'slug' );
2683  
2684                  if ( ( ! $slug_provided || $name_match->slug === $slug ) && in_array( $name, $sibling_names, true ) ) {
2685                      $existing_term = $name_match;
2686                  } elseif ( $slug_match && in_array( $slug, $sibling_slugs, true ) ) {
2687                      $existing_term = $slug_match;
2688                  }
2689  
2690                  if ( $existing_term ) {
2691                      return new WP_Error( 'term_exists', __( 'A term with the name provided already exists with this parent.' ), $existing_term->term_id );
2692                  }
2693              } else {
2694                  return new WP_Error( 'term_exists', __( 'A term with the name provided already exists in this taxonomy.' ), $name_match->term_id );
2695              }
2696          }
2697      }
2698  
2699      $slug = wp_unique_term_slug( $slug, (object) $args );
2700  
2701      $data = compact( 'name', 'slug', 'term_group' );
2702  
2703      /**
2704       * Filters term data before it is inserted into the database.
2705       *
2706       * @since 4.7.0
2707       *
2708       * @param array  $data     Term data to be inserted.
2709       * @param string $taxonomy Taxonomy slug.
2710       * @param array  $args     Arguments passed to wp_insert_term().
2711       */
2712      $data = apply_filters( 'wp_insert_term_data', $data, $taxonomy, $args );
2713  
2714      if ( false === $wpdb->insert( $wpdb->terms, $data ) ) {
2715          return new WP_Error( 'db_insert_error', __( 'Could not insert term into the database.' ), $wpdb->last_error );
2716      }
2717  
2718      $term_id = (int) $wpdb->insert_id;
2719  
2720      // Seems unreachable. However, is used in the case that a term name is provided, which sanitizes to an empty string.
2721      if ( empty( $slug ) ) {
2722          $slug = sanitize_title( $slug, $term_id );
2723  
2724          /** This action is documented in wp-includes/taxonomy.php */
2725          do_action( 'edit_terms', $term_id, $taxonomy, $args );
2726          $wpdb->update( $wpdb->terms, compact( 'slug' ), compact( 'term_id' ) );
2727  
2728          /** This action is documented in wp-includes/taxonomy.php */
2729          do_action( 'edited_terms', $term_id, $taxonomy, $args );
2730      }
2731  
2732      /** @var numeric-string|null $tt_id */
2733      $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 ) );
2734  
2735      if ( ! empty( $tt_id ) ) {
2736          return array(
2737              'term_id'          => $term_id,
2738              'term_taxonomy_id' => $tt_id,
2739          );
2740      }
2741  
2742      if ( false === $wpdb->insert( $wpdb->term_taxonomy, compact( 'term_id', 'taxonomy', 'description', 'parent' ) + array( 'count' => 0 ) ) ) {
2743          return new WP_Error( 'db_insert_error', __( 'Could not insert term taxonomy into the database.' ), $wpdb->last_error );
2744      }
2745  
2746      $tt_id = (int) $wpdb->insert_id;
2747  
2748      /*
2749       * Confidence check: if we just created a term with the same parent + taxonomy + slug but a higher term_id than
2750       * an existing term, then we have unwittingly created a duplicate term. Delete the dupe, and use the term_id
2751       * and term_taxonomy_id of the older term instead. Then return out of the function so that the "create" hooks
2752       * are not fired.
2753       */
2754      $duplicate_term = $wpdb->get_row( $wpdb->prepare( "SELECT t.term_id, t.slug, tt.term_taxonomy_id, tt.taxonomy FROM $wpdb->terms AS t INNER JOIN $wpdb->term_taxonomy AS 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 ) );
2755  
2756      /**
2757       * Filters the duplicate term check that takes place during term creation.
2758       *
2759       * Term parent + taxonomy + slug combinations are meant to be unique, and wp_insert_term()
2760       * performs a last-minute confirmation of this uniqueness before allowing a new term
2761       * to be created. Plugins with different uniqueness requirements may use this filter
2762       * to bypass or modify the duplicate-term check.
2763       *
2764       * @since 5.1.0
2765       *
2766       * @param object $duplicate_term Duplicate term row from terms table, if found.
2767       * @param string $term           Term being inserted.
2768       * @param string $taxonomy       Taxonomy name.
2769       * @param array  $args           Arguments passed to wp_insert_term().
2770       * @param int    $tt_id          term_taxonomy_id for the newly created term.
2771       */
2772      $duplicate_term = apply_filters( 'wp_insert_term_duplicate_term_check', $duplicate_term, $term, $taxonomy, $args, $tt_id );
2773  
2774      if ( $duplicate_term ) {
2775          $wpdb->delete( $wpdb->terms, array( 'term_id' => $term_id ) );
2776          $wpdb->delete( $wpdb->term_taxonomy, array( 'term_taxonomy_id' => $tt_id ) );
2777  
2778          $term_id = (int) $duplicate_term->term_id;
2779          $tt_id   = (int) $duplicate_term->term_taxonomy_id;
2780  
2781          clean_term_cache( $term_id, $taxonomy );
2782          return array(
2783              'term_id'          => $term_id,
2784              'term_taxonomy_id' => $tt_id,
2785          );
2786      }
2787  
2788      /**
2789       * Fires immediately after a new term is created, before the term cache is cleaned.
2790       *
2791       * The {@see 'create_$taxonomy'} hook is also available for targeting a specific
2792       * taxonomy.
2793       *
2794       * @since 2.3.0
2795       * @since 6.1.0 The `$args` parameter was added.
2796       *
2797       * @param int    $term_id  Term ID.
2798       * @param int    $tt_id    Term taxonomy ID.
2799       * @param string $taxonomy Taxonomy slug.
2800       * @param array  $args     Arguments passed to wp_insert_term().
2801       */
2802      do_action( 'create_term', $term_id, $tt_id, $taxonomy, $args );
2803  
2804      /**
2805       * Fires after a new term is created for a specific taxonomy.
2806       *
2807       * The dynamic portion of the hook name, `$taxonomy`, refers
2808       * to the slug of the taxonomy the term was created for.
2809       *
2810       * Possible hook names include:
2811       *
2812       *  - `create_category`
2813       *  - `create_post_tag`
2814       *
2815       * @since 2.3.0
2816       * @since 6.1.0 The `$args` parameter was added.
2817       *
2818       * @param int   $term_id Term ID.
2819       * @param int   $tt_id   Term taxonomy ID.
2820       * @param array $args    Arguments passed to wp_insert_term().
2821       */
2822      do_action( "create_{$taxonomy}", $term_id, $tt_id, $args );
2823  
2824      /**
2825       * Filters the term ID after a new term is created.
2826       *
2827       * @since 2.3.0
2828       * @since 6.1.0 The `$args` parameter was added.
2829       *
2830       * @param int   $term_id Term ID.
2831       * @param int   $tt_id   Term taxonomy ID.
2832       * @param array $args    Arguments passed to wp_insert_term().
2833       */
2834      $term_id = apply_filters( 'term_id_filter', $term_id, $tt_id, $args );
2835  
2836      clean_term_cache( $term_id, $taxonomy );
2837  
2838      /**
2839       * Fires after a new term is created, and after the term cache has been cleaned.
2840       *
2841       * The {@see 'created_$taxonomy'} hook is also available for targeting a specific
2842       * taxonomy.
2843       *
2844       * @since 2.3.0
2845       * @since 6.1.0 The `$args` parameter was added.
2846       *
2847       * @param int    $term_id  Term ID.
2848       * @param int    $tt_id    Term taxonomy ID.
2849       * @param string $taxonomy Taxonomy slug.
2850       * @param array  $args     Arguments passed to wp_insert_term().
2851       */
2852      do_action( 'created_term', $term_id, $tt_id, $taxonomy, $args );
2853  
2854      /**
2855       * Fires after a new term in a specific taxonomy is created, and after the term
2856       * cache has been cleaned.
2857       *
2858       * The dynamic portion of the hook name, `$taxonomy`, refers to the taxonomy slug.
2859       *
2860       * Possible hook names include:
2861       *
2862       *  - `created_category`
2863       *  - `created_post_tag`
2864       *
2865       * @since 2.3.0
2866       * @since 6.1.0 The `$args` parameter was added.
2867       *
2868       * @param int   $term_id Term ID.
2869       * @param int   $tt_id   Term taxonomy ID.
2870       * @param array $args    Arguments passed to wp_insert_term().
2871       */
2872      do_action( "created_{$taxonomy}", $term_id, $tt_id, $args );
2873  
2874      /**
2875       * Fires after a term has been saved, and the term cache has been cleared.
2876       *
2877       * The {@see 'saved_$taxonomy'} hook is also available for targeting a specific
2878       * taxonomy.
2879       *
2880       * @since 5.5.0
2881       * @since 6.1.0 The `$args` parameter was added.
2882       *
2883       * @param int    $term_id  Term ID.
2884       * @param int    $tt_id    Term taxonomy ID.
2885       * @param string $taxonomy Taxonomy slug.
2886       * @param bool   $update   Whether this is an existing term being updated.
2887       * @param array  $args     Arguments passed to wp_insert_term().
2888       */
2889      do_action( 'saved_term', $term_id, $tt_id, $taxonomy, false, $args );
2890  
2891      /**
2892       * Fires after a term in a specific taxonomy has been saved, and the term
2893       * cache has been cleared.
2894       *
2895       * The dynamic portion of the hook name, `$taxonomy`, refers to the taxonomy slug.
2896       *
2897       * Possible hook names include:
2898       *
2899       *  - `saved_category`
2900       *  - `saved_post_tag`
2901       *
2902       * @since 5.5.0
2903       * @since 6.1.0 The `$args` parameter was added.
2904       *
2905       * @param int   $term_id Term ID.
2906       * @param int   $tt_id   Term taxonomy ID.
2907       * @param bool  $update  Whether this is an existing term being updated.
2908       * @param array $args    Arguments passed to wp_insert_term().
2909       */
2910      do_action( "saved_{$taxonomy}", $term_id, $tt_id, false, $args );
2911  
2912      return array(
2913          'term_id'          => $term_id,
2914          'term_taxonomy_id' => $tt_id,
2915      );
2916  }
2917  
2918  /**
2919   * Creates term and taxonomy relationships.
2920   *
2921   * Relates an object (post, link, etc.) to a term and taxonomy type. Creates the
2922   * term and taxonomy relationship if it doesn't already exist. Creates a term if
2923   * it doesn't exist (using the slug).
2924   *
2925   * A relationship means that the term is grouped in or belongs to the taxonomy.
2926   * A term has no meaning until it is given context by defining which taxonomy it
2927   * exists under.
2928   *
2929   * @since 2.3.0
2930   *
2931   * @global wpdb $wpdb WordPress database abstraction object.
2932   *
2933   * @param int              $object_id The object to relate to.
2934   * @param string|int|array $terms     A single term slug, single term ID, or array of either term slugs or IDs.
2935   *                                    Will replace all existing related terms in this taxonomy. Passing an
2936   *                                    empty array will remove all related terms.
2937   * @param string           $taxonomy  The context in which to relate the term to the object.
2938   * @param bool             $append    Optional. If false will delete difference of terms. Default false.
2939   * @return array|WP_Error Term taxonomy IDs of the affected terms or WP_Error on failure.
2940   */
2941  function wp_set_object_terms( $object_id, $terms, $taxonomy, $append = false ) {
2942      global $wpdb;
2943  
2944      $object_id = (int) $object_id;
2945  
2946      if ( ! taxonomy_exists( $taxonomy ) ) {
2947          return new WP_Error( 'invalid_taxonomy', __( 'Invalid taxonomy.' ) );
2948      }
2949  
2950      if ( empty( $terms ) ) {
2951          $terms = array();
2952      } elseif ( ! is_array( $terms ) ) {
2953          $terms = array( $terms );
2954      }
2955  
2956      if ( ! $append ) {
2957          $old_tt_ids = wp_get_object_terms(
2958              $object_id,
2959              $taxonomy,
2960              array(
2961                  'fields'                 => 'tt_ids',
2962                  'orderby'                => 'none',
2963                  'update_term_meta_cache' => false,
2964              )
2965          );
2966      } else {
2967          $old_tt_ids = array();
2968      }
2969  
2970      $tt_ids     = array();
2971      $new_tt_ids = array();
2972  
2973      foreach ( (array) $terms as $term ) {
2974          if ( '' === trim( $term ) ) {
2975              continue;
2976          }
2977  
2978          $term_info = term_exists( $term, $taxonomy );
2979  
2980          if ( ! $term_info ) {
2981              // Skip if a non-existent term ID is passed.
2982              if ( is_int( $term ) ) {
2983                  continue;
2984              }
2985  
2986              $term_info = wp_insert_term( $term, $taxonomy );
2987          }
2988  
2989          if ( is_wp_error( $term_info ) ) {
2990              return $term_info;
2991          }
2992  
2993          $tt_id    = $term_info['term_taxonomy_id'];
2994          $tt_ids[] = $tt_id;
2995  
2996          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 ) ) ) {
2997              continue;
2998          }
2999  
3000          /**
3001           * Fires immediately before an object-term relationship is added.
3002           *
3003           * @since 2.9.0
3004           * @since 4.7.0 Added the `$taxonomy` parameter.
3005           *
3006           * @param int    $object_id Object ID.
3007           * @param int    $tt_id     Term taxonomy ID.
3008           * @param string $taxonomy  Taxonomy slug.
3009           */
3010          do_action( 'add_term_relationship', $object_id, $tt_id, $taxonomy );
3011  
3012          $wpdb->insert(
3013              $wpdb->term_relationships,
3014              array(
3015                  'object_id'        => $object_id,
3016                  'term_taxonomy_id' => $tt_id,
3017              )
3018          );
3019  
3020          /**
3021           * Fires immediately after an object-term relationship is added.
3022           *
3023           * @since 2.9.0
3024           * @since 4.7.0 Added the `$taxonomy` parameter.
3025           *
3026           * @param int    $object_id Object ID.
3027           * @param int    $tt_id     Term taxonomy ID.
3028           * @param string $taxonomy  Taxonomy slug.
3029           */
3030          do_action( 'added_term_relationship', $object_id, $tt_id, $taxonomy );
3031  
3032          $new_tt_ids[] = $tt_id;
3033      }
3034  
3035      if ( $new_tt_ids ) {
3036          wp_update_term_count( $new_tt_ids, $taxonomy );
3037      }
3038  
3039      if ( ! $append ) {
3040          $delete_tt_ids = array_diff( $old_tt_ids, $tt_ids );
3041  
3042          if ( $delete_tt_ids ) {
3043              $in_delete_tt_ids = "'" . implode( "', '", $delete_tt_ids ) . "'";
3044              $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 ) );
3045              $delete_term_ids  = array_map( 'intval', $delete_term_ids );
3046  
3047              $remove = wp_remove_object_terms( $object_id, $delete_term_ids, $taxonomy );
3048              if ( is_wp_error( $remove ) ) {
3049                  return $remove;
3050              }
3051          }
3052      }
3053  
3054      $t = get_taxonomy( $taxonomy );
3055  
3056      if ( ! $append && isset( $t->sort ) && $t->sort ) {
3057          $values     = array();
3058          $term_order = 0;
3059  
3060          $final_tt_ids = wp_get_object_terms(
3061              $object_id,
3062              $taxonomy,
3063              array(
3064                  'fields'                 => 'tt_ids',
3065                  'update_term_meta_cache' => false,
3066              )
3067          );
3068  
3069          foreach ( $tt_ids as $tt_id ) {
3070              if ( in_array( (int) $tt_id, $final_tt_ids, true ) ) {
3071                  $values[] = $wpdb->prepare( '(%d, %d, %d)', $object_id, $tt_id, ++$term_order );
3072              }
3073          }
3074  
3075          if ( $values ) {
3076              if ( false === $wpdb->query( "INSERT INTO $wpdb->term_relationships (object_id, term_taxonomy_id, term_order) VALUES " . implode( ',', $values ) . ' ON DUPLICATE KEY UPDATE term_order = VALUES(term_order)' ) ) {
3077                  return new WP_Error( 'db_insert_error', __( 'Could not insert term relationship into the database.' ), $wpdb->last_error );
3078              }
3079          }
3080      }
3081  
3082      wp_cache_delete( $object_id, $taxonomy . '_relationships' );
3083      wp_cache_set_terms_last_changed();
3084  
3085      /**
3086       * Fires after an object's terms have been set.
3087       *
3088       * @since 2.8.0
3089       *
3090       * @param int    $object_id  Object ID.
3091       * @param array  $terms      An array of object term IDs or slugs.
3092       * @param array  $tt_ids     An array of term taxonomy IDs.
3093       * @param string $taxonomy   Taxonomy slug.
3094       * @param bool   $append     Whether to append new terms to the old terms.
3095       * @param array  $old_tt_ids Old array of term taxonomy IDs.
3096       */
3097      do_action( 'set_object_terms', $object_id, $terms, $tt_ids, $taxonomy, $append, $old_tt_ids );
3098  
3099      return $tt_ids;
3100  }
3101  
3102  /**
3103   * Adds term(s) associated with a given object.
3104   *
3105   * @since 3.6.0
3106   *
3107   * @param int              $object_id The ID of the object to which the terms will be added.
3108   * @param string|int|array $terms     The slug(s) or ID(s) of the term(s) to add.
3109   * @param array|string     $taxonomy  Taxonomy name.
3110   * @return array|WP_Error Term taxonomy IDs of the affected terms.
3111   */
3112  function wp_add_object_terms( $object_id, $terms, $taxonomy ) {
3113      return wp_set_object_terms( $object_id, $terms, $taxonomy, true );
3114  }
3115  
3116  /**
3117   * Removes term(s) associated with a given object.
3118   *
3119   * @since 3.6.0
3120   *
3121   * @global wpdb $wpdb WordPress database abstraction object.
3122   *
3123   * @param int              $object_id The ID of the object from which the terms will be removed.
3124   * @param string|int|array $terms     The slug(s) or ID(s) of the term(s) to remove.
3125   * @param string           $taxonomy  Taxonomy name.
3126   * @return bool|WP_Error True on success, false or WP_Error on failure.
3127   */
3128  function wp_remove_object_terms( $object_id, $terms, $taxonomy ) {
3129      global $wpdb;
3130  
3131      $object_id = (int) $object_id;
3132  
3133      if ( ! taxonomy_exists( $taxonomy ) ) {
3134          return new WP_Error( 'invalid_taxonomy', __( 'Invalid taxonomy.' ) );
3135      }
3136  
3137      if ( ! is_array( $terms ) ) {
3138          $terms = array( $terms );
3139      }
3140  
3141      $tt_ids = array();
3142  
3143      foreach ( (array) $terms as $term ) {
3144          if ( '' === trim( $term ) ) {
3145              continue;
3146          }
3147  
3148          $term_info = term_exists( $term, $taxonomy );
3149          if ( ! $term_info ) {
3150              // Skip if a non-existent term ID is passed.
3151              if ( is_int( $term ) ) {
3152                  continue;
3153              }
3154          }
3155  
3156          if ( is_wp_error( $term_info ) ) {
3157              return $term_info;
3158          }
3159  
3160          $tt_ids[] = $term_info['term_taxonomy_id'];
3161      }
3162  
3163      if ( $tt_ids ) {
3164          $in_tt_ids = "'" . implode( "', '", $tt_ids ) . "'";
3165  
3166          /**
3167           * Fires immediately before an object-term relationship is deleted.
3168           *
3169           * @since 2.9.0
3170           * @since 4.7.0 Added the `$taxonomy` parameter.
3171           *
3172           * @param int    $object_id Object ID.
3173           * @param array  $tt_ids    An array of term taxonomy IDs.
3174           * @param string $taxonomy  Taxonomy slug.
3175           */
3176          do_action( 'delete_term_relationships', $object_id, $tt_ids, $taxonomy );
3177  
3178          $deleted = $wpdb->query( $wpdb->prepare( "DELETE FROM $wpdb->term_relationships WHERE object_id = %d AND term_taxonomy_id IN ($in_tt_ids)", $object_id ) );
3179  
3180          wp_cache_delete( $object_id, $taxonomy . '_relationships' );
3181          wp_cache_set_terms_last_changed();
3182  
3183          /**
3184           * Fires immediately after an object-term relationship is deleted.
3185           *
3186           * @since 2.9.0
3187           * @since 4.7.0 Added the `$taxonomy` parameter.
3188           *
3189           * @param int    $object_id Object ID.
3190           * @param array  $tt_ids    An array of term taxonomy IDs.
3191           * @param string $taxonomy  Taxonomy slug.
3192           */
3193          do_action( 'deleted_term_relationships', $object_id, $tt_ids, $taxonomy );
3194  
3195          wp_update_term_count( $tt_ids, $taxonomy );
3196  
3197          return (bool) $deleted;
3198      }
3199  
3200      return false;
3201  }
3202  
3203  /**
3204   * Makes term slug unique, if it isn't already.
3205   *
3206   * The `$slug` has to be unique global to every taxonomy, meaning that one
3207   * taxonomy term can't have a matching slug with another taxonomy term. Each
3208   * slug has to be globally unique for every taxonomy.
3209   *
3210   * The way this works is that if the taxonomy that the term belongs to is
3211   * hierarchical and has a parent, it will append that parent to the $slug.
3212   *
3213   * If that still doesn't return a unique slug, then it tries to append a number
3214   * until it finds a number that is truly unique.
3215   *
3216   * The only purpose for `$term` is for appending a parent, if one exists.
3217   *
3218   * @since 2.3.0
3219   *
3220   * @global wpdb $wpdb WordPress database abstraction object.
3221   *
3222   * @param string $slug The string that will be tried for a unique slug.
3223   * @param object $term The term object that the `$slug` will belong to.
3224   * @return string Will return a true unique slug.
3225   */
3226  function wp_unique_term_slug( $slug, $term ) {
3227      global $wpdb;
3228  
3229      $needs_suffix  = true;
3230      $original_slug = $slug;
3231  
3232      // As of 4.1, duplicate slugs are allowed as long as they're in different taxonomies.
3233      if ( ! term_exists( $slug ) || get_option( 'db_version' ) >= 30133 && ! get_term_by( 'slug', $slug, $term->taxonomy ) ) {
3234          $needs_suffix = false;
3235      }
3236  
3237      /*
3238       * If the taxonomy supports hierarchy and the term has a parent, make the slug unique
3239       * by incorporating parent slugs.
3240       */
3241      $parent_suffix = '';
3242      if ( $needs_suffix && is_taxonomy_hierarchical( $term->taxonomy ) && ! empty( $term->parent ) ) {
3243          $the_parent = $term->parent;
3244          while ( ! empty( $the_parent ) ) {
3245              $parent_term = get_term( $the_parent, $term->taxonomy );
3246              if ( is_wp_error( $parent_term ) || empty( $parent_term ) ) {
3247                  break;
3248              }
3249              $parent_suffix .= '-' . $parent_term->slug;
3250              if ( ! term_exists( $slug . $parent_suffix ) ) {
3251                  break;
3252              }
3253  
3254              if ( empty( $parent_term->parent ) ) {
3255                  break;
3256              }
3257              $the_parent = $parent_term->parent;
3258          }
3259      }
3260  
3261      // If we didn't get a unique slug, try appending a number to make it unique.
3262  
3263      /**
3264       * Filters whether the proposed unique term slug is bad.
3265       *
3266       * @since 4.3.0
3267       *
3268       * @param bool   $needs_suffix Whether the slug needs to be made unique with a suffix.
3269       * @param string $slug         The slug.
3270       * @param object $term         Term object.
3271       */
3272      if ( apply_filters( 'wp_unique_term_slug_is_bad_slug', $needs_suffix, $slug, $term ) ) {
3273          if ( $parent_suffix ) {
3274              $slug .= $parent_suffix;
3275          }
3276  
3277          if ( ! empty( $term->term_id ) ) {
3278              $query = $wpdb->prepare( "SELECT slug FROM $wpdb->terms WHERE slug = %s AND term_id != %d", $slug, $term->term_id );
3279          } else {
3280              $query = $wpdb->prepare( "SELECT slug FROM $wpdb->terms WHERE slug = %s", $slug );
3281          }
3282  
3283          if ( $wpdb->get_var( $query ) ) { // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
3284              $num = 2;
3285              do {
3286                  $alt_slug = $slug . "-$num";
3287                  ++$num;
3288                  $slug_check = $wpdb->get_var( $wpdb->prepare( "SELECT slug FROM $wpdb->terms WHERE slug = %s", $alt_slug ) );
3289              } while ( $slug_check );
3290              $slug = $alt_slug;
3291          }
3292      }
3293  
3294      /**
3295       * Filters the unique term slug.
3296       *
3297       * @since 4.3.0
3298       *
3299       * @param string $slug          Unique term slug.
3300       * @param object $term          Term object.
3301       * @param string $original_slug Slug originally passed to the function for testing.
3302       */
3303      return apply_filters( 'wp_unique_term_slug', $slug, $term, $original_slug );
3304  }
3305  
3306  /**
3307   * Updates term based on arguments provided.
3308   *
3309   * The `$args` will indiscriminately override all values with the same field name.
3310   * Care must be taken to not override important information need to update or
3311   * update will fail (or perhaps create a new term, neither would be acceptable).
3312   *
3313   * Defaults will set 'alias_of', 'description', 'parent', and 'slug' if not
3314   * defined in `$args` already.
3315   *
3316   * 'alias_of' will create a term group, if it doesn't already exist, and
3317   * update it for the `$term`.
3318   *
3319   * If the 'slug' argument in `$args` is missing, then the 'name' will be used.
3320   * If you set 'slug' and it isn't unique, then a WP_Error is returned.
3321   * If you don't pass any slug, then a unique one will be created.
3322   *
3323   * @since 2.3.0
3324   *
3325   * @global wpdb $wpdb WordPress database abstraction object.
3326   *
3327   * @param int          $term_id  The ID of the term.
3328   * @param string       $taxonomy The taxonomy of the term.
3329   * @param array        $args {
3330   *     Optional. Array of arguments for updating a term.
3331   *
3332   *     @type string $alias_of    Slug of the term to make this term an alias of.
3333   *                               Default empty string. Accepts a term slug.
3334   *     @type string $description The term description. Default empty string.
3335   *     @type int    $parent      The id of the parent term. Default 0.
3336   *     @type string $slug        The term slug to use. Default empty string.
3337   * }
3338   * @return array|WP_Error An array containing the `term_id` and `term_taxonomy_id`,
3339   *                        WP_Error otherwise.
3340   * @phpstan-param array{
3341   *     alias_of?: string,
3342   *     description?: string,
3343   *     parent?: non-negative-int,
3344   *     slug?: string|null,
3345   *     ...
3346   * } $args
3347   * @phpstan-return array{
3348   *     term_id: int,
3349   *     term_taxonomy_id: int,
3350   * }|WP_Error
3351   */
3352  function wp_update_term( $term_id, $taxonomy, $args = array() ) {
3353      global $wpdb;
3354  
3355      if ( ! taxonomy_exists( $taxonomy ) ) {
3356          return new WP_Error( 'invalid_taxonomy', __( 'Invalid taxonomy.' ) );
3357      }
3358  
3359      $term_id = (int) $term_id;
3360  
3361      // First, get all of the original args.
3362      $term = get_term( $term_id, $taxonomy );
3363  
3364      if ( is_wp_error( $term ) ) {
3365          return $term;
3366      }
3367  
3368      if ( ! $term ) {
3369          return new WP_Error( 'invalid_term', __( 'Empty Term.' ) );
3370      }
3371  
3372      $term = (array) $term->data;
3373  
3374      // Escape data pulled from DB.
3375      $term = wp_slash( $term );
3376  
3377      // Merge old and new args with new args overwriting old ones.
3378      $args = array_merge( $term, $args );
3379  
3380      $defaults    = array(
3381          'alias_of'    => '',
3382          'description' => '',
3383          'parent'      => 0,
3384          'slug'        => '',
3385      );
3386      $args        = wp_parse_args( $args, $defaults );
3387      $args        = sanitize_term( $args, $taxonomy, 'db' );
3388      $parsed_args = $args;
3389  
3390      // expected_slashed ($name)
3391      $name        = wp_unslash( $args['name'] );
3392      $description = wp_unslash( $args['description'] );
3393  
3394      $parsed_args['name']        = $name;
3395      $parsed_args['description'] = $description;
3396  
3397      if ( '' === trim( $name ) ) {
3398          return new WP_Error( 'empty_term_name', __( 'A name is required for this term.' ) );
3399      }
3400  
3401      if ( (int) $parsed_args['parent'] > 0 && ! term_exists( (int) $parsed_args['parent'] ) ) {
3402          return new WP_Error( 'missing_parent', __( 'Parent term does not exist.' ) );
3403      }
3404  
3405      $empty_slug = false;
3406      if ( empty( $args['slug'] ) ) {
3407          $empty_slug = true;
3408          $slug       = sanitize_title( $name );
3409      } else {
3410          $slug = $args['slug'];
3411      }
3412  
3413      $parsed_args['slug'] = $slug;
3414  
3415      $term_group = $parsed_args['term_group'] ?? 0;
3416      if ( $args['alias_of'] ) {
3417          $alias = get_term_by( 'slug', $args['alias_of'], $taxonomy );
3418          if ( ! empty( $alias->term_group ) ) {
3419              // The alias we want is already in a group, so let's use that one.
3420              $term_group = $alias->term_group;
3421          } elseif ( ! empty( $alias->term_id ) ) {
3422              /*
3423               * The alias is not in a group, so we create a new one
3424               * and add the alias to it.
3425               */
3426              $term_group = $wpdb->get_var( "SELECT MAX(term_group) FROM $wpdb->terms" ) + 1;
3427  
3428              wp_update_term(
3429                  $alias->term_id,
3430                  $taxonomy,
3431                  array(
3432                      'term_group' => $term_group,
3433                  )
3434              );
3435          }
3436  
3437          $parsed_args['term_group'] = $term_group;
3438      }
3439  
3440      /**
3441       * Filters the term parent.
3442       *
3443       * Hook to this filter to see if it will cause a hierarchy loop.
3444       *
3445       * @since 3.1.0
3446       *
3447       * @param int    $parent_term ID of the parent term.
3448       * @param int    $term_id     Term ID.
3449       * @param string $taxonomy    Taxonomy slug.
3450       * @param array  $parsed_args An array of potentially altered update arguments for the given term.
3451       * @param array  $args        Arguments passed to wp_update_term().
3452       */
3453      $parent = (int) apply_filters( 'wp_update_term_parent', $args['parent'], $term_id, $taxonomy, $parsed_args, $args );
3454  
3455      // Check for duplicate slug.
3456      $duplicate = get_term_by( 'slug', $slug, $taxonomy );
3457      if ( $duplicate && $duplicate->term_id !== $term_id ) {
3458          /*
3459           * If an empty slug was passed or the parent changed, reset the slug to something unique.
3460           * Otherwise, bail.
3461           */
3462          if ( $empty_slug || ( $parent !== (int) $term['parent'] ) ) {
3463              $slug = wp_unique_term_slug( $slug, (object) $args );
3464          } else {
3465              /* translators: %s: Taxonomy term slug. */
3466              return new WP_Error( 'duplicate_term_slug', sprintf( __( 'The slug &#8220;%s&#8221; is already in use by another term.' ), $slug ) );
3467          }
3468      }
3469  
3470      $tt_id = (int) $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 ) );
3471  
3472      // Check whether this is a shared term that needs splitting.
3473      $_term_id = _split_shared_term( $term_id, $tt_id );
3474      if ( ! is_wp_error( $_term_id ) ) {
3475          $term_id = $_term_id;
3476      }
3477  
3478      /**
3479       * Fires immediately before the given terms are edited.
3480       *
3481       * @since 2.9.0
3482       * @since 6.1.0 The `$args` parameter was added.
3483       *
3484       * @param int    $term_id  Term ID.
3485       * @param string $taxonomy Taxonomy slug.
3486       * @param array  $args     Arguments passed to wp_update_term().
3487       */
3488      do_action( 'edit_terms', $term_id, $taxonomy, $args );
3489  
3490      $data = compact( 'name', 'slug', 'term_group' );
3491  
3492      /**
3493       * Filters term data before it is updated in the database.
3494       *
3495       * @since 4.7.0
3496       *
3497       * @param array  $data     Term data to be updated.
3498       * @param int    $term_id  Term ID.
3499       * @param string $taxonomy Taxonomy slug.
3500       * @param array  $args     Arguments passed to wp_update_term().
3501       */
3502      $data = apply_filters( 'wp_update_term_data', $data, $term_id, $taxonomy, $args );
3503  
3504      $wpdb->update( $wpdb->terms, $data, compact( 'term_id' ) );
3505  
3506      if ( empty( $slug ) ) {
3507          $slug = sanitize_title( $name, $term_id );
3508          $wpdb->update( $wpdb->terms, compact( 'slug' ), compact( 'term_id' ) );
3509      }
3510  
3511      /**
3512       * Fires immediately after a term is updated in the database, but before its
3513       * term-taxonomy relationship is updated.
3514       *
3515       * @since 2.9.0
3516       * @since 6.1.0 The `$args` parameter was added.
3517       *
3518       * @param int    $term_id  Term ID.
3519       * @param string $taxonomy Taxonomy slug.
3520       * @param array  $args     Arguments passed to wp_update_term().
3521       */
3522      do_action( 'edited_terms', $term_id, $taxonomy, $args );
3523  
3524      /**
3525       * Fires immediate before a term-taxonomy relationship is updated.
3526       *
3527       * @since 2.9.0
3528       * @since 6.1.0 The `$args` parameter was added.
3529       *
3530       * @param int    $tt_id    Term taxonomy ID.
3531       * @param string $taxonomy Taxonomy slug.
3532       * @param array  $args     Arguments passed to wp_update_term().
3533       */
3534      do_action( 'edit_term_taxonomy', $tt_id, $taxonomy, $args );
3535  
3536      $wpdb->update( $wpdb->term_taxonomy, compact( 'term_id', 'taxonomy', 'description', 'parent' ), array( 'term_taxonomy_id' => $tt_id ) );
3537  
3538      /**
3539       * Fires immediately after a term-taxonomy relationship is updated.
3540       *
3541       * @since 2.9.0
3542       * @since 6.1.0 The `$args` parameter was added.
3543       *
3544       * @param int    $tt_id    Term taxonomy ID.
3545       * @param string $taxonomy Taxonomy slug.
3546       * @param array  $args     Arguments passed to wp_update_term().
3547       */
3548      do_action( 'edited_term_taxonomy', $tt_id, $taxonomy, $args );
3549  
3550      /**
3551       * Fires after a term has been updated, but before the term cache has been cleaned.
3552       *
3553       * The {@see 'edit_$taxonomy'} hook is also available for targeting a specific
3554       * taxonomy.
3555       *
3556       * @since 2.3.0
3557       * @since 6.1.0 The `$args` parameter was added.
3558       *
3559       * @param int    $term_id  Term ID.
3560       * @param int    $tt_id    Term taxonomy ID.
3561       * @param string $taxonomy Taxonomy slug.
3562       * @param array  $args     Arguments passed to wp_update_term().
3563       */
3564      do_action( 'edit_term', $term_id, $tt_id, $taxonomy, $args );
3565  
3566      /**
3567       * Fires after a term in a specific taxonomy has been updated, but before the term
3568       * cache has been cleaned.
3569       *
3570       * The dynamic portion of the hook name, `$taxonomy`, refers to the taxonomy slug.
3571       *
3572       * Possible hook names include:
3573       *
3574       *  - `edit_category`
3575       *  - `edit_post_tag`
3576       *
3577       * @since 2.3.0
3578       * @since 6.1.0 The `$args` parameter was added.
3579       *
3580       * @param int   $term_id Term ID.
3581       * @param int   $tt_id   Term taxonomy ID.
3582       * @param array $args    Arguments passed to wp_update_term().
3583       */
3584      do_action( "edit_{$taxonomy}", $term_id, $tt_id, $args );
3585  
3586      /** This filter is documented in wp-includes/taxonomy.php */
3587      $term_id = apply_filters( 'term_id_filter', $term_id, $tt_id, $args );
3588  
3589      clean_term_cache( $term_id, $taxonomy );
3590  
3591      /**
3592       * Fires after a term has been updated, and the term cache has been cleaned.
3593       *
3594       * The {@see 'edited_$taxonomy'} hook is also available for targeting a specific
3595       * taxonomy.
3596       *
3597       * @since 2.3.0
3598       * @since 6.1.0 The `$args` parameter was added.
3599       *
3600       * @param int    $term_id  Term ID.
3601       * @param int    $tt_id    Term taxonomy ID.
3602       * @param string $taxonomy Taxonomy slug.
3603       * @param array  $args     Arguments passed to wp_update_term().
3604       */
3605      do_action( 'edited_term', $term_id, $tt_id, $taxonomy, $args );
3606  
3607      /**
3608       * Fires after a term for a specific taxonomy has been updated, and the term
3609       * cache has been cleaned.
3610       *
3611       * The dynamic portion of the hook name, `$taxonomy`, refers to the taxonomy slug.
3612       *
3613       * Possible hook names include:
3614       *
3615       *  - `edited_category`
3616       *  - `edited_post_tag`
3617       *
3618       * @since 2.3.0
3619       * @since 6.1.0 The `$args` parameter was added.
3620       *
3621       * @param int   $term_id Term ID.
3622       * @param int   $tt_id   Term taxonomy ID.
3623       * @param array $args    Arguments passed to wp_update_term().
3624       */
3625      do_action( "edited_{$taxonomy}", $term_id, $tt_id, $args );
3626  
3627      /** This action is documented in wp-includes/taxonomy.php */
3628      do_action( 'saved_term', $term_id, $tt_id, $taxonomy, true, $args );
3629  
3630      /** This action is documented in wp-includes/taxonomy.php */
3631      do_action( "saved_{$taxonomy}", $term_id, $tt_id, true, $args );
3632  
3633      return array(
3634          'term_id'          => $term_id,
3635          'term_taxonomy_id' => $tt_id,
3636      );
3637  }
3638  
3639  /**
3640   * Enables or disables term counting.
3641   *
3642   * @since 2.5.0
3643   *
3644   * @param bool $defer Optional. Enable if true, disable if false.
3645   * @return bool Whether term counting is enabled or disabled.
3646   */
3647  function wp_defer_term_counting( $defer = null ) {
3648      static $_defer = false;
3649  
3650      if ( is_bool( $defer ) ) {
3651          $_defer = $defer;
3652          // Flush any deferred counts.
3653          if ( ! $defer ) {
3654              wp_update_term_count( null, null, true );
3655          }
3656      }
3657  
3658      return $_defer;
3659  }
3660  
3661  /**
3662   * Updates the amount of terms in taxonomy.
3663   *
3664   * If there is a taxonomy callback applied, then it will be called for updating
3665   * the count.
3666   *
3667   * The default action is to count what the amount of terms have the relationship
3668   * of term ID. Once that is done, then update the database.
3669   *
3670   * @since 2.3.0
3671   *
3672   * @param int|array $terms       The term_taxonomy_id of the terms.
3673   * @param string    $taxonomy    The context of the term.
3674   * @param bool      $do_deferred Whether to flush the deferred term counts too. Default false.
3675   * @return bool If no terms will return false, and if successful will return true.
3676   */
3677  function wp_update_term_count( $terms, $taxonomy, $do_deferred = false ) {
3678      static $_deferred = array();
3679  
3680      if ( $do_deferred ) {
3681          foreach ( (array) array_keys( $_deferred ) as $tax ) {
3682              wp_update_term_count_now( $_deferred[ $tax ], $tax );
3683              unset( $_deferred[ $tax ] );
3684          }
3685      }
3686  
3687      if ( empty( $terms ) ) {
3688          return false;
3689      }
3690  
3691      if ( ! is_array( $terms ) ) {
3692          $terms = array( $terms );
3693      }
3694  
3695      if ( wp_defer_term_counting() ) {
3696          if ( ! isset( $_deferred[ $taxonomy ] ) ) {
3697              $_deferred[ $taxonomy ] = array();
3698          }
3699          $_deferred[ $taxonomy ] = array_unique( array_merge( $_deferred[ $taxonomy ], $terms ) );
3700          return true;
3701      }
3702  
3703      return wp_update_term_count_now( $terms, $taxonomy );
3704  }
3705  
3706  /**
3707   * Performs term count update immediately.
3708   *
3709   * @since 2.5.0
3710   *
3711   * @param array  $terms    The term_taxonomy_id of terms to update.
3712   * @param string $taxonomy The context of the term.
3713   * @return true Always true when complete.
3714   */
3715  function wp_update_term_count_now( $terms, $taxonomy ) {
3716      $terms = array_map( 'intval', $terms );
3717  
3718      $taxonomy = get_taxonomy( $taxonomy );
3719      if ( ! empty( $taxonomy->update_count_callback ) ) {
3720          call_user_func( $taxonomy->update_count_callback, $terms, $taxonomy );
3721      } else {
3722          $object_types = (array) $taxonomy->object_type;
3723          foreach ( $object_types as &$object_type ) {
3724              if ( str_starts_with( $object_type, 'attachment:' ) ) {
3725                  list( $object_type ) = explode( ':', $object_type );
3726              }
3727          }
3728  
3729          if ( array_filter( $object_types, 'post_type_exists' ) == $object_types ) {
3730              // Only post types are attached to this taxonomy.
3731              _update_post_term_count( $terms, $taxonomy );
3732          } else {
3733              // Default count updater.
3734              _update_generic_term_count( $terms, $taxonomy );
3735          }
3736      }
3737  
3738      clean_term_cache( $terms, '', false );
3739  
3740      return true;
3741  }
3742  
3743  //
3744  // Cache.
3745  //
3746  
3747  /**
3748   * Removes the taxonomy relationship to terms from the cache.
3749   *
3750   * Will remove the entire taxonomy relationship containing term `$object_id`. The
3751   * term IDs have to exist within the taxonomy `$object_type` for the deletion to
3752   * take place.
3753   *
3754   * @since 2.3.0
3755   *
3756   * @global bool $_wp_suspend_cache_invalidation
3757   *
3758   * @see get_object_taxonomies() for more on $object_type.
3759   *
3760   * @param int|array    $object_ids  Single or list of term object ID(s).
3761   * @param array|string $object_type The taxonomy object type.
3762   */
3763  function clean_object_term_cache( $object_ids, $object_type ) {
3764      global $_wp_suspend_cache_invalidation;
3765  
3766      if ( ! empty( $_wp_suspend_cache_invalidation ) ) {
3767          return;
3768      }
3769  
3770      if ( ! is_array( $object_ids ) ) {
3771          $object_ids = array( $object_ids );
3772      }
3773  
3774      $taxonomies = get_object_taxonomies( $object_type );
3775  
3776      foreach ( $taxonomies as $taxonomy ) {
3777          wp_cache_delete_multiple( $object_ids, "{$taxonomy}_relationships" );
3778      }
3779  
3780      wp_cache_set_terms_last_changed();
3781  
3782      /**
3783       * Fires after the object term cache has been cleaned.
3784       *
3785       * @since 2.5.0
3786       *
3787       * @param array  $object_ids  An array of object IDs.
3788       * @param string $object_type Object type.
3789       */
3790      do_action( 'clean_object_term_cache', $object_ids, $object_type );
3791  }
3792  
3793  /**
3794   * Removes all of the term IDs from the cache.
3795   *
3796   * @since 2.3.0
3797   *
3798   * @global wpdb $wpdb                           WordPress database abstraction object.
3799   * @global bool $_wp_suspend_cache_invalidation
3800   *
3801   * @param int|int[] $ids            Single or array of term IDs.
3802   * @param string    $taxonomy       Optional. Taxonomy slug. Can be empty, in which case the taxonomies of the passed
3803   *                                  term IDs will be used. Default empty.
3804   * @param bool      $clean_taxonomy Optional. Whether to clean taxonomy wide caches (true), or just individual
3805   *                                  term object caches (false). Default true.
3806   */
3807  function clean_term_cache( $ids, $taxonomy = '', $clean_taxonomy = true ) {
3808      global $wpdb, $_wp_suspend_cache_invalidation;
3809  
3810      if ( ! empty( $_wp_suspend_cache_invalidation ) ) {
3811          return;
3812      }
3813  
3814      if ( ! is_array( $ids ) ) {
3815          $ids = array( $ids );
3816      }
3817  
3818      $taxonomies = array();
3819      // If no taxonomy, assume tt_ids.
3820      if ( empty( $taxonomy ) ) {
3821          $tt_ids = array_map( 'intval', $ids );
3822          $tt_ids = implode( ', ', $tt_ids );
3823          $terms  = $wpdb->get_results( "SELECT term_id, taxonomy FROM $wpdb->term_taxonomy WHERE term_taxonomy_id IN ($tt_ids)" );
3824          $ids    = array();
3825  
3826          foreach ( (array) $terms as $term ) {
3827              $taxonomies[] = $term->taxonomy;
3828              $ids[]        = $term->term_id;
3829          }
3830          wp_cache_delete_multiple( $ids, 'terms' );
3831          $taxonomies = array_unique( $taxonomies );
3832      } else {
3833          wp_cache_delete_multiple( $ids, 'terms' );
3834          $taxonomies = array( $taxonomy );
3835      }
3836  
3837      foreach ( $taxonomies as $taxonomy ) {
3838          if ( $clean_taxonomy ) {
3839              clean_taxonomy_cache( $taxonomy );
3840          }
3841  
3842          /**
3843           * Fires once after each taxonomy's term cache has been cleaned.
3844           *
3845           * @since 2.5.0
3846           * @since 4.5.0 Added the `$clean_taxonomy` parameter.
3847           *
3848           * @param array  $ids            An array of term IDs.
3849           * @param string $taxonomy       Taxonomy slug.
3850           * @param bool   $clean_taxonomy Whether or not to clean taxonomy-wide caches
3851           */
3852          do_action( 'clean_term_cache', $ids, $taxonomy, $clean_taxonomy );
3853      }
3854  
3855      wp_cache_set_terms_last_changed();
3856  }
3857  
3858  /**
3859   * Cleans the caches for a taxonomy.
3860   *
3861   * @since 4.9.0
3862   *
3863   * @param string $taxonomy Taxonomy slug.
3864   */
3865  function clean_taxonomy_cache( $taxonomy ) {
3866      wp_cache_delete( 'all_ids', $taxonomy );
3867      wp_cache_delete( 'get', $taxonomy );
3868      wp_cache_set_terms_last_changed();
3869  
3870      // Regenerate cached hierarchy.
3871      if ( is_taxonomy_hierarchical( $taxonomy ) ) {
3872          delete_option( "{$taxonomy}_children" );
3873          _get_term_hierarchy( $taxonomy );
3874      }
3875  
3876      /**
3877       * Fires after a taxonomy's caches have been cleaned.
3878       *
3879       * @since 4.9.0
3880       *
3881       * @param string $taxonomy Taxonomy slug.
3882       */
3883      do_action( 'clean_taxonomy_cache', $taxonomy );
3884  }
3885  
3886  /**
3887   * Retrieves the cached term objects for the given object ID.
3888   *
3889   * Upstream functions (like get_the_terms() and is_object_in_term()) are
3890   * responsible for populating the object-term relationship cache. The current
3891   * function only fetches relationship data that is already in the cache.
3892   *
3893   * @since 2.3.0
3894   * @since 4.7.0 Returns a `WP_Error` object if there's an error with
3895   *              any of the matched terms.
3896   *
3897   * @param int    $id       Term object ID, for example a post, comment, or user ID.
3898   * @param string $taxonomy Taxonomy name.
3899   * @return bool|WP_Term[]|WP_Error Array of `WP_Term` objects, if cached.
3900   *                                 False if cache is empty for `$taxonomy` and `$id`.
3901   *                                 WP_Error if get_term() returns an error object for any term.
3902   */
3903  function get_object_term_cache( $id, $taxonomy ) {
3904      $_term_ids = wp_cache_get( $id, "{$taxonomy}_relationships" );
3905  
3906      // We leave the priming of relationship caches to upstream functions.
3907      if ( false === $_term_ids ) {
3908          return false;
3909      }
3910  
3911      // Backward compatibility for if a plugin is putting objects into the cache, rather than IDs.
3912      $term_ids = array();
3913      foreach ( $_term_ids as $term_id ) {
3914          if ( is_numeric( $term_id ) ) {
3915              $term_ids[] = (int) $term_id;
3916          } elseif ( isset( $term_id->term_id ) ) {
3917              $term_ids[] = (int) $term_id->term_id;
3918          }
3919      }
3920  
3921      // Fill the term objects.
3922      _prime_term_caches( $term_ids );
3923  
3924      $terms = array();
3925      foreach ( $term_ids as $term_id ) {
3926          $term = get_term( $term_id, $taxonomy );
3927          if ( is_wp_error( $term ) ) {
3928              return $term;
3929          }
3930  
3931          $terms[] = $term;
3932      }
3933  
3934      return $terms;
3935  }
3936  
3937  /**
3938   * Updates the cache for the given term object ID(s).
3939   *
3940   * Note: Due to performance concerns, great care should be taken to only update
3941   * term caches when necessary. Processing time can increase exponentially depending
3942   * on both the number of passed term IDs and the number of taxonomies those terms
3943   * belong to.
3944   *
3945   * Caches will only be updated for terms not already cached.
3946   *
3947   * @since 2.3.0
3948   *
3949   * @param string|int[]    $object_ids  Comma-separated list or array of term object IDs.
3950   * @param string|string[] $object_type The taxonomy object type or array of the same.
3951   * @return void|false Void on success or if the `$object_ids` parameter is empty,
3952   *                    false if all of the terms in `$object_ids` are already cached.
3953   */
3954  function update_object_term_cache( $object_ids, $object_type ) {
3955      if ( empty( $object_ids ) ) {
3956          return;
3957      }
3958  
3959      if ( ! is_array( $object_ids ) ) {
3960          $object_ids = explode( ',', $object_ids );
3961      }
3962  
3963      $object_ids     = array_map( 'intval', $object_ids );
3964      $non_cached_ids = array();
3965  
3966      $taxonomies = get_object_taxonomies( $object_type );
3967  
3968      foreach ( $taxonomies as $taxonomy ) {
3969          $cache_values = wp_cache_get_multiple( (array) $object_ids, "{$taxonomy}_relationships" );
3970  
3971          foreach ( $cache_values as $id => $value ) {
3972              if ( false === $value ) {
3973                  $non_cached_ids[] = $id;
3974              }
3975          }
3976      }
3977  
3978      if ( empty( $non_cached_ids ) ) {
3979          return false;
3980      }
3981  
3982      $non_cached_ids = array_unique( $non_cached_ids );
3983  
3984      $terms = wp_get_object_terms(
3985          $non_cached_ids,
3986          $taxonomies,
3987          array(
3988              'fields'                 => 'all_with_object_id',
3989              'orderby'                => 'name',
3990              'update_term_meta_cache' => false,
3991          )
3992      );
3993  
3994      $object_terms = array();
3995      foreach ( (array) $terms as $term ) {
3996          $object_terms[ $term->object_id ][ $term->taxonomy ][] = $term->term_id;
3997      }
3998  
3999      foreach ( $non_cached_ids as $id ) {
4000          foreach ( $taxonomies as $taxonomy ) {
4001              if ( ! isset( $object_terms[ $id ][ $taxonomy ] ) ) {
4002                  if ( ! isset( $object_terms[ $id ] ) ) {
4003                      $object_terms[ $id ] = array();
4004                  }
4005                  $object_terms[ $id ][ $taxonomy ] = array();
4006              }
4007          }
4008      }
4009  
4010      $cache_values = array();
4011      foreach ( $object_terms as $id => $value ) {
4012          foreach ( $value as $taxonomy => $terms ) {
4013              $cache_values[ $taxonomy ][ $id ] = $terms;
4014          }
4015      }
4016      foreach ( $cache_values as $taxonomy => $data ) {
4017          wp_cache_add_multiple( $data, "{$taxonomy}_relationships" );
4018      }
4019  }
4020  
4021  /**
4022   * Updates terms in cache.
4023   *
4024   * @since 2.3.0
4025   *
4026   * @param WP_Term[] $terms    Array of term objects to change.
4027   * @param string    $taxonomy Not used.
4028   */
4029  function update_term_cache( $terms, $taxonomy = '' ) {
4030      $data = array();
4031      foreach ( (array) $terms as $term ) {
4032          // Create a copy in case the array was passed by reference.
4033          $_term = clone $term;
4034  
4035          // Object ID should not be cached.
4036          unset( $_term->object_id );
4037  
4038          $data[ $term->term_id ] = $_term;
4039      }
4040      wp_cache_add_multiple( $data, 'terms' );
4041  }
4042  
4043  //
4044  // Private.
4045  //
4046  
4047  /**
4048   * Retrieves children of taxonomy as term IDs.
4049   *
4050   * @access private
4051   * @since 2.3.0
4052   *
4053   * @param string $taxonomy Taxonomy name.
4054   * @return array Empty if $taxonomy isn't hierarchical or returns children as term IDs.
4055   */
4056  function _get_term_hierarchy( $taxonomy ) {
4057      if ( ! is_taxonomy_hierarchical( $taxonomy ) ) {
4058          return array();
4059      }
4060      $children = get_option( "{$taxonomy}_children" );
4061  
4062      if ( is_array( $children ) ) {
4063          return $children;
4064      }
4065      $children = array();
4066      $terms    = get_terms(
4067          array(
4068              'taxonomy'               => $taxonomy,
4069              'get'                    => 'all',
4070              'orderby'                => 'id',
4071              'fields'                 => 'id=>parent',
4072              'update_term_meta_cache' => false,
4073          )
4074      );
4075      foreach ( $terms as $term_id => $parent ) {
4076          if ( $parent > 0 ) {
4077              $children[ $parent ][] = $term_id;
4078          }
4079      }
4080      update_option( "{$taxonomy}_children", $children );
4081  
4082      return $children;
4083  }
4084  
4085  /**
4086   * Gets the subset of $terms that are descendants of $term_id.
4087   *
4088   * If `$terms` is an array of objects, then _get_term_children() returns an array of objects.
4089   * If `$terms` is an array of IDs, then _get_term_children() returns an array of IDs.
4090   *
4091   * @access private
4092   * @since 2.3.0
4093   *
4094   * @param int    $term_id   The ancestor term: all returned terms should be descendants of `$term_id`.
4095   * @param array  $terms     The set of terms - either an array of term objects or term IDs - from which those that
4096   *                          are descendants of $term_id will be chosen.
4097   * @param string $taxonomy  The taxonomy which determines the hierarchy of the terms.
4098   * @param array  $ancestors Optional. Term ancestors that have already been identified. Passed by reference, to keep
4099   *                          track of found terms when recursing the hierarchy. The array of located ancestors is used
4100   *                          to prevent infinite recursion loops. For performance, `term_ids` are used as array keys,
4101   *                          with 1 as value. Default empty array.
4102   * @return array|WP_Error The subset of $terms that are descendants of $term_id.
4103   */
4104  function _get_term_children( $term_id, $terms, $taxonomy, &$ancestors = array() ) {
4105      $empty_array = array();
4106      if ( empty( $terms ) ) {
4107          return $empty_array;
4108      }
4109  
4110      $term_id      = (int) $term_id;
4111      $term_list    = array();
4112      $has_children = _get_term_hierarchy( $taxonomy );
4113  
4114      if ( $term_id && ! isset( $has_children[ $term_id ] ) ) {
4115          return $empty_array;
4116      }
4117  
4118      // Include the term itself in the ancestors array, so we can properly detect when a loop has occurred.
4119      if ( empty( $ancestors ) ) {
4120          $ancestors[ $term_id ] = 1;
4121      }
4122  
4123      foreach ( (array) $terms as $term ) {
4124          $use_id = false;
4125          if ( ! is_object( $term ) ) {
4126              $term = get_term( $term, $taxonomy );
4127              if ( is_wp_error( $term ) ) {
4128                  return $term;
4129              }
4130              $use_id = true;
4131          }
4132  
4133          // Don't recurse if we've already identified the term as a child - this indicates a loop.
4134          if ( isset( $ancestors[ $term->term_id ] ) ) {
4135              continue;
4136          }
4137  
4138          if ( (int) $term->parent === $term_id ) {
4139              if ( $use_id ) {
4140                  $term_list[] = $term->term_id;
4141              } else {
4142                  $term_list[] = $term;
4143              }
4144  
4145              if ( ! isset( $has_children[ $term->term_id ] ) ) {
4146                  continue;
4147              }
4148  
4149              $ancestors[ $term->term_id ] = 1;
4150  
4151              $children = _get_term_children( $term->term_id, $terms, $taxonomy, $ancestors );
4152              if ( $children ) {
4153                  $term_list = array_merge( $term_list, $children );
4154              }
4155          }
4156      }
4157  
4158      return $term_list;
4159  }
4160  
4161  /**
4162   * Adds count of children to parent count.
4163   *
4164   * Recalculates term counts by including items from child terms. Assumes all
4165   * relevant children are already in the $terms argument.
4166   *
4167   * @access private
4168   * @since 2.3.0
4169   *
4170   * @global wpdb $wpdb WordPress database abstraction object.
4171   *
4172   * @param object[]|WP_Term[] $terms    List of term objects (passed by reference).
4173   * @param string             $taxonomy Term context.
4174   */
4175  function _pad_term_counts( &$terms, $taxonomy ) {
4176      global $wpdb;
4177  
4178      // This function only works for hierarchical taxonomies like post categories.
4179      if ( ! is_taxonomy_hierarchical( $taxonomy ) ) {
4180          return;
4181      }
4182  
4183      $term_hier = _get_term_hierarchy( $taxonomy );
4184  
4185      if ( empty( $term_hier ) ) {
4186          return;
4187      }
4188  
4189      $term_items  = array();
4190      $terms_by_id = array();
4191      $term_ids    = array();
4192  
4193      foreach ( (array) $terms as $key => $term ) {
4194          $terms_by_id[ $term->term_id ]       = & $terms[ $key ];
4195          $term_ids[ $term->term_taxonomy_id ] = $term->term_id;
4196      }
4197  
4198      // Get the object and term IDs and stick them in a lookup table.
4199      $tax_obj      = get_taxonomy( $taxonomy );
4200      $object_types = esc_sql( $tax_obj->object_type );
4201      $results      = $wpdb->get_results( "SELECT object_id, term_taxonomy_id FROM $wpdb->term_relationships INNER JOIN $wpdb->posts ON object_id = ID WHERE term_taxonomy_id IN (" . implode( ',', array_keys( $term_ids ) ) . ") AND post_type IN ('" . implode( "', '", $object_types ) . "') AND post_status = 'publish'" );
4202  
4203      foreach ( $results as $row ) {
4204          $id = $term_ids[ $row->term_taxonomy_id ];
4205  
4206          $term_items[ $id ][ $row->object_id ] = isset( $term_items[ $id ][ $row->object_id ] ) ? ++$term_items[ $id ][ $row->object_id ] : 1;
4207      }
4208  
4209      // Touch every ancestor's lookup row for each post in each term.
4210      foreach ( $term_ids as $term_id ) {
4211          $child     = $term_id;
4212          $ancestors = array();
4213          while ( ! empty( $terms_by_id[ $child ] ) && $parent = $terms_by_id[ $child ]->parent ) {
4214              $ancestors[] = $child;
4215  
4216              if ( ! empty( $term_items[ $term_id ] ) ) {
4217                  foreach ( $term_items[ $term_id ] as $item_id => $touches ) {
4218                      $term_items[ $parent ][ $item_id ] = isset( $term_items[ $parent ][ $item_id ] ) ? ++$term_items[ $parent ][ $item_id ] : 1;
4219                  }
4220              }
4221  
4222              $child = $parent;
4223  
4224              if ( in_array( $parent, $ancestors, true ) ) {
4225                  break;
4226              }
4227          }
4228      }
4229  
4230      // Transfer the touched cells.
4231      foreach ( (array) $term_items as $id => $items ) {
4232          if ( isset( $terms_by_id[ $id ] ) ) {
4233              $terms_by_id[ $id ]->count = count( $items );
4234          }
4235      }
4236  }
4237  
4238  /**
4239   * Adds any terms from the given IDs to the cache that do not already exist in cache.
4240   *
4241   * @since 4.6.0
4242   * @since 6.1.0 This function is no longer marked as "private".
4243   * @since 6.3.0 Use wp_lazyload_term_meta() for lazy-loading of term meta.
4244   *
4245   * @global wpdb $wpdb WordPress database abstraction object.
4246   *
4247   * @param array $term_ids          Array of term IDs.
4248   * @param bool  $update_meta_cache Optional. Whether to update the meta cache. Default true.
4249   */
4250  function _prime_term_caches( $term_ids, $update_meta_cache = true ) {
4251      global $wpdb;
4252  
4253      $non_cached_ids = _get_non_cached_ids( $term_ids, 'terms' );
4254      if ( ! empty( $non_cached_ids ) ) {
4255          $fresh_terms = $wpdb->get_results( sprintf( "SELECT t.*, tt.* FROM $wpdb->terms AS t INNER JOIN $wpdb->term_taxonomy AS tt ON t.term_id = tt.term_id WHERE t.term_id IN (%s)", implode( ',', array_map( 'intval', $non_cached_ids ) ) ) );
4256  
4257          update_term_cache( $fresh_terms );
4258      }
4259  
4260      if ( $update_meta_cache ) {
4261          wp_lazyload_term_meta( $term_ids );
4262      }
4263  }
4264  
4265  //
4266  // Default callbacks.
4267  //
4268  
4269  /**
4270   * Updates term count based on object types of the current taxonomy.
4271   *
4272   * Private function for the default callback for post_tag and category
4273   * taxonomies.
4274   *
4275   * @access private
4276   * @since 2.3.0
4277   *
4278   * @global wpdb $wpdb WordPress database abstraction object.
4279   *
4280   * @param int[]       $terms    List of term taxonomy IDs.
4281   * @param WP_Taxonomy $taxonomy Current taxonomy object of terms.
4282   */
4283  function _update_post_term_count( $terms, $taxonomy ) {
4284      global $wpdb;
4285  
4286      $object_types = (array) $taxonomy->object_type;
4287  
4288      foreach ( $object_types as &$object_type ) {
4289          list( $object_type ) = explode( ':', $object_type );
4290      }
4291  
4292      $object_types = array_unique( $object_types );
4293  
4294      $check_attachments = array_search( 'attachment', $object_types, true );
4295      if ( false !== $check_attachments ) {
4296          unset( $object_types[ $check_attachments ] );
4297          $check_attachments = true;
4298      }
4299  
4300      if ( $object_types ) {
4301          $object_types = esc_sql( array_filter( $object_types, 'post_type_exists' ) );
4302      }
4303  
4304      $post_statuses = array( 'publish' );
4305  
4306      /**
4307       * Filters the post statuses for updating the term count.
4308       *
4309       * @since 5.7.0
4310       *
4311       * @param string[]    $post_statuses List of post statuses to include in the count. Default is 'publish'.
4312       * @param WP_Taxonomy $taxonomy      Current taxonomy object.
4313       */
4314      $post_statuses = esc_sql( apply_filters( 'update_post_term_count_statuses', $post_statuses, $taxonomy ) );
4315  
4316      foreach ( (array) $terms as $tt_id ) {
4317          $count = 0;
4318  
4319          // Attachments can be 'inherit' status, we need to base count off the parent's status if so.
4320          if ( $check_attachments ) {
4321              // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.QuotedDynamicPlaceholderGeneration
4322              $count += (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM $wpdb->term_relationships, $wpdb->posts p1 WHERE p1.ID = $wpdb->term_relationships.object_id AND ( post_status IN ('" . implode( "', '", $post_statuses ) . "') OR ( post_status = 'inherit' AND post_parent > 0 AND ( SELECT post_status FROM $wpdb->posts WHERE ID = p1.post_parent ) IN ('" . implode( "', '", $post_statuses ) . "') ) ) AND post_type = 'attachment' AND term_taxonomy_id = %d", $tt_id ) );
4323          }
4324  
4325          if ( $object_types ) {
4326              // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.QuotedDynamicPlaceholderGeneration
4327              $count += (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM $wpdb->term_relationships, $wpdb->posts WHERE $wpdb->posts.ID = $wpdb->term_relationships.object_id AND post_status IN ('" . implode( "', '", $post_statuses ) . "') AND post_type IN ('" . implode( "', '", $object_types ) . "') AND term_taxonomy_id = %d", $tt_id ) );
4328          }
4329  
4330          /**
4331           * Fires when a term count is calculated, before it is updated in the database.
4332           *
4333           * @since 6.9.0
4334           *
4335           * @param int    $tt_id         Term taxonomy ID.
4336           * @param string $taxonomy_name Taxonomy slug.
4337           * @param int    $count         Term count.
4338           */
4339          do_action( 'update_term_count', $tt_id, $taxonomy->name, $count );
4340  
4341          /** This action is documented in wp-includes/taxonomy.php */
4342          do_action( 'edit_term_taxonomy', $tt_id, $taxonomy->name, array() );
4343          $wpdb->update( $wpdb->term_taxonomy, compact( 'count' ), array( 'term_taxonomy_id' => $tt_id ) );
4344  
4345          /** This action is documented in wp-includes/taxonomy.php */
4346          do_action( 'edited_term_taxonomy', $tt_id, $taxonomy->name, array() );
4347      }
4348  }
4349  
4350  /**
4351   * Updates term count based on number of objects.
4352   *
4353   * Default callback for the 'link_category' taxonomy.
4354   *
4355   * @since 3.3.0
4356   *
4357   * @global wpdb $wpdb WordPress database abstraction object.
4358   *
4359   * @param int[]       $terms    List of term taxonomy IDs.
4360   * @param WP_Taxonomy $taxonomy Current taxonomy object of terms.
4361   */
4362  function _update_generic_term_count( $terms, $taxonomy ) {
4363      global $wpdb;
4364  
4365      foreach ( (array) $terms as $term ) {
4366          $count = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM $wpdb->term_relationships WHERE term_taxonomy_id = %d", $term ) );
4367  
4368          /** This action is documented in wp-includes/taxonomy.php */
4369          do_action( 'update_term_count', $term, $taxonomy->name, $count );
4370  
4371          /** This action is documented in wp-includes/taxonomy.php */
4372          do_action( 'edit_term_taxonomy', $term, $taxonomy->name, array() );
4373          $wpdb->update( $wpdb->term_taxonomy, compact( 'count' ), array( 'term_taxonomy_id' => $term ) );
4374  
4375          /** This action is documented in wp-includes/taxonomy.php */
4376          do_action( 'edited_term_taxonomy', $term, $taxonomy->name, array() );
4377      }
4378  }
4379  
4380  /**
4381   * Creates a new term for a term_taxonomy item that currently shares its term
4382   * with another term_taxonomy.
4383   *
4384   * @ignore
4385   * @since 4.2.0
4386   * @since 4.3.0 Introduced `$record` parameter. Also, `$term_id` and
4387   *              `$term_taxonomy_id` can now accept objects.
4388   *
4389   * @global wpdb $wpdb WordPress database abstraction object.
4390   *
4391   * @param int|object $term_id          ID of the shared term, or the shared term object.
4392   * @param int|object $term_taxonomy_id ID of the term_taxonomy item to receive a new term, or the term_taxonomy object
4393   *                                     (corresponding to a row from the term_taxonomy table).
4394   * @param bool       $record           Whether to record data about the split term in the options table. The recording
4395   *                                     process has the potential to be resource-intensive, so during batch operations
4396   *                                     it can be beneficial to skip inline recording and do it just once, after the
4397   *                                     batch is processed. Only set this to `false` if you know what you are doing.
4398   *                                     Default: true.
4399   * @return int|WP_Error When the current term does not need to be split (or cannot be split on the current
4400   *                      database schema), `$term_id` is returned. When the term is successfully split, the
4401   *                      new term_id is returned. A WP_Error is returned for miscellaneous errors.
4402   */
4403  function _split_shared_term( $term_id, $term_taxonomy_id, $record = true ) {
4404      global $wpdb;
4405  
4406      if ( is_object( $term_id ) ) {
4407          $shared_term = $term_id;
4408          $term_id     = (int) $shared_term->term_id;
4409      }
4410  
4411      if ( is_object( $term_taxonomy_id ) ) {
4412          $term_taxonomy    = $term_taxonomy_id;
4413          $term_taxonomy_id = (int) $term_taxonomy->term_taxonomy_id;
4414      }
4415  
4416      // If there are no shared term_taxonomy rows, there's nothing to do here.
4417      $shared_tt_count = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM $wpdb->term_taxonomy tt WHERE tt.term_id = %d AND tt.term_taxonomy_id != %d", $term_id, $term_taxonomy_id ) );
4418  
4419      if ( ! $shared_tt_count ) {
4420          return $term_id;
4421      }
4422  
4423      /*
4424       * Verify that the term_taxonomy_id passed to the function is actually associated with the term_id.
4425       * If there's a mismatch, it may mean that the term is already split. Return the actual term_id from the db.
4426       */
4427      $check_term_id = (int) $wpdb->get_var( $wpdb->prepare( "SELECT term_id FROM $wpdb->term_taxonomy WHERE term_taxonomy_id = %d", $term_taxonomy_id ) );
4428      if ( $check_term_id !== $term_id ) {
4429          return $check_term_id;
4430      }
4431  
4432      // Pull up data about the currently shared slug, which we'll use to populate the new one.
4433      if ( empty( $shared_term ) ) {
4434          $shared_term = $wpdb->get_row( $wpdb->prepare( "SELECT t.* FROM $wpdb->terms t WHERE t.term_id = %d", $term_id ) );
4435      }
4436  
4437      $new_term_data = array(
4438          'name'       => $shared_term->name,
4439          'slug'       => $shared_term->slug,
4440          'term_group' => $shared_term->term_group,
4441      );
4442  
4443      if ( false === $wpdb->insert( $wpdb->terms, $new_term_data ) ) {
4444          return new WP_Error( 'db_insert_error', __( 'Could not split shared term.' ), $wpdb->last_error );
4445      }
4446  
4447      $new_term_id = (int) $wpdb->insert_id;
4448  
4449      // Update the existing term_taxonomy to point to the newly created term.
4450      $wpdb->update(
4451          $wpdb->term_taxonomy,
4452          array( 'term_id' => $new_term_id ),
4453          array( 'term_taxonomy_id' => $term_taxonomy_id )
4454      );
4455  
4456      // Reassign child terms to the new parent.
4457      if ( empty( $term_taxonomy ) ) {
4458          $term_taxonomy = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $wpdb->term_taxonomy WHERE term_taxonomy_id = %d", $term_taxonomy_id ) );
4459      }
4460  
4461      $children_tt_ids = $wpdb->get_col( $wpdb->prepare( "SELECT term_taxonomy_id FROM $wpdb->term_taxonomy WHERE parent = %d AND taxonomy = %s", $term_id, $term_taxonomy->taxonomy ) );
4462      if ( ! empty( $children_tt_ids ) ) {
4463          foreach ( $children_tt_ids as $child_tt_id ) {
4464              $wpdb->update(
4465                  $wpdb->term_taxonomy,
4466                  array( 'parent' => $new_term_id ),
4467                  array( 'term_taxonomy_id' => $child_tt_id )
4468              );
4469              clean_term_cache( (int) $child_tt_id, '', false );
4470          }
4471      } else {
4472          // If the term has no children, we must force its taxonomy cache to be rebuilt separately.
4473          clean_term_cache( $new_term_id, $term_taxonomy->taxonomy, false );
4474      }
4475  
4476      clean_term_cache( $term_id, $term_taxonomy->taxonomy, false );
4477  
4478      /*
4479       * Taxonomy cache clearing is delayed to avoid race conditions that may occur when
4480       * regenerating the taxonomy's hierarchy tree.
4481       */
4482      $taxonomies_to_clean = array( $term_taxonomy->taxonomy );
4483  
4484      // Clean the cache for term taxonomies formerly shared with the current term.
4485      $shared_term_taxonomies = $wpdb->get_col( $wpdb->prepare( "SELECT taxonomy FROM $wpdb->term_taxonomy WHERE term_id = %d", $term_id ) );
4486      $taxonomies_to_clean    = array_merge( $taxonomies_to_clean, $shared_term_taxonomies );
4487  
4488      foreach ( $taxonomies_to_clean as $taxonomy_to_clean ) {
4489          clean_taxonomy_cache( $taxonomy_to_clean );
4490      }
4491  
4492      // Keep a record of term_ids that have been split, keyed by old term_id. See wp_get_split_term().
4493      if ( $record ) {
4494          $split_term_data = get_option( '_split_terms', array() );
4495          if ( ! isset( $split_term_data[ $term_id ] ) ) {
4496              $split_term_data[ $term_id ] = array();
4497          }
4498  
4499          $split_term_data[ $term_id ][ $term_taxonomy->taxonomy ] = $new_term_id;
4500          update_option( '_split_terms', $split_term_data );
4501      }
4502  
4503      // If we've just split the final shared term, set the "finished" flag.
4504      $shared_terms_exist = $wpdb->get_results(
4505          "SELECT tt.term_id, t.*, count(*) as term_tt_count FROM {$wpdb->term_taxonomy} tt
4506           LEFT JOIN {$wpdb->terms} t ON t.term_id = tt.term_id
4507           GROUP BY t.term_id
4508           HAVING term_tt_count > 1
4509           LIMIT 1"
4510      );
4511      if ( ! $shared_terms_exist ) {
4512          update_option( 'finished_splitting_shared_terms', true );
4513      }
4514  
4515      /**
4516       * Fires after a previously shared taxonomy term is split into two separate terms.
4517       *
4518       * @since 4.2.0
4519       *
4520       * @param int    $term_id          ID of the formerly shared term.
4521       * @param int    $new_term_id      ID of the new term created for the $term_taxonomy_id.
4522       * @param int    $term_taxonomy_id ID for the term_taxonomy row affected by the split.
4523       * @param string $taxonomy         Taxonomy for the split term.
4524       */
4525      do_action( 'split_shared_term', $term_id, $new_term_id, $term_taxonomy_id, $term_taxonomy->taxonomy );
4526  
4527      return $new_term_id;
4528  }
4529  
4530  /**
4531   * Splits a batch of shared taxonomy terms.
4532   *
4533   * @since 4.3.0
4534   *
4535   * @global wpdb $wpdb WordPress database abstraction object.
4536   */
4537  function _wp_batch_split_terms() {
4538      global $wpdb;
4539  
4540      $lock_name = 'term_split.lock';
4541  
4542      // Try to lock.
4543      $lock_result = $wpdb->query( $wpdb->prepare( "INSERT IGNORE INTO `$wpdb->options` ( `option_name`, `option_value`, `autoload` ) VALUES (%s, %s, 'off') /* LOCK */", $lock_name, time() ) );
4544  
4545      if ( ! $lock_result ) {
4546          $lock_result = get_option( $lock_name );
4547  
4548          // Bail if we were unable to create a lock, or if the existing lock is still valid.
4549          if ( ! $lock_result || ( $lock_result > ( time() - HOUR_IN_SECONDS ) ) ) {
4550              wp_schedule_single_event( time() + ( 5 * MINUTE_IN_SECONDS ), 'wp_split_shared_term_batch' );
4551              return;
4552          }
4553      }
4554  
4555      // Update the lock, as by this point we've definitely got a lock, just need to fire the actions.
4556      update_option( $lock_name, time() );
4557  
4558      // Get a list of shared terms (those with more than one associated row in term_taxonomy).
4559      $shared_terms = $wpdb->get_results(
4560          "SELECT tt.term_id, t.*, count(*) as term_tt_count FROM {$wpdb->term_taxonomy} tt
4561           LEFT JOIN {$wpdb->terms} t ON t.term_id = tt.term_id
4562           GROUP BY t.term_id
4563           HAVING term_tt_count > 1
4564           LIMIT 10"
4565      );
4566  
4567      // No more terms, we're done here.
4568      if ( ! $shared_terms ) {
4569          update_option( 'finished_splitting_shared_terms', true );
4570          delete_option( $lock_name );
4571          return;
4572      }
4573  
4574      // Shared terms found? We'll need to run this script again.
4575      wp_schedule_single_event( time() + ( 2 * MINUTE_IN_SECONDS ), 'wp_split_shared_term_batch' );
4576  
4577      // Rekey shared term array for faster lookups.
4578      $_shared_terms = array();
4579      foreach ( $shared_terms as $shared_term ) {
4580          $term_id                   = (int) $shared_term->term_id;
4581          $_shared_terms[ $term_id ] = $shared_term;
4582      }
4583      $shared_terms = $_shared_terms;
4584  
4585      // Get term taxonomy data for all shared terms.
4586      $shared_term_ids = implode( ',', array_keys( $shared_terms ) );
4587      $shared_tts      = $wpdb->get_results( "SELECT * FROM {$wpdb->term_taxonomy} WHERE `term_id` IN ({$shared_term_ids})" );
4588  
4589      // Split term data recording is slow, so we do it just once, outside the loop.
4590      $split_term_data    = get_option( '_split_terms', array() );
4591      $skipped_first_term = array();
4592      $taxonomies         = array();
4593      foreach ( $shared_tts as $shared_tt ) {
4594          $term_id = (int) $shared_tt->term_id;
4595  
4596          // Don't split the first tt belonging to a given term_id.
4597          if ( ! isset( $skipped_first_term[ $term_id ] ) ) {
4598              $skipped_first_term[ $term_id ] = 1;
4599              continue;
4600          }
4601  
4602          if ( ! isset( $split_term_data[ $term_id ] ) ) {
4603              $split_term_data[ $term_id ] = array();
4604          }
4605  
4606          // Keep track of taxonomies whose hierarchies need flushing.
4607          if ( ! isset( $taxonomies[ $shared_tt->taxonomy ] ) ) {
4608              $taxonomies[ $shared_tt->taxonomy ] = 1;
4609          }
4610  
4611          // Split the term.
4612          $split_term_data[ $term_id ][ $shared_tt->taxonomy ] = _split_shared_term( $shared_terms[ $term_id ], $shared_tt, false );
4613      }
4614  
4615      // Rebuild the cached hierarchy for each affected taxonomy.
4616      foreach ( array_keys( $taxonomies ) as $tax ) {
4617          delete_option( "{$tax}_children" );
4618          _get_term_hierarchy( $tax );
4619      }
4620  
4621      update_option( '_split_terms', $split_term_data );
4622  
4623      delete_option( $lock_name );
4624  }
4625  
4626  /**
4627   * In order to avoid the _wp_batch_split_terms() job being accidentally removed,
4628   * checks that it's still scheduled while we haven't finished splitting terms.
4629   *
4630   * @ignore
4631   * @since 4.3.0
4632   */
4633  function _wp_check_for_scheduled_split_terms() {
4634      if ( ! get_option( 'finished_splitting_shared_terms' ) && ! wp_next_scheduled( 'wp_split_shared_term_batch' ) ) {
4635          wp_schedule_single_event( time() + MINUTE_IN_SECONDS, 'wp_split_shared_term_batch' );
4636      }
4637  }
4638  
4639  /**
4640   * Checks default categories when a term gets split to see if any of them need to be updated.
4641   *
4642   * @ignore
4643   * @since 4.2.0
4644   *
4645   * @param int    $term_id          ID of the formerly shared term.
4646   * @param int    $new_term_id      ID of the new term created for the $term_taxonomy_id.
4647   * @param int    $term_taxonomy_id ID for the term_taxonomy row affected by the split.
4648   * @param string $taxonomy         Taxonomy for the split term.
4649   */
4650  function _wp_check_split_default_terms( $term_id, $new_term_id, $term_taxonomy_id, $taxonomy ) {
4651      if ( 'category' !== $taxonomy ) {
4652          return;
4653      }
4654  
4655      foreach ( array( 'default_category', 'default_link_category', 'default_email_category' ) as $option ) {
4656          if ( (int) get_option( $option, -1 ) === $term_id ) {
4657              update_option( $option, $new_term_id );
4658          }
4659      }
4660  }
4661  
4662  /**
4663   * Checks menu items when a term gets split to see if any of them need to be updated.
4664   *
4665   * @ignore
4666   * @since 4.2.0
4667   *
4668   * @global wpdb $wpdb WordPress database abstraction object.
4669   *
4670   * @param int    $term_id          ID of the formerly shared term.
4671   * @param int    $new_term_id      ID of the new term created for the $term_taxonomy_id.
4672   * @param int    $term_taxonomy_id ID for the term_taxonomy row affected by the split.
4673   * @param string $taxonomy         Taxonomy for the split term.
4674   */
4675  function _wp_check_split_terms_in_menus( $term_id, $new_term_id, $term_taxonomy_id, $taxonomy ) {
4676      global $wpdb;
4677      $post_ids = $wpdb->get_col(
4678          $wpdb->prepare(
4679              "SELECT m1.post_id
4680          FROM {$wpdb->postmeta} AS m1
4681              INNER JOIN {$wpdb->postmeta} AS m2 ON ( m2.post_id = m1.post_id )
4682              INNER JOIN {$wpdb->postmeta} AS m3 ON ( m3.post_id = m1.post_id )
4683          WHERE ( m1.meta_key = '_menu_item_type' AND m1.meta_value = 'taxonomy' )
4684              AND ( m2.meta_key = '_menu_item_object' AND m2.meta_value = %s )
4685              AND ( m3.meta_key = '_menu_item_object_id' AND m3.meta_value = %d )",
4686              $taxonomy,
4687              $term_id
4688          )
4689      );
4690  
4691      if ( $post_ids ) {
4692          foreach ( $post_ids as $post_id ) {
4693              update_post_meta( $post_id, '_menu_item_object_id', $new_term_id, $term_id );
4694          }
4695      }
4696  }
4697  
4698  /**
4699   * If the term being split is a nav_menu, changes associations.
4700   *
4701   * @ignore
4702   * @since 4.3.0
4703   *
4704   * @param int    $term_id          ID of the formerly shared term.
4705   * @param int    $new_term_id      ID of the new term created for the $term_taxonomy_id.
4706   * @param int    $term_taxonomy_id ID for the term_taxonomy row affected by the split.
4707   * @param string $taxonomy         Taxonomy for the split term.
4708   */
4709  function _wp_check_split_nav_menu_terms( $term_id, $new_term_id, $term_taxonomy_id, $taxonomy ) {
4710      if ( 'nav_menu' !== $taxonomy ) {
4711          return;
4712      }
4713  
4714      // Update menu locations.
4715      $locations = get_nav_menu_locations();
4716      foreach ( $locations as $location => $menu_id ) {
4717          if ( $term_id === $menu_id ) {
4718              $locations[ $location ] = $new_term_id;
4719          }
4720      }
4721      set_theme_mod( 'nav_menu_locations', $locations );
4722  }
4723  
4724  /**
4725   * Gets data about terms that previously shared a single term_id, but have since been split.
4726   *
4727   * @since 4.2.0
4728   *
4729   * @param int $old_term_id Term ID. This is the old, pre-split term ID.
4730   * @return array Array of new term IDs, keyed by taxonomy.
4731   */
4732  function wp_get_split_terms( $old_term_id ) {
4733      $split_terms = get_option( '_split_terms', array() );
4734  
4735      $terms = array();
4736      if ( isset( $split_terms[ $old_term_id ] ) ) {
4737          $terms = $split_terms[ $old_term_id ];
4738      }
4739  
4740      return $terms;
4741  }
4742  
4743  /**
4744   * Gets the new term ID corresponding to a previously split term.
4745   *
4746   * @since 4.2.0
4747   *
4748   * @param int    $old_term_id Term ID. This is the old, pre-split term ID.
4749   * @param string $taxonomy    Taxonomy that the term belongs to.
4750   * @return int|false If a previously split term is found corresponding to the old term_id and taxonomy,
4751   *                   the new term_id will be returned. If no previously split term is found matching
4752   *                   the parameters, returns false.
4753   */
4754  function wp_get_split_term( $old_term_id, $taxonomy ) {
4755      $split_terms = wp_get_split_terms( $old_term_id );
4756  
4757      $term_id = false;
4758      if ( isset( $split_terms[ $taxonomy ] ) ) {
4759          $term_id = (int) $split_terms[ $taxonomy ];
4760      }
4761  
4762      return $term_id;
4763  }
4764  
4765  /**
4766   * Determines whether a term is shared between multiple taxonomies.
4767   *
4768   * Shared taxonomy terms began to be split in 4.3, but failed cron tasks or
4769   * other delays in upgrade routines may cause shared terms to remain.
4770   *
4771   * @since 4.4.0
4772   *
4773   * @global wpdb $wpdb WordPress database abstraction object.
4774   *
4775   * @param int $term_id Term ID.
4776   * @return bool Returns false if a term is not shared between multiple taxonomies or
4777   *              if splitting shared taxonomy terms is finished.
4778   */
4779  function wp_term_is_shared( $term_id ) {
4780      global $wpdb;
4781  
4782      if ( get_option( 'finished_splitting_shared_terms' ) ) {
4783          return false;
4784      }
4785  
4786      $tt_count = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM $wpdb->term_taxonomy WHERE term_id = %d", $term_id ) );
4787  
4788      return $tt_count > 1;
4789  }
4790  
4791  /**
4792   * Generates a permalink for a taxonomy term archive.
4793   *
4794   * @since 2.5.0
4795   *
4796   * @global WP_Rewrite $wp_rewrite WordPress rewrite component.
4797   *
4798   * @param WP_Term|int|string $term     The term object, ID, or slug whose link will be retrieved.
4799   * @param string             $taxonomy Optional. Taxonomy. Default empty.
4800   * @return string|WP_Error URL of the taxonomy term archive on success, WP_Error if term does not exist.
4801   */
4802  function get_term_link( $term, $taxonomy = '' ) {
4803      global $wp_rewrite;
4804  
4805      if ( ! is_object( $term ) ) {
4806          if ( is_int( $term ) ) {
4807              $term = get_term( $term, $taxonomy );
4808          } else {
4809              $term = get_term_by( 'slug', $term, $taxonomy );
4810          }
4811      }
4812  
4813      if ( ! is_object( $term ) ) {
4814          $term = new WP_Error( 'invalid_term', __( 'Empty Term.' ) );
4815      }
4816  
4817      if ( is_wp_error( $term ) ) {
4818          return $term;
4819      }
4820  
4821      $taxonomy = $term->taxonomy;
4822  
4823      $termlink = $wp_rewrite->get_extra_permastruct( $taxonomy );
4824  
4825      /**
4826       * Filters the permalink structure for a term before token replacement occurs.
4827       *
4828       * @since 4.9.0
4829       *
4830       * @param string  $termlink The permalink structure for the term's taxonomy.
4831       * @param WP_Term $term     The term object.
4832       */
4833      $termlink = apply_filters( 'pre_term_link', $termlink, $term );
4834  
4835      $slug = $term->slug;
4836      $t    = get_taxonomy( $taxonomy );
4837  
4838      if ( empty( $termlink ) ) {
4839          if ( 'category' === $taxonomy ) {
4840              $termlink = '?cat=' . $term->term_id;
4841          } elseif ( $t->query_var ) {
4842              $termlink = "?$t->query_var=$slug";
4843          } else {
4844              $termlink = "?taxonomy=$taxonomy&term=$slug";
4845          }
4846          $termlink = home_url( $termlink );
4847      } else {
4848          if ( ! empty( $t->rewrite['hierarchical'] ) ) {
4849              $hierarchical_slugs = array();
4850              $ancestors          = get_ancestors( $term->term_id, $taxonomy, 'taxonomy' );
4851              foreach ( (array) $ancestors as $ancestor ) {
4852                  $ancestor_term        = get_term( $ancestor, $taxonomy );
4853                  $hierarchical_slugs[] = $ancestor_term->slug;
4854              }
4855              $hierarchical_slugs   = array_reverse( $hierarchical_slugs );
4856              $hierarchical_slugs[] = $slug;
4857              $termlink             = str_replace( "%$taxonomy%", implode( '/', $hierarchical_slugs ), $termlink );
4858          } else {
4859              $termlink = str_replace( "%$taxonomy%", $slug, $termlink );
4860          }
4861          $termlink = home_url( user_trailingslashit( $termlink, 'category' ) );
4862      }
4863  
4864      // Back compat filters.
4865      if ( 'post_tag' === $taxonomy ) {
4866  
4867          /**
4868           * Filters the tag link.
4869           *
4870           * @since 2.3.0
4871           * @since 2.5.0 Deprecated in favor of {@see 'term_link'} filter.
4872           * @since 5.4.1 Restored (un-deprecated).
4873           *
4874           * @param string $termlink Tag link URL.
4875           * @param int    $term_id  Term ID.
4876           */
4877          $termlink = apply_filters( 'tag_link', $termlink, $term->term_id );
4878      } elseif ( 'category' === $taxonomy ) {
4879  
4880          /**
4881           * Filters the category link.
4882           *
4883           * @since 1.5.0
4884           * @since 2.5.0 Deprecated in favor of {@see 'term_link'} filter.
4885           * @since 5.4.1 Restored (un-deprecated).
4886           *
4887           * @param string $termlink Category link URL.
4888           * @param int    $term_id  Term ID.
4889           */
4890          $termlink = apply_filters( 'category_link', $termlink, $term->term_id );
4891      }
4892  
4893      /**
4894       * Filters the term link.
4895       *
4896       * @since 2.5.0
4897       *
4898       * @param string  $termlink Term link URL.
4899       * @param WP_Term $term     Term object.
4900       * @param string  $taxonomy Taxonomy slug.
4901       */
4902      return apply_filters( 'term_link', $termlink, $term, $taxonomy );
4903  }
4904  
4905  /**
4906   * Displays the taxonomies of a post with available options.
4907   *
4908   * This function can be used within the loop to display the taxonomies for a
4909   * post without specifying the Post ID. You can also use it outside the Loop to
4910   * display the taxonomies for a specific post.
4911   *
4912   * @since 2.5.0
4913   *
4914   * @param array $args {
4915   *     Arguments about which post to use and how to format the output. Shares all of the arguments
4916   *     supported by get_the_taxonomies(), in addition to the following.
4917   *
4918   *     @type int|WP_Post $post   Post ID or object to get taxonomies of. Default current post.
4919   *     @type string      $before Displays before the taxonomies. Default empty string.
4920   *     @type string      $sep    Separates each taxonomy. Default is a space.
4921   *     @type string      $after  Displays after the taxonomies. Default empty string.
4922   * }
4923   */
4924  function the_taxonomies( $args = array() ) {
4925      $defaults = array(
4926          'post'   => 0,
4927          'before' => '',
4928          'sep'    => ' ',
4929          'after'  => '',
4930      );
4931  
4932      $parsed_args = wp_parse_args( $args, $defaults );
4933  
4934      echo $parsed_args['before'] . implode( $parsed_args['sep'], get_the_taxonomies( $parsed_args['post'], $parsed_args ) ) . $parsed_args['after'];
4935  }
4936  
4937  /**
4938   * Retrieves all taxonomies associated with a post.
4939   *
4940   * This function can be used within the loop. It will also return an array of
4941   * the taxonomies with links to the taxonomy and name.
4942   *
4943   * @since 2.5.0
4944   *
4945   * @param int|WP_Post $post Optional. Post ID or WP_Post object. Default is global $post.
4946   * @param array       $args {
4947   *           Optional. Arguments about how to format the list of taxonomies. Default empty array.
4948   *
4949   *     @type string $template      Template for displaying a taxonomy label and list of terms.
4950   *                                 Default is "Label: Terms."
4951   *     @type string $term_template Template for displaying a single term in the list. Default is the term name
4952   *                                 linked to its archive.
4953   * }
4954   * @return string[] List of taxonomies.
4955   */
4956  function get_the_taxonomies( $post = 0, $args = array() ) {
4957      $post = get_post( $post );
4958  
4959      $args = wp_parse_args(
4960          $args,
4961          array(
4962              /* translators: %s: Taxonomy label, %l: List of terms formatted as per $term_template. */
4963              'template'      => __( '%s: %l.' ),
4964              'term_template' => '<a href="%1$s">%2$s</a>',
4965          )
4966      );
4967  
4968      $taxonomies = array();
4969  
4970      if ( ! $post ) {
4971          return $taxonomies;
4972      }
4973  
4974      foreach ( get_object_taxonomies( $post ) as $taxonomy ) {
4975          $t = (array) get_taxonomy( $taxonomy );
4976          if ( empty( $t['label'] ) ) {
4977              $t['label'] = $taxonomy;
4978          }
4979          if ( empty( $t['args'] ) ) {
4980              $t['args'] = array();
4981          }
4982          if ( empty( $t['template'] ) ) {
4983              $t['template'] = $args['template'];
4984          }
4985          if ( empty( $t['term_template'] ) ) {
4986              $t['term_template'] = $args['term_template'];
4987          }
4988  
4989          $terms = get_object_term_cache( $post->ID, $taxonomy );
4990          if ( false === $terms ) {
4991              $terms = wp_get_object_terms( $post->ID, $taxonomy, $t['args'] );
4992          }
4993          $links = array();
4994  
4995          foreach ( $terms as $term ) {
4996              $links[] = wp_sprintf( $t['term_template'], esc_attr( get_term_link( $term ) ), $term->name );
4997          }
4998          if ( $links ) {
4999              $taxonomies[ $taxonomy ] = wp_sprintf( $t['template'], $t['label'], $links, $terms );
5000          }
5001      }
5002      return $taxonomies;
5003  }
5004  
5005  /**
5006   * Retrieves all taxonomy names for the given post.
5007   *
5008   * @since 2.5.0
5009   *
5010   * @param int|WP_Post $post Optional. Post ID or WP_Post object. Default is global $post.
5011   * @return string[] An array of all taxonomy names for the given post.
5012   */
5013  function get_post_taxonomies( $post = 0 ) {
5014      $post = get_post( $post );
5015  
5016      return get_object_taxonomies( $post );
5017  }
5018  
5019  /**
5020   * Determines if the given object is associated with any of the given terms.
5021   *
5022   * The given terms are checked against the object's terms' term_ids, names and slugs.
5023   * Terms given as integers will only be checked against the object's terms' term_ids.
5024   * If no terms are given, determines if object is associated with any terms in the given taxonomy.
5025   *
5026   * @since 2.7.0
5027   *
5028   * @param int                       $object_id ID of the object (post ID, link ID, ...).
5029   * @param string                    $taxonomy  Single taxonomy name.
5030   * @param int|string|int[]|string[] $terms     Optional. Term ID, name, slug, or array of such
5031   *                                             to check against. Default null.
5032   * @return bool|WP_Error WP_Error on input error.
5033   */
5034  function is_object_in_term( $object_id, $taxonomy, $terms = null ) {
5035      $object_id = (int) $object_id;
5036      if ( ! $object_id ) {
5037          return new WP_Error( 'invalid_object', __( 'Invalid object ID.' ) );
5038      }
5039  
5040      $object_terms = get_object_term_cache( $object_id, $taxonomy );
5041      if ( false === $object_terms ) {
5042          $object_terms = wp_get_object_terms( $object_id, $taxonomy, array( 'update_term_meta_cache' => false ) );
5043          if ( is_wp_error( $object_terms ) ) {
5044              return $object_terms;
5045          }
5046  
5047          wp_cache_set( $object_id, wp_list_pluck( $object_terms, 'term_id' ), "{$taxonomy}_relationships" );
5048      }
5049  
5050      if ( is_wp_error( $object_terms ) ) {
5051          return $object_terms;
5052      }
5053  
5054      if ( empty( $object_terms ) ) {
5055          return false;
5056      }
5057  
5058      if ( empty( $terms ) ) {
5059          return true;
5060      }
5061  
5062      $terms = (array) $terms;
5063  
5064      $ints = array_filter( $terms, 'is_int' );
5065      if ( $ints ) {
5066          $strs = array_diff( $terms, $ints );
5067      } else {
5068          $strs =& $terms;
5069      }
5070  
5071      foreach ( $object_terms as $object_term ) {
5072          // If term is an int, check against term_ids only.
5073          if ( $ints && in_array( $object_term->term_id, $ints, true ) ) {
5074              return true;
5075          }
5076  
5077          if ( $strs ) {
5078              // Only check numeric strings against term_id, to avoid false matches due to type juggling.
5079              $numeric_strs = array_map( 'intval', array_filter( $strs, 'is_numeric' ) );
5080              if ( in_array( $object_term->term_id, $numeric_strs, true ) ) {
5081                  return true;
5082              }
5083  
5084              if ( in_array( $object_term->name, $strs, true ) ) {
5085                  return true;
5086              }
5087              if ( in_array( $object_term->slug, $strs, true ) ) {
5088                  return true;
5089              }
5090          }
5091      }
5092  
5093      return false;
5094  }
5095  
5096  /**
5097   * Determines if the given object type is associated with the given taxonomy.
5098   *
5099   * @since 3.0.0
5100   *
5101   * @param string $object_type Object type string.
5102   * @param string $taxonomy    Single taxonomy name.
5103   * @return bool True if object is associated with the taxonomy, otherwise false.
5104   */
5105  function is_object_in_taxonomy( $object_type, $taxonomy ) {
5106      $taxonomies = get_object_taxonomies( $object_type );
5107      if ( empty( $taxonomies ) ) {
5108          return false;
5109      }
5110      return in_array( $taxonomy, $taxonomies, true );
5111  }
5112  
5113  /**
5114   * Gets an array of ancestor IDs for a given object.
5115   *
5116   * @since 3.1.0
5117   * @since 4.1.0 Introduced the `$resource_type` argument.
5118   *
5119   * @param int    $object_id     Optional. The ID of the object. Default 0.
5120   * @param string $object_type   Optional. The type of object for which we'll be retrieving
5121   *                              ancestors. Accepts a post type or a taxonomy name. Default empty.
5122   * @param string $resource_type Optional. Type of resource $object_type is. Accepts 'post_type'
5123   *                              or 'taxonomy'. Default empty.
5124   * @return int[] An array of IDs of ancestors from lowest to highest in the hierarchy.
5125   */
5126  function get_ancestors( $object_id = 0, $object_type = '', $resource_type = '' ) {
5127      $object_id = (int) $object_id;
5128  
5129      $ancestors = array();
5130  
5131      if ( empty( $object_id ) ) {
5132  
5133          /** This filter is documented in wp-includes/taxonomy.php */
5134          return apply_filters( 'get_ancestors', $ancestors, $object_id, $object_type, $resource_type );
5135      }
5136  
5137      if ( ! $resource_type ) {
5138          if ( is_taxonomy_hierarchical( $object_type ) ) {
5139              $resource_type = 'taxonomy';
5140          } elseif ( post_type_exists( $object_type ) ) {
5141              $resource_type = 'post_type';
5142          }
5143      }
5144  
5145      if ( 'taxonomy' === $resource_type ) {
5146          $term = get_term( $object_id, $object_type );
5147          while ( ! is_wp_error( $term ) && ! empty( $term->parent ) && ! in_array( $term->parent, $ancestors, true ) ) {
5148              $ancestors[] = (int) $term->parent;
5149              $term        = get_term( $term->parent, $object_type );
5150          }
5151      } elseif ( 'post_type' === $resource_type ) {
5152          $ancestors = get_post_ancestors( $object_id );
5153      }
5154  
5155      /**
5156       * Filters a given object's ancestors.
5157       *
5158       * @since 3.1.0
5159       * @since 4.1.1 Introduced the `$resource_type` parameter.
5160       *
5161       * @param int[]  $ancestors     An array of IDs of object ancestors.
5162       * @param int    $object_id     Object ID.
5163       * @param string $object_type   Type of object.
5164       * @param string $resource_type Type of resource $object_type is.
5165       */
5166      return apply_filters( 'get_ancestors', $ancestors, $object_id, $object_type, $resource_type );
5167  }
5168  
5169  /**
5170   * Returns the term's parent's term ID.
5171   *
5172   * @since 3.1.0
5173   *
5174   * @param int    $term_id  Term ID.
5175   * @param string $taxonomy Taxonomy name.
5176   * @return int|false Parent term ID on success, false on failure.
5177   */
5178  function wp_get_term_taxonomy_parent_id( $term_id, $taxonomy ) {
5179      $term = get_term( $term_id, $taxonomy );
5180      if ( ! $term || is_wp_error( $term ) ) {
5181          return false;
5182      }
5183      return (int) $term->parent;
5184  }
5185  
5186  /**
5187   * Checks the given subset of the term hierarchy for hierarchy loops.
5188   * Prevents loops from forming and breaks those that it finds.
5189   *
5190   * Attached to the {@see 'wp_update_term_parent'} filter.
5191   *
5192   * @since 3.1.0
5193   *
5194   * @param int    $parent_term `term_id` of the parent for the term we're checking.
5195   * @param int    $term_id     The term we're checking.
5196   * @param string $taxonomy    The taxonomy of the term we're checking.
5197   * @return int The new parent for the term.
5198   */
5199  function wp_check_term_hierarchy_for_loops( $parent_term, $term_id, $taxonomy ) {
5200      // Nothing fancy here - bail.
5201      if ( ! $parent_term ) {
5202          return 0;
5203      }
5204  
5205      // Can't be its own parent.
5206      if ( $parent_term === $term_id ) {
5207          return 0;
5208      }
5209  
5210      // Now look for larger loops.
5211      $loop = wp_find_hierarchy_loop( 'wp_get_term_taxonomy_parent_id', $term_id, $parent_term, array( $taxonomy ) );
5212      if ( ! $loop ) {
5213          return $parent_term; // No loop.
5214      }
5215  
5216      // Setting $parent_term to the given value causes a loop.
5217      if ( isset( $loop[ $term_id ] ) ) {
5218          return 0;
5219      }
5220  
5221      // There's a loop, but it doesn't contain $term_id. Break the loop.
5222      foreach ( array_keys( $loop ) as $loop_member ) {
5223          wp_update_term( $loop_member, $taxonomy, array( 'parent' => 0 ) );
5224      }
5225  
5226      return $parent_term;
5227  }
5228  
5229  /**
5230   * Determines whether a taxonomy is considered "viewable".
5231   *
5232   * @since 5.1.0
5233   *
5234   * @param string|WP_Taxonomy $taxonomy Taxonomy name or object.
5235   * @return bool Whether the taxonomy should be considered viewable.
5236   */
5237  function is_taxonomy_viewable( $taxonomy ) {
5238      if ( is_scalar( $taxonomy ) ) {
5239          $taxonomy = get_taxonomy( $taxonomy );
5240          if ( ! $taxonomy ) {
5241              return false;
5242          }
5243      }
5244  
5245      return $taxonomy->publicly_queryable;
5246  }
5247  
5248  /**
5249   * Determines whether a term is publicly viewable.
5250   *
5251   * A term is considered publicly viewable if its taxonomy is viewable.
5252   *
5253   * @since 6.1.0
5254   *
5255   * @param int|WP_Term $term Term ID or term object.
5256   * @return bool Whether the term is publicly viewable.
5257   */
5258  function is_term_publicly_viewable( $term ) {
5259      $term = get_term( $term );
5260  
5261      if ( ! $term ) {
5262          return false;
5263      }
5264  
5265      return is_taxonomy_viewable( $term->taxonomy );
5266  }
5267  
5268  /**
5269   * Sets the last changed time for the 'terms' cache group.
5270   *
5271   * @since 5.0.0
5272   */
5273  function wp_cache_set_terms_last_changed() {
5274      wp_cache_set_last_changed( 'terms' );
5275  }
5276  
5277  /**
5278   * Aborts calls to term meta if it is not supported.
5279   *
5280   * @since 5.0.0
5281   *
5282   * @param mixed $check Skip-value for whether to proceed term meta function execution.
5283   * @return mixed Original value of $check, or false if term meta is not supported.
5284   */
5285  function wp_check_term_meta_support_prefilter( $check ) {
5286      if ( get_option( 'db_version' ) < 34370 ) {
5287          return false;
5288      }
5289  
5290      return $check;
5291  }


Generated : Sun Sep 20 08:20:30 2026 Cross-referenced by PHPXref