<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head><meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>[30241] trunk: Split shared taxonomy terms during term update.</title>
</head>
<body>
<style type="text/css"><!--
#msg dl.meta { border: 1px #006 solid; background: #369; padding: 6px; color: #fff; }
#msg dl.meta dt { float: left; width: 6em; font-weight: bold; }
#msg dt:after { content:':';}
#msg dl, #msg dt, #msg ul, #msg li, #header, #footer, #logmsg { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; }
#msg dl a { font-weight: bold}
#msg dl a:link { color:#fc3; }
#msg dl a:active { color:#ff0; }
#msg dl a:visited { color:#cc6; }
h3 { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; font-weight: bold; }
#msg pre { overflow: auto; background: #ffc; border: 1px #fa0 solid; padding: 6px; }
#logmsg { background: #ffc; border: 1px #fa0 solid; padding: 1em 1em 0 1em; }
#logmsg p, #logmsg pre, #logmsg blockquote { margin: 0 0 1em 0; }
#logmsg p, #logmsg li, #logmsg dt, #logmsg dd { line-height: 14pt; }
#logmsg h1, #logmsg h2, #logmsg h3, #logmsg h4, #logmsg h5, #logmsg h6 { margin: .5em 0; }
#logmsg h1:first-child, #logmsg h2:first-child, #logmsg h3:first-child, #logmsg h4:first-child, #logmsg h5:first-child, #logmsg h6:first-child { margin-top: 0; }
#logmsg ul, #logmsg ol { padding: 0; list-style-position: inside; margin: 0 0 0 1em; }
#logmsg ul { text-indent: -1em; padding-left: 1em; }#logmsg ol { text-indent: -1.5em; padding-left: 1.5em; }
#logmsg > ul, #logmsg > ol { margin: 0 0 1em 0; }
#logmsg pre { background: #eee; padding: 1em; }
#logmsg blockquote { border: 1px solid #fa0; border-left-width: 10px; padding: 1em 1em 0 1em; background: white;}
#logmsg dl { margin: 0; }
#logmsg dt { font-weight: bold; }
#logmsg dd { margin: 0; padding: 0 0 0.5em 0; }
#logmsg dd:before { content:'\00bb';}
#logmsg table { border-spacing: 0px; border-collapse: collapse; border-top: 4px solid #fa0; border-bottom: 1px solid #fa0; background: #fff; }
#logmsg table th { text-align: left; font-weight: normal; padding: 0.2em 0.5em; border-top: 1px dotted #fa0; }
#logmsg table td { text-align: right; border-top: 1px dotted #fa0; padding: 0.2em 0.5em; }
#logmsg table thead th { text-align: center; border-bottom: 1px solid #fa0; }
#logmsg table th.Corner { text-align: left; }
#logmsg hr { border: none 0; border-top: 2px dashed #fa0; height: 1px; }
#header, #footer { color: #fff; background: #636; border: 1px #300 solid; padding: 6px; }
#patch { width: 100%; }
#patch h4 {font-family: verdana,arial,helvetica,sans-serif;font-size:10pt;padding:8px;background:#369;color:#fff;margin:0;}
#patch .propset h4, #patch .binary h4 {margin:0;}
#patch pre {padding:0;line-height:1.2em;margin:0;}
#patch .diff {width:100%;background:#eee;padding: 0 0 10px 0;overflow:auto;}
#patch .propset .diff, #patch .binary .diff {padding:10px 0;}
#patch span {display:block;padding:0 10px;}
#patch .modfile, #patch .addfile, #patch .delfile, #patch .propset, #patch .binary, #patch .copfile {border:1px solid #ccc;margin:10px 0;}
#patch ins {background:#dfd;text-decoration:none;display:block;padding:0 10px;}
#patch del {background:#fdd;text-decoration:none;display:block;padding:0 10px;}
#patch .lines, .info {color:#888;background:#fff;}
--></style>
<div id="msg">
<dl class="meta" style="font-size: 105%">
<dt style="float: left; width: 6em; font-weight: bold">Revision</dt> <dd><a style="font-weight: bold" href="https://core.trac.wordpress.org/changeset/30241">30241</a><script type="application/ld+json">{"@context":"http://schema.org","@type":"EmailMessage","description":"Review this Commit","action":{"@type":"ViewAction","url":"https://core.trac.wordpress.org/changeset/30241","name":"Review Commit"}}</script></dd>
<dt style="float: left; width: 6em; font-weight: bold">Author</dt> <dd>boonebgorges</dd>
<dt style="float: left; width: 6em; font-weight: bold">Date</dt> <dd>2014-11-05 02:02:48 +0000 (Wed, 05 Nov 2014)</dd>
</dl>
<pre style='padding-left: 1em; margin: 2em 0; border-left: 2px solid #ccc; line-height: 1.25; font-size: 105%; font-family: sans-serif'>Split shared taxonomy terms during 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.
Note that this term splitting only occurs on installations whose database
schemas have been upgraded to version 30133 or higher. Note also that shared
terms are only split when run through `wp_update_term()`, as on edit-tags.php;
we will wait until a future release of WordPress to force the splitting of all
shared taxonomy terms.
Props boonebgorges, rmccue, greuben, garyc40, wonderboymusic, imath, jesin.
Fixes <a href="https://core.trac.wordpress.org/ticket/5809">#5809</a>.</pre>
<h3>Modified Paths</h3>
<ul>
<li><a href="#trunksrcwpincludestaxonomyphp">trunk/src/wp-includes/taxonomy.php</a></li>
<li><a href="#trunktestsphpunitteststermphp">trunk/tests/phpunit/tests/term.php</a></li>
</ul>
</div>
<div id="patch">
<h3>Diff</h3>
<a id="trunksrcwpincludestaxonomyphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-includes/taxonomy.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/taxonomy.php 2014-11-05 01:41:58 UTC (rev 30240)
+++ trunk/src/wp-includes/taxonomy.php 2014-11-05 02:02:48 UTC (rev 30241)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -3385,6 +3385,14 @@
</span><span class="cx" style="display: block; padding: 0 10px"> return new WP_Error('duplicate_term_slug', sprintf(__('The slug “%s” is already in use by another term'), $slug));
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px">
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ $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) );
+
+ // 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;
+ }
+
</ins><span class="cx" style="display: block; padding: 0 10px"> /**
</span><span class="cx" style="display: block; padding: 0 10px"> * Fires immediately before the given terms are edited.
</span><span class="cx" style="display: block; padding: 0 10px"> *
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -3410,8 +3418,6 @@
</span><span class="cx" style="display: block; padding: 0 10px"> */
</span><span class="cx" style="display: block; padding: 0 10px"> do_action( 'edited_terms', $term_id, $taxonomy );
</span><span class="cx" style="display: block; padding: 0 10px">
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- $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) );
-
</del><span class="cx" style="display: block; padding: 0 10px"> /**
</span><span class="cx" style="display: block; padding: 0 10px"> * Fires immediate before a term-taxonomy relationship is updated.
</span><span class="cx" style="display: block; padding: 0 10px"> *
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -4041,6 +4047,62 @@
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px">
</span><span class="cx" style="display: block; padding: 0 10px"> /**
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ * Create a new term for a term_taxonomy item that currently shares its term.
+ *
+ * @since 4.1.0
+ * @access private
+ *
+ * @param int $term_id ID of the shared term.
+ * @param int $term_taxonomy_id ID of the term taxonomy item to receive a new term.
+ * @param array $shared_tts Sibling term taxonomies, used for busting caches.
+ * @return int Term ID.
+ */
+function _split_shared_term( $term_id, $term_taxonomy_id ) {
+ global $wpdb;
+
+ // Don't try to split terms if database schema does not support shared slugs.
+ $current_db_version = get_option( 'db_version' );
+ if ( $current_db_version < 30133 ) {
+ return $term_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;
+ }
+
+ // Pull up data about the currently shared slug, which we'll use to populate the new one.
+ $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 )
+ );
+
+ // 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 ) );
+ foreach ( (array) $shared_term_taxonomies as $shared_term_taxonomy ) {
+ clean_term_cache( $term_id, $shared_term_taxonomy );
+ }
+
+ return $new_term_id;
+}
+
+/**
</ins><span class="cx" style="display: block; padding: 0 10px"> * Generate a permalink for a taxonomy term archive.
</span><span class="cx" style="display: block; padding: 0 10px"> *
</span><span class="cx" style="display: block; padding: 0 10px"> * @since 2.5.0
</span></span></pre></div>
<a id="trunktestsphpunitteststermphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/tests/phpunit/tests/term.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/term.php 2014-11-05 01:41:58 UTC (rev 30240)
+++ trunk/tests/phpunit/tests/term.php 2014-11-05 02:02:48 UTC (rev 30241)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -637,6 +637,141 @@
</span><span class="cx" style="display: block; padding: 0 10px"> _unregister_taxonomy( 'wptests_tax' );
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px">
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ /**
+ * @ticket 5809
+ */
+ public function test_wp_update_term_duplicate_slug_same_taxonomy() {
+ register_taxonomy( 'wptests_tax', 'post' );
+
+ $t1 = $this->factory->term->create( array(
+ 'name' => 'Foo',
+ 'slug' => 'foo',
+ 'taxonomy' => 'wptests_tax',
+ ) );
+
+ $t2 = $this->factory->term->create( array(
+ 'name' => 'Foo',
+ 'slug' => 'bar',
+ 'taxonomy' => 'wptests_tax',
+ ) );
+
+ $updated = wp_update_term( $t2, 'wptests_tax', array(
+ 'slug' => 'foo',
+ ) );
+
+ $this->assertWPError( $updated );
+ $this->assertSame( 'duplicate_term_slug', $updated->get_error_code() );
+ }
+
+ /**
+ * @ticket 5809
+ */
+ public function test_wp_update_term_duplicate_slug_different_taxonomy() {
+ register_taxonomy( 'wptests_tax', 'post' );
+ register_taxonomy( 'wptests_tax_2', 'post' );
+
+ $t1 = $this->factory->term->create( array(
+ 'name' => 'Foo',
+ 'slug' => 'foo',
+ 'taxonomy' => 'wptests_tax',
+ ) );
+
+ $t2 = $this->factory->term->create( array(
+ 'name' => 'Foo',
+ 'slug' => 'bar',
+ 'taxonomy' => 'wptests_tax_2',
+ ) );
+
+ $updated = wp_update_term( $t2, 'wptests_tax_2', array(
+ 'slug' => 'foo',
+ ) );
+
+ $this->assertWPError( $updated );
+ $this->assertSame( 'duplicate_term_slug', $updated->get_error_code() );
+ }
+
+ /**
+ * @ticket 5809
+ */
+ public function test_wp_update_term_should_split_shared_term() {
+ global $wpdb;
+
+ register_taxonomy( 'wptests_tax', 'post' );
+ register_taxonomy( 'wptests_tax_2', 'post' );
+
+ $t1 = wp_insert_term( 'Foo', 'wptests_tax' );
+ $t2 = wp_insert_term( 'Foo', 'wptests_tax_2' );
+
+ // Manually modify because split terms shouldn't naturally occur.
+ $wpdb->update( $wpdb->term_taxonomy,
+ array( 'term_id' => $t1['term_id'] ),
+ array( 'term_taxonomy_id' => $t2['term_taxonomy_id'] ),
+ array( '%d' ),
+ array( '%d' )
+ );
+
+ $posts = $this->factory->post->create_many( 2 );
+ wp_set_object_terms( $posts[0], array( 'Foo' ), 'wptests_tax' );
+ wp_set_object_terms( $posts[1], array( 'Foo' ), 'wptests_tax_2' );
+
+ // Verify that the terms are shared.
+ $t1_terms = wp_get_object_terms( $posts[0], 'wptests_tax' );
+ $t2_terms = wp_get_object_terms( $posts[1], 'wptests_tax_2' );
+ $this->assertSame( $t1_terms[0]->term_id, $t2_terms[0]->term_id );
+
+ wp_update_term( $t2_terms[0]->term_id, 'wptests_tax_2', array(
+ 'name' => 'New Foo',
+ ) );
+
+ $t1_terms = wp_get_object_terms( $posts[0], 'wptests_tax' );
+ $t2_terms = wp_get_object_terms( $posts[1], 'wptests_tax_2' );
+ $this->assertNotEquals( $t1_terms[0]->term_id, $t2_terms[0]->term_id );
+ }
+
+ /**
+ * @ticket 5809
+ */
+ public function test_wp_update_term_should_not_split_shared_term_before_410_schema_change() {
+ global $wpdb;
+
+ $db_version = get_option( 'db_version' );
+ update_option( 'db_version', 30055 );
+
+ register_taxonomy( 'wptests_tax', 'post' );
+ register_taxonomy( 'wptests_tax_2', 'post' );
+
+ $t1 = wp_insert_term( 'Foo', 'wptests_tax' );
+ $t2 = wp_insert_term( 'Foo', 'wptests_tax_2' );
+
+ // Manually modify because split terms shouldn't naturally occur.
+ $wpdb->update( $wpdb->term_taxonomy,
+ array( 'term_id' => $t1['term_id'] ),
+ array( 'term_taxonomy_id' => $t2['term_taxonomy_id'] ),
+ array( '%d' ),
+ array( '%d' )
+ );
+
+ $posts = $this->factory->post->create_many( 2 );
+ wp_set_object_terms( $posts[0], array( 'Foo' ), 'wptests_tax' );
+ wp_set_object_terms( $posts[1], array( 'Foo' ), 'wptests_tax_2' );
+
+ // Verify that the term is shared.
+ $t1_terms = wp_get_object_terms( $posts[0], 'wptests_tax' );
+ $t2_terms = wp_get_object_terms( $posts[1], 'wptests_tax_2' );
+ $this->assertSame( $t1_terms[0]->term_id, $t2_terms[0]->term_id );
+
+ wp_update_term( $t2_terms[0]->term_id, 'wptests_tax_2', array(
+ 'name' => 'New Foo',
+ ) );
+
+ // Term should still be shared.
+ $t1_terms = wp_get_object_terms( $posts[0], 'wptests_tax' );
+ $t2_terms = wp_get_object_terms( $posts[1], 'wptests_tax_2' );
+ $this->assertSame( $t1_terms[0]->term_id, $t2_terms[0]->term_id );
+
+ update_option( 'db_version', $db_version );
+ }
+
</ins><span class="cx" style="display: block; padding: 0 10px"> public function test_wp_update_term_alias_of_no_term_group() {
</span><span class="cx" style="display: block; padding: 0 10px"> register_taxonomy( 'wptests_tax', 'post' );
</span><span class="cx" style="display: block; padding: 0 10px"> $t1 = $this->factory->term->create( array(
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1466,52 +1601,6 @@
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px">
</span><span class="cx" style="display: block; padding: 0 10px"> /**
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- * @ticket 5809
- */
- function test_update_shared_term() {
- $random_tax = __FUNCTION__;
-
- register_taxonomy( $random_tax, 'post' );
-
- $post_id = $this->factory->post->create();
-
- $old_name = 'Initial';
-
- $t1 = wp_insert_term( $old_name, 'category' );
- $t2 = wp_insert_term( $old_name, 'post_tag' );
-
- $this->assertEquals( $t1['term_id'], $t2['term_id'] );
-
- wp_set_post_categories( $post_id, array( $t1['term_id'] ) );
- wp_set_post_tags( $post_id, array( (int) $t2['term_id'] ) );
-
- $new_name = 'Updated';
-
- // create the term in a third taxonomy, just to keep things interesting
- $t3 = wp_insert_term( $old_name, $random_tax );
- wp_set_post_terms( $post_id, array( (int) $t3['term_id'] ), $random_tax );
- $this->assertPostHasTerms( $post_id, array( $t3['term_id'] ), $random_tax );
-
- $t2_updated = wp_update_term( $t2['term_id'], 'post_tag', array(
- 'name' => $new_name
- ) );
-
- $this->assertNotEquals( $t2_updated['term_id'], $t3['term_id'] );
-
- // make sure the terms have split
- $this->assertEquals( $old_name, get_term_field( 'name', $t1['term_id'], 'category' ) );
- $this->assertEquals( $new_name, get_term_field( 'name', $t2_updated['term_id'], 'post_tag' ) );
-
- // and that they are still assigned to the correct post
- $this->assertPostHasTerms( $post_id, array( $t1['term_id'] ), 'category' );
- $this->assertPostHasTerms( $post_id, array( $t2_updated['term_id'] ), 'post_tag' );
- $this->assertPostHasTerms( $post_id, array( $t3['term_id'] ), $random_tax );
-
- // clean up
- unset( $GLOBALS['wp_taxonomies'][ $random_tax ] );
- }
-
- /**
</del><span class="cx" style="display: block; padding: 0 10px"> * @ticket 25852
</span><span class="cx" style="display: block; padding: 0 10px"> */
</span><span class="cx" style="display: block; padding: 0 10px"> function test_sanitize_term_field() {
</span></span></pre>
</div>
</div>
</body>
</html>