exists under. * * @since 2.3.0 * * @global wpdb $wpdb WordPress database abstraction object. * * @param int $object_id The object to relate to. * @param string|int|array $terms A single term slug, single term ID, or array of either term slugs or IDs. * Will replace all existing related terms in this taxonomy. Passing an * empty value will remove all related terms. * @param string $taxonomy The context in which to relate the term to the object. * @param bool $append Optional. If false will delete difference of terms. Default false. * @return array|WP_Error Term taxonomy IDs of the affected terms or WP_Error on failure. */ function wp_set_object_terms( $object_id, $terms, $taxonomy, $append = false ) { global $wpdb; $object_id = (int) $object_id; if ( ! taxonomy_exists( $taxonomy ) ) { return new WP_Error( 'invalid_taxonomy', __( 'Invalid taxonomy.' ) ); } if ( ! is_array( $terms ) ) { $terms = array( $terms ); } if ( ! $append ) { $old_tt_ids = wp_get_object_terms( $object_id, $taxonomy, array( 'fields' => 'tt_ids', 'orderby' => 'none', 'update_term_meta_cache' => false, ) ); } else { $old_tt_ids = array(); } $tt_ids = array(); $term_ids = array(); $new_tt_ids = array(); foreach ( (array) $terms as $term ) { if ( '' === trim( $term ) ) { continue; } $term_info = term_exists( $term, $taxonomy ); if ( ! $term_info ) { // Skip if a non-existent term ID is passed. if ( is_int( $term ) ) { continue; } $term_info = wp_insert_term( $term, $taxonomy ); } if ( is_wp_error( $term_info ) ) { return $term_info; } $term_ids[] = $term_info['term_id']; $tt_id = $term_info['term_taxonomy_id']; $tt_ids[] = $tt_id; 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 ) ) ) { continue; } /** * Fires immediately before an object-term relationship is added. * * @since 2.9.0 * @since 4.7.0 Added the `$taxonomy` parameter. * * @param int $object_id Object ID. * @param int $tt_id Term taxonomy ID. * @param string $taxonomy Taxonomy slug. */ do_action( 'add_term_relationship', $object_id, $tt_id, $taxonomy ); $wpdb->insert( $wpdb->term_relationships, array( 'object_id' => $object_id, 'term_taxonomy_id' => $tt_id, ) ); /** * Fires immediately after an object-term relationship is added. * * @since 2.9.0 * @since 4.7.0 Added the `$taxonomy` parameter. * * @param int $object_id Object ID. * @param int $tt_id Term taxonomy ID. * @param string $taxonomy Taxonomy slug. */ do_action( 'added_term_relationship', $object_id, $tt_id, $taxonomy ); $new_tt_ids[] = $tt_id; } if ( $new_tt_ids ) { wp_update_term_count( $new_tt_ids, $taxonomy ); } if ( ! $append ) { $delete_tt_ids = array_diff( $old_tt_ids, $tt_ids ); if ( $delete_tt_ids ) { $in_delete_tt_ids = "'" . implode( "', '", $delete_tt_ids ) . "'"; $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 ) ); $delete_term_ids = array_map( 'intval', $delete_term_ids ); $remove = wp_remove_object_terms( $object_id, $delete_term_ids, $taxonomy ); if ( is_wp_error( $remove ) ) { return $remove; } } } $t = get_taxonomy( $taxonomy ); if ( ! $append && isset( $t->sort ) && $t->sort ) { $values = array(); $term_order = 0; $final_tt_ids = wp_get_object_terms( $object_id, $taxonomy, array( 'fields' => 'tt_ids', 'update_term_meta_cache' => false, ) ); foreach ( $tt_ids as $tt_id ) { if ( in_array( (int) $tt_id, $final_tt_ids, true ) ) { $values[] = $wpdb->prepare( '(%d, %d, %d)', $object_id, $tt_id, ++$term_order ); } } if ( $values ) { 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)' ) ) { return new WP_Error( 'db_insert_error', __( 'Could not insert term relationship into the database.' ), $wpdb->last_error ); } } } wp_cache_delete( $object_id, $taxonomy . '_relationships' ); wp_cache_delete( 'last_changed', 'terms' ); /** * Fires after an object's terms have been set. * * @since 2.8.0 * * @param int $object_id Object ID. * @param array $terms An array of object terms. * @param array $tt_ids An array of term taxonomy IDs. * @param string $taxonomy Taxonomy slug. * @param bool $append Whether to append new terms to the old terms. * @param array $old_tt_ids Old array of term taxonomy IDs. */ do_action( 'set_object_terms', $object_id, $terms, $tt_ids, $taxonomy, $append, $old_tt_ids ); return $tt_ids; } /** * Add term(s) associated with a given object. * * @since 3.6.0 * * @param int $object_id The ID of the object to which the terms will be added. * @param string|int|array $terms The slug(s) or ID(s) of the term(s) to add. * @param array|string $taxonomy Taxonomy name. * @return array|WP_Error Term taxonomy IDs of the affected terms. */ function wp_add_object_terms( $object_id, $terms, $taxonomy ) { return wp_set_object_terms( $object_id, $terms, $taxonomy, true ); } /** * Remove term(s) associated with a given object. * * @since 3.6.0 * * @global wpdb $wpdb WordPress database abstraction object. * * @param int $object_id The ID of the object from which the terms will be removed. * @param string|int|array $terms The slug(s) or ID(s) of the term(s) to remove. * @param array|string $taxonomy Taxonomy name. * @return bool|WP_Error True on success, false or WP_Error on failure. */ function wp_remove_object_terms( $object_id, $terms, $taxonomy ) { global $wpdb; $object_id = (int) $object_id; if ( ! taxonomy_exists( $taxonomy ) ) { return new WP_Error( 'invalid_taxonomy', __( 'Invalid taxonomy.' ) ); } if ( ! is_array( $terms ) ) { $terms = array( $terms ); } $tt_ids = array(); foreach ( (array) $terms as $term ) { if ( '' === trim( $term ) ) { continue; } $term_info = term_exists( $term, $taxonomy ); if ( ! $term_info ) { // Skip if a non-existent term ID is passed. if ( is_int( $term ) ) { continue; } } if ( is_wp_error( $term_info ) ) { return $term_info; } $tt_ids[] = $term_info['term_taxonomy_id']; } if ( $tt_ids ) { $in_tt_ids = "'" . implode( "', '", $tt_ids ) . "'"; /** * Fires immediately before an object-term relationship is deleted. * * @since 2.9.0 * @since 4.7.0 Added the `$taxonomy` parameter. * * @param int $object_id Object ID. * @param array $tt_ids An array of term taxonomy IDs. * @param string $taxonomy Taxonomy slug. */ do_action( 'delete_term_relationships', $object_id, $tt_ids, $taxonomy ); $deleted = $wpdb->query( $wpdb->prepare( "DELETE FROM $wpdb->term_relationships WHERE object_id = %d AND term_taxonomy_id IN ($in_tt_ids)", $object_id ) ); wp_cache_delete( $object_id, $taxonomy . '_relationships' ); wp_cache_delete( 'last_changed', 'terms' ); /** * Fires immediately after an object-term relationship is deleted. * * @since 2.9.0 * @since 4.7.0 Added the `$taxonomy` parameter. * * @param int $object_id Object ID. * @param array $tt_ids An array of term taxonomy IDs. * @param string $taxonomy Taxonomy slug. */ do_action( 'deleted_term_relationships', $object_id, $tt_ids, $taxonomy ); wp_update_term_count( $tt_ids, $taxonomy ); return (bool) $deleted; } return false; } /** * Will make slug unique, if it isn't already. * * The `$slug` has to be unique global to every taxonomy, meaning that one * taxonomy term can't have a matching slug with another taxonomy term. Each * slug has to be globally unique for every taxonomy. * * The way this works is that if the taxonomy that the term belongs to is * hierarchical and has a parent, it will append that parent to the $slug. * * If that still doesn't return a unique slug, then it tries to append a number * until it finds a number that is truly unique. * * The only purpose for `$term` is for appending a parent, if one exists. * * @since 2.3.0 * * @global wpdb $wpdb WordPress database abstraction object. * * @param string $slug The string that will be tried for a unique slug. * @param object $term The term object that the `$slug` will belong to. * @return string Will return a true unique slug. */ function wp_unique_term_slug( $slug, $term ) { global $wpdb; $needs_suffix = true; $original_slug = $slug; // As of 4.1, duplicate slugs are allowed as long as they're in different taxonomies. if ( ! term_exists( $slug ) || get_option( 'db_version' ) >= 30133 && ! get_term_by( 'slug', $slug, $term->taxonomy ) ) { $needs_suffix = false; } /* * If the taxonomy supports hierarchy and the term has a parent, make the slug unique * by incorporating parent slugs. */ $parent_suffix = ''; if ( $needs_suffix && is_taxonomy_hierarchical( $term->taxonomy ) && ! empty( $term->parent ) ) { $the_parent = $term->parent; while ( ! empty( $the_parent ) ) { $parent_term = get_term( $the_parent, $term->taxonomy ); if ( is_wp_error( $parent_term ) || empty( $parent_term ) ) { break; } $parent_suffix .= '-' . $parent_term->slug; if ( ! term_exists( $slug . $parent_suffix ) ) { break; } if ( empty( $parent_term->parent ) ) { break; } $the_parent = $parent_term->parent; } } // If we didn't get a unique slug, try appending a number to make it unique. /** * Filters whether the proposed unique term slug is bad. * * @since 4.3.0 * * @param bool $needs_suffix Whether the slug needs to be made unique with a suffix. * @param string $slug The slug. * @param object $term Term object. */ if ( apply_filters( 'wp_unique_term_slug_is_bad_slug', $needs_suffix, $slug, $term ) ) { if ( $parent_suffix ) { $slug .= $parent_suffix; } if ( ! empty( $term->term_id ) ) { $query = $wpdb->prepare( "SELECT slug FROM $wpdb->terms WHERE slug = %s AND term_id != %d", $slug, $term->term_id ); } else { $query = $wpdb->prepare( "SELECT slug FROM $wpdb->terms WHERE slug = %s", $slug ); } if ( $wpdb->get_var( $query ) ) { // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared $num = 2; do { $alt_slug = $slug . "-$num"; $num++; $slug_check = $wpdb->get_var( $wpdb->prepare( "SELECT slug FROM $wpdb->terms WHERE slug = %s", $alt_slug ) ); } while ( $slug_check ); $slug = $alt_slug; } } /** * Filters the unique term slug. * * @since 4.3.0 * * @param string $slug Unique term slug. * @param object $term Term object. * @param string $original_slug Slug originally passed to the function for testing. */ return apply_filters( 'wp_unique_term_slug', $slug, $term, $original_slug ); } /** * Update term based on arguments provided. * * The `$args` will indiscriminately override all values with the same field name. * Care must be taken to not override important information need to update or * update will fail (or perhaps create a new term, neither would be acceptable). * * Defaults will set 'alias_of', 'description', 'parent', and 'slug' if not * defined in `$args` already. * * 'alias_of' will create a term group, if it doesn't already exist, and * update it for the `$term`. * * If the 'slug' argument in `$args` is missing, then the 'name' will be used. * If you set 'slug' and it isn't unique, then a WP_Error is returned. * If you don't pass any slug, then a unique one will be created. * * @since 2.3.0 * * @global wpdb $wpdb WordPress database abstraction object. * * @param int $term_id The ID of the term. * @param string $taxonomy The taxonomy of the term. * @param array|string $args { * Optional. Array or string of arguments for updating a term. * * @type string $alias_of Slug of the term to make this term an alias of. * Default empty string. Accepts a term slug. * @type string $description The term description. Default empty string. * @type int $parent The id of the parent term. Default 0. * @type string $slug The term slug to use. Default empty string. * } * @return array|WP_Error An array containing the `term_id` and `term_taxonomy_id`, * WP_Error otherwise. */ function wp_update_term( $term_id, $taxonomy, $args = array() ) { global $wpdb; if ( ! taxonomy_exists( $taxonomy ) ) { return new WP_Error( 'invalid_taxonomy', __( 'Invalid taxonomy.' ) ); } $term_id = (int) $term_id; // First, get all of the original args. $term = get_term( $term_id, $taxonomy ); if ( is_wp_error( $term ) ) { return $term; } if ( ! $term ) { return new WP_Error( 'invalid_term', __( 'Empty Term.' ) ); } $term = (array) $term->data; // Escape data pulled from DB. $term = wp_slash( $term ); // Merge old and new args with new args overwriting old ones. $args = array_merge( $term, $args ); $defaults = array( 'alias_of' => '', 'description' => '', 'parent' => 0, 'slug' => '', ); $args = wp_parse_args( $args, $defaults ); $args = sanitize_term( $args, $taxonomy, 'db' ); $parsed_args = $args; // expected_slashed ($name) $name = wp_unslash( $args['name'] ); $description = wp_unslash( $args['description'] ); $parsed_args['name'] = $name; $parsed_args['description'] = $description; if ( '' === trim( $name ) ) { return new WP_Error( 'empty_term_name', __( 'A name is required for this term.' ) ); } if ( (int) $parsed_args['parent'] > 0 && ! term_exists( (int) $parsed_args['parent'] ) ) { return new WP_Error( 'missing_parent', __( 'Parent term does not exist.' ) ); } $empty_slug = false; if ( empty( $args['slug'] ) ) { $empty_slug = true; $slug = sanitize_title( $name ); } else { $slug = $args['slug']; } $parsed_args['slug'] = $slug; $term_group = isset( $parsed_args['term_group'] ) ? $parsed_args['term_group'] : 0; if ( $args['alias_of'] ) { $alias = get_term_by( 'slug', $args['alias_of'], $taxonomy ); if ( ! empty( $alias->term_group ) ) { // The alias we want is already in a group, so let's use that one. $term_group = $alias->term_group; } elseif ( ! empty( $alias->term_id ) ) { /* * The alias is not in a group, so we create a new one * and add the alias to it. */ $term_group = $wpdb->get_var( "SELECT MAX(term_group) FROM $wpdb->terms" ) + 1; wp_update_term( $alias->term_id, $taxonomy, array( 'term_group' => $term_group, ) ); } $parsed_args['term_group'] = $term_group; } /** * Filters the term parent. * * Hook to this filter to see if it will cause a hierarchy loop. * * @since 3.1.0 * * @param int $parent ID of the parent term. * @param int $term_id Term ID. * @param string $taxonomy Taxonomy slug. * @param array $parsed_args An array of potentially altered update arguments for the given term. * @param array $args An array of update arguments for the given term. */ $parent = (int) apply_filters( 'wp_update_term_parent', $args['parent'], $term_id, $taxonomy, $parsed_args, $args ); // Check for duplicate slug. $duplicate = get_term_by( 'slug', $slug, $taxonomy ); if ( $duplicate && $duplicate->term_id !== $term_id ) { // If an empty slug was passed or the parent changed, reset the slug to something unique. // Otherwise, bail. if ( $empty_slug || ( $parent !== (int) $term['parent'] ) ) { $slug = wp_unique_term_slug( $slug, (object) $args ); } else { /* translators: %s: Taxonomy term slug. */ return new WP_Error( 'duplicate_term_slug', sprintf( __( 'The slug “%s” is already in use by another term.' ), $slug ) ); } } $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 ) ); // Check whether this is a shared term that needs splitting. $_term_id = _split_shared_term( $term_id, $tt_id ); if ( ! is_wp_error( $_term_id ) ) { $term_id = $_term_id; } /** * Fires immediately before the given terms are edited. * * @since 2.9.0 * * @param int $term_id Term ID. * @param string $taxonomy Taxonomy slug. */ do_action( 'edit_terms', $term_id, $taxonomy ); $data = compact( 'name', 'slug', 'term_group' ); /** * Filters term data before it is updated in the database. * * @since 4.7.0 * * @param array $data Term data to be updated. * @param int $term_id Term ID. * @param string $taxonomy Taxonomy slug. * @param array $args Arguments passed to wp_update_term(). */ $data = apply_filters( 'wp_update_term_data', $data, $term_id, $taxonomy, $args ); $wpdb->update( $wpdb->terms, $data, compact( 'term_id' ) ); if ( empty( $slug ) ) { $slug = sanitize_title( $name, $term_id ); $wpdb->update( $wpdb->terms, compact( 'slug' ), compact( 'term_id' ) ); } /** * Fires immediately after a term is updated in the database, but before its * term-taxonomy relationship is updated. * * @since 2.9.0 * * @param int $term_id Term ID * @param string $taxonomy Taxonomy slug. */ do_action( 'edited_terms', $term_id, $taxonomy ); /** * Fires immediate before a term-taxonomy relationship is updated. * * @since 2.9.0 * * @param int $tt_id Term taxonomy ID. * @param string $taxonomy Taxonomy slug. */ do_action( 'edit_term_taxonomy', $tt_id, $taxonomy ); $wpdb->update( $wpdb->term_taxonomy, compact( 'term_id', 'taxonomy', 'description', 'parent' ), array( 'term_taxonomy_id' => $tt_id ) ); /** * Fires immediately after a term-taxonomy relationship is updated. * * @since 2.9.0 * * @param int $tt_id Term taxonomy ID. * @param string $taxonomy Taxonomy slug. */ do_action( 'edited_term_taxonomy', $tt_id, $taxonomy ); /** * Fires after a term has been updated, but before the term cache has been cleaned. * * The {@see 'edit_$taxonomy'} hook is also available for targeting a specific * taxonomy. * * @since 2.3.0 * * @param int $term_id Term ID. * @param int $tt_id Term taxonomy ID. * @param string $taxonomy Taxonomy slug. */ do_action( 'edit_term', $term_id, $tt_id, $taxonomy ); /** * Fires after a term in a specific taxonomy has been updated, but before the term * cache has been cleaned. * * The dynamic portion of the hook name, `$taxonomy`, refers to the taxonomy slug. * * @since 2.3.0 * * @param int $term_id Term ID. * @param int $tt_id Term taxonomy ID. */ do_action( "edit_{$taxonomy}", $term_id, $tt_id ); /** This filter is documented in wp-includes/taxonomy.php */ $term_id = apply_filters( 'term_id_filter', $term_id, $tt_id ); clean_term_cache( $term_id, $taxonomy ); /** * Fires after a term has been updated, and the term cache has been cleaned. * * The {@see 'edited_$taxonomy'} hook is also available for targeting a specific * taxonomy. * * @since 2.3.0 * * @param int $term_id Term ID. * @param int $tt_id Term taxonomy ID. * @param string $taxonomy Taxonomy slug. */ do_action( 'edited_term', $term_id, $tt_id, $taxonomy ); /** * Fires after a term for a specific taxonomy has been updated, and the term * cache has been cleaned. * * The dynamic portion of the hook name, `$taxonomy`, refers to the taxonomy slug. * * @since 2.3.0 * * @param int $term_id Term ID. * @param int $tt_id Term taxonomy ID. */ do_action( "edited_{$taxonomy}", $term_id, $tt_id ); /** This action is documented in wp-includes/taxonomy.php */ do_action( 'saved_term', $term_id, $tt_id, $taxonomy, true ); /** This action is documented in wp-includes/taxonomy.php */ do_action( "saved_{$taxonomy}", $term_id, $tt_id, true ); return array( 'term_id' => $term_id, 'term_taxonomy_id' => $tt_id, ); } /** * Enable or disable term counting. * * @since 2.5.0 * * @param bool $defer Optional. Enable if true, disable if false. * @return bool Whether term counting is enabled or disabled. */ function wp_defer_term_counting( $defer = null ) { static $_defer = false; if ( is_bool( $defer ) ) { $_defer = $defer; // Flush any deferred counts. if ( ! $defer ) { wp_update_term_count( null, null, true ); } } return $_defer; } /** * Updates the amount of terms in taxonomy. * * If there is a taxonomy callback applied, then it will be called for updating * the count. * * The default action is to count what the amount of terms have the relationship * of term ID. Once that is done, then update the database. * * @since 2.3.0 * * @param int|array $terms The term_taxonomy_id of the terms. * @param string $taxonomy The context of the term. * @param bool $do_deferred Whether to flush the deferred term counts too. Default false. * @return bool If no terms will return false, and if successful will return true. */ function wp_update_term_count( $terms, $taxonomy, $do_deferred = false ) { static $_deferred = array(); if ( $do_deferred ) { foreach ( (array) array_keys( $_deferred ) as $tax ) { wp_update_term_count_now( $_deferred[ $tax ], $tax ); unset( $_deferred[ $tax ] ); } } if ( empty( $terms ) ) { return false; } if ( ! is_array( $terms ) ) { $terms = array( $terms ); } if ( wp_defer_term_counting() ) { if ( ! isset( $_deferred[ $taxonomy ] ) ) { $_deferred[ $taxonomy ] = array(); } $_deferred[ $taxonomy ] = array_unique( array_merge( $_deferred[ $taxonomy ], $terms ) ); return true; } return wp_update_term_count_now( $terms, $taxonomy ); } /** * Perform term count update immediately. * * @since 2.5.0 * * @param array $terms The term_taxonomy_id of terms to update. * @param string $taxonomy The context of the term. * @return true Always true when complete. */ function wp_update_term_count_now( $terms, $taxonomy ) { $terms = array_map( 'intval', $terms ); $taxonomy = get_taxonomy( $taxonomy ); if ( ! empty( $taxonomy->update_count_callback ) ) { call_user_func( $taxonomy->update_count_callback, $terms, $taxonomy ); } else { $object_types = (array) $taxonomy->object_type; foreach ( $object_types as &$object_type ) { if ( 0 === strpos( $object_type, 'attachment:' ) ) { list( $object_type ) = explode( ':', $object_type ); } } if ( array_filter( $object_types, 'post_type_exists' ) == $object_types ) { // Only post types are attached to this taxonomy. _update_post_term_count( $terms, $taxonomy ); } else { // Default count updater. _update_generic_term_count( $terms, $taxonomy ); } } clean_term_cache( $terms, '', false ); return true; } // // Cache. // /** * Removes the taxonomy relationship to terms from the cache. * * Will remove the entire taxonomy relationship containing term `$object_id`. The * term IDs have to exist within the taxonomy `$object_type` for the deletion to * take place. * * @since 2.3.0 * * @global bool $_wp_suspend_cache_invalidation * * @see get_object_taxonomies() for more on $object_type. * * @param int|array $object_ids Single or list of term object ID(s). * @param array|string $object_type The taxonomy object type. */ function clean_object_term_cache( $object_ids, $object_type ) { global $_wp_suspend_cache_invalidation; if ( ! empty( $_wp_suspend_cache_invalidation ) ) { return; } if ( ! is_array( $object_ids ) ) { $object_ids = array( $object_ids ); } $taxonomies = get_object_taxonomies( $object_type ); foreach ( $object_ids as $id ) { foreach ( $taxonomies as $taxonomy ) { wp_cache_delete( $id, "{$taxonomy}_relationships" ); } } /** * Fires after the object term cache has been cleaned. * * @since 2.5.0 * * @param array $object_ids An array of object IDs. * @param string $object_type Object type. */ do_action( 'clean_object_term_cache', $object_ids, $object_type ); } /** * Will remove all of the term IDs from the cache. * * @since 2.3.0 * * @global wpdb $wpdb WordPress database abstraction object. * @global bool $_wp_suspend_cache_invalidation * * @param int|int[] $ids Single or array of term IDs. * @param string $taxonomy Optional. Taxonomy slug. Can be empty, in which case the taxonomies of the passed * term IDs will be used. Default empty. * @param bool $clean_taxonomy Optional. Whether to clean taxonomy wide caches (true), or just individual * term object caches (false). Default true. */ function clean_term_cache( $ids, $taxonomy = '', $clean_taxonomy = true ) { global $wpdb, $_wp_suspend_cache_invalidation; if ( ! empty( $_wp_suspend_cache_invalidation ) ) { return; } if ( ! is_array( $ids ) ) { $ids = array( $ids ); } $taxonomies = array(); // If no taxonomy, assume tt_ids. if ( empty( $taxonomy ) ) { $tt_ids = array_map( 'intval', $ids ); $tt_ids = implode( ', ', $tt_ids ); $terms = $wpdb->get_results( "SELECT term_id, taxonomy FROM $wpdb->term_taxonomy WHERE term_taxonomy_id IN ($tt_ids)" ); $ids = array(); foreach ( (array) $terms as $term ) { $taxonomies[] = $term->taxonomy; $ids[] = $term->term_id; wp_cache_delete( $term->term_id, 'terms' ); } $taxonomies = array_unique( $taxonomies ); } else { $taxonomies = array( $taxonomy ); foreach ( $taxonomies as $taxonomy ) { foreach ( $ids as $id ) { wp_cache_delete( $id, 'terms' ); } } } foreach ( $taxonomies as $taxonomy ) { if ( $clean_taxonomy ) { clean_taxonomy_cache( $taxonomy ); } /** * Fires once after each taxonomy's term cache has been cleaned. * * @since 2.5.0 * @since 4.5.0 Added the `$clean_taxonomy` parameter. * * @param array $ids An array of term IDs. * @param string $taxonomy Taxonomy slug. * @param bool $clean_taxonomy Whether or not to clean taxonomy-wide caches */ do_action( 'clean_term_cache', $ids, $taxonomy, $clean_taxonomy ); } wp_cache_set( 'last_changed', microtime(), 'terms' ); } /** * Clean the caches for a taxonomy. * * @since 4.9.0 * * @param string $taxonomy Taxonomy slug. */ function clean_taxonomy_cache( $taxonomy ) { wp_cache_delete( 'all_ids', $taxonomy ); wp_cache_delete( 'get', $taxonomy ); // Regenerate cached hierarchy. delete_option( "{$taxonomy}_children" ); _get_term_hierarchy( $taxonomy ); /** * Fires after a taxonomy's caches have been cleaned. * * @since 4.9.0 * * @param string $taxonomy Taxonomy slug. */ do_action( 'clean_taxonomy_cache', $taxonomy ); } /** * Retrieves the cached term objects for the given object ID. * * Upstream functions (like get_the_terms() and is_object_in_term()) are * responsible for populating the object-term relationship cache. The current * function only fetches relationship data that is already in the cache. * * @since 2.3.0 * @since 4.7.0 Returns a `WP_Error` object if there's an error with * any of the matched terms. * * @param int $id Term object ID, for example a post, comment, or user ID. * @param string $taxonomy Taxonomy name. * @return bool|WP_Term[]|WP_Error Array of `WP_Term` objects, if cached. * False if cache is empty for `$taxonomy` and `$id`. * WP_Error if get_term() returns an error object for any term. */ function get_object_term_cache( $id, $taxonomy ) { $_term_ids = wp_cache_get( $id, "{$taxonomy}_relationships" ); // We leave the priming of relationship caches to upstream functions. if ( false === $_term_ids ) { return false; } // Backward compatibility for if a plugin is putting objects into the cache, rather than IDs. $term_ids = array(); foreach ( $_term_ids as $term_id ) { if ( is_numeric( $term_id ) ) { $term_ids[] = (int) $term_id; } elseif ( isset( $term_id->term_id ) ) { $term_ids[] = (int) $term_id->term_id; } } // Fill the term objects. _prime_term_caches( $term_ids ); $terms = array(); foreach ( $term_ids as $term_id ) { $term = get_term( $term_id, $taxonomy ); if ( is_wp_error( $term ) ) { return $term; } $terms[] = $term; } return $terms; } /** * Updates the cache for the given term object ID(s). * * Note: Due to performance concerns, great care should be taken to only update * term caches when necessary. Processing time can increase exponentially depending * on both the number of passed term IDs and the number of taxonomies those terms * belong to. * * Caches will only be updated for terms not already cached. * * @since 2.3.0 * * @param string|int[] $object_ids Comma-separated list or array of term object IDs. * @param string|string[] $object_type The taxonomy object type or array of the same. * @return void|false Void on success or if the `$object_ids` parameter is empty, * false if all of the terms in `$object_ids` are already cached. */ function update_object_term_cache( $object_ids, $object_type ) { if ( empty( $object_ids ) ) { return; } if ( ! is_array( $object_ids ) ) { $object_ids = explode( ',', $object_ids ); } $object_ids = array_map( 'intval', $object_ids ); $non_cached_ids = array(); $taxonomies = get_object_taxonomies( $object_type ); foreach ( $taxonomies as $taxonomy ) { $cache_values = wp_cache_get_multiple( (array) $object_ids, "{$taxonomy}_relationships" ); foreach ( $cache_values as $id => $value ) { if ( false === $value ) { $non_cached_ids[] = $id; } } } if ( empty( $non_cached_ids ) ) { return false; } $non_cached_ids = array_unique( $non_cached_ids ); $terms = wp_get_object_terms( $non_cached_ids, $taxonomies, array( 'fields' => 'all_with_object_id', 'orderby' => 'name', 'update_term_meta_cache' => false, ) ); $object_terms = array(); foreach ( (array) $terms as $term ) { $object_terms[ $term->object_id ][ $term->taxonomy ][] = $term->term_id; } foreach ( $non_cached_ids as $id ) { foreach ( $taxonomies as $taxonomy ) { if ( ! isset( $object_terms[ $id ][ $taxonomy ] ) ) { if ( ! isset( $object_terms[ $id ] ) ) { $object_terms[ $id ] = array(); } $object_terms[ $id ][ $taxonomy ] = array(); } } } foreach ( $object_terms as $id => $value ) { foreach ( $value as $taxonomy => $terms ) { wp_cache_add( $id, $terms, "{$taxonomy}_relationships" ); } } } /** * Updates Terms to Taxonomy in cache. * * @since 2.3.0 * * @param WP_Term[] $terms Array of term objects to change. * @param string $taxonomy Not used. */ function update_term_cache( $terms, $taxonomy = '' ) { foreach ( (array) $terms as $term ) { // Create a copy in case the array was passed by reference. $_term = clone $term; // Object ID should not be cached. unset( $_term->object_id ); wp_cache_add( $term->term_id, $_term, 'terms' ); } } // // Private. // /** * Retrieves children of taxonomy as Term IDs. * * @access private * @since 2.3.0 * * @param string $taxonomy Taxonomy name. * @return array Empty if $taxonomy isn't hierarchical or returns children as Term IDs. */ function _get_term_hierarchy( $taxonomy ) { if ( ! is_taxonomy_hierarchical( $taxonomy ) ) { return array(); } $children = get_option( "{$taxonomy}_children" ); if ( is_array( $children ) ) { return $children; } $children = array(); $terms = get_terms( array( 'taxonomy' => $taxonomy, 'get' => 'all', 'orderby' => 'id', 'fields' => 'id=>parent', 'update_term_meta_cache' => false, ) ); foreach ( $terms as $term_id => $parent ) { if ( $parent > 0 ) { $children[ $parent ][] = $term_id; } } update_option( "{$taxonomy}_children", $children ); return $children; } /** * Get the subset of $terms that are descendants of $term_id. * * If `$terms` is an array of objects, then _get_term_children() returns an array of objects. * If `$terms` is an array of IDs, then _get_term_children() returns an array of IDs. * * @access private * @since 2.3.0 * * @param int $term_id The ancestor term: all returned terms should be descendants of `$term_id`. * @param array $terms The set of terms - either an array of term objects or term IDs - from which those that * are descendants of $term_id will be chosen. * @param string $taxonomy The taxonomy which determines the hierarchy of the terms. * @param array $ancestors Optional. Term ancestors that have already been identified. Passed by reference, to keep * track of found terms when recursing the hierarchy. The array of located ancestors is used * to prevent infinite recursion loops. For performance, `term_ids` are used as array keys, * with 1 as value. Default empty array. * @return array|WP_Error The subset of $terms that are descendants of $term_id. */ function _get_term_children( $term_id, $terms, $taxonomy, &$ancestors = array() ) { $empty_array = array(); if ( empty( $terms ) ) { return $empty_array; } $term_id = (int) $term_id; $term_list = array(); $has_children = _get_term_hierarchy( $taxonomy ); if ( $term_id && ! isset( $has_children[ $term_id ] ) ) { return $empty_array; } // Include the term itself in the ancestors array, so we can properly detect when a loop has occurred. if ( empty( $ancestors ) ) { $ancestors[ $term_id ] = 1; } foreach ( (array) $terms as $term ) { $use_id = false; if ( ! is_object( $term ) ) { $term = get_term( $term, $taxonomy ); if ( is_wp_error( $term ) ) { return $term; } $use_id = true; } // Don't recurse if we've already identified the term as a child - this indicates a loop. if ( isset( $ancestors[ $term->term_id ] ) ) { continue; } if ( (int) $term->parent === $term_id ) { if ( $use_id ) { $term_list[] = $term->term_id; } else { $term_list[] = $term; } if ( ! isset( $has_children[ $term->term_id ] ) ) { continue; } $ancestors[ $term->term_id ] = 1; $children = _get_term_children( $term->term_id, $terms, $taxonomy, $ancestors ); if ( $children ) { $term_list = array_merge( $term_list, $children ); } } } return $term_list; } /** * Add count of children to parent count. * * Recalculates term counts by including items from child terms. Assumes all * relevant children are already in the $terms argument. * * @access private * @since 2.3.0 * * @global wpdb $wpdb WordPress database abstraction object. * * @param object[]|WP_Term[] $terms List of term objects (passed by reference). * @param string $taxonomy Term context. */ function _pad_term_counts( &$terms, $taxonomy ) { global $wpdb; // This function only works for hierarchical taxonomies like post categories. if ( ! is_taxonomy_hierarchical( $taxonomy ) ) { return; } $term_hier = _get_term_hierarchy( $taxonomy ); if ( empty( $term_hier ) ) { return; } $term_items = array(); $terms_by_id = array(); $term_ids = array(); foreach ( (array) $terms as $key => $term ) { $terms_by_id[ $term->term_id ] = & $terms[ $key ]; $term_ids[ $term->term_taxonomy_id ] = $term->term_id; } // Get the object and term IDs and stick them in a lookup table. $tax_obj = get_taxonomy( $taxonomy ); $object_types = esc_sql( $tax_obj->object_type ); $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'" ); foreach ( $results as $row ) { $id = $term_ids[ $row->term_taxonomy_id ]; $term_items[ $id ][ $row->object_id ] = isset( $term_items[ $id ][ $row->object_id ] ) ? ++$term_items[ $id ][ $row->object_id ] : 1; } // Touch every ancestor's lookup row for each post in each term. foreach ( $term_ids as $term_id ) { $child = $term_id; $ancestors = array(); while ( ! empty( $terms_by_id[ $child ] ) && $parent = $terms_by_id[ $child ]->parent ) { $ancestors[] = $child; if ( ! empty( $term_items[ $term_id ] ) ) { foreach ( $term_items[ $term_id ] as $item_id => $touches ) { $term_items[ $parent ][ $item_id ] = isset( $term_items[ $parent ][ $item_id ] ) ? ++$term_items[ $parent ][ $item_id ] : 1; } } $child = $parent; if ( in_array( $parent, $ancestors, true ) ) { break; } } } // Transfer the touched cells. foreach ( (array) $term_items as $id => $items ) { if ( isset( $terms_by_id[ $id ] ) ) { $terms_by_id[ $id ]->count = count( $items ); } } } /** * Adds any terms from the given IDs to the cache that do not already exist in cache. * * @since 4.6.0 * @access private * * @global wpdb $wpdb WordPress database abstraction object. * * @param array $term_ids Array of term IDs. * @param bool $update_meta_cache Optional. Whether to update the meta cache. Default true. */ function _prime_term_caches( $term_ids, $update_meta_cache = true ) { global $wpdb; $non_cached_ids = _get_non_cached_ids( $term_ids, 'terms' ); if ( ! empty( $non_cached_ids ) ) { $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 ) ) ) ); update_term_cache( $fresh_terms, $update_meta_cache ); if ( $update_meta_cache ) { update_termmeta_cache( $non_cached_ids ); } } } // // Default callbacks. // /** * Will update term count based on object types of the current taxonomy. * * Private function for the default callback for post_tag and category * taxonomies. * * @access private * @since 2.3.0 * * @global wpdb $wpdb WordPress database abstraction object. * * @param int[] $terms List of Term taxonomy IDs. * @param WP_Taxonomy $taxonomy Current taxonomy object of terms. */ function _update_post_term_count( $terms, $taxonomy ) { global $wpdb; $object_types = (array) $taxonomy->object_type; foreach ( $object_types as &$object_type ) { list( $object_type ) = explode( ':', $object_type ); } $object_types = array_unique( $object_types ); $check_attachments = array_search( 'attachment', $object_types, true ); if ( false !== $check_attachments ) { unset( $object_types[ $check_attachments ] ); $check_attachments = true; } if ( $object_types ) { $object_types = esc_sql( array_filter( $object_types, 'post_type_exists' ) ); } $post_statuses = array( 'publish' ); /** * Filters the post statuses for updating the term count. * * @since 5.7.0 * * @param string[] $post_statuses List of post statuses to include in the count. Default is 'publish'. * @param WP_Taxonomy $taxonomy Current taxonomy object. */ $post_statuses = esc_sql( apply_filters( 'update_post_term_count_statuses', $post_statuses, $taxonomy ) ); foreach ( (array) $terms as $term ) { $count = 0; // Attachments can be 'inherit' status, we need to base count off the parent's status if so. if ( $check_attachments ) { // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.QuotedDynamicPlaceholderGeneration $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", $term ) ); } if ( $object_types ) { // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.QuotedDynamicPlaceholderGeneration $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", $term ) ); } /** This action is documented in wp-includes/taxonomy.php */ do_action( 'edit_term_taxonomy', $term, $taxonomy->name ); $wpdb->update( $wpdb->term_taxonomy, compact( 'count' ), array( 'term_taxonomy_id' => $term ) ); /** This action is documented in wp-includes/taxonomy.php */ do_action( 'edited_term_taxonomy', $term, $taxonomy->name ); } } /** * Will update term count based on number of objects. * * Default callback for the 'link_category' taxonomy. * * @since 3.3.0 * * @global wpdb $wpdb WordPress database abstraction object. * * @param int[] $terms List of term taxonomy IDs. * @param WP_Taxonomy $taxonomy Current taxonomy object of terms. */ function _update_generic_term_count( $terms, $taxonomy ) { global $wpdb; foreach ( (array) $terms as $term ) { $count = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM $wpdb->term_relationships WHERE term_taxonomy_id = %d", $term ) ); /** This action is documented in wp-includes/taxonomy.php */ do_action( 'edit_term_taxonomy', $term, $taxonomy->name ); $wpdb->update( $wpdb->term_taxonomy, compact( 'count' ), array( 'term_taxonomy_id' => $term ) ); /** This action is documented in wp-includes/taxonomy.php */ do_action( 'edited_term_taxonomy', $term, $taxonomy->name ); } } /** * Create a new term for a term_taxonomy item that currently shares its term * with another term_taxonomy. * * @ignore * @since 4.2.0 * @since 4.3.0 Introduced `$record` parameter. Also, `$term_id` and * `$term_taxonomy_id` can now accept objects. * * @global wpdb $wpdb WordPress database abstraction object. * * @param int|object $term_id ID of the shared term, or the shared term object. * @param int|object $term_taxonomy_id ID of the term_taxonomy item to receive a new term, or the term_taxonomy object * (corresponding to a row from the term_taxonomy table). * @param bool $record Whether to record data about the split term in the options table. The recording * process has the potential to be resource-intensive, so during batch operations * it can be beneficial to skip inline recording and do it just once, after the * batch is processed. Only set this to `false` if you know what you are doing. * Default: true. * @return int|WP_Error When the current term does not need to be split (or cannot be split on the current * database schema), `$term_id` is returned. When the term is successfully split, the * new term_id is returned. A WP_Error is returned for miscellaneous errors. */ function _split_shared_term( $term_id, $term_taxonomy_id, $record = true ) { global $wpdb; if ( is_object( $term_id ) ) { $shared_term = $term_id; $term_id = (int) $shared_term->term_id; } if ( is_object( $term_taxonomy_id ) ) { $term_taxonomy = $term_taxonomy_id; $term_taxonomy_id = (int) $term_taxonomy->term_taxonomy_id; } // If there are no shared term_taxonomy rows, there's nothing to do here. $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 ) ); if ( ! $shared_tt_count ) { return $term_id; } /* * Verify that the term_taxonomy_id passed to the function is actually associated with the term_id. * If there's a mismatch, it may mean that the term is already split. Return the actual term_id from the db. */ $check_term_id = (int) $wpdb->get_var( $wpdb->prepare( "SELECT term_id FROM $wpdb->term_taxonomy WHERE term_taxonomy_id = %d", $term_taxonomy_id ) ); if ( $check_term_id !== $term_id ) { return $check_term_id; } // Pull up data about the currently shared slug, which we'll use to populate the new one. if ( empty( $shared_term ) ) { $shared_term = $wpdb->get_row( $wpdb->prepare( "SELECT t.* FROM $wpdb->terms t WHERE t.term_id = %d", $term_id ) ); } $new_term_data = array( 'name' => $shared_term->name, 'slug' => $shared_term->slug, 'term_group' => $shared_term->term_group, ); if ( false === $wpdb->insert( $wpdb->terms, $new_term_data ) ) { return new WP_Error( 'db_insert_error', __( 'Could not split shared term.' ), $wpdb->last_error ); } $new_term_id = (int) $wpdb->insert_id; // Update the existing term_taxonomy to point to the newly created term. $wpdb->update( $wpdb->term_taxonomy, array( 'term_id' => $new_term_id ), array( 'term_taxonomy_id' => $term_taxonomy_id ) ); // Reassign child terms to the new parent. if ( empty( $term_taxonomy ) ) { $term_taxonomy = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $wpdb->term_taxonomy WHERE term_taxonomy_id = %d", $term_taxonomy_id ) ); } $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 ) ); if ( ! empty( $children_tt_ids ) ) { foreach ( $children_tt_ids as $child_tt_id ) { $wpdb->update( $wpdb->term_taxonomy, array( 'parent' => $new_term_id ), array( 'term_taxonomy_id' => $child_tt_id ) ); clean_term_cache( (int) $child_tt_id, '', false ); } } else { // If the term has no children, we must force its taxonomy cache to be rebuilt separately. clean_term_cache( $new_term_id, $term_taxonomy->taxonomy, false ); } clean_term_cache( $term_id, $term_taxonomy->taxonomy, false ); /* * Taxonomy cache clearing is delayed to avoid race conditions that may occur when * regenerating the taxonomy's hierarchy tree. */ $taxonomies_to_clean = array( $term_taxonomy->taxonomy ); // Clean the cache for term taxonomies formerly shared with the current term. $shared_term_taxonomies = $wpdb->get_col( $wpdb->prepare( "SELECT taxonomy FROM $wpdb->term_taxonomy WHERE term_id = %d", $term_id ) ); $taxonomies_to_clean = array_merge( $taxonomies_to_clean, $shared_term_taxonomies ); foreach ( $taxonomies_to_clean as $taxonomy_to_clean ) { clean_taxonomy_cache( $taxonomy_to_clean ); } // Keep a record of term_ids that have been split, keyed by old term_id. See wp_get_split_term(). if ( $record ) { $split_term_data = get_option( '_split_terms', array() ); if ( ! isset( $split_term_data[ $term_id ] ) ) { $split_term_data[ $term_id ] = array(); } $split_term_data[ $term_id ][ $term_taxonomy->taxonomy ] = $new_term_id; update_option( '_split_terms', $split_term_data ); } // If we've just split the final shared term, set the "finished" flag. $shared_terms_exist = $wpdb->get_results( "SELECT tt.term_id, t.*, count(*) as term_tt_count FROM {$wpdb->term_taxonomy} tt LEFT JOIN {$wpdb->terms} t ON t.term_id = tt.term_id GROUP BY t.term_id HAVING term_tt_count > 1 LIMIT 1" ); if ( ! $shared_terms_exist ) { update_option( 'finished_splitting_shared_terms', true ); } /** * Fires after a previously shared taxonomy term is split into two separate terms. * * @since 4.2.0 * * @param int $term_id ID of the formerly shared term. * @param int $new_term_id ID of the new term created for the $term_taxonomy_id. * @param int $term_taxonomy_id ID for the term_taxonomy row affected by the split. * @param string $taxonomy Taxonomy for the split term. */ do_action( 'split_shared_term', $term_id, $new_term_id, $term_taxonomy_id, $term_taxonomy->taxonomy ); return $new_term_id; } /** * Splits a batch of shared taxonomy terms. * * @since 4.3.0 * * @global wpdb $wpdb WordPress database abstraction object. */ function _wp_batch_split_terms() { global $wpdb; $lock_name = 'term_split.lock'; // Try to lock. $lock_result = $wpdb->query( $wpdb->prepare( "INSERT IGNORE INTO `$wpdb->options` ( `option_name`, `option_value`, `autoload` ) VALUES (%s, %s, 'no') /* LOCK */", $lock_name, time() ) ); if ( ! $lock_result ) { $lock_result = get_option( $lock_name ); // Bail if we were unable to create a lock, or if the existing lock is still valid. if ( ! $lock_result || ( $lock_result > ( time() - HOUR_IN_SECONDS ) ) ) { wp_schedule_single_event( time() + ( 5 * MINUTE_IN_SECONDS ), 'wp_split_shared_term_batch' ); return; } } // Update the lock, as by this point we've definitely got a lock, just need to fire the actions. update_option( $lock_name, time() ); // Get a list of shared terms (those with more than one associated row in term_taxonomy). $shared_terms = $wpdb->get_results( "SELECT tt.term_id, t.*, count(*) as term_tt_count FROM {$wpdb->term_taxonomy} tt LEFT JOIN {$wpdb->terms} t ON t.term_id = tt.term_id GROUP BY t.term_id HAVING term_tt_count > 1 LIMIT 10" ); // No more terms, we're done here. if ( ! $shared_terms ) { update_option( 'finished_splitting_shared_terms', true ); delete_option( $lock_name ); return; } // Shared terms found? We'll need to run this script again. wp_schedule_single_event( time() + ( 2 * MINUTE_IN_SECONDS ), 'wp_split_shared_term_batch' ); // Rekey shared term array for faster lookups. $_shared_terms = array(); foreach ( $shared_terms as $shared_term ) { $term_id = (int) $shared_term->term_id; $_shared_terms[ $term_id ] = $shared_term; } $shared_terms = $_shared_terms; // Get term taxonomy data for all shared terms. $shared_term_ids = implode( ',', array_keys( $shared_terms ) ); $shared_tts = $wpdb->get_results( "SELECT * FROM {$wpdb->term_taxonomy} WHERE `term_id` IN ({$shared_term_ids})" ); // Split term data recording is slow, so we do it just once, outside the loop. $split_term_data = get_option( '_split_terms', array() ); $skipped_first_term = array(); $taxonomies = array(); foreach ( $shared_tts as $shared_tt ) { $term_id = (int) $shared_tt->term_id; // Don't split the first tt belonging to a given term_id. if ( ! isset( $skipped_first_term[ $term_id ] ) ) { $skipped_first_term[ $term_id ] = 1; continue; } if ( ! isset( $split_term_data[ $term_id ] ) ) { $split_term_data[ $term_id ] = array(); } // Keep track of taxonomies whose hierarchies need flushing. if ( ! isset( $taxonomies[ $shared_tt->taxonomy ] ) ) { $taxonomies[ $shared_tt->taxonomy ] = 1; } // Split the term. $split_term_data[ $term_id ][ $shared_tt->taxonomy ] = _split_shared_term( $shared_terms[ $term_id ], $shared_tt, false ); } // Rebuild the cached hierarchy for each affected taxonomy. foreach ( array_keys( $taxonomies ) as $tax ) { delete_option( "{$tax}_children" ); _get_term_hierarchy( $tax ); } update_option( '_split_terms', $split_term_data ); delete_option( $lock_name ); } /** * In order to avoid the _wp_batch_split_terms() job being accidentally removed, * check that it's still scheduled while we haven't finished splitting terms. * * @ignore * @since 4.3.0 */ function _wp_check_for_scheduled_split_terms() { if ( ! get_option( 'finished_splitting_shared_terms' ) && ! wp_next_scheduled( 'wp_split_shared_term_batch' ) ) { wp_schedule_single_event( time() + MINUTE_IN_SECONDS, 'wp_split_shared_term_batch' ); } } /** * Check default categories when a term gets split to see if any of them need to be updated. * * @ignore * @since 4.2.0 * * @param int $term_id ID of the formerly shared term. * @param int $new_term_id ID of the new term created for the $term_taxonomy_id. * @param int $term_taxonomy_id ID for the term_taxonomy row affected by the split. * @param string $taxonomy Taxonomy for the split term. */ function _wp_check_split_default_terms( $term_id, $new_term_id, $term_taxonomy_id, $taxonomy ) { if ( 'category' !== $taxonomy ) { return; } foreach ( array( 'default_category', 'default_link_category', 'default_email_category' ) as $option ) { if ( (int) get_option( $option, -1 ) === $term_id ) { update_option( $option, $new_term_id ); } } } /** * Check menu items when a term gets split to see if any of them need to be updated. * * @ignore * @since 4.2.0 * * @global wpdb $wpdb WordPress database abstraction object. * * @param int $term_id ID of the formerly shared term. * @param int $new_term_id ID of the new term created for the $term_taxonomy_id. * @param int $term_taxonomy_id ID for the term_taxonomy row affected by the split. * @param string $taxonomy Taxonomy for the split term. */ function _wp_check_split_terms_in_menus( $term_id, $new_term_id, $term_taxonomy_id, $taxonomy ) { global $wpdb; $post_ids = $wpdb->get_col( $wpdb->prepare( "SELECT m1.post_id FROM {$wpdb->postmeta} AS m1 INNER JOIN {$wpdb->postmeta} AS m2 ON ( m2.post_id = m1.post_id ) INNER JOIN {$wpdb->postmeta} AS m3 ON ( m3.post_id = m1.post_id ) WHERE ( m1.meta_key = '_menu_item_type' AND m1.meta_value = 'taxonomy' ) AND ( m2.meta_key = '_menu_item_object' AND m2.meta_value = %s ) AND ( m3.meta_key = '_menu_item_object_id' AND m3.meta_value = %d )", $taxonomy, $term_id ) ); if ( $post_ids ) { foreach ( $post_ids as $post_id ) { update_post_meta( $post_id, '_menu_item_object_id', $new_term_id, $term_id ); } } } /** * If the term being split is a nav_menu, change associations. * * @ignore * @since 4.3.0 * * @param int $term_id ID of the formerly shared term. * @param int $new_term_id ID of the new term created for the $term_taxonomy_id. * @param int $term_taxonomy_id ID for the term_taxonomy row affected by the split. * @param string $taxonomy Taxonomy for the split term. */ function _wp_check_split_nav_menu_terms( $term_id, $new_term_id, $term_taxonomy_id, $taxonomy ) { if ( 'nav_menu' !== $taxonomy ) { return; } // Update menu locations. $locations = get_nav_menu_locations(); foreach ( $locations as $location => $menu_id ) { if ( $term_id === $menu_id ) { $locations[ $location ] = $new_term_id; } } set_theme_mod( 'nav_menu_locations', $locations ); } /** * Get data about terms that previously shared a single term_id, but have since been split. * * @since 4.2.0 * * @param int $old_term_id Term ID. This is the old, pre-split term ID. * @return array Array of new term IDs, keyed by taxonomy. */ function wp_get_split_terms( $old_term_id ) { $split_terms = get_option( '_split_terms', array() ); $terms = array(); if ( isset( $split_terms[ $old_term_id ] ) ) { $terms = $split_terms[ $old_term_id ]; } return $terms; } /** * Get the new term ID corresponding to a previously split term. * * @since 4.2.0 * * @param int $old_term_id Term ID. This is the old, pre-split term ID. * @param string $taxonomy Taxonomy that the term belongs to. * @return int|false If a previously split term is found corresponding to the old term_id and taxonomy, * the new term_id will be returned. If no previously split term is found matching * the parameters, returns false. */ function wp_get_split_term( $old_term_id, $taxonomy ) { $split_terms = wp_get_split_terms( $old_term_id ); $term_id = false; if ( isset( $split_terms[ $taxonomy ] ) ) { $term_id = (int) $split_terms[ $taxonomy ]; } return $term_id; } /** * Determine whether a term is shared between multiple taxonomies. * * Shared taxonomy terms began to be split in 4.3, but failed cron tasks or * other delays in upgrade routines may cause shared terms to remain. * * @since 4.4.0 * * @param int $term_id Term ID. * @return bool Returns false if a term is not shared between multiple taxonomies or * if splitting shared taxonomy terms is finished. */ function wp_term_is_shared( $term_id ) { global $wpdb; if ( get_option( 'finished_splitting_shared_terms' ) ) { return false; } $tt_count = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM $wpdb->term_taxonomy WHERE term_id = %d", $term_id ) ); return $tt_count > 1; } /** * Generate a permalink for a taxonomy term archive. * * @since 2.5.0 * * @global WP_Rewrite $wp_rewrite WordPress rewrite component. * * @param WP_Term|int|string $term The term object, ID, or slug whose link will be retrieved. * @param string $taxonomy Optional. Taxonomy. Default empty. * @return string|WP_Error URL of the taxonomy term archive on success, WP_Error if term does not exist. */ function get_term_link( $term, $taxonomy = '' ) { global $wp_rewrite; if ( ! is_object( $term ) ) { if ( is_int( $term ) ) { $term = get_term( $term, $taxonomy ); } else { $term = get_term_by( 'slug', $term, $taxonomy ); } } if ( ! is_object( $term ) ) { $term = new WP_Error( 'invalid_term', __( 'Empty Term.' ) ); } if ( is_wp_error( $term ) ) { return $term; } $taxonomy = $term->taxonomy; $termlink = $wp_rewrite->get_extra_permastruct( $taxonomy ); /** * Filters the permalink structure for a terms before token replacement occurs. * * @since 4.9.0 * * @param string $termlink The permalink structure for the term's taxonomy. * @param WP_Term $term The term object. */ $termlink = apply_filters( 'pre_term_link', $termlink, $term ); $slug = $term->slug; $t = get_taxonomy( $taxonomy ); if ( empty( $termlink ) ) { if ( 'category' === $taxonomy ) { $termlink = '?cat=' . $term->term_id; } elseif ( $t->query_var ) { $termlink = "?$t->query_var=$slug"; } else { $termlink = "?taxonomy=$taxonomy&term=$slug"; } $termlink = home_url( $termlink ); } else { if ( $t->rewrite['hierarchical'] ) { $hierarchical_slugs = array(); $ancestors = get_ancestors( $term->term_id, $taxonomy, 'taxonomy' ); foreach ( (array) $ancestors as $ancestor ) { $ancestor_term = get_term( $ancestor, $taxonomy ); $hierarchical_slugs[] = $ancestor_term->slug; } $hierarchical_slugs = array_reverse( $hierarchical_slugs ); $hierarchical_slugs[] = $slug; $termlink = str_replace( "%$taxonomy%", implode( '/', $hierarchical_slugs ), $termlink ); } else { $termlink = str_replace( "%$taxonomy%", $slug, $termlink ); } $termlink = home_url( user_trailingslashit( $termlink, 'category' ) ); } // Back compat filters. if ( 'post_tag' === $taxonomy ) { /** * Filters the tag link. * * @since 2.3.0 * @since 2.5.0 Deprecated in favor of {@see 'term_link'} filter. * @since 5.4.1 Restored (un-deprecated). * * @param string $termlink Tag link URL. * @param int $term_id Term ID. */ $termlink = apply_filters( 'tag_link', $termlink, $term->term_id ); } elseif ( 'category' === $taxonomy ) { /** * Filters the category link. * * @since 1.5.0 * @since 2.5.0 Deprecated in favor of {@see 'term_link'} filter. * @since 5.4.1 Restored (un-deprecated). * * @param string $termlink Category link URL. * @param int $term_id Term ID. */ $termlink = apply_filters( 'category_link', $termlink, $term->term_id ); } /** * Filters the term link. * * @since 2.5.0 * * @param string $termlink Term link URL. * @param WP_Term $term Term object. * @param string $taxonomy Taxonomy slug. */ return apply_filters( 'term_link', $termlink, $term, $taxonomy ); } /** * Display the taxonomies of a post with available options. * * This function can be used within the loop to display the taxonomies for a * post without specifying the Post ID. You can also use it outside the Loop to * display the taxonomies for a specific post. * * @since 2.5.0 * * @param array $args { * Arguments about which post to use and how to format the output. Shares all of the arguments * supported by get_the_taxonomies(), in addition to the following. * * @type int|WP_Post $post Post ID or object to get taxonomies of. Default current post. * @type string $before Displays before the taxonomies. Default empty string. * @type string $sep Separates each taxonomy. Default is a space. * @type string $after Displays after the taxonomies. Default empty string. * } */ function the_taxonomies( $args = array() ) { $defaults = array( 'post' => 0, 'before' => '', 'sep' => ' ', 'after' => '', ); $parsed_args = wp_parse_args( $args, $defaults ); echo $parsed_args['before'] . implode( $parsed_args['sep'], get_the_taxonomies( $parsed_args['post'], $parsed_args ) ) . $parsed_args['after']; } /** * Retrieve all taxonomies associated with a post. * * This function can be used within the loop. It will also return an array of * the taxonomies with links to the taxonomy and name. * * @since 2.5.0 * * @param int|WP_Post $post Optional. Post ID or WP_Post object. Default is global $post. * @param array $args { * Optional. Arguments about how to format the list of taxonomies. Default empty array. * * @type string $template Template for displaying a taxonomy label and list of terms. * Default is "Label: Terms." * @type string $term_template Template for displaying a single term in the list. Default is the term name * linked to its archive. * } * @return array List of taxonomies. */ function get_the_taxonomies( $post = 0, $args = array() ) { $post = get_post( $post ); $args = wp_parse_args( $args, array( /* translators: %s: Taxonomy label, %l: List of terms formatted as per $term_template. */ 'template' => __( '%s: %l.' ), 'term_template' => '%2$s', ) ); $taxonomies = array(); if ( ! $post ) { return $taxonomies; } foreach ( get_object_taxonomies( $post ) as $taxonomy ) { $t = (array) get_taxonomy( $taxonomy ); if ( empty( $t['label'] ) ) { $t['label'] = $taxonomy; } if ( empty( $t['args'] ) ) { $t['args'] = array(); } if ( empty( $t['template'] ) ) { $t['template'] = $args['template']; } if ( empty( $t['term_template'] ) ) { $t['term_template'] = $args['term_template']; } $terms = get_object_term_cache( $post->ID, $taxonomy ); if ( false === $terms ) { $terms = wp_get_object_terms( $post->ID, $taxonomy, $t['args'] ); } $links = array(); foreach ( $terms as $term ) { $links[] = wp_sprintf( $t['term_template'], esc_attr( get_term_link( $term ) ), $term->name ); } if ( $links ) { $taxonomies[ $taxonomy ] = wp_sprintf( $t['template'], $t['label'], $links, $terms ); } } return $taxonomies; } /** * Retrieve all taxonomy names for the given post. * * @since 2.5.0 * * @param int|WP_Post $post Optional. Post ID or WP_Post object. Default is global $post. * @return string[] An array of all taxonomy names for the given post. */ function get_post_taxonomies( $post = 0 ) { $post = get_post( $post ); return get_object_taxonomies( $post ); } /** * Determine if the given object is associated with any of the given terms. * * The given terms are checked against the object's terms' term_ids, names and slugs. * Terms given as integers will only be checked against the object's terms' term_ids. * If no terms are given, determines if object is associated with any terms in the given taxonomy. * * @since 2.7.0 * * @param int $object_id ID of the object (post ID, link ID, ...). * @param string $taxonomy Single taxonomy name. * @param int|string|int[]|string[] $terms Optional. Term ID, name, slug, or array of such * to check against. Default null. * @return bool|WP_Error WP_Error on input error. */ function is_object_in_term( $object_id, $taxonomy, $terms = null ) { $object_id = (int) $object_id; if ( ! $object_id ) { return new WP_Error( 'invalid_object', __( 'Invalid object ID.' ) ); } $object_terms = get_object_term_cache( $object_id, $taxonomy ); if ( false === $object_terms ) { $object_terms = wp_get_object_terms( $object_id, $taxonomy, array( 'update_term_meta_cache' => false ) ); if ( is_wp_error( $object_terms ) ) { return $object_terms; } wp_cache_set( $object_id, wp_list_pluck( $object_terms, 'term_id' ), "{$taxonomy}_relationships" ); } if ( is_wp_error( $object_terms ) ) { return $object_terms; } if ( empty( $object_terms ) ) { return false; } if ( empty( $terms ) ) { return ( ! empty( $object_terms ) ); } $terms = (array) $terms; $ints = array_filter( $terms, 'is_int' ); if ( $ints ) { $strs = array_diff( $terms, $ints ); } else { $strs =& $terms; } foreach ( $object_terms as $object_term ) { // If term is an int, check against term_ids only. if ( $ints && in_array( $object_term->term_id, $ints, true ) ) { return true; } if ( $strs ) { // Only check numeric strings against term_id, to avoid false matches due to type juggling. $numeric_strs = array_map( 'intval', array_filter( $strs, 'is_numeric' ) ); if ( in_array( $object_term->term_id, $numeric_strs, true ) ) { return true; } if ( in_array( $object_term->name, $strs, true ) ) { return true; } if ( in_array( $object_term->slug, $strs, true ) ) { return true; } } } return false; } /** * Determine if the given object type is associated with the given taxonomy. * * @since 3.0.0 * * @param string $object_type Object type string. * @param string $taxonomy Single taxonomy name. * @return bool True if object is associated with the taxonomy, otherwise false. */ function is_object_in_taxonomy( $object_type, $taxonomy ) { $taxonomies = get_object_taxonomies( $object_type ); if ( empty( $taxonomies ) ) { return false; } return in_array( $taxonomy, $taxonomies, true ); } /** * Get an array of ancestor IDs for a given object. * * @since 3.1.0 * @since 4.1.0 Introduced the `$resource_type` argument. * * @param int $object_id Optional. The ID of the object. Default 0. * @param string $object_type Optional. The type of object for which we'll be retrieving * ancestors. Accepts a post type or a taxonomy name. Default empty. * @param string $resource_type Optional. Type of resource $object_type is. Accepts 'post_type' * or 'taxonomy'. Default empty. * @return int[] An array of IDs of ancestors from lowest to highest in the hierarchy. */ function get_ancestors( $object_id = 0, $object_type = '', $resource_type = '' ) { $object_id = (int) $object_id; $ancestors = array(); if ( empty( $object_id ) ) { /** This filter is documented in wp-includes/taxonomy.php */ return apply_filters( 'get_ancestors', $ancestors, $object_id, $object_type, $resource_type ); } if ( ! $resource_type ) { if ( is_taxonomy_hierarchical( $object_type ) ) { $resource_type = 'taxonomy'; } elseif ( post_type_exists( $object_type ) ) { $resource_type = 'post_type'; } } if ( 'taxonomy' === $resource_type ) { $term = get_term( $object_id, $object_type ); while ( ! is_wp_error( $term ) && ! empty( $term->parent ) && ! in_array( $term->parent, $ancestors, true ) ) { $ancestors[] = (int) $term->parent; $term = get_term( $term->parent, $object_type ); } } elseif ( 'post_type' === $resource_type ) { $ancestors = get_post_ancestors( $object_id ); } /** * Filters a given object's ancestors. * * @since 3.1.0 * @since 4.1.1 Introduced the `$resource_type` parameter. * * @param int[] $ancestors An array of IDs of object ancestors. * @param int $object_id Object ID. * @param string $object_type Type of object. * @param string $resource_type Type of resource $object_type is. */ return apply_filters( 'get_ancestors', $ancestors, $object_id, $object_type, $resource_type ); } /** * Returns the term's parent's term_ID. * * @since 3.1.0 * * @param int $term_id Term ID. * @param string $taxonomy Taxonomy name. * @return int|false Parent term ID on success, false on failure. */ function wp_get_term_taxonomy_parent_id( $term_id, $taxonomy ) { $term = get_term( $term_id, $taxonomy ); if ( ! $term || is_wp_error( $term ) ) { return false; } return (int) $term->parent; } /** * Checks the given subset of the term hierarchy for hierarchy loops. * Prevents loops from forming and breaks those that it finds. * * Attached to the {@see 'wp_update_term_parent'} filter. * * @since 3.1.0 * * @param int $parent `term_id` of the parent for the term we're checking. * @param int $term_id The term we're checking. * @param string $taxonomy The taxonomy of the term we're checking. * @return int The new parent for the term. */ function wp_check_term_hierarchy_for_loops( $parent, $term_id, $taxonomy ) { // Nothing fancy here - bail. if ( ! $parent ) { return 0; } // Can't be its own parent. if ( $parent === $term_id ) { return 0; } // Now look for larger loops. $loop = wp_find_hierarchy_loop( 'wp_get_term_taxonomy_parent_id', $term_id, $parent, array( $taxonomy ) ); if ( ! $loop ) { return $parent; // No loop. } // Setting $parent to the given value causes a loop. if ( isset( $loop[ $term_id ] ) ) { return 0; } // There's a loop, but it doesn't contain $term_id. Break the loop. foreach ( array_keys( $loop ) as $loop_member ) { wp_update_term( $loop_member, $taxonomy, array( 'parent' => 0 ) ); } return $parent; } /** * Determines whether a taxonomy is considered "viewable". * * @since 5.1.0 * * @param string|WP_Taxonomy $taxonomy Taxonomy name or object. * @return bool Whether the taxonomy should be considered viewable. */ function is_taxonomy_viewable( $taxonomy ) { if ( is_scalar( $taxonomy ) ) { $taxonomy = get_taxonomy( $taxonomy ); if ( ! $taxonomy ) { return false; } } return $taxonomy->publicly_queryable; } /** * Sets the last changed time for the 'terms' cache group. * * @since 5.0.0 */ function wp_cache_set_terms_last_changed() { wp_cache_set( 'last_changed', microtime(), 'terms' ); } /** * Aborts calls to term meta if it is not supported. * * @since 5.0.0 * * @param mixed $check Skip-value for whether to proceed term meta function execution. * @return mixed Original value of $check, or false if term meta is not supported. */ function wp_check_term_meta_support_prefilter( $check ) { if ( get_option( 'db_version' ) < 34370 ) { return false; } return $check; }