+
+ /** 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 = intval( $shared_term->term_id );
+ }
+
+ if ( is_object( $term_taxonomy_id ) ) {
+ $term_taxonomy = $term_taxonomy_id;
+ $term_taxonomy_id = intval( $term_taxonomy->term_taxonomy_id );
+ }
+
+ // If there are no shared term_taxonomy rows, there's nothing to do here.
+ $shared_tt_count = $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 = $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( $term_id, $term_taxonomy->taxonomy );
+ }
+ } 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 );
+ }
+
+ // Clean the cache for term taxonomies formerly shared with the current term.
+ $shared_term_taxonomies = $wpdb->get_row( $wpdb->prepare( "SELECT taxonomy FROM $wpdb->term_taxonomy WHERE term_id = %d", $term_id ) );
+ if ( $shared_term_taxonomies ) {
+ foreach ( $shared_term_taxonomies as $shared_term_taxonomy ) {
+ clean_term_cache( $term_id, $shared_term_taxonomy );
+ }
+ }
+
+ // Keep a record of term_ids that have been split, keyed by old term_id. See {@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 = intval( $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 = $taxonomies = array();
+ foreach ( $shared_tts as $shared_tt ) {
+ $term_id = intval( $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 ( $term_id == get_option( $option, -1 ) ) {
+ 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
+ * @return bool
+ */
+function wp_term_is_shared( $term_id ) {
+ global $wpdb;
+
+ if ( get_option( 'finished_splitting_shared_terms' ) ) {
+ return false;