if ( ! $shared_tt_count ) {
* 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 ) {
// 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 ) );
'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.
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 ) {
array( 'parent' => $new_term_id ),
array( 'term_taxonomy_id' => $child_tt_id )
clean_term_cache( (int) $child_tt_id, '', false );
// 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().
$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
if ( ! $shared_terms_exist ) {
update_option( 'finished_splitting_shared_terms', true );
* Fires after a previously shared taxonomy term is split into two separate terms.
* @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 );
* Splits a batch of shared taxonomy terms.
* @global wpdb $wpdb WordPress database abstraction object.
function _wp_batch_split_terms() {
$lock_name = 'term_split.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() ) );
$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' );
// 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
// No more terms, we're done here.
update_option( 'finished_splitting_shared_terms', true );
delete_option( $lock_name );
// 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();
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;
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_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.
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.
* @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 ) {
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.
* @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 ) {
$post_ids = $wpdb->get_col(
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 )",
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.
* @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 ) {
// 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.
* @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() );
if ( isset( $split_terms[ $old_term_id ] ) ) {
$terms = $split_terms[ $old_term_id ];
* Get the new term ID corresponding to a previously split term.
* @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 );
if ( isset( $split_terms[ $taxonomy ] ) ) {
$term_id = (int) $split_terms[ $taxonomy ];
* 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.
* @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 ) {
if ( get_option( 'finished_splitting_shared_terms' ) ) {
$tt_count = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM $wpdb->term_taxonomy WHERE term_id = %d", $term_id ) );
* Generate a permalink for a taxonomy term archive.
* @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 = '' ) {
if ( ! is_object( $term ) ) {
$term = get_term( $term, $taxonomy );
$term = get_term_by( 'slug', $term, $taxonomy );
if ( ! is_object( $term ) ) {
$term = new WP_Error( 'invalid_term', __( 'Empty Term.' ) );
if ( is_wp_error( $term ) ) {
$taxonomy = $term->taxonomy;
$termlink = $wp_rewrite->get_extra_permastruct( $taxonomy );
* Filters the permalink structure for a terms before token replacement occurs.
* @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 );
$t = get_taxonomy( $taxonomy );
if ( empty( $termlink ) ) {
if ( 'category' === $taxonomy ) {
$termlink = '?cat=' . $term->term_id;
} elseif ( $t->query_var ) {
$termlink = "?$t->query_var=$slug";
$termlink = "?taxonomy=$taxonomy&term=$slug";
$termlink = home_url( $termlink );
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 );
$termlink = str_replace( "%$taxonomy%", $slug, $termlink );
$termlink = home_url( user_trailingslashit( $termlink, 'category' ) );
if ( 'post_tag' === $taxonomy ) {
* @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 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 );
* @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.
* 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.