<!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>[35515] trunk: Don't allow term meta to be added to shared taxonomy terms.</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/35515">35515</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/35515","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>2015-11-04 21:23:28 +0000 (Wed, 04 Nov 2015)</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'>Don't allow term meta to be added to shared taxonomy terms.

`add_term_meta()` and `update_term_meta()` identify terms by `$term_id`. In
cases where a term is shared between taxonomies, `$term_id` is insufficient to
distinguish where the metadata belongs.

When attempting to add/update termmeta on a shared term, a `WP_Error` object
is returned. This gives developers enough information to decide whether they'd
like to force the term to be split and retry the save, or show an error in the
UI, or whatever.

Props boonebgorges, mboynes, DH-Shredder, jorbin, aaroncampbell.
Fixes <a href="https://core.trac.wordpress.org/ticket/34544">#34544</a>.</pre>

<h3>Modified Paths</h3>
<ul>
<li><a href="#trunksrcwpincludestaxonomyfunctionsphp">trunk/src/wp-includes/taxonomy-functions.php</a></li>
<li><a href="#trunktestsphpunitteststermmetaphp">trunk/tests/phpunit/tests/term/meta.php</a></li>
</ul>

</div>
<div id="patch">
<h3>Diff</h3>
<a id="trunksrcwpincludestaxonomyfunctionsphp"></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-functions.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/taxonomy-functions.php      2015-11-04 21:22:21 UTC (rev 35514)
+++ trunk/src/wp-includes/taxonomy-functions.php        2015-11-04 21:23:28 UTC (rev 35515)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1578,7 +1578,8 @@
</span><span class="cx" style="display: block; padding: 0 10px">  * @param mixed  $meta_value Metadata value.
</span><span class="cx" style="display: block; padding: 0 10px">  * @param bool   $unique     Optional. Whether to bail if an entry with the same key is found for the term.
</span><span class="cx" style="display: block; padding: 0 10px">  *                           Default false.
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- * @return int|bool Meta ID on success, false on failure.
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ * @return int|WP_Error|bool Meta ID on success. WP_Error when term_id is ambiguous between taxonomies.
+ *                           False on failure.
</ins><span class="cx" style="display: block; padding: 0 10px">  */
</span><span class="cx" style="display: block; padding: 0 10px"> function add_term_meta( $term_id, $meta_key, $meta_value, $unique = false ) {
</span><span class="cx" style="display: block; padding: 0 10px">        // Bail if term meta table is not installed.
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1586,6 +1587,10 @@
</span><span class="cx" style="display: block; padding: 0 10px">                return false;
</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">+        if ( wp_term_is_shared( $term_id ) ) {
+               return new WP_Error( 'ambiguous_term_id', __( 'Term meta cannot be added to terms that are shared between taxonomies.'), $term_id );
+       }
+
</ins><span class="cx" style="display: block; padding: 0 10px">         $added = add_metadata( 'term', $term_id, $meta_key, $meta_value, $unique );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        // Bust term query cache.
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1655,7 +1660,8 @@
</span><span class="cx" style="display: block; padding: 0 10px">  * @param string $meta_key   Metadata key.
</span><span class="cx" style="display: block; padding: 0 10px">  * @param mixed  $meta_value Metadata value.
</span><span class="cx" style="display: block; padding: 0 10px">  * @param mixed  $prev_value Optional. Previous value to check before removing.
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- * @return int|bool Meta ID if the key didn't previously exist. True on successful update. False on failure.
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ * @return int|WP_Error|bool Meta ID if the key didn't previously exist. True on successful update.
+ *                           WP_Error when term_id is ambiguous between taxonomies. False on failure.
</ins><span class="cx" style="display: block; padding: 0 10px">  */
</span><span class="cx" style="display: block; padding: 0 10px"> function update_term_meta( $term_id, $meta_key, $meta_value, $prev_value = '' ) {
</span><span class="cx" style="display: block; padding: 0 10px">        // Bail if term meta table is not installed.
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1663,6 +1669,10 @@
</span><span class="cx" style="display: block; padding: 0 10px">                return false;
</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">+        if ( wp_term_is_shared( $term_id ) ) {
+               return new WP_Error( 'ambiguous_term_id', __( 'Term meta cannot be added to terms that are shared between taxonomies.'), $term_id );
+       }
+
</ins><span class="cx" style="display: block; padding: 0 10px">         $updated = update_metadata( 'term', $term_id, $meta_key, $meta_value, $prev_value );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        // Bust term query cache.
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -4007,6 +4017,18 @@
</span><span class="cx" style="display: block; padding: 0 10px">                update_option( '_split_terms', $split_term_data );
</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">+        // 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 );
+       }
+
</ins><span class="cx" style="display: block; padding: 0 10px">         /**
</span><span class="cx" style="display: block; padding: 0 10px">         * Fires after a previously shared taxonomy term is split into two separate terms.
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -4255,6 +4277,29 @@
</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">+ * 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;
+       }
+
+       $tt_count = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM $wpdb->term_taxonomy WHERE term_id = %d", $term_id ) );
+
+       return $tt_count > 1;
+}
+
+/**
</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="trunktestsphpunitteststermmetaphp"></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/meta.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/term/meta.php   2015-11-04 21:22:21 UTC (rev 35514)
+++ trunk/tests/phpunit/tests/term/meta.php     2015-11-04 21:23:28 UTC (rev 35515)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -307,6 +307,80 @@
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertEqualSets( array( $terms[0] ), $found );
</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 34544
+        */
+       public function test_add_term_meta_should_return_error_when_term_id_is_shared() {
+               global $wpdb;
+
+               update_option( 'finished_splitting_shared_terms', false );
+
+               register_taxonomy( 'wptests_tax', 'post' );
+               register_taxonomy( 'wptests_tax_2', 'post' );
+               register_taxonomy( 'wptests_tax_3', 'post' );
+
+               $t1 = wp_insert_term( 'Foo', 'wptests_tax' );
+               $t2 = wp_insert_term( 'Foo', 'wptests_tax_2' );
+               $t3 = wp_insert_term( 'Foo', 'wptests_tax_3' );
+
+               // Manually modify because shared 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' )
+               );
+
+               $wpdb->update( $wpdb->term_taxonomy,
+                       array( 'term_id' => $t1['term_id'] ),
+                       array( 'term_taxonomy_id' => $t3['term_taxonomy_id'] ),
+                       array( '%d' ),
+                       array( '%d' )
+               );
+
+               $found = add_term_meta( $t1['term_id'], 'bar', 'baz' );
+               $this->assertWPError( $found );
+               $this->assertSame( 'ambiguous_term_id', $found->get_error_code() );
+       }
+
+       /**
+        * @ticket 34544
+        */
+       public function test_update_term_meta_should_return_error_when_term_id_is_shared() {
+               global $wpdb;
+
+               update_option( 'finished_splitting_shared_terms', false );
+
+               register_taxonomy( 'wptests_tax', 'post' );
+               $t1 = wp_insert_term( 'Foo', 'wptests_tax' );
+               add_term_meta( $t1, 'foo', 'bar' );
+
+               register_taxonomy( 'wptests_tax_2', 'post' );
+               register_taxonomy( 'wptests_tax_3', 'post' );
+
+               $t2 = wp_insert_term( 'Foo', 'wptests_tax_2' );
+               $t3 = wp_insert_term( 'Foo', 'wptests_tax_3' );
+
+               // Manually modify because shared 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' )
+               );
+
+               $wpdb->update( $wpdb->term_taxonomy,
+                       array( 'term_id' => $t1['term_id'] ),
+                       array( 'term_taxonomy_id' => $t3['term_taxonomy_id'] ),
+                       array( '%d' ),
+                       array( '%d' )
+               );
+
+               $found = update_term_meta( $t1['term_id'], 'foo', 'baz' );
+               $this->assertWPError( $found );
+               $this->assertSame( 'ambiguous_term_id', $found->get_error_code() );
+       }
+
</ins><span class="cx" style="display: block; padding: 0 10px">         public static function set_cache_results( $q ) {
</span><span class="cx" style="display: block; padding: 0 10px">                $q->set( 'cache_results', true );
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span></span></pre>
</div>
</div>

</body>
</html>