Make WordPress Core

Changeset 31418


Ignore:
Timestamp:
02/11/2015 07:41:54 PM (12 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.