<!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>[41839] trunk: Customize: Add changeset locking in Customizer to prevent users from overriding each other's changes.</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/41839">41839</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/41839","name":"Review Commit"}}</script></dd>
<dt style="float: left; width: 6em; font-weight: bold">Author</dt> <dd>westonruter</dd>
<dt style="float: left; width: 6em; font-weight: bold">Date</dt> <dd>2017-10-12 04:00:15 +0000 (Thu, 12 Oct 2017)</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'>Customize: Add changeset locking in Customizer to prevent users from overriding each other's changes.

* Customization locking is checked when changesets are saved and when heartbeat ticks.
* Lock is lifted immediately upon a user closing the Customizer.
* Heartbeat is introduced into Customizer.
* Changes made to user after it was locked by another user are stored as an autosave revision for restoration.
* Lock notification displays link to preview the other user's changes on the frontend.
* A user loading a locked Customizer changeset will be presented with an option to take over.
* Autosave revisions attached to a published changeset are converted into auto-drafts so that they will be presented to users for restoration.
* Focus constraining is improved in overlay notifications.
* Escape key is stopped from propagating in overlay notifications, and it dismisses dismissible overlay notifications.
* Introduces `changesetLocked` state which is used to disable the Save button and suppress the AYS dialog when leaving the Customizer.
* Fixes bug where users could be presented with each other's autosave revisions.

Props sayedwp, westonruter, melchoyce.
See <a href="https://core.trac.wordpress.org/ticket/31436">#31436</a>, <a href="https://core.trac.wordpress.org/ticket/31897">#31897</a>, <a href="https://core.trac.wordpress.org/ticket/39896">#39896</a>.
Fixes <a href="https://core.trac.wordpress.org/ticket/42024">#42024</a>.</pre>

<h3>Modified Paths</h3>
<ul>
<li><a href="#trunksrcwpadmincsscustomizecontrolscss">trunk/src/wp-admin/css/customize-controls.css</a></li>
<li><a href="#trunksrcwpadmincustomizephp">trunk/src/wp-admin/customize.php</a></li>
<li><a href="#trunksrcwpadminjscustomizecontrolsjs">trunk/src/wp-admin/js/customize-controls.js</a></li>
<li><a href="#trunksrcwpincludesclasswpcustomizemanagerphp">trunk/src/wp-includes/class-wp-customize-manager.php</a></li>
<li><a href="#trunksrcwpincludesjsheartbeatjs">trunk/src/wp-includes/js/heartbeat.js</a></li>
<li><a href="#trunksrcwpincludesscriptloaderphp">trunk/src/wp-includes/script-loader.php</a></li>
<li><a href="#trunktestsphpunittestsajaxCustomizeManagerphp">trunk/tests/phpunit/tests/ajax/CustomizeManager.php</a></li>
<li><a href="#trunktestsphpunittestscustomizemanagerphp">trunk/tests/phpunit/tests/customize/manager.php</a></li>
<li><a href="#trunktestsqunitfixturescustomizesettingsjs">trunk/tests/qunit/fixtures/customize-settings.js</a></li>
<li><a href="#trunktestsqunitindexhtml">trunk/tests/qunit/index.html</a></li>
</ul>

</div>
<div id="patch">
<h3>Diff</h3>
<a id="trunksrcwpadmincsscustomizecontrolscss"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-admin/css/customize-controls.css</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-admin/css/customize-controls.css     2017-10-12 03:54:59 UTC (rev 41838)
+++ trunk/src/wp-admin/css/customize-controls.css       2017-10-12 04:00:15 UTC (rev 41839)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -20,6 +20,46 @@
</span><span class="cx" style="display: block; padding: 0 10px">        text-align: center;
</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">+#customize-controls #customize-notifications-area .notice.notification-overlay.notification-changeset-locked {
+       background-color: rgba( 0, 0, 0, 0.7 );
+       padding: 25px;
+}
+
+#customize-controls #customize-notifications-area .notice.notification-overlay.notification-changeset-locked .customize-changeset-locked-message {
+       margin-left: auto;
+       margin-right: auto;
+       max-width: 366px;
+       min-height: 64px;
+       width: auto;
+       padding: 25px 25px 25px 109px;
+       position: relative;
+       background: #fff;
+       box-shadow: 0 3px 6px rgba( 0, 0, 0, 0.3 );
+       line-height: 1.5;
+       overflow-y: auto;
+       text-align: left;
+       top: calc( 50% - 100px );
+}
+
+#customize-controls #customize-notifications-area .notice.notification-overlay.notification-changeset-locked .currently-editing {
+       margin-top: 0;
+}
+#customize-controls #customize-notifications-area .notice.notification-overlay.notification-changeset-locked .action-buttons {
+       margin-bottom: 0;
+}
+
+.customize-changeset-locked-avatar {
+       width: 64px;
+       position: absolute;
+       left: 25px;
+       top: 25px;
+}
+
+.wp-core-ui.wp-customizer .customize-changeset-locked-message a.button {
+       margin-right: 10px;
+       margin-top: 0;
+}
+
</ins><span class="cx" style="display: block; padding: 0 10px"> #customize-controls .description {
</span><span class="cx" style="display: block; padding: 0 10px">        color: #555d66;
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span></span></pre></div>
<a id="trunksrcwpadmincustomizephp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-admin/customize.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-admin/customize.php  2017-10-12 03:54:59 UTC (rev 41838)
+++ trunk/src/wp-admin/customize.php    2017-10-12 04:00:15 UTC (rev 41839)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -87,6 +87,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">  */
</span><span class="cx" style="display: block; padding: 0 10px"> do_action( 'customize_controls_init' );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+wp_enqueue_script( 'heartbeat' );
</ins><span class="cx" style="display: block; padding: 0 10px"> wp_enqueue_script( 'customize-controls' );
</span><span class="cx" style="display: block; padding: 0 10px"> wp_enqueue_style( 'customize-controls' );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span></span></pre></div>
<a id="trunksrcwpadminjscustomizecontrolsjs"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-admin/js/customize-controls.js</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-admin/js/customize-controls.js       2017-10-12 03:54:59 UTC (rev 41838)
+++ trunk/src/wp-admin/js/customize-controls.js 2017-10-12 04:00:15 UTC (rev 41839)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -34,6 +34,37 @@
</span><span class="cx" style="display: block; padding: 0 10px">                        if ( notification.loading ) {
</span><span class="cx" style="display: block; padding: 0 10px">                                notification.containerClasses += ' notification-loading';
</span><span class="cx" style="display: block; padding: 0 10px">                        }
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                },
+
+               /**
+                * Render notification.
+                *
+                * @since 4.9.0
+                *
+                * @return {jQuery} Notification container.
+                */
+               render: function() {
+                       var li = api.Notification.prototype.render.call( this );
+                       li.on( 'keydown', _.bind( this.handleEscape, this ) );
+                       return li;
+               },
+
+               /**
+                * Stop propagation on escape key presses, but also dismiss notification if it is dismissible.
+                *
+                * @since 4.9.0
+                *
+                * @param {jQuery.Event} event - Event.
+                * @returns {void}
+                */
+               handleEscape: function( event ) {
+                       var notification = this;
+                       if ( 27 === event.which ) {
+                               event.stopPropagation();
+                               if ( notification.dismissible && notification.parent ) {
+                                       notification.parent.remove( notification.code );
+                               }
+                       }
</ins><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><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -282,11 +313,30 @@
</span><span class="cx" style="display: block; padding: 0 10px">                 * @returns {void}
</span><span class="cx" style="display: block; padding: 0 10px">                 */
</span><span class="cx" style="display: block; padding: 0 10px">                constrainFocus: function constrainFocus( event ) {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        var collection = this;
-                       if ( ! collection.focusContainer || collection.focusContainer.is( event.target ) || $.contains( collection.focusContainer[0], event.target[0] ) ) {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 var collection = this, focusableElements;
+
+                       // Prevent keys from escaping.
+                       event.stopPropagation();
+
+                       if ( 9 !== event.which ) { // Tab key.
</ins><span class="cx" style="display: block; padding: 0 10px">                                 return;
</span><span class="cx" style="display: block; padding: 0 10px">                        }
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        collection.focusContainer.focus();
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
+                       focusableElements = collection.focusContainer.find( ':focusable' );
+                       if ( 0 === focusableElements.length ) {
+                               focusableElements = collection.focusContainer;
+                       }
+
+                       if ( ! $.contains( collection.focusContainer[0], event.target ) || ! $.contains( collection.focusContainer[0], document.activeElement ) ) {
+                               event.preventDefault();
+                               focusableElements.first().focus();
+                       } else if ( focusableElements.last().is( event.target ) && ! event.shiftKey ) {
+                               event.preventDefault();
+                               focusableElements.first().focus();
+                       } else if ( focusableElements.first().is( event.target ) && event.shiftKey ) {
+                               event.preventDefault();
+                               focusableElements.last().focus();
+                       }
</ins><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><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -6737,7 +6787,8 @@
</span><span class="cx" style="display: block; padding: 0 10px">                'selectedChangesetStatus',
</span><span class="cx" style="display: block; padding: 0 10px">                'remainingTimeToPublish',
</span><span class="cx" style="display: block; padding: 0 10px">                'previewerAlive',
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                'editShortcutVisibility'
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         'editShortcutVisibility',
+               'changesetLocked'
</ins><span class="cx" style="display: block; padding: 0 10px">         ], function( name ) {
</span><span class="cx" style="display: block; padding: 0 10px">                api.state.create( name );
</span><span class="cx" style="display: block; padding: 0 10px">        });
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -7184,14 +7235,14 @@
</span><span class="cx" style="display: block; padding: 0 10px">                                                } else if ( response.code ) {
</span><span class="cx" style="display: block; padding: 0 10px">                                                        if ( 'not_future_date' === response.code && api.section.has( 'publish_settings' ) && api.section( 'publish_settings' ).active.get() && api.control.has( 'changeset_scheduled_date' ) ) {
</span><span class="cx" style="display: block; padding: 0 10px">                                                                api.control( 'changeset_scheduled_date' ).toggleFutureDateNotification( true ).focus();
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                                        } else {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                                                 } else if ( 'changeset_locked' !== response.code ) {
</ins><span class="cx" style="display: block; padding: 0 10px">                                                                 notification = new api.Notification( response.code, _.extend( notificationArgs, {
</span><span class="cx" style="display: block; padding: 0 10px">                                                                        message: response.message
</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">                                                } else {
</span><span class="cx" style="display: block; padding: 0 10px">                                                        notification = new api.Notification( 'unknown_error', _.extend( notificationArgs, {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                                                message: api.l10n.serverSaveError
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                                                         message: api.l10n.unknownRequestFail
</ins><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><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -7497,6 +7548,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">                                selectedChangesetDate = state.instance( 'selectedChangesetDate' ),
</span><span class="cx" style="display: block; padding: 0 10px">                                previewerAlive = state.instance( 'previewerAlive' ),
</span><span class="cx" style="display: block; padding: 0 10px">                                editShortcutVisibility  = state.instance( 'editShortcutVisibility' ),
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                                changesetLocked = state.instance( 'changesetLocked' ),
</ins><span class="cx" style="display: block; padding: 0 10px">                                 populateChangesetUuidParam;
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                        state.bind( 'change', function() {
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -7547,7 +7599,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">                                 * Save (publish) button should be enabled if saving is not currently happening,
</span><span class="cx" style="display: block; padding: 0 10px">                                 * and if the theme is not active or the changeset exists but is not published.
</span><span class="cx" style="display: block; padding: 0 10px">                                 */
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                canSave = ! saving() && ! trashing() && ( ! activated() || ! saved() || ( changesetStatus() !== selectedChangesetStatus() && '' !== changesetStatus() ) || ( 'future' === selectedChangesetStatus() && changesetDate.get() !== selectedChangesetDate.get() ) );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                         canSave = ! saving() && ! trashing() && ! changesetLocked() && ( ! activated() || ! saved() || ( changesetStatus() !== selectedChangesetStatus() && '' !== changesetStatus() ) || ( 'future' === selectedChangesetStatus() && changesetDate.get() !== selectedChangesetDate.get() ) );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                                saveBtn.prop( 'disabled', ! canSave );
</span><span class="cx" style="display: block; padding: 0 10px">                        });
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -7561,6 +7613,7 @@
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                        // Set default states.
</span><span class="cx" style="display: block; padding: 0 10px">                        changesetStatus( api.settings.changeset.status );
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                        changesetLocked( Boolean( api.settings.changeset.lockUser ) );
</ins><span class="cx" style="display: block; padding: 0 10px">                         changesetDate( api.settings.changeset.publishDate );
</span><span class="cx" style="display: block; padding: 0 10px">                        selectedChangesetDate( api.settings.changeset.publishDate );
</span><span class="cx" style="display: block; padding: 0 10px">                        selectedChangesetStatus( '' === api.settings.changeset.status || 'auto-draft' === api.settings.changeset.status ? 'publish' : api.settings.changeset.status );
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -7660,6 +7713,185 @@
</span><span class="cx" style="display: block; padding: 0 10px">                        }
</span><span class="cx" style="display: block; padding: 0 10px">                }( api.state ) );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                /**
+                * Handles lock notice and take over request.
+                *
+                * @since 4.9.0
+                */
+               ( function checkAndDisplayLockNotice() {
+
+                       /**
+                        * A notification that is displayed in a full-screen overlay with information about the locked changeset.
+                        *
+                        * @since 4.9.0
+                        * @class
+                        * @augments wp.customize.Notification
+                        * @augments wp.customize.OverlayNotification
+                        */
+                       var LockedNotification = api.OverlayNotification.extend({
+
+                               /**
+                                * Template ID.
+                                *
+                                * @type {string}
+                                */
+                               templateId: 'customize-changeset-locked-notification',
+
+                               /**
+                                * Lock user.
+                                *
+                                * @type {object}
+                                */
+                               lockUser: null,
+
+                               /**
+                                * Initialize.
+                                *
+                                * @since 4.9.0
+                                *
+                                * @param {string} [code] - Code.
+                                * @param {object} [params] - Params.
+                                */
+                               initialize: function( code, params ) {
+                                       var notification = this, _code, _params;
+                                       _code = code || 'changeset_locked';
+                                       _params = _.extend(
+                                               {
+                                                       type: 'warning',
+                                                       containerClasses: '',
+                                                       lockUser: {}
+                                               },
+                                               params
+                                       );
+                                       _params.containerClasses += ' notification-changeset-locked';
+                                       api.OverlayNotification.prototype.initialize.call( notification, _code, _params );
+                               },
+
+                               /**
+                                * Render notification.
+                                *
+                                * @since 4.9.0
+                                *
+                                * @return {jQuery} Notification container.
+                                */
+                               render: function() {
+                                       var notification = this, li, data, takeOverButton, request;
+                                       data = _.extend(
+                                               {
+                                                       allowOverride: false,
+                                                       returnUrl: api.settings.url['return'],
+                                                       previewUrl: api.previewer.previewUrl.get(),
+                                                       frontendPreviewUrl: api.previewer.getFrontendPreviewUrl()
+                                               },
+                                               this
+                                       );
+
+                                       li = api.OverlayNotification.prototype.render.call( data );
+
+                                       // Try to autosave the changeset now.
+                                       api.requestChangesetUpdate( {}, { autosave: true } ).fail( function( response ) {
+                                               if ( ! response.autosaved ) {
+                                                       li.find( '.notice-error' ).prop( 'hidden', false ).text( response.message || api.l10n.unknownRequestFail );
+                                               }
+                                       } );
+
+                                       takeOverButton = li.find( '.customize-notice-take-over-button' );
+                                       takeOverButton.on( 'click', function( event ) {
+                                               event.preventDefault();
+                                               if ( request ) {
+                                                       return;
+                                               }
+
+                                               takeOverButton.addClass( 'disabled' );
+                                               request = wp.ajax.post( 'customize_override_changeset_lock', {
+                                                       wp_customize: 'on',
+                                                       customize_theme: api.settings.theme.stylesheet,
+                                                       customize_changeset_uuid: api.settings.changeset.uuid,
+                                                       nonce: api.settings.nonce.override_lock
+                                               } );
+
+                                               request.done( function() {
+                                                       api.notifications.remove( notification.code ); // Remove self.
+                                                       api.state( 'changesetLocked' ).set( false );
+                                               } );
+
+                                               request.fail( function( response ) {
+                                                       var message = response.message || api.l10n.unknownRequestFail;
+                                                       li.find( '.notice-error' ).prop( 'hidden', false ).text( message );
+
+                                                       request.always( function() {
+                                                               takeOverButton.removeClass( 'disabled' );
+                                                       } );
+                                               } );
+
+                                               request.always( function() {
+                                                       request = null;
+                                               } );
+                                       } );
+
+                                       return li;
+                               }
+                       });
+
+                       /**
+                        * Start lock.
+                        *
+                        * @since 4.9.0
+                        *
+                        * @param {object} [args] - Args.
+                        * @param {object} [args.lockUser] - Lock user data.
+                        * @param {boolean} [args.allowOverride=false] - Whether override is allowed.
+                        * @returns {void}
+                        */
+                       function startLock( args ) {
+                               if ( args && args.lockUser ) {
+                                       api.settings.changeset.lockUser = args.lockUser;
+                               }
+                               api.state( 'changesetLocked' ).set( true );
+                               api.notifications.add( new LockedNotification( 'changeset_locked', {
+                                       lockUser: api.settings.changeset.lockUser,
+                                       allowOverride: Boolean( args && args.allowOverride )
+                               } ) );
+                       }
+
+                       // Show initial notification.
+                       if ( api.settings.changeset.lockUser ) {
+                               startLock( { allowOverride: true } );
+                       }
+
+                       // Check for lock when sending heartbeat requests.
+                       $( document ).on( 'heartbeat-send.update_lock_notice', function( event, data ) {
+                               data.check_changeset_lock = true;
+                       } );
+
+                       // Handle heartbeat ticks.
+                       $( document ).on( 'heartbeat-tick.update_lock_notice', function( event, data ) {
+                               var notification, code = 'changeset_locked';
+                               if ( ! data.customize_changeset_lock_user ) {
+                                       return;
+                               }
+
+                               // Update notification when a different user takes over.
+                               notification = api.notifications( code );
+                               if ( notification && notification.lockUser.id !== api.settings.changeset.lockUser.id ) {
+                                       api.notifications.remove( code );
+                               }
+
+                               startLock( {
+                                       lockUser: data.customize_changeset_lock_user
+                               } );
+                       } );
+
+                       // Handle locking in response to changeset save errors.
+                       api.bind( 'error', function( response ) {
+                               if ( 'changeset_locked' === response.code && response.lock_user ) {
+                                       startLock( {
+                                               lockUser: response.lock_user
+                                       } );
+                               }
+                       } );
+               } )();
+
</ins><span class="cx" style="display: block; padding: 0 10px">                 // Set up initial notifications.
</span><span class="cx" style="display: block; padding: 0 10px">                (function() {
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -7733,11 +7965,12 @@
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                                                // Handle dismissal of notice.
</span><span class="cx" style="display: block; padding: 0 10px">                                                li.find( '.notice-dismiss' ).on( 'click', function() {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                                        wp.ajax.post( 'customize_dismiss_autosave', {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                                                 wp.ajax.post( 'customize_dismiss_autosave_or_lock', {
</ins><span class="cx" style="display: block; padding: 0 10px">                                                                 wp_customize: 'on',
</span><span class="cx" style="display: block; padding: 0 10px">                                                                customize_theme: api.settings.theme.stylesheet,
</span><span class="cx" style="display: block; padding: 0 10px">                                                                customize_changeset_uuid: api.settings.changeset.uuid,
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                                                nonce: api.settings.nonce.dismiss_autosave
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                                                         nonce: api.settings.nonce.dismiss_autosave_or_lock,
+                                                               dismiss_autosave: true
</ins><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><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -8167,7 +8400,7 @@
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                                // Prompt user with AYS dialog if leaving the Customizer with unsaved changes
</span><span class="cx" style="display: block; padding: 0 10px">                                $( window ).on( 'beforeunload.customize-confirm', function() {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                        if ( ! isCleanState() ) {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                                 if ( ! isCleanState() && ! api.state( 'changesetLocked' ).get() ) {
</ins><span class="cx" style="display: block; padding: 0 10px">                                                 setTimeout( function() {
</span><span class="cx" style="display: block; padding: 0 10px">                                                        overlay.removeClass( 'customize-loading' );
</span><span class="cx" style="display: block; padding: 0 10px">                                                }, 1 );
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -8178,11 +8411,14 @@
</span><span class="cx" style="display: block; padding: 0 10px">                        api.bind( 'change', startPromptingBeforeUnload );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                        function requestClose() {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                var clearedToClose = $.Deferred();
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                         var clearedToClose = $.Deferred(), dismissAutoSave = false, dismissLock = false;
+
</ins><span class="cx" style="display: block; padding: 0 10px">                                 if ( isCleanState() ) {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                        clearedToClose.resolve();
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                                 dismissLock = true;
</ins><span class="cx" style="display: block; padding: 0 10px">                                 } else if ( confirm( api.l10n.saveAlert ) ) {
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                                        dismissLock = true;
+
</ins><span class="cx" style="display: block; padding: 0 10px">                                         // Mark all settings as clean to prevent another call to requestChangesetUpdate.
</span><span class="cx" style="display: block; padding: 0 10px">                                        api.each( function( setting ) {
</span><span class="cx" style="display: block; padding: 0 10px">                                                setting._dirty = false;
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -8191,24 +8427,29 @@
</span><span class="cx" style="display: block; padding: 0 10px">                                        $( window ).off( 'beforeunload.wp-customize-changeset-update' );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                                        closeBtn.css( 'cursor', 'progress' );
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                        if ( '' === api.state( 'changesetStatus' ).get() ) {
-                                               clearedToClose.resolve();
-                                       } else {
-                                               wp.ajax.send( 'customize_dismiss_autosave', {
-                                                       timeout: 500, // Don't wait too long.
-                                                       data: {
-                                                               wp_customize: 'on',
-                                                               customize_theme: api.settings.theme.stylesheet,
-                                                               customize_changeset_uuid: api.settings.changeset.uuid,
-                                                               nonce: api.settings.nonce.dismiss_autosave
-                                                       }
-                                               } ).always( function() {
-                                                       clearedToClose.resolve();
-                                               } );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                                 if ( '' !== api.state( 'changesetStatus' ).get() ) {
+                                               dismissAutoSave = true;
</ins><span class="cx" style="display: block; padding: 0 10px">                                         }
</span><span class="cx" style="display: block; padding: 0 10px">                                } else {
</span><span class="cx" style="display: block; padding: 0 10px">                                        clearedToClose.reject();
</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 ( dismissLock || dismissAutoSave ) {
+                                       wp.ajax.send( 'customize_dismiss_autosave_or_lock', {
+                                               timeout: 500, // Don't wait too long.
+                                               data: {
+                                                       wp_customize: 'on',
+                                                       customize_theme: api.settings.theme.stylesheet,
+                                                       customize_changeset_uuid: api.settings.changeset.uuid,
+                                                       nonce: api.settings.nonce.dismiss_autosave_or_lock,
+                                                       dismiss_autosave: dismissAutoSave,
+                                                       dismiss_lock: dismissLock
+                                               }
+                                       } ).always( function() {
+                                               clearedToClose.resolve();
+                                       } );
+                               }
+
</ins><span class="cx" style="display: block; padding: 0 10px">                                 return clearedToClose.promise();
</span><span class="cx" style="display: block; padding: 0 10px">                        }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span></span></pre></div>
<a id="trunksrcwpincludesclasswpcustomizemanagerphp"></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/class-wp-customize-manager.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/class-wp-customize-manager.php      2017-10-12 03:54:59 UTC (rev 41838)
+++ trunk/src/wp-includes/class-wp-customize-manager.php        2017-10-12 04:00:15 UTC (rev 41839)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -174,7 +174,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">        protected $messenger_channel;
</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">-         * Whether the autosave revision of the changeset should should be loaded.
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+  * Whether the autosave revision of the changeset should be loaded.
</ins><span class="cx" style="display: block; padding: 0 10px">          *
</span><span class="cx" style="display: block; padding: 0 10px">         * @since 4.9.0
</span><span class="cx" style="display: block; padding: 0 10px">         * @var bool
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -373,11 +373,14 @@
</span><span class="cx" style="display: block; padding: 0 10px">                remove_action( 'admin_init', '_maybe_update_plugins' );
</span><span class="cx" style="display: block; padding: 0 10px">                remove_action( 'admin_init', '_maybe_update_themes' );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                add_action( 'wp_ajax_customize_save',             array( $this, 'save' ) );
-               add_action( 'wp_ajax_customize_trash',            array( $this, 'handle_changeset_trash_request' ) );
-               add_action( 'wp_ajax_customize_refresh_nonces',   array( $this, 'refresh_nonces' ) );
-               add_action( 'wp_ajax_customize_load_themes',      array( $this, 'handle_load_themes_request' ) );
-               add_action( 'wp_ajax_customize_dismiss_autosave', array( $this, 'handle_dismiss_autosave_request' ) );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         add_action( 'wp_ajax_customize_save',                     array( $this, 'save' ) );
+               add_action( 'wp_ajax_customize_trash',                    array( $this, 'handle_changeset_trash_request' ) );
+               add_action( 'wp_ajax_customize_refresh_nonces',           array( $this, 'refresh_nonces' ) );
+               add_action( 'wp_ajax_customize_load_themes',              array( $this, 'handle_load_themes_request' ) );
+               add_filter( 'heartbeat_settings',                         array( $this, 'add_customize_screen_to_heartbeat_settings' ) );
+               add_filter( 'heartbeat_received',                         array( $this, 'check_changeset_lock_with_heartbeat' ), 10, 3 );
+               add_action( 'wp_ajax_customize_override_changeset_lock',  array( $this, 'handle_override_changeset_lock_request' ) );
+               add_action( 'wp_ajax_customize_dismiss_autosave_or_lock', array( $this, 'handle_dismiss_autosave_or_lock_request' ) );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                add_action( 'customize_register',                 array( $this, 'register_controls' ) );
</span><span class="cx" style="display: block; padding: 0 10px">                add_action( 'customize_register',                 array( $this, 'register_dynamic_settings' ), 11 ); // allow code to create settings first
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -629,6 +632,8 @@
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                        $this->_changeset_uuid = $changeset_uuid;
</span><span class="cx" style="display: block; padding: 0 10px">                }
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
+               $this->set_changeset_lock( $this->changeset_post_id() );
</ins><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><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1106,7 +1111,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">                        $this->_changeset_data = array();
</span><span class="cx" style="display: block; padding: 0 10px">                } else {
</span><span class="cx" style="display: block; padding: 0 10px">                        if ( $this->autosaved() ) {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                $autosave_post = wp_get_post_autosave( $changeset_post_id );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                         $autosave_post = wp_get_post_autosave( $changeset_post_id, get_current_user_id() );
</ins><span class="cx" style="display: block; padding: 0 10px">                                 if ( $autosave_post ) {
</span><span class="cx" style="display: block; padding: 0 10px">                                        $data = $this->get_changeset_post_data( $autosave_post->ID );
</span><span class="cx" style="display: block; padding: 0 10px">                                        if ( ! is_wp_error( $data ) ) {
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -2376,11 +2381,24 @@
</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">+                $lock_user_id = null;
</ins><span class="cx" style="display: block; padding: 0 10px">                 $autosave = ! empty( $_POST['customize_changeset_autosave'] );
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                if ( ! $is_new_changeset ) {
+                       $lock_user_id = wp_check_post_lock( $this->changeset_post_id() );
+               }
+
+               // Force request to autosave when changeset is locked.
+               if ( $lock_user_id && ! $autosave ) {
+                       $autosave = true;
+                       $changeset_status = null;
+                       $changeset_date_gmt = null;
+               }
+
</ins><span class="cx" style="display: block; padding: 0 10px">                 if ( $autosave && ! defined( 'DOING_AUTOSAVE' ) ) { // Back-compat.
</span><span class="cx" style="display: block; padding: 0 10px">                        define( 'DOING_AUTOSAVE', true );
</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">+                $autosaved = false;
</ins><span class="cx" style="display: block; padding: 0 10px">                 $r = $this->save_changeset_post( array(
</span><span class="cx" style="display: block; padding: 0 10px">                        'status' => $changeset_status,
</span><span class="cx" style="display: block; padding: 0 10px">                        'title' => $changeset_title,
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -2388,6 +2406,21 @@
</span><span class="cx" style="display: block; padding: 0 10px">                        'data' => $input_changeset_data,
</span><span class="cx" style="display: block; padding: 0 10px">                        'autosave' => $autosave,
</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 ( $autosave && ! is_wp_error( $r ) ) {
+                       $autosaved = true;
+               }
+
+               // If the changeset was locked and an autosave request wasn't itself an error, then now explicitly return with a failure.
+               if ( $lock_user_id && ! is_wp_error( $r ) ) {
+                       $r = new WP_Error(
+                               'changeset_locked',
+                               __( 'Changeset is being edited by other user.' ),
+                               array(
+                                       'lock_user' => $this->get_lock_user_data( $lock_user_id ),
+                               )
+                       );
+               }
+
</ins><span class="cx" style="display: block; padding: 0 10px">                 if ( is_wp_error( $r ) ) {
</span><span class="cx" style="display: block; padding: 0 10px">                        $response = array(
</span><span class="cx" style="display: block; padding: 0 10px">                                'message' => $r->get_error_message(),
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -2413,6 +2446,10 @@
</span><span class="cx" style="display: block; padding: 0 10px">                                $response['changeset_status'] = 'publish';
</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 ( 'publish' !== $response['changeset_status'] ) {
+                               $this->set_changeset_lock( $changeset_post->ID );
+                       }
+
</ins><span class="cx" style="display: block; padding: 0 10px">                         if ( 'future' === $response['changeset_status'] ) {
</span><span class="cx" style="display: block; padding: 0 10px">                                $response['changeset_date'] = $changeset_post->post_date;
</span><span class="cx" style="display: block; padding: 0 10px">                        }
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -2422,6 +2459,10 @@
</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">+                if ( $autosave ) {
+                       $response['autosaved'] = $autosaved;
+               }
+
</ins><span class="cx" style="display: block; padding: 0 10px">                 if ( isset( $response['setting_validities'] ) ) {
</span><span class="cx" style="display: block; padding: 0 10px">                        $response['setting_validities'] = array_map( array( $this, 'prepare_setting_validity_for_js' ), $response['setting_validities'] );
</span><span class="cx" style="display: block; padding: 0 10px">                }
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -2684,6 +2725,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">                                        array(
</span><span class="cx" style="display: block; padding: 0 10px">                                                'type' => $setting->type,
</span><span class="cx" style="display: block; padding: 0 10px">                                                'user_id' => $args['user_id'],
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                                                'date_modified_gmt' => current_time( 'mysql', true ),
</ins><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><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -2798,7 +2840,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">                                $r = wp_update_post( wp_slash( $post_array ), true );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                                // Delete autosave revision when the changeset is updated.
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                $autosave_draft = wp_get_post_autosave( $changeset_post_id );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                         $autosave_draft = wp_get_post_autosave( $changeset_post_id, get_current_user_id() );
</ins><span class="cx" style="display: block; padding: 0 10px">                                 if ( $autosave_draft ) {
</span><span class="cx" style="display: block; padding: 0 10px">                                        wp_delete_post( $autosave_draft->ID, true );
</span><span class="cx" style="display: block; padding: 0 10px">                                }
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -2990,6 +3032,157 @@
</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">+         * Marks the changeset post as being currently edited by the current user.
+        *
+        * @since 4.9.0
+        *
+        * @param int  $changeset_post_id Changeset post id.
+        * @param bool $take_over Take over the changeset, default is false.
+        */
+       public function set_changeset_lock( $changeset_post_id, $take_over = false ) {
+               if ( $changeset_post_id ) {
+                       $can_override = ! (bool) get_post_meta( $changeset_post_id, '_edit_lock', true );
+
+                       if ( $take_over ) {
+                               $can_override = true;
+                       }
+
+                       if ( $can_override ) {
+                               $lock = sprintf( '%s:%s', time(), get_current_user_id() );
+                               update_post_meta( $changeset_post_id, '_edit_lock', $lock );
+                       } else {
+                               $this->refresh_changeset_lock( $changeset_post_id );
+                       }
+               }
+       }
+
+       /**
+        * Refreshes changeset lock with the current time if current user edited the changeset before.
+        *
+        * @since 4.9.0
+        *
+        * @param int $changeset_post_id Changeset post id.
+        */
+       public function refresh_changeset_lock( $changeset_post_id ) {
+               if ( ! $changeset_post_id ) {
+                       return;
+               }
+               $lock = get_post_meta( $changeset_post_id, '_edit_lock', true );
+               $lock = explode( ':', $lock );
+
+               if ( $lock && ! empty( $lock[1] ) ) {
+                       $user_id = intval( $lock[1] );
+                       $current_user_id = get_current_user_id();
+                       if ( $user_id === $current_user_id ) {
+                               $lock = sprintf( '%s:%s', time(), $user_id );
+                               update_post_meta( $changeset_post_id, '_edit_lock', $lock );
+                       }
+               }
+       }
+
+       /**
+        * Filter heartbeat settings for the Customizer.
+        *
+        * @since 4.9.0
+        * @param array $settings Current settings to filter.
+        * @return array Heartbeat settings.
+        */
+       public function add_customize_screen_to_heartbeat_settings( $settings ) {
+               global $pagenow;
+               if ( 'customize.php' === $pagenow ) {
+                       $settings['screenId'] = 'customize';
+               }
+               return $settings;
+       }
+
+       /**
+        * Get lock user data.
+        *
+        * @since 4.9.0
+        *
+        * @param int $user_id User ID.
+        * @return array|null User data formatted for client.
+        */
+       protected function get_lock_user_data( $user_id ) {
+               if ( ! $user_id ) {
+                       return null;
+               }
+               $lock_user = get_userdata( $user_id );
+               if ( ! $lock_user ) {
+                       return null;
+               }
+               return array(
+                       'id' => $lock_user->ID,
+                       'name' => $lock_user->display_name,
+                       'avatar' => get_avatar_url( $lock_user->ID, array( 'size' => 128 ) ),
+               );
+       }
+
+       /**
+        * Check locked changeset with heartbeat API.
+        *
+        * @since 4.9.0
+        *
+        * @param array  $response  The Heartbeat response.
+        * @param array  $data      The $_POST data sent.
+        * @param string $screen_id The screen id.
+        * @return array The Heartbeat response.
+        */
+       public function check_changeset_lock_with_heartbeat( $response, $data, $screen_id ) {
+               if ( array_key_exists( 'check_changeset_lock', $data ) && 'customize' === $screen_id && current_user_can( 'customize' ) && $this->changeset_post_id() ) {
+                       $lock_user_id = wp_check_post_lock( $this->changeset_post_id() );
+
+                       if ( $lock_user_id ) {
+                               $response['customize_changeset_lock_user'] = $this->get_lock_user_data( $lock_user_id );
+                       } else {
+
+                               // Refreshing time will ensure that the user is sitting on customizer and has not closed the customizer tab.
+                               $this->refresh_changeset_lock( $this->changeset_post_id() );
+                       }
+               }
+
+               return $response;
+       }
+
+       /**
+        * Removes changeset lock when take over request is sent via Ajax.
+        *
+        * @since 4.9.0
+        */
+       public function handle_override_changeset_lock_request() {
+               if ( ! $this->is_preview() ) {
+                       wp_send_json_error( 'not_preview', 400 );
+               }
+
+               if ( ! check_ajax_referer( 'customize_override_changeset_lock', 'nonce', false ) ) {
+                       wp_send_json_error( array(
+                               'code' => 'invalid_nonce',
+                               'message' => __( 'Security check failed.' ),
+                       ) );
+               }
+
+               $changeset_post_id = $this->changeset_post_id();
+
+               if ( empty( $changeset_post_id ) ) {
+                       wp_send_json_error( array(
+                               'code' => 'no_changeset_found_to_take_over',
+                               'message' => __( 'No changeset found to take over' ),
+                       ) );
+               }
+
+               if ( ! current_user_can( get_post_type_object( 'customize_changeset' )->cap->edit_post, $changeset_post_id ) ) {
+                       wp_send_json_error( array(
+                               'code' => 'cannot_remove_changeset_lock',
+                               'message' => __( 'Sorry you are not allowed to take over.' ),
+                       ) );
+               }
+
+               $this->set_changeset_lock( $changeset_post_id, true );
+
+               wp_send_json_success( 'changeset_taken_over' );
+       }
+
+       /**
</ins><span class="cx" style="display: block; padding: 0 10px">          * Whether a changeset revision should be made.
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * @since 4.7.0
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -3033,11 +3226,14 @@
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * @since 4.7.0
</span><span class="cx" style="display: block; padding: 0 10px">         * @see _wp_customize_publish_changeset()
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         * @global wpdb $wpdb
</ins><span class="cx" style="display: block; padding: 0 10px">          *
</span><span class="cx" style="display: block; padding: 0 10px">         * @param int $changeset_post_id ID for customize_changeset post. Defaults to the changeset for the current manager instance.
</span><span class="cx" style="display: block; padding: 0 10px">         * @return true|WP_Error True or error info.
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><span class="cx" style="display: block; padding: 0 10px">        public function _publish_changeset_values( $changeset_post_id ) {
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                global $wpdb;
+
</ins><span class="cx" style="display: block; padding: 0 10px">                 $publishing_changeset_data = $this->get_changeset_post_data( $changeset_post_id );
</span><span class="cx" style="display: block; padding: 0 10px">                if ( is_wp_error( $publishing_changeset_data ) ) {
</span><span class="cx" style="display: block; padding: 0 10px">                        return $publishing_changeset_data;
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -3175,6 +3371,30 @@
</span><span class="cx" style="display: block; padding: 0 10px">                $this->_changeset_post_id = $previous_changeset_post_id;
</span><span class="cx" style="display: block; padding: 0 10px">                $this->_changeset_uuid    = $previous_changeset_uuid;
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                /*
+                * Convert all autosave revisions into their own auto-drafts so that users can be prompted to
+                * restore them when a changeset is published, but they had been locked out from including
+                * their changes in the changeset.
+                */
+               $revisions = wp_get_post_revisions( $changeset_post_id, array( 'check_enabled' => false ) );
+               foreach ( $revisions as $revision ) {
+                       if ( false !== strpos( $revision->post_name, "{$changeset_post_id}-autosave" ) ) {
+                               $wpdb->update(
+                                       $wpdb->posts,
+                                       array(
+                                               'post_status' => 'auto-draft',
+                                               'post_type' => 'customize_changeset',
+                                               'post_name' => wp_generate_uuid4(),
+                                               'post_parent' => 0,
+                                       ),
+                                       array(
+                                               'ID' => $revision->ID,
+                                       )
+                               );
+                               clean_post_cache( $revision->ID );
+                       }
+               }
+
</ins><span class="cx" style="display: block; padding: 0 10px">                 return true;
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -3229,45 +3449,65 @@
</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">-         * Delete a given auto-draft changeset or the autosave revision for a given changeset.
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+  * Delete a given auto-draft changeset or the autosave revision for a given changeset or delete changeset lock.
</ins><span class="cx" style="display: block; padding: 0 10px">          *
</span><span class="cx" style="display: block; padding: 0 10px">         * @since 4.9.0
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        public function handle_dismiss_autosave_request() {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ public function handle_dismiss_autosave_or_lock_request() {
</ins><span class="cx" style="display: block; padding: 0 10px">                 if ( ! $this->is_preview() ) {
</span><span class="cx" style="display: block; padding: 0 10px">                        wp_send_json_error( 'not_preview', 400 );
</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">-                if ( ! check_ajax_referer( 'customize_dismiss_autosave', 'nonce', false ) ) {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         if ( ! check_ajax_referer( 'customize_dismiss_autosave_or_lock', 'nonce', false ) ) {
</ins><span class="cx" style="display: block; padding: 0 10px">                         wp_send_json_error( 'invalid_nonce', 403 );
</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">                $changeset_post_id = $this->changeset_post_id();
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                $dismiss_lock = ! empty( $_POST['dismiss_lock'] );
+               $dismiss_autosave = ! empty( $_POST['dismiss_autosave'] );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                if ( empty( $changeset_post_id ) || 'auto-draft' === get_post_status( $changeset_post_id ) ) {
-                       $dismissed = $this->dismiss_user_auto_draft_changesets();
-                       if ( $dismissed > 0 ) {
-                               wp_send_json_success( 'auto_draft_dismissed' );
-                       } else {
-                               wp_send_json_error( 'no_auto_draft_to_delete', 404 );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         if ( $dismiss_lock ) {
+                       if ( empty( $changeset_post_id ) && ! $dismiss_autosave ) {
+                               wp_send_json_error( 'no_changeset_to_dismiss_lock', 404 );
</ins><span class="cx" style="display: block; padding: 0 10px">                         }
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                } else {
-                       $revision = wp_get_post_autosave( $changeset_post_id );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 if ( ! current_user_can( get_post_type_object( 'customize_changeset' )->cap->edit_post, $changeset_post_id ) && ! $dismiss_autosave ) {
+                               wp_send_json_error( 'cannot_remove_changeset_lock', 403 );
+                       }
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        if ( $revision ) {
-                               if ( ! current_user_can( get_post_type_object( 'customize_changeset' )->cap->delete_post, $changeset_post_id ) ) {
-                                       wp_send_json_error( 'cannot_delete_autosave_revision', 403 );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 delete_post_meta( $changeset_post_id, '_edit_lock' );
+
+                       if ( ! $dismiss_autosave ) {
+                               wp_send_json_success( 'changeset_lock_dismissed' );
+                       }
+               }
+
+               if ( $dismiss_autosave ) {
+                       if ( empty( $changeset_post_id ) || 'auto-draft' === get_post_status( $changeset_post_id ) ) {
+                               $dismissed = $this->dismiss_user_auto_draft_changesets();
+                               if ( $dismissed > 0 ) {
+                                       wp_send_json_success( 'auto_draft_dismissed' );
+                               } else {
+                                       wp_send_json_error( 'no_auto_draft_to_delete', 404 );
</ins><span class="cx" style="display: block; padding: 0 10px">                                 }
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                        } else {
+                               $revision = wp_get_post_autosave( $changeset_post_id, get_current_user_id() );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                if ( ! wp_delete_post( $revision->ID, true ) ) {
-                                       wp_send_json_error( 'autosave_revision_deletion_failure', 500 );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                         if ( $revision ) {
+                                       if ( ! current_user_can( get_post_type_object( 'customize_changeset' )->cap->delete_post, $changeset_post_id ) ) {
+                                               wp_send_json_error( 'cannot_delete_autosave_revision', 403 );
+                                       }
+
+                                       if ( ! wp_delete_post( $revision->ID, true ) ) {
+                                               wp_send_json_error( 'autosave_revision_deletion_failure', 500 );
+                                       } else {
+                                               wp_send_json_success( 'autosave_revision_deleted' );
+                                       }
</ins><span class="cx" style="display: block; padding: 0 10px">                                 } else {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                        wp_send_json_success( 'autosave_revision_deleted' );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                                 wp_send_json_error( 'no_autosave_revision_to_delete', 404 );
</ins><span class="cx" style="display: block; padding: 0 10px">                                 }
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        } else {
-                               wp_send_json_error( 'no_autosave_revision_to_delete', 404 );
</del><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">+
</ins><span class="cx" style="display: block; padding: 0 10px">                 wp_send_json_error( 'unknown_error', 500 );
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -3817,6 +4057,39 @@
</span><span class="cx" style="display: block; padding: 0 10px">                        </li>
</span><span class="cx" style="display: block; padding: 0 10px">                </script>
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                <script type="text/html" id="tmpl-customize-changeset-locked-notification">
+                       <li class="notice notice-{{ data.type || 'info' }} {{ data.containerClasses || '' }}" data-code="{{ data.code }}" data-type="{{ data.type }}">
+                               <div class="notification-message customize-changeset-locked-message">
+                                       <img class="customize-changeset-locked-avatar" src="{{ data.lockUser.avatar }}" alt="{{ data.lockUser.name }}">
+                                       <p class="currently-editing">
+                                               <# if ( data.message ) { #>
+                                                       {{{ data.message }}}
+                                               <# } else if ( data.allowOverride ) { #>
+                                                       <?php
+                                                       /* translators: %s: User who is customizing the changeset in customizer. */
+                                                       printf( __( '%s is already customizing this site. Do you want to take over?' ), '{{ data.lockUser.name }}' );
+                                                       ?>
+                                               <# } else { #>
+                                                       <?php
+                                                       /* translators: %s: User who is customizing the changeset in customizer. */
+                                                       printf( __( '%s is already customizing this site. Please wait until they are done to try customizing. Your latest changes have been autosaved.' ), '{{ data.lockUser.name }}' );
+                                                       ?>
+                                               <# } #>
+                                       </p>
+                                       <p class="notice notice-error notice-alt" hidden></p>
+                                       <p class="action-buttons">
+                                               <# if ( data.returnUrl !== data.previewUrl ) { #>
+                                                       <a class="button customize-notice-go-back-button" href="{{ data.returnUrl }}"><?php _e( 'Go back' ); ?></a>
+                                               <# } #>
+                                               <a class="button customize-notice-preview-button" href="{{ data.frontendPreviewUrl }}"><?php _e( 'Preview' ); ?></a>
+                                               <# if ( data.allowOverride ) { #>
+                                                       <button class="button button-primary wp-tab-last customize-notice-take-over-button"><?php _e( 'Take over' ); ?></button>
+                                               <# } #>
+                                       </p>
+                               </div>
+                       </li>
+               </script>
+
</ins><span class="cx" style="display: block; padding: 0 10px">                 <?php
</span><span class="cx" style="display: block; padding: 0 10px">                /* The following template is obsolete in core but retained for plugins. */
</span><span class="cx" style="display: block; padding: 0 10px">                ?>
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -4188,7 +4461,8 @@
</span><span class="cx" style="display: block; padding: 0 10px">                        'save' => wp_create_nonce( 'save-customize_' . $this->get_stylesheet() ),
</span><span class="cx" style="display: block; padding: 0 10px">                        'preview' => wp_create_nonce( 'preview-customize_' . $this->get_stylesheet() ),
</span><span class="cx" style="display: block; padding: 0 10px">                        'switch_themes' => wp_create_nonce( 'switch_themes' ),
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        'dismiss_autosave' => wp_create_nonce( 'customize_dismiss_autosave' ),
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 'dismiss_autosave_or_lock' => wp_create_nonce( 'customize_dismiss_autosave_or_lock' ),
+                       'override_lock' => wp_create_nonce( 'customize_override_changeset_lock' ),
</ins><span class="cx" style="display: block; padding: 0 10px">                         'trash' => wp_create_nonce( 'trash_customize_changeset' ),
</span><span class="cx" style="display: block; padding: 0 10px">                );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -4231,7 +4505,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">                $changeset_post_id = $this->changeset_post_id();
</span><span class="cx" style="display: block; padding: 0 10px">                if ( ! $this->saved_starter_content_changeset && ! $this->autosaved() ) {
</span><span class="cx" style="display: block; padding: 0 10px">                        if ( $changeset_post_id ) {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                $autosave_revision_post = wp_get_post_autosave( $changeset_post_id );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                         $autosave_revision_post = wp_get_post_autosave( $changeset_post_id, get_current_user_id() );
</ins><span class="cx" style="display: block; padding: 0 10px">                         } else {
</span><span class="cx" style="display: block; padding: 0 10px">                                $autosave_autodraft_posts = $this->get_changeset_posts( array(
</span><span class="cx" style="display: block; padding: 0 10px">                                        'posts_per_page' => 1,
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -4277,6 +4551,11 @@
</span><span class="cx" style="display: block; padding: 0 10px">                        $initial_date = current_time( 'mysql', 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">+                $lock_user_id = false;
+               if ( $this->changeset_post_id() ) {
+                       $lock_user_id = wp_check_post_lock( $this->changeset_post_id() );
+               }
+
</ins><span class="cx" style="display: block; padding: 0 10px">                 $settings = array(
</span><span class="cx" style="display: block; padding: 0 10px">                        'changeset' => array(
</span><span class="cx" style="display: block; padding: 0 10px">                                'uuid' => $this->changeset_uuid(),
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -4288,6 +4567,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">                                'currentUserCanPublish' => $current_user_can_publish,
</span><span class="cx" style="display: block; padding: 0 10px">                                'publishDate' => $initial_date,
</span><span class="cx" style="display: block; padding: 0 10px">                                'statusChoices' => $status_choices,
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                                'lockUser' => $lock_user_id ? $this->get_lock_user_data( $lock_user_id ) : null,
</ins><span class="cx" style="display: block; padding: 0 10px">                         ),
</span><span class="cx" style="display: block; padding: 0 10px">                        'initialServerDate' => current_time( 'mysql', false ),
</span><span class="cx" style="display: block; padding: 0 10px">                        'dateFormat' => get_option( 'date_format' ),
</span></span></pre></div>
<a id="trunksrcwpincludesjsheartbeatjs"></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/js/heartbeat.js</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/js/heartbeat.js     2017-10-12 03:54:59 UTC (rev 41838)
+++ trunk/src/wp-includes/js/heartbeat.js       2017-10-12 04:00:15 UTC (rev 41839)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -367,6 +367,10 @@
</span><span class="cx" style="display: block; padding: 0 10px">                                has_focus: settings.hasFocus
</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 ( 'customize' === settings.screenId  ) {
+                               ajaxData.wp_customize = 'on';
+                       }
+
</ins><span class="cx" style="display: block; padding: 0 10px">                         settings.connecting = true;
</span><span class="cx" style="display: block; padding: 0 10px">                        settings.xhr = $.ajax({
</span><span class="cx" style="display: block; padding: 0 10px">                                url: settings.url,
</span></span></pre></div>
<a id="trunksrcwpincludesscriptloaderphp"></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/script-loader.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/script-loader.php   2017-10-12 03:54:59 UTC (rev 41838)
+++ trunk/src/wp-includes/script-loader.php     2017-10-12 04:00:15 UTC (rev 41839)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -547,7 +547,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">        $scripts->add( 'customize-preview',  "/wp-includes/js/customize-preview$suffix.js",  array( 'wp-a11y', 'customize-base' ), false, 1 );
</span><span class="cx" style="display: block; padding: 0 10px">        $scripts->add( 'customize-models',   "/wp-includes/js/customize-models.js", array( 'underscore', 'backbone' ), false, 1 );
</span><span class="cx" style="display: block; padding: 0 10px">        $scripts->add( 'customize-views',    "/wp-includes/js/customize-views.js",  array( 'jquery', 'underscore', 'imgareaselect', 'customize-models', 'media-editor', 'media-views' ), false, 1 );
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        $scripts->add( 'customize-controls', "/wp-admin/js/customize-controls$suffix.js", array( 'customize-base', 'wp-a11y', 'wp-util' ), false, 1 );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ $scripts->add( 'customize-controls', "/wp-admin/js/customize-controls$suffix.js", array( 'customize-base', 'wp-a11y', 'wp-util', 'jquery-ui-core' ), false, 1 );
</ins><span class="cx" style="display: block; padding: 0 10px">         did_action( 'init' ) && $scripts->localize( 'customize-controls', '_wpCustomizeControlsL10n', array(
</span><span class="cx" style="display: block; padding: 0 10px">                'activate'           => __( 'Activate &amp; Publish' ),
</span><span class="cx" style="display: block; padding: 0 10px">                'save'               => __( 'Save &amp; Publish' ), // @todo Remove as not required.
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -574,11 +574,13 @@
</span><span class="cx" style="display: block; padding: 0 10px">                'collapseSidebar'    => _x( 'Hide Controls', 'label for hide controls button without length constraints' ),
</span><span class="cx" style="display: block; padding: 0 10px">                'expandSidebar'      => _x( 'Show Controls', 'label for hide controls button without length constraints' ),
</span><span class="cx" style="display: block; padding: 0 10px">                'untitledBlogName'   => __( '(Untitled)' ),
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                'serverSaveError'    => __( 'Failed connecting to the server. Please try saving again.' ),
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         'unknownRequestFail' => __( 'Looks like something&#8217;s gone wrong. Wait a couple seconds, and then try again.' ),
</ins><span class="cx" style="display: block; padding: 0 10px">                 'themeDownloading'   => __( 'Downloading your new theme&hellip;' ),
</span><span class="cx" style="display: block; padding: 0 10px">                'themePreviewWait'   => __( 'Setting up your live preview. This may take a bit.' ),
</span><span class="cx" style="display: block; padding: 0 10px">                'revertingChanges'   => __( 'Reverting unpublished changes&hellip;' ),
</span><span class="cx" style="display: block; padding: 0 10px">                'trashConfirm'       => __( 'Are you sure you&#8217;d like to discard your unpublished changes?' ),
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                /* translators: %s: Display name of the user who has taken over the changeset in customizer. */
+               'takenOverMessage'   => __( '%s has taken over and is currently customizing.' ),
</ins><span class="cx" style="display: block; padding: 0 10px">                 /* translators: %s: URL to the Customizer to load the autosaved version */
</span><span class="cx" style="display: block; padding: 0 10px">                'autosaveNotice'     => __( 'There is a more recent autosave of your changes than the one you are previewing. <a href="%s">Restore the autosave</a>' ),
</span><span class="cx" style="display: block; padding: 0 10px">                'videoHeaderNotice'  => __( 'This theme doesn&#8217;t support video headers on this page. Navigate to the front page or another page that supports video headers.' ),
</span></span></pre></div>
<a id="trunktestsphpunittestsajaxCustomizeManagerphp"></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/ajax/CustomizeManager.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/ajax/CustomizeManager.php       2017-10-12 03:54:59 UTC (rev 41838)
+++ trunk/tests/phpunit/tests/ajax/CustomizeManager.php 2017-10-12 04:00:15 UTC (rev 41839)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -516,21 +516,28 @@
</span><span class="cx" style="display: block; padding: 0 10px">         * Test request for dismissing autosave changesets.
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * @ticket 39896
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-         * @covers WP_Customize_Manager::handle_dismiss_autosave_request()
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+  * @covers WP_Customize_Manager::handle_dismiss_autosave_or_lock_request()
</ins><span class="cx" style="display: block; padding: 0 10px">          * @covers WP_Customize_Manager::dismiss_user_auto_draft_changesets()
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        public function test_handle_dismiss_autosave_request() {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ public function test_handle_dismiss_autosave_or_lock_request() {
</ins><span class="cx" style="display: block; padding: 0 10px">                 $uuid = wp_generate_uuid4();
</span><span class="cx" style="display: block; padding: 0 10px">                $wp_customize = $this->set_up_valid_state( $uuid );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                $this->make_ajax_call( 'customize_dismiss_autosave' );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         $this->make_ajax_call( 'customize_dismiss_autosave_or_lock' );
</ins><span class="cx" style="display: block; padding: 0 10px">                 $this->assertFalse( $this->_last_response_parsed['success'] );
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertEquals( 'invalid_nonce', $this->_last_response_parsed['data'] );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                $nonce = wp_create_nonce( 'customize_dismiss_autosave' );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         $nonce = wp_create_nonce( 'customize_dismiss_autosave_or_lock' );
</ins><span class="cx" style="display: block; padding: 0 10px">                 $_POST['nonce'] = $_GET['nonce'] = $_REQUEST['nonce'] = $nonce;
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                $this->make_ajax_call( 'customize_dismiss_autosave' );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
+               $_POST['dismiss_lock'] = $_GET['dismiss_lock'] = $_REQUEST['dismiss_lock'] = true;
+               $this->make_ajax_call( 'customize_dismiss_autosave_or_lock' );
</ins><span class="cx" style="display: block; padding: 0 10px">                 $this->assertFalse( $this->_last_response_parsed['success'] );
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                $this->assertEquals( 'no_changeset_to_dismiss_lock', $this->_last_response_parsed['data'] );
+
+               $_POST['dismiss_autosave'] = $_GET['dismiss_autosave'] = $_REQUEST['dismiss_autosave'] = true;
+               $this->make_ajax_call( 'customize_dismiss_autosave_or_lock' );
+               $this->assertFalse( $this->_last_response_parsed['success'] );
</ins><span class="cx" style="display: block; padding: 0 10px">                 $this->assertEquals( 'no_auto_draft_to_delete', $this->_last_response_parsed['data'] );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                $other_user_id = $this->factory()->user->create();
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -559,7 +566,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">                foreach ( array_merge( $user_auto_draft_ids, $other_user_auto_draft_ids ) as $post_id ) {
</span><span class="cx" style="display: block; padding: 0 10px">                        $this->assertFalse( (bool) get_post_meta( $post_id, '_customize_restore_dismissed', true ) );
</span><span class="cx" style="display: block; padding: 0 10px">                }
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                $this->make_ajax_call( 'customize_dismiss_autosave' );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         $this->make_ajax_call( 'customize_dismiss_autosave_or_lock' );
</ins><span class="cx" style="display: block; padding: 0 10px">                 $this->assertTrue( $this->_last_response_parsed['success'] );
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertEquals( 'auto_draft_dismissed', $this->_last_response_parsed['data'] );
</span><span class="cx" style="display: block; padding: 0 10px">                foreach ( $user_auto_draft_ids as $post_id ) {
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -572,7 +579,7 @@
</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">                // Subsequent test results in none dismissed.
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                $this->make_ajax_call( 'customize_dismiss_autosave' );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         $this->make_ajax_call( 'customize_dismiss_autosave_or_lock' );
</ins><span class="cx" style="display: block; padding: 0 10px">                 $this->assertFalse( $this->_last_response_parsed['success'] );
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertEquals( 'no_auto_draft_to_delete', $this->_last_response_parsed['data'] );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -585,12 +592,19 @@
</span><span class="cx" style="display: block; padding: 0 10px">                        ),
</span><span class="cx" style="display: block; padding: 0 10px">                        'status' => 'draft',
</span><span class="cx" style="display: block; padding: 0 10px">                ) );
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
+               $_POST['dismiss_autosave'] = $_GET['dismiss_autosave'] = $_REQUEST['dismiss_autosave'] = false;
+               $this->make_ajax_call( 'customize_dismiss_autosave_or_lock' );
+               $this->assertTrue( $this->_last_response_parsed['success'] );
+               $this->assertEquals( 'changeset_lock_dismissed', $this->_last_response_parsed['data'] );
+
+               $_POST['dismiss_autosave'] = $_GET['dismiss_autosave'] = $_REQUEST['dismiss_autosave'] = true;
</ins><span class="cx" style="display: block; padding: 0 10px">                 $this->assertNotInstanceOf( 'WP_Error', $r );
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertFalse( wp_get_post_autosave( $wp_customize->changeset_post_id() ) );
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertContains( 'Foo', get_post( $wp_customize->changeset_post_id() )->post_content );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                // Since no autosave yet, confirm no action.
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                $this->make_ajax_call( 'customize_dismiss_autosave' );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         $this->make_ajax_call( 'customize_dismiss_autosave_or_lock' );
</ins><span class="cx" style="display: block; padding: 0 10px">                 $this->assertFalse( $this->_last_response_parsed['success'] );
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertEquals( 'no_autosave_revision_to_delete', $this->_last_response_parsed['data'] );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -610,13 +624,13 @@
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertContains( 'Bar', $autosave_revision->post_content );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                // Confirm autosave gets deleted.
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                $this->make_ajax_call( 'customize_dismiss_autosave' );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         $this->make_ajax_call( 'customize_dismiss_autosave_or_lock' );
</ins><span class="cx" style="display: block; padding: 0 10px">                 $this->assertTrue( $this->_last_response_parsed['success'] );
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertEquals( 'autosave_revision_deleted', $this->_last_response_parsed['data'] );
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertFalse( wp_get_post_autosave( $wp_customize->changeset_post_id() ) );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                // Since no autosave yet, confirm no action.
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                $this->make_ajax_call( 'customize_dismiss_autosave' );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         $this->make_ajax_call( 'customize_dismiss_autosave_or_lock' );
</ins><span class="cx" style="display: block; padding: 0 10px">                 $this->assertFalse( $this->_last_response_parsed['success'] );
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertEquals( 'no_autosave_revision_to_delete', $this->_last_response_parsed['data'] );
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span></span></pre></div>
<a id="trunktestsphpunittestscustomizemanagerphp"></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/customize/manager.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/customize/manager.php   2017-10-12 03:54:59 UTC (rev 41838)
+++ trunk/tests/phpunit/tests/customize/manager.php     2017-10-12 04:00:15 UTC (rev 41839)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1455,7 +1455,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">                        ),
</span><span class="cx" style="display: block; padding: 0 10px">                        'autosave' => true,
</span><span class="cx" style="display: block; padding: 0 10px">                ) );
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                $this->assertFalse( wp_get_post_autosave( $changeset_post_id ) );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         $this->assertFalse( wp_get_post_autosave( $changeset_post_id, get_current_user_id() ) );
</ins><span class="cx" style="display: block; padding: 0 10px">                 $this->assertContains( 'Autosaved Auto-draft Title', get_post( $changeset_post_id )->post_content );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                // Update status to draft for subsequent tests.
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1493,7 +1493,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertEquals( 'illegal_autosave_with_non_current_user', $r->get_error_code() );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                // Try autosave.
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                $this->assertFalse( wp_get_post_autosave( $changeset_post_id ) );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         $this->assertFalse( wp_get_post_autosave( $changeset_post_id, get_current_user_id() ) );
</ins><span class="cx" style="display: block; padding: 0 10px">                 $r = $wp_customize->save_changeset_post( array(
</span><span class="cx" style="display: block; padding: 0 10px">                        'data' => array(
</span><span class="cx" style="display: block; padding: 0 10px">                                'blogname' => array(
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1505,7 +1505,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertInternalType( 'array', $r );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                // Verify that autosave happened.
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                $autosave_revision = wp_get_post_autosave( $changeset_post_id );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         $autosave_revision = wp_get_post_autosave( $changeset_post_id, get_current_user_id() );
</ins><span class="cx" style="display: block; padding: 0 10px">                 $this->assertInstanceOf( 'WP_Post', $autosave_revision );
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertContains( 'Draft Title', get_post( $changeset_post_id )->post_content );
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertContains( 'Autosave Title', $autosave_revision->post_content );
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -2635,6 +2635,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">                                'currentUserCanPublish',
</span><span class="cx" style="display: block; padding: 0 10px">                                'publishDate',
</span><span class="cx" style="display: block; padding: 0 10px">                                'statusChoices',
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                                'lockUser',
</ins><span class="cx" style="display: block; padding: 0 10px">                         ),
</span><span class="cx" style="display: block; padding: 0 10px">                        array_keys( $data['changeset'] )
</span><span class="cx" style="display: block; padding: 0 10px">                );
</span></span></pre></div>
<a id="trunktestsqunitfixturescustomizesettingsjs"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/tests/qunit/fixtures/customize-settings.js</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/qunit/fixtures/customize-settings.js  2017-10-12 03:54:59 UTC (rev 41838)
+++ trunk/tests/qunit/fixtures/customize-settings.js    2017-10-12 04:00:15 UTC (rev 41839)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -167,7 +167,8 @@
</span><span class="cx" style="display: block; padding: 0 10px">                currentUserCanPublish: false,
</span><span class="cx" style="display: block; padding: 0 10px">                hasAutosaveRevision: false,
</span><span class="cx" style="display: block; padding: 0 10px">                latestAutoDraftUuid: '341b06f6-3c1f-454f-96df-3cf197f3e347',
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                publishDate: ''
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         publishDate: '',
+               locked: false
</ins><span class="cx" style="display: block; padding: 0 10px">         },
</span><span class="cx" style="display: block; padding: 0 10px">        timeouts: {
</span><span class="cx" style="display: block; padding: 0 10px">                windowRefresh: 250,
</span></span></pre></div>
<a id="trunktestsqunitindexhtml"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/tests/qunit/index.html</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/qunit/index.html      2017-10-12 03:54:59 UTC (rev 41838)
+++ trunk/tests/qunit/index.html        2017-10-12 04:00:15 UTC (rev 41839)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -2210,6 +2210,25 @@
</span><span class="cx" style="display: block; padding: 0 10px">                <script src="wp-includes/js/tinymce/plugins/wptextpattern/plugin.js"></script>
</span><span class="cx" style="display: block; padding: 0 10px">                <script src="wp-includes/js/tinymce/tinymce-obsolete.js"></script>
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                <!-- Changeset locked notice template -->
+               <script type="text/html" id="tmpl-customize-changeset-locked-notice">
+                       <div id="customize-changeset-lock-dialog" class="notification-dialog-wrap hidden">
+                               <div class="notification-dialog-background"></div>
+                               <div class="notification-dialog">
+                                       <div class="customize-changeset-locked-message">
+                                               <div class="customize-changeset-locked-avatar"></div>
+                                               <p class="currently-editing wp-tab-first" tabindex="0">
+                                                       <span class="customize-notice-user-name"></span> <span class="customize-take-over-message">is already customizing this site. Do you want to take over?</span></p>
+                                               <p>
+                                                       <a class="button customize-notice-go-back-button" href="/wp-admin/post.php?post=505&#038;action=edit">Go back</a>
+                                                       <a class="button customize-notice-preview-button" href="http://example.org/?customize_changeset_uuid=7a796f7a-255c-49f5-9d25-cef0c315a4ba">Preview</a>
+                                                       <a class="button button-primary wp-tab-last customize-notice-take-over-button" href="http://example.org/wp-admin/customize.php?changeset_uuid=7a796f7a-255c-49f5-9d25-cef0c315a4ba&action=customize_take_over_changeset&nonce=e3a1df16d2&return=/wp-admin/post.php?post=505&action=edit">Take over</a>
+                                               </p>
+                                       </div>
+                               </div>
+                       </div>
+               </script>
+
</ins><span class="cx" style="display: block; padding: 0 10px">                 <!-- Updates templates and HTML fixtures -->
</span><span class="cx" style="display: block; padding: 0 10px">                <script id="tmpl-wp-updates-admin-notice" type="text/html">
</span><span class="cx" style="display: block; padding: 0 10px">                        <div <# if ( data.id ) { #>id="{{ data.id }}"<# } #> class="notice {{ data.className }}"><p>{{{ data.message }}}</p></div>
</span></span></pre>
</div>
</div>

</body>
</html>