<!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>[23823] trunk: Fix a longstanding &quot;off by one&quot; revision authorship bug.</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, #logmsg > ol { margin-left: 0; 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">
<dt>Revision</dt> <dd><a href="http://core.trac.wordpress.org/changeset/23823">23823</a></dd>
<dt>Author</dt> <dd>markjaquith</dd>
<dt>Date</dt> <dd>2013-03-27 20:21:38 +0000 (Wed, 27 Mar 2013)</dd>
</dl>

<h3>Log Message</h3>
<pre>Fix a longstanding &quot;off by one&quot; revision authorship bug.

* Fixes old revision data on the fly when you open a post for editing.
* Uses post_name of revisions to store a post version number (-v1), so we know what has been fixed.
* Latest version should also have a revision stored, whereas before it did not.

props adamsilverstein, mdawaffe. fixes <a href="http://core.trac.wordpress.org/ticket/16215">#16215</a>.</pre>

<h3>Modified Paths</h3>
<ul>
<li><a href="#trunkwpadminpostphp">trunk/wp-admin/post.php</a></li>
<li><a href="#trunkwpincludesdefaultfiltersphp">trunk/wp-includes/default-filters.php</a></li>
<li><a href="#trunkwpincludespostphp">trunk/wp-includes/post.php</a></li>
<li><a href="#trunkwpincludesrevisionphp">trunk/wp-includes/revision.php</a></li>
</ul>

</div>
<div id="patch">
<h3>Diff</h3>
<a id="trunkwpadminpostphp"></a>
<div class="modfile"><h4>Modified: trunk/wp-admin/post.php (23822 => 23823)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/wp-admin/post.php        2013-03-27 19:21:56 UTC (rev 23822)
+++ trunk/wp-admin/post.php        2013-03-27 20:21:38 UTC (rev 23823)
</span><span class="lines">@@ -135,6 +135,7 @@
</span><span class="cx"> 
</span><span class="cx">         $p = $post_id;
</span><span class="cx"> 
</span><ins>+
</ins><span class="cx">         if ( empty($post-&gt;ID) )
</span><span class="cx">                 wp_die( __('You attempted to edit an item that doesn&amp;#8217;t exist. Perhaps it was deleted?') );
</span><span class="cx"> 
</span><span class="lines">@@ -153,6 +154,9 @@
</span><span class="cx">                 exit();
</span><span class="cx">         }
</span><span class="cx"> 
</span><ins>+        //upgrade any old bad revision data (#16215)
+        _wp_upgrade_revisions_of_post( $p );
+
</ins><span class="cx">         $post_type = $post-&gt;post_type;
</span><span class="cx">         if ( 'post' == $post_type ) {
</span><span class="cx">                 $parent_file = &quot;edit.php&quot;;
</span></span></pre></div>
<a id="trunkwpincludesdefaultfiltersphp"></a>
<div class="modfile"><h4>Modified: trunk/wp-includes/default-filters.php (23822 => 23823)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/wp-includes/default-filters.php        2013-03-27 19:21:56 UTC (rev 23822)
+++ trunk/wp-includes/default-filters.php        2013-03-27 20:21:38 UTC (rev 23823)
</span><span class="lines">@@ -250,7 +250,7 @@
</span><span class="cx"> add_action( 'plugins_loaded',             'wp_maybe_load_widgets',                    0    );
</span><span class="cx"> add_action( 'plugins_loaded',             'wp_maybe_load_embeds',                     0    );
</span><span class="cx"> add_action( 'shutdown',                   'wp_ob_end_flush_all',                      1    );
</span><del>-add_action( 'pre_post_update',            'wp_save_post_revision',                   10, 2 );
</del><ins>+add_action( 'post_updated',               'wp_save_post_revision',                   10, 1 );
</ins><span class="cx"> add_action( 'publish_post',               '_publish_post_hook',                       5, 1 );
</span><span class="cx"> add_action( 'transition_post_status',     '_transition_post_status',                  5, 3 );
</span><span class="cx"> add_action( 'transition_post_status',     '_update_term_count_on_transition_post_status', 10, 3 );
</span></span></pre></div>
<a id="trunkwpincludespostphp"></a>
<div class="modfile"><h4>Modified: trunk/wp-includes/post.php (23822 => 23823)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/wp-includes/post.php        2013-03-27 19:21:56 UTC (rev 23822)
+++ trunk/wp-includes/post.php        2013-03-27 20:21:38 UTC (rev 23823)
</span><span class="lines">@@ -3012,7 +3012,7 @@
</span><span class="cx">  * @return string unique slug for the post, based on $post_name (with a -1, -2, etc. suffix)
</span><span class="cx">  */
</span><span class="cx"> function wp_unique_post_slug( $slug, $post_ID, $post_status, $post_type, $post_parent ) {
</span><del>-        if ( in_array( $post_status, array( 'draft', 'pending', 'auto-draft' ) ) )
</del><ins>+        if ( in_array( $post_status, array( 'draft', 'pending', 'auto-draft' ) ) || ( 'inherit' == $post_status &amp;&amp; 'revision' == $post_type ) )
</ins><span class="cx">                 return $slug;
</span><span class="cx"> 
</span><span class="cx">         global $wpdb, $wp_rewrite;
</span></span></pre></div>
<a id="trunkwpincludesrevisionphp"></a>
<div class="modfile"><h4>Modified: trunk/wp-includes/revision.php (23822 => 23823)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/wp-includes/revision.php        2013-03-27 19:21:56 UTC (rev 23822)
+++ trunk/wp-includes/revision.php        2013-03-27 20:21:38 UTC (rev 23823)
</span><span class="lines">@@ -52,9 +52,10 @@
</span><span class="cx">         $return['post_parent']   = $post['ID'];
</span><span class="cx">         $return['post_status']   = 'inherit';
</span><span class="cx">         $return['post_type']     = 'revision';
</span><del>-        $return['post_name']     = $autosave ? &quot;$post[ID]-autosave&quot; : &quot;$post[ID]-revision&quot;;
</del><ins>+        $return['post_name']     = $autosave ? &quot;$post[ID]-autosave-v1&quot; : &quot;$post[ID]-revision-v1&quot;; // &quot;1&quot; is the revisioning system version
</ins><span class="cx">         $return['post_date']     = isset($post['post_modified']) ? $post['post_modified'] : '';
</span><span class="cx">         $return['post_date_gmt'] = isset($post['post_modified_gmt']) ? $post['post_modified_gmt'] : '';
</span><ins>+        $return['post_author']   = get_post_meta( $post['ID'], '_edit_last', true );
</ins><span class="cx"> 
</span><span class="cx">         return $return;
</span><span class="cx"> }
</span><span class="lines">@@ -62,19 +63,27 @@
</span><span class="cx"> /**
</span><span class="cx">  * Saves an already existing post as a post revision.
</span><span class="cx">  *
</span><del>- * Typically used immediately prior to post updates.
</del><ins>+ * Typically used immediately prior and after post updates.
+ * Prior to update checks for old revision data (latest revision != current post before update) and adds a copy of the current post as a revision if missing
+ * After update adds a copy of the current post as a revision, so latest revision always matches current post
</ins><span class="cx">  *
</span><span class="cx">  * @package WordPress
</span><span class="cx">  * @subpackage Post_Revisions
</span><span class="cx">  * @since 2.6.0
</span><span class="cx">  *
</span><span class="cx">  * @uses _wp_put_post_revision()
</span><ins>+ * @uses wp_first_revision_matches_current_version()
</ins><span class="cx">  *
</span><span class="cx">  * @param int $post_id The ID of the post to save as a revision.
</span><span class="cx">  * @return mixed Null or 0 if error, new revision ID, if success.
</span><span class="cx">  */
</span><del>-function wp_save_post_revision( $post_id, $new_data = null ) {
-        // We do autosaves manually with wp_create_post_autosave()
</del><ins>+function wp_save_post_revision( $post_id ) {
+        //check to see if the post's first revision already matches the post data
+        //should be true before post update, _except_ for old data which
+        //doesn't include a copy of the current post data in revisions
+        if ( wp_first_revision_matches_current_version( $post_id ) )
+                return;
+
</ins><span class="cx">         if ( defined( 'DOING_AUTOSAVE' ) &amp;&amp; DOING_AUTOSAVE )
</span><span class="cx">                 return;
</span><span class="cx"> 
</span><span class="lines">@@ -90,18 +99,32 @@
</span><span class="cx">         if ( ! post_type_supports( $post['post_type'], 'revisions' ) )
</span><span class="cx">                 return;
</span><span class="cx"> 
</span><del>-        // if new data is supplied, check that it is different from last saved revision, unless a plugin tells us to always save regardless
-        if ( apply_filters( 'wp_save_post_revision_check_for_changes', true, $post, $new_data ) &amp;&amp; is_array( $new_data ) ) {
-                $post_has_changed = false;
-                foreach ( array_keys( _wp_post_revision_fields() ) as $field ) {
-                        if ( normalize_whitespace( $new_data[ $field ] ) != normalize_whitespace( $post[ $field ] ) ) {
-                                $post_has_changed = true;
-                                break;
</del><ins>+        // compare the proposed update with the last stored revision, verify
+        // different, unless a plugin tells us to always save regardless
+        if ( $revisions = wp_get_post_revisions( $post_id ) ) {
+                // grab the last revision
+                $last_revision = array_shift( $revisions );
+
+                //if no previous revisions, save one for sure
+                if ( $last_revision_array = get_post( $last_revision-&gt;ID, ARRAY_A ) ) {
+
+                        if ( apply_filters( 'wp_save_post_revision_check_for_changes', true, $last_revision_array, $post ) &amp;&amp; is_array( $post ) ) {
+                                $post_has_changed = false;
+
+                                foreach ( array_keys( _wp_post_revision_fields() ) as $field ) {
+
+                                        if ( normalize_whitespace( $post[ $field ] ) != normalize_whitespace( $last_revision_array[ $field ] ) ) {
+                                                $post_has_changed = true;
+                                                break;
+
+                                        }
+                                }
+
+                                //don't save revision if post unchanged
+                                if( ! $post_has_changed )
+                                        return;
</ins><span class="cx">                         }
</span><span class="cx">                 }
</span><del>-                //don't save revision if post unchanged
-                if( ! $post_has_changed )
-                        return;
</del><span class="cx">         }
</span><span class="cx"> 
</span><span class="cx">         $return = _wp_put_post_revision( $post );
</span><span class="lines">@@ -122,9 +145,9 @@
</span><span class="cx">         $revisions = array_slice( $revisions, 0, $delete );
</span><span class="cx"> 
</span><span class="cx">         for ( $i = 0; isset($revisions[$i]); $i++ ) {
</span><del>-                if ( false !== strpos( $revisions[$i]-&gt;post_name, 'autosave' ) )
</del><ins>+                if ( false !== strpos( $revisions[ $i ]-&gt;post_name, 'autosave' ) )
</ins><span class="cx">                         continue;
</span><del>-                wp_delete_post_revision( $revisions[$i]-&gt;ID );
</del><ins>+                wp_delete_post_revision( $revisions[ $i ]-&gt;ID );
</ins><span class="cx">         }
</span><span class="cx"> 
</span><span class="cx">         return $return;
</span><span class="lines">@@ -441,6 +464,148 @@
</span><span class="cx">         return $post;
</span><span class="cx"> }
</span><span class="cx"> 
</span><ins>+function _wp_get_post_revision_version( $post ) {
+        if ( is_array( $post ) ) {
+                if ( ! isset( $post['post_name'] ) ) {
+                        return false;
+                }
+
+                $name = $post['post_name'];
+        } elseif ( is_object( $post ) ) {
+                if ( ! isset( $post-&gt;post_name ) ) {
+                        return false;
+                }
+
+                $name = $post-&gt;post_name;
+        } else {
+                return false;
+        }
+
+        if ( ! preg_match( '/^(\d+-)(?:autosave|revision)(?:-v)(\d+)$/', $name, $matches ) ) {
+                return 0;
+        }
+
+        if ( '1' === $matches[2] ) {
+                return 1;
+        }
+
+        return 0;
+}
+
+/**
+ * Upgrade the data
+ *
+ * @package WordPress
+ * @subpackage Post_Revisions
+ * @since 3.6.0
+ *
+ * @uses get_post()
+ * @uses post_type_supports()
+ * @uses wp_get_post_revisions()
+ * @uses wp_save_post_revision()
+ *
+ * @param int|object $post_id Post ID or post object
+ * @return true if success, false if problems
+ */
+function _wp_upgrade_revisions_of_post( $post ) {
+        global $wpdb;
+
+        $post = get_post( $post );
+        if ( ! $post )
+                return false;
+
+        //make sure we have a current revision, only adds one if missing
+        wp_save_post_revision( $post-&gt;ID );
+
+        if ( ! post_type_supports( $post-&gt;post_type, 'revisions' ) )
+                return false;
+
+        $revisions = wp_get_post_revisions( $post-&gt;ID ); // array( 'order' =&gt; 'DESC', 'orderby' =&gt; 'date' ); // Always work from most recent to oldest
+
+
+        if ( ! $revisions )
+                return true;
+
+        // Add post option exclusively
+        $lock      = &quot;revision-upgrade-{$post-&gt;ID}&quot;;
+        $locked_at = number_format( microtime( true ), 10, '.', '' );
+        $result = $wpdb-&gt;query( $wpdb-&gt;prepare( &quot;INSERT IGNORE INTO `$wpdb-&gt;options` (`option_name`, `option_value`, `autoload`) VALUES (%s, %s, 'no') /* LOCK */&quot;, $lock, $locked_at ) );
+        if ( ! $result ) {
+                // If we couldn't get a lock, see how old the previous lock is
+                $locked_at = get_option( $lock );
+                if ( !$locked_at ) {
+                        // Can't write to the lock, and can't read the lock.
+                        // Something broken has happened
+                        return false;
+                }
+
+                if ( $lock_at &lt; number_format( microtime( true ), 10, '.', '' ) - 3600 ) {
+                        // Lock is too old - try again
+                        delete_option( $lock );
+                        return wp_upgrade_revisions_of_post( $post );
+                }
+
+                // Lock is not too old: some other process may be upgrading this post.  Bail.
+                return;
+        } else {
+                // If we could get a lock, re-&quot;add&quot; the option to fire all the correct filters.
+                add_option( $lock, $locked_at );
+        }
+
+        $success = true;
+
+        reset( $revisions );
+        do {
+                $this_revision = current( $revisions );
+                $prev_revision = next( $revisions );
+
+                $this_revision_version = _wp_get_post_revision_version( $this_revision );
+
+                error_log($this_revision_version);
+
+                // Something terrible happened
+                if ( false === $this_revision_version )
+                        continue;
+
+                // 1 is the latest revision version, so we're already up to date
+                if ( 0 &lt; $this_revision_version )
+                        continue;
+
+                // This revision is the oldest revision of the post.
+                // The correct post_author is probably $post-&gt;post_author, but that's only a good guess.
+                // Leave un-upgraded.
+                if ( ! $prev_revision ) {
+                        continue;
+                }
+
+                $prev_revision_version = _wp_get_post_revision_version( $prev_revision );
+
+                // If the previous revision is already up to date, it no longer has the information we need :(
+                if ( 0 &lt; $prev_revision_version ) {
+                        continue;
+                }
+
+                // Upgrade this revision
+                // Cast as object so that wp_update_post() handles slashing for us
+                $update = (object) array(
+                        'ID'          =&gt; $this_revision-&gt;ID,
+                        'post_name'   =&gt; preg_replace( '/^(\d+-)(autosave|revision)-(\d+)$/', '$1$2-v1', $this_revision-&gt;post_name ),
+                        'post_author' =&gt; $prev_revision-&gt;post_author,
+                );
+                //error_log(json_encode($update));
+                $result = wp_update_post( $update );
+                if ( ! $result || is_wp_error( $result ) ) {
+                        // Wilhelm!
+                        $success = false;
+                        break;
+                }
+        } while ( $prev_revision );
+
+        delete_option( $lock );
+        return true;
+}
+
+
</ins><span class="cx"> function _show_post_preview() {
</span><span class="cx"> 
</span><span class="cx">         if ( isset($_GET['preview_id']) &amp;&amp; isset($_GET['preview_nonce']) ) {
</span></span></pre>
</div>
</div>

</body>
</html>