Make WordPress Core

Changeset 31418


Ignore:
Timestamp:
02/11/2015 07:41:54 PM (10 years ago)
Author:
boonebgorges
Message:

Split shared taxonomy terms on term update.

When updating an existing taxonomy term that shares its term_id with
another term, we generate a new row in wp_terms and associate the updated
term_taxonomy_id with the new term. This separates the terms, such that
updating the name of one term does not change the name of any others.

In cases where a plugin or theme stores term IDs in the database, term splitting
can cause backward compatibility issues. The current changeset introduces
two utilities to aid developers with the transition. The 'split_shared_term'
action fires when the split takes place, and should be used to catch changes in
term_id. In cases where 'split_shared_term' cannot be used, the
wp_get_split_term() function gives developers access to data about terms
that have previously been split. Documentation for these functions, with
examples, can be found in the Plugin Developer Handbook. WordPress itself
stores term IDs in this way in two places; _wp_check_split_default_terms()
and _wp_check_split_terms_in_menus() are hooked to 'split_shared_term' to
perform the necessary cleanup.

See [30241] for a previous attempt at the split. It was reverted in [30585]
for 4.1.0.

Props boonebgorges, mboynes.
See #5809.

Location:
trunk
Files:
1 added
3 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-includes/default-filters.php

    r31168 r31418  
    307307add_filter( 'determine_current_user', 'wp_validate_logged_in_cookie', 20 );
    308308
     309// Split term updates.
     310add_action( 'split_shared_term', '_wp_check_split_default_terms',  10, 4 );
     311add_action( 'split_shared_term', '_wp_check_split_terms_in_menus', 10, 4 );
     312
    309313/**
    310314 * Filters formerly mixed into wp-includes
  • trunk/src/wp-includes/taxonomy.php

    r31367 r31418  
    34313431    $tt_id = $wpdb->get_var( $wpdb->prepare( "SELECT tt.term_taxonomy_id FROM $wpdb->term_taxonomy AS tt INNER JOIN $wpdb->terms AS t ON tt.term_id = t.term_id WHERE tt.taxonomy = %s AND t.term_id = %d", $taxonomy, $term_id) );
    34323432
     3433    // Check whether this is a shared term that needs splitting.
     3434    $_term_id = _split_shared_term( $term_id, $tt_id );
     3435    if ( ! is_wp_error( $_term_id ) ) {
     3436        $term_id = $_term_id;
     3437    }
     3438
    34333439    /**
    34343440     * Fires immediately before the given terms are edited.
     
    40904096        do_action( 'edited_term_taxonomy', $term, $taxonomy );
    40914097    }
     4098}
     4099
     4100/**
     4101 * Create a new term for a term_taxonomy item that currently shares its term with another term_taxonomy.
     4102 *
     4103 * @since 4.2.0
     4104 * @access private
     4105 *
     4106 * @param int  $term_id          ID of the shared term.
     4107 * @param int  $term_taxonomy_id ID of the term_taxonomy item to receive a new term.
     4108 * @return int|WP_Error When the current term does not need to be split (or cannot be split on the current database
     4109 *                      schema), `$term_id` is returned. When the term is successfully split, the new term_id is
     4110 *                      returned. A `WP_Error` is returned for miscellaneous errors.
     4111 */
     4112function _split_shared_term( $term_id, $term_taxonomy_id ) {
     4113    global $wpdb;
     4114
     4115    // Don't try to split terms if database schema does not support shared slugs.
     4116    $current_db_version = get_option( 'db_version' );
     4117    if ( $current_db_version < 30133 ) {
     4118        return $term_id;
     4119    }
     4120
     4121    // If there are no shared term_taxonomy rows, there's nothing to do here.
     4122    $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 ) );
     4123    if ( ! $shared_tt_count ) {
     4124        return $term_id;
     4125    }
     4126
     4127    // Pull up data about the currently shared slug, which we'll use to populate the new one.
     4128    $shared_term = $wpdb->get_row( $wpdb->prepare( "SELECT t.* FROM $wpdb->terms t WHERE t.term_id = %d", $term_id ) );
     4129
     4130    $new_term_data = array(
     4131        'name' => $shared_term->name,
     4132        'slug' => $shared_term->slug,
     4133        'term_group' => $shared_term->term_group,
     4134    );
     4135
     4136    if ( false === $wpdb->insert( $wpdb->terms, $new_term_data ) ) {
     4137        return new WP_Error( 'db_insert_error', __( 'Could not split shared term.' ), $wpdb->last_error );
     4138    }
     4139
     4140    $new_term_id = (int) $wpdb->insert_id;
     4141
     4142    // Update the existing term_taxonomy to point to the newly created term.
     4143    $wpdb->update( $wpdb->term_taxonomy,
     4144        array( 'term_id' => $new_term_id ),
     4145        array( 'term_taxonomy_id' => $term_taxonomy_id )
     4146    );
     4147
     4148    // Reassign child terms to the new parent.
     4149    $term_taxonomy = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $wpdb->term_taxonomy WHERE term_taxonomy_id = %d", $term_taxonomy_id ) );
     4150    $children_tt_ids = $wpdb->get_col( $wpdb->prepare( "SELECT term_taxonomy_id FROM $wpdb->term_taxonomy WHERE taxonomy = %s AND parent = %d", $term_taxonomy->taxonomy, $term_id ) );
     4151
     4152    if ( ! empty( $children_tt_ids ) ) {
     4153        foreach ( $children_tt_ids as $child_tt_id ) {
     4154            $wpdb->update( $wpdb->term_taxonomy,
     4155                array( 'parent' => $new_term_id ),
     4156                array( 'term_taxonomy_id' => $child_tt_id )
     4157            );
     4158            clean_term_cache( $term_id, $term_taxonomy->taxonomy );
     4159        }
     4160    } else {
     4161        // If the term has no children, we must force its taxonomy cache to be rebuilt separately.
     4162        clean_term_cache( $new_term_id, $term_taxonomy->taxonomy );
     4163    }
     4164
     4165    // Clean the cache for term taxonomies formerly shared with the current term.
     4166    $shared_term_taxonomies = $wpdb->get_row( $wpdb->prepare( "SELECT taxonomy FROM $wpdb->term_taxonomy WHERE term_id = %d", $term_id ) );
     4167    if ( $shared_term_taxonomies ) {
     4168        foreach ( $shared_term_taxonomies as $shared_term_taxonomy ) {
     4169            clean_term_cache( $term_id, $shared_term_taxonomy );
     4170        }
     4171    }
     4172
     4173    // Keep a record of term_ids that have been split, keyed by old term_id. See {@see wp_get_split_term()}.
     4174    $split_term_data = get_option( '_split_terms', array() );
     4175    if ( ! isset( $split_term_data[ $term_id ] ) ) {
     4176        $split_term_data[ $term_id ] = array();
     4177    }
     4178
     4179    $split_term_data[ $term_id ][ $term_taxonomy->taxonomy ] = $new_term_id;
     4180
     4181    update_option( '_split_terms', $split_term_data );
     4182
     4183    /**
     4184     * Fires after a previously shared taxonomy term is split into two separate terms.
     4185     *
     4186     * @since 4.2.0
     4187     *
     4188     * @param int    $term_id          ID of the formerly shared term.
     4189     * @param int    $new_term_id      ID of the new term created for the $term_taxonomy_id.
     4190     * @param int    $term_taxonomy_id ID for the term_taxonomy row affected by the split.
     4191     * @param string $taxonomy         Taxonomy for the split term.
     4192     */
     4193    do_action( 'split_shared_term', $term_id, $new_term_id, $term_taxonomy_id, $term_taxonomy->taxonomy );
     4194
     4195    return $new_term_id;
     4196}
     4197
     4198/**
     4199 * Check default categories when a term gets split to see if any of them need to be updated.
     4200 *
     4201 * @since 4.2.0
     4202 * @access private
     4203 *
     4204 * @param int    $term_id          ID of the formerly shared term.
     4205 * @param int    $new_term_id      ID of the new term created for the $term_taxonomy_id.
     4206 * @param int    $term_taxonomy_id ID for the term_taxonomy row affected by the split.
     4207 * @param string $taxonomy         Taxonomy for the split term.
     4208 */
     4209function _wp_check_split_default_terms( $term_id, $new_term_id, $term_taxonomy_id, $taxonomy ) {
     4210    if ( 'category' != $taxonomy ) {
     4211        return;
     4212    }
     4213
     4214    foreach ( array( 'default_category', 'default_link_category', 'default_email_category' ) as $option ) {
     4215        if ( $term_id == get_option( $option, -1 ) ) {
     4216            update_option( $option, $new_term_id );
     4217        }
     4218    }
     4219}
     4220
     4221/**
     4222 * Check menu items when a term gets split to see if any of them need to be updated.
     4223 *
     4224 * @since 4.2.0
     4225 * @access private
     4226 *
     4227 * @param int    $term_id          ID of the formerly shared term.
     4228 * @param int    $new_term_id      ID of the new term created for the $term_taxonomy_id.
     4229 * @param int    $term_taxonomy_id ID for the term_taxonomy row affected by the split.
     4230 * @param string $taxonomy         Taxonomy for the split term.
     4231 */
     4232function _wp_check_split_terms_in_menus( $term_id, $new_term_id, $term_taxonomy_id, $taxonomy ) {
     4233    global $wpdb;
     4234    $post_ids = $wpdb->get_col( $wpdb->prepare(
     4235        "SELECT m1.post_id
     4236        FROM {$wpdb->postmeta} AS m1
     4237            INNER JOIN {$wpdb->postmeta} AS m2 ON ( m2.post_id = m1.post_id )
     4238            INNER JOIN {$wpdb->postmeta} AS m3 ON ( m3.post_id = m1.post_id )
     4239        WHERE ( m1.meta_key = '_menu_item_type' AND m1.meta_value = 'taxonomy' )
     4240            AND ( m2.meta_key = '_menu_item_object' AND m2.meta_value = '%s' )
     4241            AND ( m3.meta_key = '_menu_item_object_id' AND m3.meta_value = %d )",
     4242        $taxonomy,
     4243        $term_id
     4244    ) );
     4245
     4246    if ( $post_ids ) {
     4247        foreach ( $post_ids as $post_id ) {
     4248            update_post_meta( $post_id, '_menu_item_object_id', $new_term_id, $term_id );
     4249        }
     4250    }
     4251}
     4252
     4253/**
     4254 * Get data about terms that previously shared a single term_id, but have since been split.
     4255 *
     4256 * @since 4.2.0
     4257 *
     4258 * @param int $old_term_id Term ID. This is the old, pre-split term ID.
     4259 * @return array Array of new term IDs, keyed by taxonomy.
     4260 */
     4261function wp_get_split_terms( $old_term_id ) {
     4262    $split_terms = get_option( '_split_terms', array() );
     4263
     4264    $terms = array();
     4265    if ( isset( $split_terms[ $old_term_id ] ) ) {
     4266        $terms = $split_terms[ $old_term_id ];
     4267    }
     4268
     4269    return $terms;
     4270}
     4271
     4272/**
     4273 * Get the new term ID corresponding to a previously split term.
     4274 *
     4275 * @since 4.2.0
     4276 *
     4277 * @param int    $old_term_id Term ID. This is the old, pre-split term ID.
     4278 * @param string $taxonomy    Taxonomy that the term belongs to.
     4279 * @return bool|int If a previously split term is found corresponding to the old term_id and taxonomy, the new term_id
     4280 *                  will be returned. If no previously split term is found matching the parameters, returns false.
     4281 */
     4282function wp_get_split_term( $old_term_id, $taxonomy ) {
     4283    $split_terms = wp_get_split_terms( $old_term_id );
     4284
     4285    $term_id = false;
     4286    if ( isset( $split_terms[ $taxonomy ] ) ) {
     4287        $term_id = (int) $split_terms[ $taxonomy ];
     4288    }
     4289
     4290    return $term_id;
    40924291}
    40934292
  • trunk/tests/phpunit/tests/term.php

    r31287 r31418  
    761761    }
    762762
     763    /**
     764     * @ticket 5809
     765     */
     766    public function test_wp_update_term_should_split_shared_term() {
     767        global $wpdb;
     768
     769        register_taxonomy( 'wptests_tax', 'post' );
     770        register_taxonomy( 'wptests_tax_2', 'post' );
     771
     772        $t1 = wp_insert_term( 'Foo', 'wptests_tax' );
     773        $t2 = wp_insert_term( 'Foo', 'wptests_tax_2' );
     774
     775        // Manually modify because split terms shouldn't naturally occur.
     776        $wpdb->update( $wpdb->term_taxonomy,
     777            array( 'term_id' => $t1['term_id'] ),
     778            array( 'term_taxonomy_id' => $t2['term_taxonomy_id'] ),
     779            array( '%d' ),
     780            array( '%d' )
     781        );
     782
     783        $posts = $this->factory->post->create_many( 2 );
     784        wp_set_object_terms( $posts[0], array( 'Foo' ), 'wptests_tax' );
     785        wp_set_object_terms( $posts[1], array( 'Foo' ), 'wptests_tax_2' );
     786
     787        // Verify that the terms are shared.
     788        $t1_terms = wp_get_object_terms( $posts[0], 'wptests_tax' );
     789        $t2_terms = wp_get_object_terms( $posts[1], 'wptests_tax_2' );
     790        $this->assertSame( $t1_terms[0]->term_id, $t2_terms[0]->term_id );
     791
     792        wp_update_term( $t2_terms[0]->term_id, 'wptests_tax_2', array(
     793            'name' => 'New Foo',
     794        ) );
     795
     796        $t1_terms = wp_get_object_terms( $posts[0], 'wptests_tax' );
     797        $t2_terms = wp_get_object_terms( $posts[1], 'wptests_tax_2' );
     798        $this->assertNotEquals( $t1_terms[0]->term_id, $t2_terms[0]->term_id );
     799    }
     800
     801    /**
     802     * @ticket 5809
     803     */
     804    public function test_wp_update_term_should_not_split_shared_term_before_410_schema_change() {
     805        global $wpdb;
     806
     807        $db_version = get_option( 'db_version' );
     808        update_option( 'db_version', 30055 );
     809
     810        register_taxonomy( 'wptests_tax', 'post' );
     811        register_taxonomy( 'wptests_tax_2', 'post' );
     812
     813        $t1 = wp_insert_term( 'Foo', 'wptests_tax' );
     814        $t2 = wp_insert_term( 'Foo', 'wptests_tax_2' );
     815
     816        // Manually modify because split terms shouldn't naturally occur.
     817        $wpdb->update( $wpdb->term_taxonomy,
     818            array( 'term_id' => $t1['term_id'] ),
     819            array( 'term_taxonomy_id' => $t2['term_taxonomy_id'] ),
     820            array( '%d' ),
     821            array( '%d' )
     822        );
     823
     824        $posts = $this->factory->post->create_many( 2 );
     825        wp_set_object_terms( $posts[0], array( 'Foo' ), 'wptests_tax' );
     826        wp_set_object_terms( $posts[1], array( 'Foo' ), 'wptests_tax_2' );
     827
     828        // Verify that the term is shared.
     829        $t1_terms = wp_get_object_terms( $posts[0], 'wptests_tax' );
     830        $t2_terms = wp_get_object_terms( $posts[1], 'wptests_tax_2' );
     831        $this->assertSame( $t1_terms[0]->term_id, $t2_terms[0]->term_id );
     832
     833        wp_update_term( $t2_terms[0]->term_id, 'wptests_tax_2', array(
     834            'name' => 'New Foo',
     835        ) );
     836
     837        // Term should still be shared.
     838        $t1_terms = wp_get_object_terms( $posts[0], 'wptests_tax' );
     839        $t2_terms = wp_get_object_terms( $posts[1], 'wptests_tax_2' );
     840        $this->assertSame( $t1_terms[0]->term_id, $t2_terms[0]->term_id );
     841
     842        update_option( 'db_version', $db_version );
     843    }
     844
    763845    public function test_wp_update_term_alias_of_no_term_group() {
    764846        register_taxonomy( 'wptests_tax', 'post' );
Note: See TracChangeset for help on using the changeset viewer.