<!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>[38810] trunk: Customize: Implement customized state persistence with changesets.</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/38810">38810</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/38810","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>2016-10-18 20:04:36 +0000 (Tue, 18 Oct 2016)</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: Implement customized state persistence with changesets.

Includes infrastructure developed in the Customize Snapshots feature plugin.

See https://make.wordpress.org/core/2016/10/12/customize-changesets-technical-design-decisions/

Props westonruter, valendesigns, utkarshpatel, stubgo, lgedeon, ocean90, ryankienstra, mihai2u, dlh, aaroncampbell, jonathanbardo, jorbin.
See <a href="https://core.trac.wordpress.org/ticket/28721">#28721</a>.
See <a href="https://core.trac.wordpress.org/ticket/31089">#31089</a>.
Fixes <a href="https://core.trac.wordpress.org/ticket/30937">#30937</a>.
Fixes <a href="https://core.trac.wordpress.org/ticket/31517">#31517</a>.
Fixes <a href="https://core.trac.wordpress.org/ticket/30028">#30028</a>.
Fixes <a href="https://core.trac.wordpress.org/ticket/23225">#23225</a>.
Fixes <a href="https://core.trac.wordpress.org/ticket/34142">#34142</a>.
Fixes <a href="https://core.trac.wordpress.org/ticket/36485">#36485</a>.</pre>

<h3>Modified Paths</h3>
<ul>
<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="#trunksrcwpadminjscustomizewidgetsjs">trunk/src/wp-admin/js/customize-widgets.js</a></li>
<li><a href="#trunksrcwpincludesadminbarphp">trunk/src/wp-includes/admin-bar.php</a></li>
<li><a href="#trunksrcwpincludesclasswpcustomizemanagerphp">trunk/src/wp-includes/class-wp-customize-manager.php</a></li>
<li><a href="#trunksrcwpincludesclasswpcustomizenavmenusphp">trunk/src/wp-includes/class-wp-customize-nav-menus.php</a></li>
<li><a href="#trunksrcwpincludesclasswpcustomizewidgetsphp">trunk/src/wp-includes/class-wp-customize-widgets.php</a></li>
<li><a href="#trunksrcwpincludescustomizeclasswpcustomizeselectiverefreshphp">trunk/src/wp-includes/customize/class-wp-customize-selective-refresh.php</a></li>
<li><a href="#trunksrcwpincludescustomizeclasswpcustomizethemecontrolphp">trunk/src/wp-includes/customize/class-wp-customize-theme-control.php</a></li>
<li><a href="#trunksrcwpincludesdefaultfiltersphp">trunk/src/wp-includes/default-filters.php</a></li>
<li><a href="#trunksrcwpincludesfunctionsphp">trunk/src/wp-includes/functions.php</a></li>
<li><a href="#trunksrcwpincludesfunctionswpscriptsphp">trunk/src/wp-includes/functions.wp-scripts.php</a></li>
<li><a href="#trunksrcwpincludesjscustomizebasejs">trunk/src/wp-includes/js/customize-base.js</a></li>
<li><a href="#trunksrcwpincludesjscustomizeloaderjs">trunk/src/wp-includes/js/customize-loader.js</a></li>
<li><a href="#trunksrcwpincludesjscustomizepreviewnavmenusjs">trunk/src/wp-includes/js/customize-preview-nav-menus.js</a></li>
<li><a href="#trunksrcwpincludesjscustomizepreviewwidgetsjs">trunk/src/wp-includes/js/customize-preview-widgets.js</a></li>
<li><a href="#trunksrcwpincludesjscustomizepreviewjs">trunk/src/wp-includes/js/customize-preview.js</a></li>
<li><a href="#trunksrcwpincludesjscustomizeselectiverefreshjs">trunk/src/wp-includes/js/customize-selective-refresh.js</a></li>
<li><a href="#trunksrcwpincludespostphp">trunk/src/wp-includes/post.php</a></li>
<li><a href="#trunksrcwpincludesscriptloaderphp">trunk/src/wp-includes/script-loader.php</a></li>
<li><a href="#trunksrcwpincludesthemephp">trunk/src/wp-includes/theme.php</a></li>
<li><a href="#trunktestsphpunittestsadminbarphp">trunk/tests/phpunit/tests/adminbar.php</a></li>
<li><a href="#trunktestsphpunittestscustomizemanagerphp">trunk/tests/phpunit/tests/customize/manager.php</a></li>
<li><a href="#trunktestsphpunittestscustomizeselectiverefreshajaxphp">trunk/tests/phpunit/tests/customize/selective-refresh-ajax.php</a></li>
<li><a href="#trunktestsphpunittestscustomizesettingphp">trunk/tests/phpunit/tests/customize/setting.php</a></li>
<li><a href="#trunktestsphpunittestsfunctionsphp">trunk/tests/phpunit/tests/functions.php</a></li>
<li><a href="#trunktestsphpunittestspostphp">trunk/tests/phpunit/tests/post.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>
<li><a href="#trunktestsqunitwpadminjscustomizebasejs">trunk/tests/qunit/wp-admin/js/customize-base.js</a></li>
<li><a href="#trunktestsqunitwpadminjscustomizecontrolsjs">trunk/tests/qunit/wp-admin/js/customize-controls.js</a></li>
</ul>

<h3>Added Paths</h3>
<ul>
<li><a href="#trunktestsphpunittestsajaxCustomizeManagerphp">trunk/tests/phpunit/tests/ajax/CustomizeManager.php</a></li>
</ul>

</div>
<div id="patch">
<h3>Diff</h3>
<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  2016-10-17 23:53:20 UTC (rev 38809)
+++ trunk/src/wp-admin/customize.php    2016-10-18 20:04:36 UTC (rev 38810)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -20,6 +20,31 @@
</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">+/**
+ * @global WP_Scripts           $wp_scripts
+ * @global WP_Customize_Manager $wp_customize
+ */
+global $wp_scripts, $wp_customize;
+
+if ( $wp_customize->changeset_post_id() ) {
+       if ( ! current_user_can( get_post_type_object( 'customize_changeset' )->cap->edit_post, $wp_customize->changeset_post_id() ) ) {
+               wp_die(
+                       '<h1>' . __( 'Cheatin&#8217; uh?' ) . '</h1>' .
+                       '<p>' . __( 'Sorry, you are not allowed to edit this changeset.' ) . '</p>',
+                       403
+               );
+       }
+       if ( in_array( get_post_status( $wp_customize->changeset_post_id() ), array( 'publish', 'trash' ), true ) ) {
+               wp_die(
+                       '<h1>' . __( 'Cheatin&#8217; uh?' ) . '</h1>' .
+                       '<p>' . __( 'This changeset has already been published and cannot be further modified.' ) . '</p>' .
+                       '<p><a href="' . esc_url( remove_query_arg( 'changeset_uuid' ) ) . '">' . __( 'Customize New Changes' ) . '</a></p>',
+                       403
+               );
+       }
+}
+
+
</ins><span class="cx" style="display: block; padding: 0 10px"> wp_reset_vars( array( 'url', 'return', 'autofocus' ) );
</span><span class="cx" style="display: block; padding: 0 10px"> if ( ! empty( $url ) ) {
</span><span class="cx" style="display: block; padding: 0 10px">        $wp_customize->set_preview_url( wp_unslash( $url ) );
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -31,12 +56,6 @@
</span><span class="cx" style="display: block; padding: 0 10px">        $wp_customize->set_autofocus( wp_unslash( $autofocus ) );
</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">-/**
- * @global WP_Scripts           $wp_scripts
- * @global WP_Customize_Manager $wp_customize
- */
-global $wp_scripts, $wp_customize;
-
</del><span class="cx" style="display: block; padding: 0 10px"> $registered = $wp_scripts->registered;
</span><span class="cx" style="display: block; padding: 0 10px"> $wp_scripts = new WP_Scripts;
</span><span class="cx" style="display: block; padding: 0 10px"> $wp_scripts->registered = $registered;
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -115,7 +134,11 @@
</span><span class="cx" style="display: block; padding: 0 10px">                <div id="customize-header-actions" class="wp-full-overlay-header">
</span><span class="cx" style="display: block; padding: 0 10px">                        <?php
</span><span class="cx" style="display: block; padding: 0 10px">                        $save_text = $wp_customize->is_theme_active() ? __( 'Save &amp; Publish' ) : __( 'Save &amp; Activate' );
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        submit_button( $save_text, 'primary save', 'save', false );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 $save_attrs = array();
+                       if ( ! current_user_can( get_post_type_object( 'customize_changeset' )->cap->publish_posts ) ) {
+                               $save_attrs['style'] = 'display: none';
+                       }
+                       submit_button( $save_text, 'primary save', 'save', false, $save_attrs );
</ins><span class="cx" style="display: block; padding: 0 10px">                         ?>
</span><span class="cx" style="display: block; padding: 0 10px">                        <span class="spinner"></span>
</span><span class="cx" style="display: block; padding: 0 10px">                        <button type="button" class="customize-controls-preview-toggle">
</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       2016-10-17 23:53:20 UTC (rev 38809)
+++ trunk/src/wp-admin/js/customize-controls.js 2016-10-18 20:04:36 UTC (rev 38810)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -22,27 +22,43 @@
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><span class="cx" style="display: block; padding: 0 10px">        api.Setting = api.Value.extend({
</span><span class="cx" style="display: block; padding: 0 10px">                initialize: function( id, value, options ) {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        api.Value.prototype.initialize.call( this, value, options );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 var setting = this;
+                       api.Value.prototype.initialize.call( setting, value, options );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        this.id = id;
-                       this.transport = this.transport || 'refresh';
-                       this._dirty = options.dirty || false;
-                       this.notifications = new api.Values({ defaultConstructor: api.Notification });
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 setting.id = id;
+                       setting.transport = setting.transport || 'refresh';
+                       setting._dirty = options.dirty || false;
+                       setting.notifications = new api.Values({ defaultConstructor: api.Notification });
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                        // Whenever the setting's value changes, refresh the preview.
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        this.bind( this.preview );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 setting.bind( setting.preview );
</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="cx" style="display: block; padding: 0 10px">                 * Refresh the preview, respective of the setting's refresh policy.
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 *
+                * If the preview hasn't sent a keep-alive message and is likely
+                * disconnected by having navigated to a non-allowed URL, then the
+                * refresh transport will be forced when postMessage is the transport.
+                * Note that postMessage does not throw an error when the recipient window
+                * fails to match the origin window, so using try/catch around the
+                * previewer.send() call to then fallback to refresh will not work.
+                *
+                * @since 3.4.0
</ins><span class="cx" style="display: block; padding: 0 10px">                  */
</span><span class="cx" style="display: block; padding: 0 10px">                preview: function() {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        switch ( this.transport ) {
-                               case 'refresh':
-                                       return this.previewer.refresh();
-                               case 'postMessage':
-                                       return this.previewer.send( 'setting', [ this.id, this() ] );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 var setting = this, transport;
+                       transport = setting.transport;
+
+                       if ( 'postMessage' === transport && ! api.state( 'previewerAlive' ).get() ) {
+                               transport = 'refresh';
</ins><span class="cx" style="display: block; padding: 0 10px">                         }
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
+                       if ( 'postMessage' === transport ) {
+                               return setting.previewer.send( 'setting', [ setting.id, setting() ] );
+                       } else if ( 'refresh' === transport ) {
+                               return setting.previewer.refresh();
+                       }
</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">@@ -65,11 +81,168 @@
</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">-         * Utility function namespace
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+  * Current change count.
+        *
+        * @since 4.7.0
+        * @type {number}
+        * @protected
</ins><span class="cx" style="display: block; padding: 0 10px">          */
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        api.utils = {};
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ api._latestRevision = 0;
</ins><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">+         * Last revision that was saved.
+        *
+        * @since 4.7.0
+        * @type {number}
+        * @protected
+        */
+       api._lastSavedRevision = 0;
+
+       /**
+        * Latest revisions associated with the updated setting.
+        *
+        * @since 4.7.0
+        * @type {object}
+        * @protected
+        */
+       api._latestSettingRevisions = {};
+
+       /*
+        * Keep track of the revision associated with each updated setting so that
+        * requestChangesetUpdate knows which dirty settings to include. Also, once
+        * ready is triggered and all initial settings have been added, increment
+        * revision for each newly-created initially-dirty setting so that it will
+        * also be included in changeset update requests.
+        */
+       api.bind( 'change', function incrementChangedSettingRevision( setting ) {
+               api._latestRevision += 1;
+               api._latestSettingRevisions[ setting.id ] = api._latestRevision;
+       } );
+       api.bind( 'ready', function() {
+               api.bind( 'add', function incrementCreatedSettingRevision( setting ) {
+                       if ( setting._dirty ) {
+                               api._latestRevision += 1;
+                               api._latestSettingRevisions[ setting.id ] = api._latestRevision;
+                       }
+               } );
+       } );
+
+       /**
+        * Get the dirty setting values.
+        *
+        * @param {object} [options] Options.
+        * @param {boolean} [options.unsaved=false] Whether only values not saved yet into a changeset will be returned (differential changes).
+        * @returns {object} Dirty setting values.
+        */
+       api.dirtyValues = function dirtyValues( options ) {
+               var values = {};
+               api.each( function( setting ) {
+                       var settingRevision;
+
+                       if ( ! setting._dirty ) {
+                               return;
+                       }
+
+                       settingRevision = api._latestSettingRevisions[ setting.id ];
+
+                       // Skip including settings that have already been included in the changeset, if only requesting unsaved.
+                       if ( ( options && options.unsaved ) && ( _.isUndefined( settingRevision ) || settingRevision <= api._lastSavedRevision ) ) {
+                               return;
+                       }
+
+                       values[ setting.id ] = setting.get();
+               } );
+               return values;
+       };
+
+       /**
+        * Request updates to the changeset.
+        *
+        * @param {object} [changes] Mapping of setting IDs to setting params each normally including a value property, or mapping to null.
+        *                           If not provided, then the changes will still be obtained from unsaved dirty settings.
+        * @returns {jQuery.Promise}
+        */
+       api.requestChangesetUpdate = function requestChangesetUpdate( changes ) {
+               var deferred, request, submittedChanges = {}, data;
+               deferred = new $.Deferred();
+
+               if ( changes ) {
+                       _.extend( submittedChanges, changes );
+               }
+
+               // Ensure all revised settings (changes pending save) are also included, but not if marked for deletion in changes.
+               _.each( api.dirtyValues( { unsaved: true } ), function( dirtyValue, settingId ) {
+                       if ( ! changes || null !== changes[ settingId ] ) {
+                               submittedChanges[ settingId ] = _.extend(
+                                       {},
+                                       submittedChanges[ settingId ] || {},
+                                       { value: dirtyValue }
+                               );
+                       }
+               } );
+
+               // Short-circuit when there are no pending changes.
+               if ( _.isEmpty( submittedChanges ) ) {
+                       deferred.resolve( {} );
+                       return deferred.promise();
+               }
+
+               // Make sure that publishing a changeset waits for all changeset update requests to complete.
+               api.state( 'processing' ).set( api.state( 'processing' ).get() + 1 );
+               deferred.always( function() {
+                       api.state( 'processing' ).set( api.state( 'processing' ).get() - 1 );
+               } );
+
+               // Allow plugins to attach additional params to the settings.
+               api.trigger( 'changeset-save', submittedChanges );
+
+               // Ensure that if any plugins add data to save requests by extending query() that they get included here.
+               data = api.previewer.query( { excludeCustomizedSaved: true } );
+               delete data.customized; // Being sent in customize_changeset_data instead.
+               _.extend( data, {
+                       nonce: api.settings.nonce.save,
+                       customize_theme: api.settings.theme.stylesheet,
+                       customize_changeset_data: JSON.stringify( submittedChanges )
+               } );
+
+               request = wp.ajax.post( 'customize_save', data );
+
+               request.done( function requestChangesetUpdateDone( data ) {
+                       var savedChangesetValues = {};
+
+                       // Ensure that all settings updated subsequently will be included in the next changeset update request.
+                       api._lastSavedRevision = Math.max( api._latestRevision, api._lastSavedRevision );
+
+                       api.state( 'changesetStatus' ).set( data.changeset_status );
+                       deferred.resolve( data );
+                       api.trigger( 'changeset-saved', data );
+
+                       if ( data.setting_validities ) {
+                               _.each( data.setting_validities, function( validity, settingId ) {
+                                       if ( true === validity && _.isObject( submittedChanges[ settingId ] ) && ! _.isUndefined( submittedChanges[ settingId ].value ) ) {
+                                               savedChangesetValues[ settingId ] = submittedChanges[ settingId ].value;
+                                       }
+                               } );
+                       }
+
+                       api.previewer.send( 'changeset-saved', _.extend( {}, data, { saved_changeset_values: savedChangesetValues } ) );
+               } );
+               request.fail( function requestChangesetUpdateFail( data ) {
+                       deferred.reject( data );
+                       api.trigger( 'changeset-error', data );
+               } );
+               request.always( function( data ) {
+                       if ( data.setting_validities ) {
+                               api._handleSettingValidities( {
+                                       settingValidities: data.setting_validities
+                               } );
+                       }
+               } );
+
+               return deferred.promise();
+       };
+
+       /**
</ins><span class="cx" style="display: block; padding: 0 10px">          * Watch all changes to Value properties, and bubble changes to parent Values instance
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * @since 4.1.0
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1216,6 +1389,57 @@
</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">+                 * Load theme preview.
+                *
+                * @since 4.7.0
+                *
+                * @param {string} themeId Theme ID.
+                * @returns {jQuery.promise} Promise.
+                */
+               loadThemePreview: function( themeId ) {
+                       var deferred = $.Deferred(), onceProcessingComplete, overlay, urlParser;
+
+                       urlParser = document.createElement( 'a' );
+                       urlParser.href = location.href;
+                       urlParser.search = $.param( _.extend(
+                               api.utils.parseQueryString( urlParser.search.substr( 1 ) ),
+                               {
+                                       theme: themeId,
+                                       changeset_uuid: api.settings.changeset.uuid
+                               }
+                       ) );
+
+                       overlay = $( '.wp-full-overlay' );
+                       overlay.addClass( 'customize-loading' );
+
+                       onceProcessingComplete = function() {
+                               var request;
+                               if ( api.state( 'processing' ).get() > 0 ) {
+                                       return;
+                               }
+
+                               api.state( 'processing' ).unbind( onceProcessingComplete );
+
+                               request = api.requestChangesetUpdate();
+                               request.done( function() {
+                                       $( window ).off( 'beforeunload.customize-confirm' );
+                                       window.location.href = urlParser.href;
+                               } );
+                               request.fail( function() {
+                                       overlay.removeClass( 'customize-loading' );
+                               } );
+                       };
+
+                       if ( 0 === api.state( 'processing' ).get() ) {
+                               onceProcessingComplete();
+                       } else {
+                               api.state( 'processing' ).bind( onceProcessingComplete );
+                       }
+
+                       return deferred.promise();
+               },
+
+               /**
</ins><span class="cx" style="display: block; padding: 0 10px">                  * Render & show the theme details for a given theme model.
</span><span class="cx" style="display: block; padding: 0 10px">                 *
</span><span class="cx" style="display: block; padding: 0 10px">                 * @since 4.2.0
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1223,7 +1447,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">                 * @param {Object}   theme
</span><span class="cx" style="display: block; padding: 0 10px">                 */
</span><span class="cx" style="display: block; padding: 0 10px">                showDetails: function ( theme, callback ) {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        var section = this;
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 var section = this, link;
</ins><span class="cx" style="display: block; padding: 0 10px">                         callback = callback || function(){};
</span><span class="cx" style="display: block; padding: 0 10px">                        section.currentTheme = theme.id;
</span><span class="cx" style="display: block; padding: 0 10px">                        section.overlay.html( section.template( theme ) )
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1232,6 +1456,22 @@
</span><span class="cx" style="display: block; padding: 0 10px">                        $( 'body' ).addClass( 'modal-open' );
</span><span class="cx" style="display: block; padding: 0 10px">                        section.containFocus( section.overlay );
</span><span class="cx" style="display: block; padding: 0 10px">                        section.updateLimits();
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
+                       link = section.overlay.find( '.inactive-theme > a' );
+
+                       link.on( 'click', function( event ) {
+                               event.preventDefault();
+
+                               // Short-circuit if request is currently being made.
+                               if ( link.hasClass( 'disabled' ) ) {
+                                       return;
+                               }
+                               link.addClass( 'disabled' );
+
+                               section.loadThemePreview( theme.id ).fail( function() {
+                                       link.removeClass( 'disabled' );
+                               } );
+                       } );
</ins><span class="cx" style="display: block; padding: 0 10px">                         callback();
</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">@@ -2227,7 +2467,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">                        wp.ajax.post( 'custom-background-add', {
</span><span class="cx" style="display: block; padding: 0 10px">                                nonce: _wpCustomizeBackground.nonces.add,
</span><span class="cx" style="display: block; padding: 0 10px">                                wp_customize: 'on',
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                theme: api.settings.theme.stylesheet,
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                         customize_theme: api.settings.theme.stylesheet,
</ins><span class="cx" style="display: block; padding: 0 10px">                                 attachment_id: this.params.attachment.id
</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">@@ -2592,7 +2832,7 @@
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                        // Ensure custom-header-crop Ajax requests bootstrap the Customizer to activate the previewed theme.
</span><span class="cx" style="display: block; padding: 0 10px">                        wp.media.controller.Cropper.prototype.defaults.doCropArgs.wp_customize = 'on';
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        wp.media.controller.Cropper.prototype.defaults.doCropArgs.theme = api.settings.theme.stylesheet;
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 wp.media.controller.Cropper.prototype.defaults.doCropArgs.customize_theme = api.settings.theme.stylesheet;
</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">@@ -2883,11 +3123,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">                                        return;
</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">-                                var previewUrl = $( this ).data( 'previewUrl' );
-
-                               $( '.wp-full-overlay' ).addClass( 'customize-loading' );
-
-                               window.parent.location = previewUrl;
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                         api.section( control.section() ).loadThemePreview( control.params.theme.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">                        control.container.on( 'click keydown', '.theme-actions .theme-details', function( event ) {
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -2948,13 +3184,12 @@
</span><span class="cx" style="display: block; padding: 0 10px">         * @mixes wp.customize.Events
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><span class="cx" style="display: block; padding: 0 10px">        api.PreviewFrame = api.Messenger.extend({
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                sensitivity: 2000,
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         sensitivity: null, // Will get set to api.settings.timeouts.previewFrameSensitivity.
</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">                 * Initialize the PreviewFrame.
</span><span class="cx" style="display: block; padding: 0 10px">                 *
</span><span class="cx" style="display: block; padding: 0 10px">                 * @param {object} params.container
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                 * @param {object} params.signature
</del><span class="cx" style="display: block; padding: 0 10px">                  * @param {object} params.previewUrl
</span><span class="cx" style="display: block; padding: 0 10px">                 * @param {object} params.query
</span><span class="cx" style="display: block; padding: 0 10px">                 * @param {object} options
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -2969,7 +3204,6 @@
</span><span class="cx" style="display: block; padding: 0 10px">                        deferred.promise( this );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                        this.container = params.container;
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        this.signature = params.signature;
</del><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                        $.extend( params, { channel: api.PreviewFrame.uuid() });
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -2989,143 +3223,118 @@
</span><span class="cx" style="display: block; padding: 0 10px">                 *                          the request.
</span><span class="cx" style="display: block; padding: 0 10px">                 */
</span><span class="cx" style="display: block; padding: 0 10px">                run: function( deferred ) {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        var self   = this,
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 var previewFrame = this,
</ins><span class="cx" style="display: block; padding: 0 10px">                                 loaded = false,
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                ready  = false;
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                         ready = false,
+                               readyData = null,
+                               hasPendingChangesetUpdate = '{}' !== previewFrame.query.customized,
+                               urlParser,
+                               params,
+                               form;
</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 ( this._ready ) {
-                               this.unbind( 'ready', this._ready );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 if ( previewFrame._ready ) {
+                               previewFrame.unbind( 'ready', previewFrame._ready );
</ins><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">-                        this._ready = function() {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 previewFrame._ready = function( data ) {
</ins><span class="cx" style="display: block; padding: 0 10px">                                 ready = true;
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                                readyData = data;
+                               previewFrame.container.addClass( 'iframe-ready' );
+                               if ( ! data ) {
+                                       return;
+                               }
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                                if ( loaded ) {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                        deferred.resolveWith( self );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                                 deferred.resolveWith( previewFrame, [ data ] );
</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><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        this.bind( 'ready', this._ready );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 previewFrame.bind( 'ready', previewFrame._ready );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        this.bind( 'ready', function ( data ) {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 urlParser = document.createElement( 'a' );
+                       urlParser.href = previewFrame.previewUrl();
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                this.container.addClass( 'iframe-ready' );
-
-                               if ( ! data ) {
-                                       return;
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 params = _.extend(
+                               api.utils.parseQueryString( urlParser.search.substr( 1 ) ),
+                               {
+                                       customize_changeset_uuid: previewFrame.query.customize_changeset_uuid,
+                                       customize_theme: previewFrame.query.customize_theme,
+                                       customize_messenger_channel: previewFrame.query.customize_messenger_channel
</ins><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"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                /*
-                                * Walk over all panels, sections, and controls and set their
-                                * respective active states to true if the preview explicitly
-                                * indicates as such.
-                                */
-                               var constructs = {
-                                       panel: data.activePanels,
-                                       section: data.activeSections,
-                                       control: data.activeControls
-                               };
-                               _( constructs ).each( function ( activeConstructs, type ) {
-                                       api[ type ].each( function ( construct, id ) {
-                                               var isDynamicallyCreated = _.isUndefined( api.settings[ type + 's' ][ id ] );
-
-                                               /*
-                                                * If the construct was created statically in PHP (not dynamically in JS)
-                                                * then consider a missing (undefined) value in the activeConstructs to
-                                                * mean it should be deactivated (since it is gone). But if it is
-                                                * dynamically created then only toggle activation if the value is defined,
-                                                * as this means that the construct was also then correspondingly
-                                                * created statically in PHP and the active callback is available.
-                                                * Otherwise, dynamically-created constructs should normally have
-                                                * their active states toggled in JS rather than from PHP.
-                                                */
-                                               if ( ! isDynamicallyCreated || ! _.isUndefined( activeConstructs[ id ] ) ) {
-                                                       if ( activeConstructs[ id ] ) {
-                                                               construct.activate();
-                                                       } else {
-                                                               construct.deactivate();
-                                                       }
-                                               }
-                                       } );
-                               } );
-
-                               if ( data.settingValidities ) {
-                                       api._handleSettingValidities( {
-                                               settingValidities: data.settingValidities,
-                                               focusInvalidControl: false
-                                       } );
-                               }
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 urlParser.search = $.param( params );
+                       previewFrame.iframe = $( '<iframe />', {
+                               title: api.l10n.previewIframeTitle,
+                               name: 'customize-' + previewFrame.channel()
</ins><span class="cx" style="display: block; padding: 0 10px">                         } );
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                        previewFrame.iframe.attr( 'onmousewheel', '' ); // Workaround for Safari bug. See WP Trac #38149.
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        this.request = $.ajax( this.previewUrl(), {
-                               type: 'POST',
-                               data: this.query,
-                               xhrFields: {
-                                       withCredentials: true
-                               }
-                       } );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 if ( ! hasPendingChangesetUpdate ) {
+                               previewFrame.iframe.attr( 'src', urlParser.href );
+                       } else {
+                               previewFrame.iframe.attr( 'data-src', urlParser.href ); // For debugging purposes.
+                       }
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        this.request.fail( function() {
-                               deferred.rejectWith( self, [ 'request failure' ] );
-                       });
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 previewFrame.iframe.appendTo( previewFrame.container );
+                       previewFrame.targetWindow( previewFrame.iframe[0].contentWindow );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        this.request.done( function( response ) {
-                               var location = self.request.getResponseHeader('Location'),
-                                       signature = self.signature,
-                                       index;
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 /*
+                        * Submit customized data in POST request to preview frame window since
+                        * there are setting value changes not yet written to changeset.
+                        */
+                       if ( hasPendingChangesetUpdate ) {
+                               form = $( '<form>', {
+                                       action: urlParser.href,
+                                       target: previewFrame.iframe.attr( 'name' ),
+                                       method: 'post',
+                                       hidden: 'hidden'
+                               } );
+                               form.append( $( '<input>', {
+                                       type: 'hidden',
+                                       name: '_method',
+                                       value: 'GET'
+                               } ) );
+                               _.each( previewFrame.query, function( value, key ) {
+                                       form.append( $( '<input>', {
+                                               type: 'hidden',
+                                               name: key,
+                                               value: value
+                                       } ) );
+                               } );
+                               previewFrame.container.append( form );
+                               form.submit();
+                               form.remove(); // No need to keep the form around after submitted.
+                       }
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                // Check if the location response header differs from the current URL.
-                               // If so, the request was redirected; try loading the requested page.
-                               if ( location && location !== self.previewUrl() ) {
-                                       deferred.rejectWith( self, [ 'redirect', location ] );
-                                       return;
-                               }
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 previewFrame.bind( 'iframe-loading-error', function( error ) {
+                               previewFrame.iframe.remove();
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                                // Check if the user is not logged in.
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                if ( '0' === response ) {
-                                       self.login( deferred );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                         if ( 0 === error ) {
+                                       previewFrame.login( deferred );
</ins><span class="cx" style="display: block; padding: 0 10px">                                         return;
</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">                                // Check for cheaters.
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                if ( '-1' === response ) {
-                                       deferred.rejectWith( self, [ 'cheatin' ] );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                         if ( -1 === error ) {
+                                       deferred.rejectWith( previewFrame, [ 'cheatin' ] );
</ins><span class="cx" style="display: block; padding: 0 10px">                                         return;
</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">-                                // Check for a signature in the request.
-                               index = response.lastIndexOf( signature );
-                               if ( -1 === index || index < response.lastIndexOf('</html>') ) {
-                                       deferred.rejectWith( self, [ 'unsigned' ] );
-                                       return;
-                               }
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                         deferred.rejectWith( previewFrame, [ 'request failure' ] );
+                       } );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                // Strip the signature from the request.
-                               response = response.slice( 0, index ) + response.slice( index + signature.length );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 previewFrame.iframe.one( 'load', function() {
+                               loaded = true;
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                // Create the iframe and inject the html content.
-                               self.iframe = $( '<iframe />', { 'title': api.l10n.previewIframeTitle } ).appendTo( self.container );
-                               self.iframe.attr( 'onmousewheel', '' ); // Workaround for Safari bug. See WP Trac #38149.
-
-                               // Bind load event after the iframe has been added to the page;
-                               // otherwise it will fire when injected into the DOM.
-                               self.iframe.one( 'load', function() {
-                                       loaded = true;
-
-                                       if ( ready ) {
-                                               deferred.resolveWith( self );
-                                       } else {
-                                               setTimeout( function() {
-                                                       deferred.rejectWith( self, [ 'ready timeout' ] );
-                                               }, self.sensitivity );
-                                       }
-                               });
-
-                               self.targetWindow( self.iframe[0].contentWindow );
-
-                               self.targetWindow().document.open();
-                               self.targetWindow().document.write( response );
-                               self.targetWindow().document.close();
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                         if ( ready ) {
+                                       deferred.resolveWith( previewFrame, [ readyData ] );
+                               } else {
+                                       setTimeout( function() {
+                                               deferred.rejectWith( previewFrame, [ 'ready timeout' ] );
+                                       }, previewFrame.sensitivity );
+                               }
</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">@@ -3164,26 +3373,29 @@
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                destroy: function() {
</span><span class="cx" style="display: block; padding: 0 10px">                        api.Messenger.prototype.destroy.call( this );
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        this.request.abort();
</del><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        if ( this.iframe )
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 if ( this.iframe ) {
</ins><span class="cx" style="display: block; padding: 0 10px">                                 this.iframe.remove();
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                        }
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        delete this.request;
</del><span class="cx" style="display: block; padding: 0 10px">                         delete this.iframe;
</span><span class="cx" style="display: block; padding: 0 10px">                        delete this.targetWindow;
</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><span class="cx" style="display: block; padding: 0 10px">        (function(){
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                var uuid = 0;
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         var id = 0;
</ins><span class="cx" style="display: block; padding: 0 10px">                 /**
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                 * Create a universally unique identifier.
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+          * Return an incremented ID for a preview messenger channel.
</ins><span class="cx" style="display: block; padding: 0 10px">                  *
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                 * @return {int}
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+          * This function is named "uuid" for historical reasons, but it is a
+                * misnomer as it is not an actual UUID, and it is not universally unique.
+                * This is not to be confused with `api.settings.changeset.uuid`.
+                *
+                * @return {string}
</ins><span class="cx" style="display: block; padding: 0 10px">                  */
</span><span class="cx" style="display: block; padding: 0 10px">                api.PreviewFrame.uuid = function() {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        return 'preview-' + uuid++;
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 return 'preview-' + String( 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">@@ -3209,7 +3421,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">         * @mixes wp.customize.Events
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><span class="cx" style="display: block; padding: 0 10px">        api.Previewer = api.Messenger.extend({
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                refreshBuffer: 250,
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         refreshBuffer: null, // Will get set to api.settings.timeouts.windowRefresh.
</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">                 * @param {array}  params.allowedUrls
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -3217,64 +3429,50 @@
</span><span class="cx" style="display: block; padding: 0 10px">                 *                                    frame to be placed.
</span><span class="cx" style="display: block; padding: 0 10px">                 * @param {string} params.form
</span><span class="cx" style="display: block; padding: 0 10px">                 * @param {string} params.previewUrl  The URL to preview.
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                 * @param {string} params.signature
</del><span class="cx" style="display: block; padding: 0 10px">                  * @param {object} options
</span><span class="cx" style="display: block; padding: 0 10px">                 */
</span><span class="cx" style="display: block; padding: 0 10px">                initialize: function( params, options ) {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        var self = this,
-                               rscheme = /^https?/;
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 var previewer = this,
+                               urlParser = document.createElement( 'a' );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        $.extend( this, options || {} );
-                       this.deferred = {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 $.extend( previewer, options || {} );
+                       previewer.deferred = {
</ins><span class="cx" style="display: block; padding: 0 10px">                                 active: $.Deferred()
</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">-                        /*
-                        * Wrap this.refresh to prevent it from hammering the servers:
-                        *
-                        * If refresh is called once and no other refresh requests are
-                        * loading, trigger the request immediately.
-                        *
-                        * If refresh is called while another refresh request is loading,
-                        * debounce the refresh requests:
-                        * 1. Stop the loading request (as it is instantly outdated).
-                        * 2. Trigger the new request once refresh hasn't been called for
-                        *    self.refreshBuffer milliseconds.
-                        */
-                       this.refresh = (function( self ) {
-                               var refresh  = self.refresh,
-                                       callback = function() {
-                                               timeout = null;
-                                               refresh.call( self );
-                                       },
-                                       timeout;
-
-                               return function() {
-                                       if ( typeof timeout !== 'number' ) {
-                                               if ( self.loading ) {
-                                                       self.abort();
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 // Debounce to prevent hammering server and then wait for any pending update requests.
+                       previewer.refresh = _.debounce(
+                               ( function( originalRefresh ) {
+                                       return function() {
+                                               var isProcessingComplete, refreshOnceProcessingComplete;
+                                               isProcessingComplete = function() {
+                                                       return 0 === api.state( 'processing' ).get();
+                                               };
+                                               if ( isProcessingComplete() ) {
+                                                       originalRefresh.call( previewer );
</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">-                                                        return callback();
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                                                 refreshOnceProcessingComplete = function() {
+                                                               if ( isProcessingComplete() ) {
+                                                                       originalRefresh.call( previewer );
+                                                                       api.state( 'processing' ).unbind( refreshOnceProcessingComplete );
+                                                               }
+                                                       };
+                                                       api.state( 'processing' ).bind( refreshOnceProcessingComplete );
</ins><span class="cx" style="display: block; padding: 0 10px">                                                 }
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                        }
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                                 };
+                               }( previewer.refresh ) ),
+                               previewer.refreshBuffer
+                       );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                        clearTimeout( timeout );
-                                       timeout = setTimeout( callback, self.refreshBuffer );
-                               };
-                       })( this );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 previewer.container   = api.ensure( params.container );
+                       previewer.allowedUrls = params.allowedUrls;
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        this.container   = api.ensure( params.container );
-                       this.allowedUrls = params.allowedUrls;
-                       this.signature   = params.signature;
-
</del><span class="cx" style="display: block; padding: 0 10px">                         params.url = window.location.href;
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        api.Messenger.prototype.initialize.call( this, params );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 api.Messenger.prototype.initialize.call( previewer, params );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        this.add( 'scheme', this.origin() ).link( this.origin ).setter( function( to ) {
-                               var match = to.match( rscheme );
-                               return match ? match[0] : '';
-                       });
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 urlParser.href = previewer.origin();
+                       previewer.add( 'scheme', urlParser.protocol.replace( /:$/, '' ) );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                        // Limit the URL to internal, front-end links.
</span><span class="cx" style="display: block; padding: 0 10px">                        //
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -3284,8 +3482,8 @@
</span><span class="cx" style="display: block; padding: 0 10px">                        // are on different domains to avoid the case where the front end doesn't have
</span><span class="cx" style="display: block; padding: 0 10px">                        // ssl certs.
</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.add( 'previewUrl', params.previewUrl ).setter( function( to ) {
-                               var result, urlParser;
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 previewer.add( 'previewUrl', params.previewUrl ).setter( function( to ) {
+                               var result, urlParser, newPreviewUrl, schemeMatchingPreviewUrl, queryParams;
</ins><span class="cx" style="display: block; padding: 0 10px">                                 urlParser = document.createElement( 'a' );
</span><span class="cx" style="display: block; padding: 0 10px">                                urlParser.href = to;
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -3294,10 +3492,27 @@
</span><span class="cx" style="display: block; padding: 0 10px">                                        return null;
</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">+                                // Remove state query params.
+                               if ( urlParser.search.length > 1 ) {
+                                       queryParams = api.utils.parseQueryString( urlParser.search.substr( 1 ) );
+                                       delete queryParams.customize_changeset_uuid;
+                                       delete queryParams.customize_theme;
+                                       delete queryParams.customize_messenger_channel;
+                                       if ( _.isEmpty( queryParams ) ) {
+                                               urlParser.search = '';
+                                       } else {
+                                               urlParser.search = $.param( queryParams );
+                                       }
+                               }
+
+                               newPreviewUrl = urlParser.href;
+                               urlParser.protocol = previewer.scheme.get() + ':';
+                               schemeMatchingPreviewUrl = urlParser.href;
+
</ins><span class="cx" style="display: block; padding: 0 10px">                                 // Attempt to match the URL to the control frame's scheme
</span><span class="cx" style="display: block; padding: 0 10px">                                // and check if it's allowed. If not, try the original URL.
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                $.each([ to.replace( rscheme, self.scheme() ), to ], function( i, url ) {
-                                       $.each( self.allowedUrls, function( i, allowed ) {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                         $.each( [ schemeMatchingPreviewUrl, newPreviewUrl ], function( i, url ) {
+                                       $.each( previewer.allowedUrls, function( i, allowed ) {
</ins><span class="cx" style="display: block; padding: 0 10px">                                                 var path;
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                                                allowed = allowed.replace( /\/+$/, '' );
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -3308,32 +3523,173 @@
</span><span class="cx" style="display: block; padding: 0 10px">                                                        return false;
</span><span class="cx" style="display: block; padding: 0 10px">                                                }
</span><span class="cx" style="display: block; padding: 0 10px">                                        });
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                        if ( result )
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                                 if ( result ) {
</ins><span class="cx" style="display: block; padding: 0 10px">                                                 return false;
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                                        }
</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">                                // If we found a matching result, return it. If not, bail.
</span><span class="cx" style="display: block; padding: 0 10px">                                return result ? result : null;
</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">+                        previewer.bind( 'ready', previewer.ready );
+
+                       // Start listening for keep-alive messages when iframe first loads.
+                       previewer.deferred.active.done( _.bind( previewer.keepPreviewAlive, previewer ) );
+
+                       previewer.bind( 'synced', function() {
+                               previewer.send( 'active' );
+                       } );
+
</ins><span class="cx" style="display: block; padding: 0 10px">                         // Refresh the preview when the URL is changed (but not yet).
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        this.previewUrl.bind( this.refresh );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 previewer.previewUrl.bind( previewer.refresh );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        this.scroll = 0;
-                       this.bind( 'scroll', function( distance ) {
-                               this.scroll = distance;
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 previewer.scroll = 0;
+                       previewer.bind( 'scroll', function( distance ) {
+                               previewer.scroll = distance;
</ins><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">-                        // Update the URL when the iframe sends a URL message.
-                       this.bind( 'url', this.previewUrl );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 // Update the URL when the iframe sends a URL message, resetting scroll position. If URL is unchanged, then refresh.
+                       previewer.bind( 'url', function( url ) {
+                               var onUrlChange, urlChanged = false;
+                               previewer.scroll = 0;
+                               onUrlChange = function() {
+                                       urlChanged = true;
+                               };
+                               previewer.previewUrl.bind( onUrlChange );
+                               previewer.previewUrl.set( url );
+                               previewer.previewUrl.unbind( onUrlChange );
+                               if ( ! urlChanged ) {
+                                       previewer.refresh();
+                               }
+                       } );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                        // Update the document title when the preview changes.
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        this.bind( 'documentTitle', function ( title ) {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 previewer.bind( 'documentTitle', function ( title ) {
</ins><span class="cx" style="display: block; padding: 0 10px">                                 api.setDocumentTitle( title );
</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><span class="cx" style="display: block; padding: 0 10px">                /**
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 * Handle the preview receiving the ready message.
+                *
+                * @since 4.7.0
+                *
+                * @param {object} data - Data from preview.
+                * @param {string} data.currentUrl - Current URL.
+                * @param {object} data.activePanels - Active panels.
+                * @param {object} data.activeSections Active sections.
+                * @param {object} data.activeControls Active controls.
+                * @returns {void}
+                */
+               ready: function( data ) {
+                       var previewer = this, synced = {}, constructs;
+
+                       synced.settings = api.get();
+                       if ( 'resolved' !== previewer.deferred.active.state() || previewer.loading ) {
+                               synced.scroll = previewer.scroll;
+                       }
+                       previewer.send( 'sync', synced );
+
+                       // Set the previewUrl without causing the url to set the iframe.
+                       if ( data.currentUrl ) {
+                               previewer.previewUrl.unbind( previewer.refresh );
+                               previewer.previewUrl.set( data.currentUrl );
+                               previewer.previewUrl.bind( previewer.refresh );
+                       }
+
+                       /*
+                        * Walk over all panels, sections, and controls and set their
+                        * respective active states to true if the preview explicitly
+                        * indicates as such.
+                        */
+                       constructs = {
+                               panel: data.activePanels,
+                               section: data.activeSections,
+                               control: data.activeControls
+                       };
+                       _( constructs ).each( function ( activeConstructs, type ) {
+                               api[ type ].each( function ( construct, id ) {
+                                       var isDynamicallyCreated = _.isUndefined( api.settings[ type + 's' ][ id ] );
+
+                                       /*
+                                        * If the construct was created statically in PHP (not dynamically in JS)
+                                        * then consider a missing (undefined) value in the activeConstructs to
+                                        * mean it should be deactivated (since it is gone). But if it is
+                                        * dynamically created then only toggle activation if the value is defined,
+                                        * as this means that the construct was also then correspondingly
+                                        * created statically in PHP and the active callback is available.
+                                        * Otherwise, dynamically-created constructs should normally have
+                                        * their active states toggled in JS rather than from PHP.
+                                        */
+                                       if ( ! isDynamicallyCreated || ! _.isUndefined( activeConstructs[ id ] ) ) {
+                                               if ( activeConstructs[ id ] ) {
+                                                       construct.activate();
+                                               } else {
+                                                       construct.deactivate();
+                                               }
+                                       }
+                               } );
+                       } );
+
+                       if ( data.settingValidities ) {
+                               api._handleSettingValidities( {
+                                       settingValidities: data.settingValidities,
+                                       focusInvalidControl: false
+                               } );
+                       }
+               },
+
+               /**
+                * Keep the preview alive by listening for ready and keep-alive messages.
+                *
+                * If a message is not received in the allotted time then the iframe will be set back to the last known valid URL.
+                *
+                * @since 4.7.0
+                *
+                * @returns {void}
+                */
+               keepPreviewAlive: function keepPreviewAlive() {
+                       var previewer = this, keepAliveTick, timeoutId, handleMissingKeepAlive, scheduleKeepAliveCheck;
+
+                       /**
+                        * Schedule a preview keep-alive check.
+                        *
+                        * Note that if a page load takes longer than keepAliveCheck milliseconds,
+                        * the keep-alive messages will still be getting sent from the previous
+                        * URL.
+                        */
+                       scheduleKeepAliveCheck = function() {
+                               timeoutId = setTimeout( handleMissingKeepAlive, api.settings.timeouts.keepAliveCheck );
+                       };
+
+                       /**
+                        * Set the previewerAlive state to true when receiving a message from the preview.
+                        */
+                       keepAliveTick = function() {
+                               api.state( 'previewerAlive' ).set( true );
+                               clearTimeout( timeoutId );
+                               scheduleKeepAliveCheck();
+                       };
+
+                       /**
+                        * Set the previewerAlive state to false if keepAliveCheck milliseconds have transpired without a message.
+                        *
+                        * This is most likely to happen in the case of a connectivity error, or if the theme causes the browser
+                        * to navigate to a non-allowed URL. Setting this state to false will force settings with a postMessage
+                        * transport to use refresh instead, causing the preview frame also to be replaced with the current
+                        * allowed preview URL.
+                        */
+                       handleMissingKeepAlive = function() {
+                               api.state( 'previewerAlive' ).set( false );
+                       };
+                       scheduleKeepAliveCheck();
+
+                       previewer.bind( 'ready', keepAliveTick );
+                       previewer.bind( 'keep-alive', keepAliveTick );
+               },
+
+               /**
</ins><span class="cx" style="display: block; padding: 0 10px">                  * Query string data sent with each preview request.
</span><span class="cx" style="display: block; padding: 0 10px">                 *
</span><span class="cx" style="display: block; padding: 0 10px">                 * @abstract
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -3348,62 +3704,59 @@
</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">-                 * Refresh the preview.
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+          * Refresh the preview seamlessly.
</ins><span class="cx" style="display: block; padding: 0 10px">                  */
</span><span class="cx" style="display: block; padding: 0 10px">                refresh: function() {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        var self = this;
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 var previewer = this;
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                        // Display loading indicator
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        this.send( 'loading-initiated' );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 previewer.send( 'loading-initiated' );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        this.abort();
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 previewer.abort();
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        this.loading = new api.PreviewFrame({
-                               url:        this.url(),
-                               previewUrl: this.previewUrl(),
-                               query:      this.query() || {},
-                               container:  this.container,
-                               signature:  this.signature
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 previewer.loading = new api.PreviewFrame({
+                               url:        previewer.url(),
+                               previewUrl: previewer.previewUrl(),
+                               query:      previewer.query( { excludeCustomizedSaved: true } ) || {},
+                               container:  previewer.container
</ins><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">-                        this.loading.done( function() {
-                               // 'this' is the loading frame
-                               this.bind( 'synced', function() {
-                                       if ( self.preview )
-                                               self.preview.destroy();
-                                       self.preview = this;
-                                       delete self.loading;
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 previewer.loading.done( function( readyData ) {
+                               var loadingFrame = this, previousPreview, onceSynced;
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                        self.targetWindow( this.targetWindow() );
-                                       self.channel( this.channel() );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                         previousPreview = previewer.preview;
+                               previewer.preview = loadingFrame;
+                               previewer.targetWindow( loadingFrame.targetWindow() );
+                               previewer.channel( loadingFrame.channel() );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                        self.deferred.active.resolve();
-                                       self.send( 'active' );
-                               });
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                         onceSynced = function() {
+                                       loadingFrame.unbind( 'synced', onceSynced );
+                                       if ( previousPreview ) {
+                                               previousPreview.destroy();
+                                       }
+                                       previewer.deferred.active.resolve();
+                                       delete previewer.loading;
+                               };
+                               loadingFrame.bind( 'synced', onceSynced );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                this.send( 'sync', {
-                                       scroll:   self.scroll,
-                                       settings: api.get()
-                               });
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                         // This event will be received directly by the previewer in normal navigation; this is only needed for seamless refresh.
+                               previewer.trigger( 'ready', readyData );
</ins><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">-                        this.loading.fail( function( reason, location ) {
-                               self.send( 'loading-failed' );
-                               if ( 'redirect' === reason && location ) {
-                                       self.previewUrl( location );
-                               }
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 previewer.loading.fail( function( reason ) {
+                               previewer.send( 'loading-failed' );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                                if ( 'logged out' === reason ) {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                        if ( self.preview ) {
-                                               self.preview.destroy();
-                                               delete self.preview;
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                                 if ( previewer.preview ) {
+                                               previewer.preview.destroy();
+                                               delete previewer.preview;
</ins><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">-                                        self.login().done( self.refresh );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                                 previewer.login().done( previewer.refresh );
</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">                                if ( 'cheatin' === reason ) {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                        self.cheatin();
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                                 previewer.cheatin();
</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">@@ -3463,7 +3816,7 @@
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                        request = wp.ajax.post( 'customize_refresh_nonces', {
</span><span class="cx" style="display: block; padding: 0 10px">                                wp_customize: 'on',
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                theme: api.settings.theme.stylesheet
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                         customize_theme: api.settings.theme.stylesheet
</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">                        request.done( function( response ) {
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -3679,6 +4032,13 @@
</span><span class="cx" style="display: block; padding: 0 10px">                        return;
</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 ( null === api.PreviewFrame.prototype.sensitivity ) {
+                       api.PreviewFrame.prototype.sensitivity = api.settings.timeouts.previewFrameSensitivity;
+               }
+               if ( null === api.Previewer.prototype.refreshBuffer ) {
+                       api.Previewer.prototype.refreshBuffer = api.settings.timeouts.windowRefresh;
+               }
+
</ins><span class="cx" style="display: block; padding: 0 10px">                 var parent,
</span><span class="cx" style="display: block; padding: 0 10px">                        body = $( document.body ),
</span><span class="cx" style="display: block; padding: 0 10px">                        overlay = body.children( '.wp-full-overlay' ),
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -3722,8 +4082,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">                        container:   '#customize-preview',
</span><span class="cx" style="display: block; padding: 0 10px">                        form:        '#customize-controls',
</span><span class="cx" style="display: block; padding: 0 10px">                        previewUrl:  api.settings.url.preview,
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        allowedUrls: api.settings.url.allowed,
-                       signature:   'WP_CUSTOMIZER_SIGNATURE'
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 allowedUrls: api.settings.url.allowed
</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">                        nonce: api.settings.nonce,
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -3731,26 +4090,51 @@
</span><span class="cx" style="display: block; padding: 0 10px">                        /**
</span><span class="cx" style="display: block; padding: 0 10px">                         * Build the query to send along with the Preview request.
</span><span class="cx" style="display: block; padding: 0 10px">                         *
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                         * @return {object}
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                  * @since 4.7.0 Added options param.
+                        *
+                        * @param {object}  [options] Options.
+                        * @param {boolean} [options.excludeCustomizedSaved=false] Exclude saved settings in customized response (values pending writing to changeset).
+                        * @return {object} Query vars.
</ins><span class="cx" style="display: block; padding: 0 10px">                          */
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        query: function() {
-                               var dirtyCustomized = {};
-                               api.each( function ( value, key ) {
-                                       if ( value._dirty ) {
-                                               dirtyCustomized[ key ] = value();
-                                       }
-                               } );
-
-                               return {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 query: function( options ) {
+                               var queryVars = {
</ins><span class="cx" style="display: block; padding: 0 10px">                                         wp_customize: 'on',
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                        theme:      api.settings.theme.stylesheet,
-                                       customized: JSON.stringify( dirtyCustomized ),
-                                       nonce:      this.nonce.preview
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                                 customize_theme: api.settings.theme.stylesheet,
+                                       nonce: this.nonce.preview,
+                                       customize_changeset_uuid: api.settings.changeset.uuid
</ins><span class="cx" style="display: block; padding: 0 10px">                                 };
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
+                               /*
+                                * Exclude customized data if requested especially for calls to requestChangesetUpdate.
+                                * Changeset updates are differential and so it is a performance waste to send all of
+                                * the dirty settings with each update.
+                                */
+                               queryVars.customized = JSON.stringify( api.dirtyValues( {
+                                       unsaved: options && options.excludeCustomizedSaved
+                               } ) );
+
+                               return queryVars;
</ins><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">-                        save: function() {
-                               var self = this,
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 /**
+                        * Save (and publish) the customizer changeset.
+                        *
+                        * Updates to the changeset are transactional. If any of the settings
+                        * are invalid then none of them will be written into the changeset.
+                        * A revision will be made for the changeset post if revisions support
+                        * has been added to the post type.
+                        *
+                        * @param {object} [args] Args.
+                        * @param {string} [args.status=publish] Status.
+                        * @param {string} [args.date] Date, in local time in MySQL format.
+                        * @param {string} [args.title] Title
+                        *
+                        * @returns {jQuery.promise}
+                        */
+                       save: function( args ) {
+                               var previewer = this,
+                                       deferred = $.Deferred(),
+                                       changesetStatus = 'publish',
</ins><span class="cx" style="display: block; padding: 0 10px">                                         processing = api.state( 'processing' ),
</span><span class="cx" style="display: block; padding: 0 10px">                                        submitWhenDoneProcessing,
</span><span class="cx" style="display: block; padding: 0 10px">                                        submit,
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -3758,15 +4142,24 @@
</span><span class="cx" style="display: block; padding: 0 10px">                                        invalidSettings = [],
</span><span class="cx" style="display: block; padding: 0 10px">                                        invalidControls;
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                body.addClass( 'saving' );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                         if ( args && args.status ) {
+                                       changesetStatus = args.status;
+                               }
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                                if ( api.state( 'saving' ).get() ) {
+                                       deferred.reject( 'already_saving' );
+                                       deferred.promise();
+                               }
+
+                               api.state( 'saving' ).set( true );
+
</ins><span class="cx" style="display: block; padding: 0 10px">                                 function captureSettingModifiedDuringSave( setting ) {
</span><span class="cx" style="display: block; padding: 0 10px">                                        modifiedWhileSaving[ setting.id ] = true;
</span><span class="cx" style="display: block; padding: 0 10px">                                }
</span><span class="cx" style="display: block; padding: 0 10px">                                api.bind( 'change', captureSettingModifiedDuringSave );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                                submit = function () {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                        var request, query;
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                                 var request, query, settingInvalidities = {};
</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">                                         * Block saving if there are any settings that are marked as
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -3777,20 +4170,50 @@
</span><span class="cx" style="display: block; padding: 0 10px">                                                setting.notifications.each( function( notification ) {
</span><span class="cx" style="display: block; padding: 0 10px">                                                        if ( 'error' === notification.type && ! notification.fromServer ) {
</span><span class="cx" style="display: block; padding: 0 10px">                                                                invalidSettings.push( setting.id );
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                                                                if ( ! settingInvalidities[ setting.id ] ) {
+                                                                       settingInvalidities[ setting.id ] = {};
+                                                               }
+                                                               settingInvalidities[ setting.id ][ notification.code ] = notification;
</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="cx" style="display: block; padding: 0 10px">                                        invalidControls = api.findControlsForSettings( invalidSettings );
</span><span class="cx" style="display: block; padding: 0 10px">                                        if ( ! _.isEmpty( invalidControls ) ) {
</span><span class="cx" style="display: block; padding: 0 10px">                                                _.values( invalidControls )[0][0].focus();
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                                body.removeClass( 'saving' );
</del><span class="cx" style="display: block; padding: 0 10px">                                                 api.unbind( 'change', captureSettingModifiedDuringSave );
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                                return;
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                                         deferred.rejectWith( previewer, [
+                                                       { setting_invalidities: settingInvalidities }
+                                               ] );
+                                               api.state( 'saving' ).set( false );
+                                               return deferred.promise();
</ins><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">-                                        query = $.extend( self.query(), {
-                                               nonce:  self.nonce.save
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                                 /*
+                                        * Note that excludeCustomizedSaved is intentionally false so that the entire
+                                        * set of customized data will be included if bypassed changeset update.
+                                        */
+                                       query = $.extend( previewer.query( { excludeCustomizedSaved: false } ), {
+                                               nonce: previewer.nonce.save,
+                                               customize_changeset_status: changesetStatus
</ins><span class="cx" style="display: block; padding: 0 10px">                                         } );
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                                        if ( args && args.date ) {
+                                               query.customize_changeset_date = args.date;
+                                       }
+                                       if ( args && args.title ) {
+                                               query.customize_changeset_title = args.title;
+                                       }
+
+                                       /*
+                                        * Note that the dirty customized values will have already been set in the
+                                        * changeset and so technically query.customized could be deleted. However,
+                                        * it is remaining here to make sure that any settings that got updated
+                                        * quietly which may have not triggered an update request will also get
+                                        * included in the values that get saved to the changeset. This will ensure
+                                        * that values that get injected via the saved event will be included in
+                                        * the changeset. This also ensures that setting values that were invalid
+                                        * will get re-validated, perhaps in the case of settings that are invalid
+                                        * due to dependencies on other settings.
+                                        */
</ins><span class="cx" style="display: block; padding: 0 10px">                                         request = wp.ajax.post( 'customize_save', query );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                                        // Disable save button during the save request.
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -3799,12 +4222,13 @@
</span><span class="cx" style="display: block; padding: 0 10px">                                        api.trigger( 'save', request );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                                        request.always( function () {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                                body.removeClass( 'saving' );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                                         api.state( 'saving' ).set( false );
</ins><span class="cx" style="display: block; padding: 0 10px">                                                 saveBtn.prop( 'disabled', false );
</span><span class="cx" style="display: block; padding: 0 10px">                                                api.unbind( 'change', captureSettingModifiedDuringSave );
</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">                                        request.fail( function ( response ) {
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
</ins><span class="cx" style="display: block; padding: 0 10px">                                                 if ( '0' === response ) {
</span><span class="cx" style="display: block; padding: 0 10px">                                                        response = 'not_logged_in';
</span><span class="cx" style="display: block; padding: 0 10px">                                                } else if ( '-1' === response ) {
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -3813,12 +4237,12 @@
</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">                                                if ( 'invalid_nonce' === response ) {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                                        self.cheatin();
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                                                 previewer.cheatin();
</ins><span class="cx" style="display: block; padding: 0 10px">                                                 } else if ( 'not_logged_in' === response ) {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                                        self.preview.iframe.hide();
-                                                       self.login().done( function() {
-                                                               self.save();
-                                                               self.preview.iframe.show();
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                                                 previewer.preview.iframe.hide();
+                                                       previewer.login().done( function() {
+                                                               previewer.save();
+                                                               previewer.preview.iframe.show();
</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">@@ -3829,19 +4253,20 @@
</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">+                                                deferred.rejectWith( previewer, [ response ] );
</ins><span class="cx" style="display: block; padding: 0 10px">                                                 api.trigger( 'error', response );
</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">                                        request.done( function( response ) {
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                                // Clear setting dirty states, if setting wasn't modified while saving.
-                                               api.each( function( setting ) {
-                                                       if ( ! modifiedWhileSaving[ setting.id ] ) {
-                                                               setting._dirty = false;
-                                                       }
-                                               } );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                                         previewer.send( 'saved', response );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                                api.previewer.send( 'saved', response );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                                         api.state( 'changesetStatus' ).set( response.changeset_status );
+                                               if ( 'publish' === response.changeset_status ) {
+                                                       api.state( 'changesetStatus' ).set( '' );
+                                                       api.settings.changeset.uuid = response.next_changeset_uuid;
+                                                       parent.send( 'changeset-uuid', api.settings.changeset.uuid );
+                                               }
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                                                if ( response.setting_validities ) {
</span><span class="cx" style="display: block; padding: 0 10px">                                                        api._handleSettingValidities( {
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -3850,6 +4275,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"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                                                deferred.resolveWith( previewer, [ response ] );
</ins><span class="cx" style="display: block; padding: 0 10px">                                                 api.trigger( 'saved', response );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                                                // Restore the global dirty state if any settings were modified during save.
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -3871,6 +4297,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">                                        api.state.bind( 'change', submitWhenDoneProcessing );
</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">+                                return deferred.promise();
</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">@@ -3957,55 +4384,71 @@
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                api.bind( 'ready', api.reflowPaneContents );
</span><span class="cx" style="display: block; padding: 0 10px">                $( [ api.panel, api.section, api.control ] ).each( function ( i, values ) {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        var debouncedReflowPaneContents = _.debounce( api.reflowPaneContents, 100 );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 var debouncedReflowPaneContents = _.debounce( api.reflowPaneContents, api.settings.timeouts.reflowPaneContents );
</ins><span class="cx" style="display: block; padding: 0 10px">                         values.bind( 'add', debouncedReflowPaneContents );
</span><span class="cx" style="display: block; padding: 0 10px">                        values.bind( 'change', debouncedReflowPaneContents );
</span><span class="cx" style="display: block; padding: 0 10px">                        values.bind( 'remove', debouncedReflowPaneContents );
</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">-                // Check if preview url is valid and load the preview frame.
-               if ( api.previewer.previewUrl() ) {
-                       api.previewer.refresh();
-               } else {
-                       api.previewer.previewUrl( api.settings.url.home );
-               }
-
</del><span class="cx" style="display: block; padding: 0 10px">                 // Save and activated states
</span><span class="cx" style="display: block; padding: 0 10px">                (function() {
</span><span class="cx" style="display: block; padding: 0 10px">                        var state = new api.Values(),
</span><span class="cx" style="display: block; padding: 0 10px">                                saved = state.create( 'saved' ),
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                                saving = state.create( 'saving' ),
</ins><span class="cx" style="display: block; padding: 0 10px">                                 activated = state.create( 'activated' ),
</span><span class="cx" style="display: block; padding: 0 10px">                                processing = state.create( 'processing' ),
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                paneVisible = state.create( 'paneVisible' );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                         paneVisible = state.create( 'paneVisible' ),
+                               changesetStatus = state.create( 'changesetStatus' ),
+                               previewerAlive = state.create( 'previewerAlive' ),
+                               populateChangesetUuidParam;
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                        state.bind( 'change', function() {
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                                var canSave;
+
</ins><span class="cx" style="display: block; padding: 0 10px">                                 if ( ! activated() ) {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                        saveBtn.val( api.l10n.activate ).prop( 'disabled', false );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                                 saveBtn.val( api.l10n.activate );
</ins><span class="cx" style="display: block; padding: 0 10px">                                         closeBtn.find( '.screen-reader-text' ).text( api.l10n.cancel );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                } else if ( saved() ) {
-                                       saveBtn.val( api.l10n.saved ).prop( 'disabled', true );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                         } else if ( '' === changesetStatus.get() && saved() ) {
+                                       saveBtn.val( api.l10n.saved );
</ins><span class="cx" style="display: block; padding: 0 10px">                                         closeBtn.find( '.screen-reader-text' ).text( api.l10n.close );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                                } else {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                        saveBtn.val( api.l10n.save ).prop( 'disabled', false );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                                 saveBtn.val( api.l10n.save );
</ins><span class="cx" style="display: block; padding: 0 10px">                                         closeBtn.find( '.screen-reader-text' ).text( api.l10n.cancel );
</span><span class="cx" style="display: block; padding: 0 10px">                                }
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
+                               /*
+                                * Save (publish) button should be enabled if saving is not currently happening,
+                                * and if the theme is not active or the changeset exists but is not published.
+                                */
+                               canSave = ! saving() && ( ! activated() || ! saved() || ( '' !== changesetStatus() && 'publish' !== changesetStatus() ) );
+
+                               saveBtn.prop( 'disabled', ! canSave );
</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">                        // Set default states.
</span><span class="cx" style="display: block; padding: 0 10px">                        saved( true );
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                        saving( false );
</ins><span class="cx" style="display: block; padding: 0 10px">                         activated( api.settings.theme.active );
</span><span class="cx" style="display: block; padding: 0 10px">                        processing( 0 );
</span><span class="cx" style="display: block; padding: 0 10px">                        paneVisible( true );
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                        previewerAlive( true );
+                       changesetStatus( api.settings.changeset.status );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                        api.bind( 'change', function() {
</span><span class="cx" style="display: block; padding: 0 10px">                                state('saved').set( false );
</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">-                        api.bind( 'saved', function() {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 saving.bind( function( isSaving ) {
+                               body.toggleClass( 'saving', isSaving );
+                       } );
+
+                       api.bind( 'saved', function( response ) {
</ins><span class="cx" style="display: block; padding: 0 10px">                                 state('saved').set( true );
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                state('activated').set( true );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                         if ( 'publish' === response.changeset_status ) {
+                                       state( 'activated' ).set( 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">                        activated.bind( function( to ) {
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -4014,10 +4457,48 @@
</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">+                        populateChangesetUuidParam = function( isIncluded ) {
+                               var urlParser, queryParams;
+                               urlParser = document.createElement( 'a' );
+                               urlParser.href = location.href;
+                               queryParams = api.utils.parseQueryString( urlParser.search.substr( 1 ) );
+                               if ( isIncluded ) {
+                                       if ( queryParams.changeset_uuid === api.settings.changeset.uuid ) {
+                                               return;
+                                       }
+                                       queryParams.changeset_uuid = api.settings.changeset.uuid;
+                               } else {
+                                       if ( ! queryParams.changeset_uuid ) {
+                                               return;
+                                       }
+                                       delete queryParams.changeset_uuid;
+                               }
+                               urlParser.search = $.param( queryParams );
+                               history.replaceState( {}, document.title, urlParser.href );
+                       };
+
+                       if ( history.replaceState ) {
+                               saved.bind( function( isSaved ) {
+                                       if ( ! isSaved ) {
+                                               populateChangesetUuidParam( true );
+                                       }
+                               } );
+                               changesetStatus.bind( function( newStatus ) {
+                                       populateChangesetUuidParam( '' !== newStatus );
+                               } );
+                       }
+
</ins><span class="cx" style="display: block; padding: 0 10px">                         // Expose states to the API.
</span><span class="cx" style="display: block; padding: 0 10px">                        api.state = state;
</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">+                // Check if preview url is valid and load the preview frame.
+               if ( api.previewer.previewUrl() ) {
+                       api.previewer.refresh();
+               } else {
+                       api.previewer.previewUrl( api.settings.url.home );
+               }
+
</ins><span class="cx" style="display: block; padding: 0 10px">                 // Button bindings.
</span><span class="cx" style="display: block; padding: 0 10px">                saveBtn.click( function( event ) {
</span><span class="cx" style="display: block; padding: 0 10px">                        api.previewer.save();
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -4169,7 +4650,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">                // Prompt user with AYS dialog if leaving the Customizer with unsaved changes
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                $( window ).on( 'beforeunload', function () {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         $( window ).on( 'beforeunload.customize-confirm', function () {
</ins><span class="cx" style="display: block; padding: 0 10px">                         if ( ! api.state( 'saved' )() ) {
</span><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="lines" style="display: block; padding: 0 10px; color: #888">@@ -4190,6 +4671,8 @@
</span><span class="cx" style="display: block; padding: 0 10px">                        parent.send( 'title', newTitle );
</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">+                parent.send( 'changeset-uuid', api.settings.changeset.uuid );
+
</ins><span class="cx" style="display: block; padding: 0 10px">                 // Initialize the connection with the parent frame.
</span><span class="cx" style="display: block; padding: 0 10px">                parent.send( 'ready' );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -4292,6 +4775,51 @@
</span><span class="cx" style="display: block; padding: 0 10px">                        api.previewer.refresh();
</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">+                // Autosave changeset.
+               ( function() {
+                       var timeoutId, updateChangesetWithReschedule, scheduleChangesetUpdate, updatePending = false;
+
+                       /**
+                        * Request changeset update and then re-schedule the next changeset update time.
+                        *
+                        * @private
+                        */
+                       updateChangesetWithReschedule = function() {
+                               if ( ! updatePending ) {
+                                       updatePending = true;
+                                       api.requestChangesetUpdate().always( function() {
+                                               updatePending = false;
+                                       } );
+                               }
+                               scheduleChangesetUpdate();
+                       };
+
+                       /**
+                        * Schedule changeset update.
+                        *
+                        * @private
+                        */
+                       scheduleChangesetUpdate = function() {
+                               clearTimeout( timeoutId );
+                               timeoutId = setTimeout( function() {
+                                       updateChangesetWithReschedule();
+                               }, api.settings.timeouts.changesetAutoSave );
+                       };
+
+                       // Start auto-save interval for updating changeset.
+                       scheduleChangesetUpdate();
+
+                       // Save changeset when focus removed from window.
+                       $( window ).on( 'blur.wp-customize-changeset-update', function() {
+                               updateChangesetWithReschedule();
+                       } );
+
+                       // Save changeset before unloading window.
+                       $( window ).on( 'beforeunload.wp-customize-changeset-update', function() {
+                               updateChangesetWithReschedule();
+                       } );
+               } ());
+
</ins><span class="cx" style="display: block; padding: 0 10px">                 api.trigger( 'ready' );
</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="trunksrcwpadminjscustomizewidgetsjs"></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-widgets.js</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-admin/js/customize-widgets.js        2016-10-17 23:53:20 UTC (rev 38809)
+++ trunk/src/wp-admin/js/customize-widgets.js  2016-10-18 20:04:36 UTC (rev 38810)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1154,7 +1154,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">                        params.action = 'update-widget';
</span><span class="cx" style="display: block; padding: 0 10px">                        params.wp_customize = 'on';
</span><span class="cx" style="display: block; padding: 0 10px">                        params.nonce = api.settings.nonce['update-widget'];
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        params.theme = api.settings.theme.stylesheet;
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 params.customize_theme = api.settings.theme.stylesheet;
</ins><span class="cx" style="display: block; padding: 0 10px">                         params.customized = wp.customize.previewer.query().customized;
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                        data = $.param( params );
</span></span></pre></div>
<a id="trunksrcwpincludesadminbarphp"></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/admin-bar.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/admin-bar.php       2016-10-17 23:53:20 UTC (rev 38809)
+++ trunk/src/wp-includes/admin-bar.php 2016-10-18 20:04:36 UTC (rev 38810)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -366,15 +366,30 @@
</span><span class="cx" style="display: block; padding: 0 10px">  * @since 4.3.0
</span><span class="cx" style="display: block; padding: 0 10px">  *
</span><span class="cx" style="display: block; padding: 0 10px">  * @param WP_Admin_Bar $wp_admin_bar WP_Admin_Bar instance.
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ * @global WP_Customize_Manager $wp_customize
</ins><span class="cx" style="display: block; padding: 0 10px">  */
</span><span class="cx" style="display: block; padding: 0 10px"> function wp_admin_bar_customize_menu( $wp_admin_bar ) {
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+        global $wp_customize;
+
</ins><span class="cx" style="display: block; padding: 0 10px">         // Don't show for users who can't access the customizer or when in the admin.
</span><span class="cx" style="display: block; padding: 0 10px">        if ( ! current_user_can( 'customize' ) || is_admin() ) {
</span><span class="cx" style="display: block; padding: 0 10px">                return;
</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">+        // Don't show if the user cannot edit a given customize_changeset post currently being previewed.
+       if ( is_customize_preview() && $wp_customize->changeset_post_id() && ! current_user_can( get_post_type_object( 'customize_changeset' )->cap->edit_post, $wp_customize->changeset_post_id() ) ) {
+               return;
+       }
+
</ins><span class="cx" style="display: block; padding: 0 10px">         $current_url = ( is_ssl() ? 'https://' : 'http://' ) . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'];
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+        if ( is_customize_preview() && $wp_customize->changeset_uuid() ) {
+               $current_url = remove_query_arg( 'customize_changeset_uuid', $current_url );
+       }
+
</ins><span class="cx" style="display: block; padding: 0 10px">         $customize_url = add_query_arg( 'url', urlencode( $current_url ), wp_customize_url() );
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+        if ( is_customize_preview() ) {
+               $customize_url = add_query_arg( array( 'changeset_uuid' => $wp_customize->changeset_uuid() ), $customize_url );
+       }
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        $wp_admin_bar->add_menu( array(
</span><span class="cx" style="display: block; padding: 0 10px">                'id'     => 'customize',
</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      2016-10-17 23:53:20 UTC (rev 38809)
+++ trunk/src/wp-includes/class-wp-customize-manager.php        2016-10-18 20:04:36 UTC (rev 38810)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -130,15 +130,6 @@
</span><span class="cx" style="display: block; padding: 0 10px">        protected $controls = array();
</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">-         * Return value of check_ajax_referer() in customize_preview_init() method.
-        *
-        * @since 3.5.0
-        * @access protected
-        * @var false|int
-        */
-       protected $nonce_tick;
-
-       /**
</del><span class="cx" style="display: block; padding: 0 10px">          * Panel types that may be rendered from JS templates.
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * @since 4.3.0
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -193,6 +184,15 @@
</span><span class="cx" style="display: block; padding: 0 10px">        protected $autofocus = array();
</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">+         * Messenger channel.
+        *
+        * @since 4.7.0
+        * @access protected
+        * @var string
+        */
+       protected $messenger_channel;
+
+       /**
</ins><span class="cx" style="display: block; padding: 0 10px">          * Unsanitized values for Customize Settings parsed from $_POST['customized'].
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * @var array
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -200,11 +200,75 @@
</span><span class="cx" style="display: block; padding: 0 10px">        private $_post_values;
</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">+         * Changeset UUID.
+        *
+        * @since 4.7.0
+        * @access private
+        * @var string
+        */
+       private $_changeset_uuid;
+
+       /**
+        * Changeset post ID.
+        *
+        * @since 4.7.0
+        * @access private
+        * @var int|false
+        */
+       private $_changeset_post_id;
+
+       /**
+        * Changeset data loaded from a customize_changeset post.
+        *
+        * @since 4.7.0
+        * @access private
+        * @var array
+        */
+       private $_changeset_data;
+
+       /**
</ins><span class="cx" style="display: block; padding: 0 10px">          * Constructor.
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * @since 3.4.0
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         * @since 4.7.0 Added $args param.
+        *
+        * @param array $args {
+        *     Args.
+        *
+        *     @type string $changeset_uuid    Changeset UUID, the post_name for the customize_changeset post containing the customized state. Defaults to new UUID.
+        *     @type string $theme             Theme to be previewed (for theme switch). Defaults to customize_theme or theme query params.
+        *     @type string $messenger_channel Messenger channel. Defaults to customize_messenger_channel query param.
+        * }
</ins><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 __construct() {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ public function __construct( $args = array() ) {
+
+               $args = array_merge(
+                       array_fill_keys( array( 'changeset_uuid', 'theme', 'messenger_channel' ), null ),
+                       $args
+               );
+
+               // Note that the UUID format will be validated in the setup_theme() method.
+               if ( ! isset( $args['changeset_uuid'] ) ) {
+                       $args['changeset_uuid'] = wp_generate_uuid4();
+               }
+
+               // The theme and messenger_channel should be supplied via $args, but they are also looked at in the $_REQUEST global here for back-compat.
+               if ( ! isset( $args['theme'] ) ) {
+                       if ( isset( $_REQUEST['customize_theme'] ) ) {
+                               $args['theme'] = wp_unslash( $_REQUEST['customize_theme'] );
+                       } elseif ( isset( $_REQUEST['theme'] ) ) { // Deprecated.
+                               $args['theme'] = wp_unslash( $_REQUEST['theme'] );
+                       }
+               }
+               if ( ! isset( $args['messenger_channel'] ) && isset( $_REQUEST['customize_messenger_channel'] ) ) {
+                       $args['messenger_channel'] = sanitize_key( wp_unslash( $_REQUEST['customize_messenger_channel'] ) );
+               }
+
+               $this->original_stylesheet = get_stylesheet();
+               $this->theme = wp_get_theme( $args['theme'] );
+               $this->messenger_channel = $args['messenger_channel'];
+               $this->_changeset_uuid = $args['changeset_uuid'];
+
</ins><span class="cx" style="display: block; padding: 0 10px">                 require_once( ABSPATH . WPINC . '/class-wp-customize-setting.php' );
</span><span class="cx" style="display: block; padding: 0 10px">                require_once( ABSPATH . WPINC . '/class-wp-customize-panel.php' );
</span><span class="cx" style="display: block; padding: 0 10px">                require_once( ABSPATH . WPINC . '/class-wp-customize-section.php' );
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -271,14 +335,9 @@
</span><span class="cx" style="display: block; padding: 0 10px">                        $this->nav_menus = new WP_Customize_Nav_Menus( $this );
</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">-                add_filter( 'wp_die_handler', array( $this, 'wp_die_handler' ) );
-
</del><span class="cx" style="display: block; padding: 0 10px">                 add_action( 'setup_theme', array( $this, 'setup_theme' ) );
</span><span class="cx" style="display: block; padding: 0 10px">                add_action( 'wp_loaded',   array( $this, 'wp_loaded' ) );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                // Run wp_redirect_status late to make sure we override the status last.
-               add_action( 'wp_redirect_status', array( $this, 'wp_redirect_status' ), 1000 );
-
</del><span class="cx" style="display: block; padding: 0 10px">                 // Do not spawn cron (especially the alternate cron) while running the Customizer.
</span><span class="cx" style="display: block; padding: 0 10px">                remove_action( 'init', 'wp_cron' );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -340,7 +399,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">         * @param mixed $message UI message
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><span class="cx" style="display: block; padding: 0 10px">        protected function wp_die( $ajax_message, $message = null ) {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                if ( $this->doing_ajax() || isset( $_POST['customized'] ) ) {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         if ( $this->doing_ajax() ) {
</ins><span class="cx" style="display: block; padding: 0 10px">                         wp_die( $ajax_message );
</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">@@ -348,6 +407,29 @@
</span><span class="cx" style="display: block; padding: 0 10px">                        $message = __( 'Cheatin&#8217; uh?' );
</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 ( $this->messenger_channel ) {
+                       ob_start();
+                       wp_enqueue_scripts();
+                       wp_print_scripts( array( 'customize-base' ) );
+
+                       $settings = array(
+                               'messengerArgs' => array(
+                                       'channel' => $this->messenger_channel,
+                                       'url' => wp_customize_url(),
+                               ),
+                               'error' => $ajax_message,
+                       );
+                       ?>
+                       <script>
+                       ( function( api, settings ) {
+                               var preview = new api.Messenger( settings.messengerArgs );
+                               preview.send( 'iframe-loading-error', settings.error );
+                       } )( wp.customize, <?php echo wp_json_encode( $settings ) ?> );
+                       </script>
+                       <?php
+                       $message .= ob_get_clean();
+               }
+
</ins><span class="cx" style="display: block; padding: 0 10px">                 wp_die( $message );
</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">@@ -355,10 +437,13 @@
</span><span class="cx" style="display: block; padding: 0 10px">         * Return the Ajax wp_die() handler if it's a customized request.
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * @since 3.4.0
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         * @deprecated 4.7.0
</ins><span class="cx" style="display: block; padding: 0 10px">          *
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-         * @return string
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+  * @return callable Die handler.
</ins><span class="cx" style="display: block; padding: 0 10px">          */
</span><span class="cx" style="display: block; padding: 0 10px">        public function wp_die_handler() {
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                _deprecated_function( __METHOD__, '4.7.0' );
+
</ins><span class="cx" style="display: block; padding: 0 10px">                 if ( $this->doing_ajax() || isset( $_POST['customized'] ) ) {
</span><span class="cx" style="display: block; padding: 0 10px">                        return '_ajax_wp_die_handler';
</span><span class="cx" style="display: block; padding: 0 10px">                }
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -374,24 +459,43 @@
</span><span class="cx" style="display: block; padding: 0 10px">         * @since 3.4.0
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><span class="cx" style="display: block; padding: 0 10px">        public function setup_theme() {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                send_origin_headers();
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         global $pagenow;
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                $doing_ajax_or_is_customized = ( $this->doing_ajax() || isset( $_POST['customized'] ) );
-               if ( is_admin() && ! $doing_ajax_or_is_customized ) {
-                       auth_redirect();
-               } elseif ( $doing_ajax_or_is_customized && ! is_user_logged_in() ) {
-                       $this->wp_die( 0, __( 'You must be logged in to complete this action.' ) );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         // Check permissions for customize.php access since this method is called before customize.php can run any code,
+               if ( 'customize.php' === $pagenow && ! current_user_can( 'customize' ) ) {
+                       if ( ! is_user_logged_in() ) {
+                               auth_redirect();
+                       } else {
+                               wp_die(
+                                       '<h1>' . __( 'Cheatin&#8217; uh?' ) . '</h1>' .
+                                       '<p>' . __( 'Sorry, you are not allowed to customize this site.' ) . '</p>',
+                                       403
+                               );
+                       }
+                       return;
</ins><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">-                show_admin_bar( false );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         if ( ! preg_match( '/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/', $this->_changeset_uuid ) ) {
+                       $this->wp_die( -1, __( 'Invalid changeset UUID' ) );
+               }
</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 ( ! current_user_can( 'customize' ) ) {
-                       $this->wp_die( -1, __( 'Sorry, you are not allowed to customize this site.' ) );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         /*
+                * If unauthenticated then require a valid changeset UUID to load the preview.
+                * In this way, the UUID serves as a secret key. If the messenger channel is present,
+                * then send unauthenticated code to prompt re-auth.
+                */
+               if ( ! current_user_can( 'customize' ) && ! $this->changeset_post_id() ) {
+                       $this->wp_die( $this->messenger_channel ? 0 : -1, __( 'Non-existent changeset UUID.' ) );
</ins><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">-                $this->original_stylesheet = get_stylesheet();
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         if ( ! headers_sent() ) {
+                       send_origin_headers();
+               }
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                $this->theme = wp_get_theme( isset( $_REQUEST['theme'] ) ? $_REQUEST['theme'] : null );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         // Hide the admin bar if we're embedded in the customizer iframe.
+               if ( $this->messenger_channel ) {
+                       show_admin_bar( false );
+               }
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                if ( $this->is_theme_active() ) {
</span><span class="cx" style="display: block; padding: 0 10px">                        // Once the theme is loaded, we'll validate it.
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -507,6 +611,18 @@
</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">+         * Get the changeset UUID.
+        *
+        * @since 4.7.0
+        * @access public
+        *
+        * @return string UUID.
+        */
+       public function changeset_uuid() {
+               return $this->_changeset_uuid;
+       }
+
+       /**
</ins><span class="cx" style="display: block; padding: 0 10px">          * Get the theme being customized.
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * @since 3.4.0
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -603,8 +719,23 @@
</span><span class="cx" style="display: block; padding: 0 10px">                 */
</span><span class="cx" style="display: block; padding: 0 10px">                do_action( 'customize_register', $this );
</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 ( $this->is_preview() && ! is_admin() )
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         /*
+                * Note that settings must be previewed here even outside the customizer preview
+                * and also in the customizer pane itself. This is to enable loading an existing
+                * changeset into the customizer. Previewing the settings only has to be prevented
+                * in the case of a customize_save action because then update_option()
+                * may short-circuit because it will detect that there are no changes to
+                * make.
+                */
+               if ( ! $this->doing_ajax( 'customize_save' ) ) {
+                       foreach ( $this->settings as $setting ) {
+                               $setting->preview();
+                       }
+               }
+
+               if ( $this->is_preview() && ! is_admin() ) {
</ins><span class="cx" style="display: block; padding: 0 10px">                         $this->customize_preview_init();
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                }
</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">@@ -614,44 +745,225 @@
</span><span class="cx" style="display: block; padding: 0 10px">         * Instead, the JS will sniff out the location header.
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * @since 3.4.0
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         * @deprecated 4.7.0
</ins><span class="cx" style="display: block; padding: 0 10px">          *
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-         * @param $status
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+  * @param int $status Status.
</ins><span class="cx" style="display: block; padding: 0 10px">          * @return int
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><span class="cx" style="display: block; padding: 0 10px">        public function wp_redirect_status( $status ) {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                if ( $this->is_preview() && ! is_admin() )
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         _deprecated_function( __FUNCTION__, '4.7.0' );
+
+               if ( $this->is_preview() && ! is_admin() ) {
</ins><span class="cx" style="display: block; padding: 0 10px">                         return 200;
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                }
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                return $status;
</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">-         * Parse the incoming $_POST['customized'] JSON data and store the unsanitized
-        * settings for subsequent post_value() lookups.
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+  * Find the changeset post ID for a given changeset UUID.
</ins><span class="cx" style="display: block; padding: 0 10px">          *
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         * @since 4.7.0
+        * @access public
+        *
+        * @param string $uuid Changeset UUID.
+        * @return int|null Returns post ID on success and null on failure.
+        */
+       public function find_changeset_post_id( $uuid ) {
+               $cache_group = 'customize_changeset_post';
+               $changeset_post_id = wp_cache_get( $uuid, $cache_group );
+               if ( $changeset_post_id && 'customize_changeset' === get_post_type( $changeset_post_id ) ) {
+                       return $changeset_post_id;
+               }
+
+               $changeset_post_query = new WP_Query( array(
+                       'post_type' => 'customize_changeset',
+                       'post_status' => get_post_stati(),
+                       'name' => $uuid,
+                       'number' => 1,
+                       'no_found_rows' => true,
+                       'cache_results' => true,
+                       'update_post_meta_cache' => false,
+                       'update_term_meta_cache' => false,
+               ) );
+               if ( ! empty( $changeset_post_query->posts ) ) {
+                       // Note: 'fields'=>'ids' is not being used in order to cache the post object as it will be needed.
+                       $changeset_post_id = $changeset_post_query->posts[0]->ID;
+                       wp_cache_set( $this->_changeset_uuid, $changeset_post_id, $cache_group );
+                       return $changeset_post_id;
+               }
+
+               return null;
+       }
+
+       /**
+        * Get the changeset post id for the loaded changeset.
+        *
+        * @since 4.7.0
+        * @access public
+        *
+        * @return int|null Post ID on success or null if there is no post yet saved.
+        */
+       public function changeset_post_id() {
+               if ( ! isset( $this->_changeset_post_id ) ) {
+                       $post_id = $this->find_changeset_post_id( $this->_changeset_uuid );
+                       if ( ! $post_id ) {
+                               $post_id = false;
+                       }
+                       $this->_changeset_post_id = $post_id;
+               }
+               if ( false === $this->_changeset_post_id ) {
+                       return null;
+               }
+               return $this->_changeset_post_id;
+       }
+
+       /**
+        * Get the data stored in a changeset post.
+        *
+        * @since 4.7.0
+        * @access protected
+        *
+        * @param int $post_id Changeset post ID.
+        * @return array|WP_Error Changeset data or WP_Error on error.
+        */
+       protected function get_changeset_post_data( $post_id ) {
+               if ( ! $post_id ) {
+                       return new WP_Error( 'empty_post_id' );
+               }
+               $changeset_post = get_post( $post_id );
+               if ( ! $changeset_post ) {
+                       return new WP_Error( 'missing_post' );
+               }
+               if ( 'customize_changeset' !== $changeset_post->post_type ) {
+                       return new WP_Error( 'wrong_post_type' );
+               }
+               $changeset_data = json_decode( $changeset_post->post_content, true );
+               if ( function_exists( 'json_last_error' ) && json_last_error() ) {
+                       return new WP_Error( 'json_parse_error', '', json_last_error() );
+               }
+               if ( ! is_array( $changeset_data ) ) {
+                       return new WP_Error( 'expected_array' );
+               }
+               return $changeset_data;
+       }
+
+       /**
+        * Get changeset data.
+        *
+        * @since 4.7.0
+        * @access public
+        *
+        * @return array Changeset data.
+        */
+       public function changeset_data() {
+               if ( isset( $this->_changeset_data ) ) {
+                       return $this->_changeset_data;
+               }
+               $changeset_post_id = $this->changeset_post_id();
+               if ( ! $changeset_post_id ) {
+                       $this->_changeset_data = array();
+               } else {
+                       $data = $this->get_changeset_post_data( $changeset_post_id );
+                       if ( ! is_wp_error( $data ) ) {
+                               $this->_changeset_data = $data;
+                       } else {
+                               $this->_changeset_data = array();
+                       }
+               }
+               return $this->_changeset_data;
+       }
+
+       /**
+        * Get dirty pre-sanitized setting values in the current customized state.
+        *
+        * The returned array consists of a merge of three sources:
+        * 1. If the theme is not currently active, then the base array is any stashed
+        *    theme mods that were modified previously but never published.
+        * 2. The values from the current changeset, if it exists.
+        * 3. If the user can customize, the values parsed from the incoming
+        *    `$_POST['customized']` JSON data.
+        * 4. Any programmatically-set post values via `WP_Customize_Manager::set_post_value()`.
+        *
+        * The name "unsanitized_post_values" is a carry-over from when the customized
+        * state was exclusively sourced from `$_POST['customized']`. Nevertheless,
+        * the value returned will come from the current changeset post and from the
+        * incoming post data.
+        *
</ins><span class="cx" style="display: block; padding: 0 10px">          * @since 4.1.1
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         * @since 4.7.0 Added $args param and merging with changeset values and stashed theme mods.
</ins><span class="cx" style="display: block; padding: 0 10px">          *
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         * @param array $args {
+        *     Args.
+        *
+        *     @type bool $exclude_changeset Whether the changeset values should also be excluded. Defaults to false.
+        *     @type bool $exclude_post_data Whether the post input values should also be excluded. Defaults to false when lacking the customize capability.
+        * }
</ins><span class="cx" style="display: block; padding: 0 10px">          * @return array
</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 unsanitized_post_values() {
-               if ( ! isset( $this->_post_values ) ) {
-                       if ( isset( $_POST['customized'] ) ) {
-                               $this->_post_values = json_decode( wp_unslash( $_POST['customized'] ), true );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ public function unsanitized_post_values( $args = array() ) {
+               $args = array_merge(
+                       array(
+                               'exclude_changeset' => false,
+                               'exclude_post_data' => ! current_user_can( 'customize' ),
+                       ),
+                       $args
+               );
+
+               $values = array();
+
+               // Let default values be from the stashed theme mods if doing a theme switch and if no changeset is present.
+               if ( ! $this->is_theme_active() ) {
+                       $stashed_theme_mods = get_option( 'customize_stashed_theme_mods' );
+                       $stylesheet = $this->get_stylesheet();
+                       if ( isset( $stashed_theme_mods[ $stylesheet ] ) ) {
+                               $values = array_merge( $values, wp_list_pluck( $stashed_theme_mods[ $stylesheet ], 'value' ) );
</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( $this->_post_values ) ) { // if not isset or if JSON error
-                               $this->_post_values = array();
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         }
+
+               if ( ! $args['exclude_changeset'] ) {
+                       foreach ( $this->changeset_data() as $setting_id => $setting_params ) {
+                               if ( ! array_key_exists( 'value', $setting_params ) ) {
+                                       continue;
+                               }
+                               if ( isset( $setting_params['type'] ) && 'theme_mod' === $setting_params['type'] ) {
+
+                                       // Ensure that theme mods values are only used if they were saved under the current theme.
+                                       $namespace_pattern = '/^(?P<stylesheet>.+?)::(?P<setting_id>.+)$/';
+                                       if ( preg_match( $namespace_pattern, $setting_id, $matches ) && $this->get_stylesheet() === $matches['stylesheet'] ) {
+                                               $values[ $matches['setting_id'] ] = $setting_params['value'];
+                                       }
+                               } else {
+                                       $values[ $setting_id ] = $setting_params['value'];
+                               }
</ins><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 ( empty( $this->_post_values ) ) {
-                       return array();
-               } else {
-                       return $this->_post_values;
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
+               if ( ! $args['exclude_post_data'] ) {
+                       if ( ! isset( $this->_post_values ) ) {
+                               if ( isset( $_POST['customized'] ) ) {
+                                       $post_values = json_decode( wp_unslash( $_POST['customized'] ), true );
+                               } else {
+                                       $post_values = array();
+                               }
+                               if ( is_array( $post_values ) ) {
+                                       $this->_post_values = $post_values;
+                               } else {
+                                       $this->_post_values = array();
+                               }
+                       }
+                       $values = array_merge( $values, $this->_post_values );
</ins><span class="cx" style="display: block; padding: 0 10px">                 }
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                return $values;
</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><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-         * Returns the sanitized value for a given setting from the request's POST data.
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+  * Returns the sanitized value for a given setting from the current customized state.
</ins><span class="cx" style="display: block; padding: 0 10px">          *
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         * The name "post_value" is a carry-over from when the customized state was exclusively
+        * sourced from `$_POST['customized']`. Nevertheless, the value returned will come
+        * from the current changeset post and from the incoming post data.
+        *
</ins><span class="cx" style="display: block; padding: 0 10px">          * @since 3.4.0
</span><span class="cx" style="display: block; padding: 0 10px">         * @since 4.1.1 Introduced the `$default` parameter.
</span><span class="cx" style="display: block; padding: 0 10px">         * @since 4.6.0 `$default` is now returned early when the setting post value is invalid.
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -684,8 +996,11 @@
</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">-         * Override a setting's (unsanitized) value as found in any incoming $_POST['customized'].
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+  * Override a setting's value in the current customized state.
</ins><span class="cx" style="display: block; padding: 0 10px">          *
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         * The name "post_value" is a carry-over from when the customized state was
+        * exclusively sourced from `$_POST['customized']`.
+        *
</ins><span class="cx" style="display: block; padding: 0 10px">          * @since 4.2.0
</span><span class="cx" style="display: block; padding: 0 10px">         * @access public
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -693,7 +1008,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">         * @param mixed  $value      Post value.
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><span class="cx" style="display: block; padding: 0 10px">        public function set_post_value( $setting_id, $value ) {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                $this->unsanitized_post_values();
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         $this->unsanitized_post_values(); // Populate _post_values from $_POST['customized'].
</ins><span class="cx" style="display: block; padding: 0 10px">                 $this->_post_values[ $setting_id ] = $value;
</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">@@ -733,22 +1048,40 @@
</span><span class="cx" style="display: block; padding: 0 10px">         * @since 3.4.0
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><span class="cx" style="display: block; padding: 0 10px">        public function customize_preview_init() {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                $this->nonce_tick = check_ajax_referer( 'preview-customize_' . $this->get_stylesheet(), 'nonce' );
</del><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                /*
+                * Now that Customizer previews are loaded into iframes via GET requests
+                * and natural URLs with transaction UUIDs added, we need to ensure that
+                * the responses are never cached by proxies. In practice, this will not
+                * be needed if the user is logged-in anyway. But if anonymous access is
+                * allowed then the auth cookies would not be sent and WordPress would
+                * not send no-cache headers by default.
+                */
+               if ( ! headers_sent() ) {
+                       nocache_headers();
+                       header( 'X-Robots: noindex, nofollow, noarchive' );
+               }
+               add_action( 'wp_head', 'wp_no_robots' );
+               add_filter( 'wp_headers', array( $this, 'filter_iframe_security_headers' ) );
+
+               /*
+                * If preview is being served inside the customizer preview iframe, and
+                * if the user doesn't have customize capability, then it is assumed
+                * that the user's session has expired and they need to re-authenticate.
+                */
+               if ( $this->messenger_channel && ! current_user_can( 'customize' ) ) {
+                       $this->wp_die( -1, __( 'Unauthorized. You may remove the customize_messenger_channel param to preview as frontend.' ) );
+                       return;
+               }
+
</ins><span class="cx" style="display: block; padding: 0 10px">                 $this->prepare_controls();
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                add_filter( 'wp_redirect', array( $this, 'add_state_query_params' ) );
+
</ins><span class="cx" style="display: block; padding: 0 10px">                 wp_enqueue_script( 'customize-preview' );
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                add_action( 'wp', array( $this, 'customize_preview_override_404_status' ) );
-               add_action( 'wp_head', array( $this, 'customize_preview_base' ) );
</del><span class="cx" style="display: block; padding: 0 10px">                 add_action( 'wp_head', array( $this, 'customize_preview_loading_style' ) );
</span><span class="cx" style="display: block; padding: 0 10px">                add_action( 'wp_footer', array( $this, 'customize_preview_settings' ), 20 );
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                add_action( 'shutdown', array( $this, 'customize_preview_signature' ), 1000 );
-               add_filter( 'wp_die_handler', array( $this, 'remove_preview_signature' ) );
</del><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                foreach ( $this->settings as $setting ) {
-                       $setting->preview();
-               }
-
</del><span class="cx" style="display: block; padding: 0 10px">                 /**
</span><span class="cx" style="display: block; padding: 0 10px">                 * Fires once the Customizer preview has initialized and JavaScript
</span><span class="cx" style="display: block; padding: 0 10px">                 * settings have been printed.
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -761,25 +1094,85 @@
</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">+         * Filter the X-Frame-Options and Content-Security-Policy headers to ensure frontend can load in customizer.
+        *
+        * @since 4.7.0
+        * @access public
+        *
+        * @param array $headers Headers.
+        * @return array Headers.
+        */
+       public function filter_iframe_security_headers( $headers ) {
+               $customize_url = admin_url( 'customize.php' );
+               $headers['X-Frame-Options'] = 'ALLOW-FROM ' . $customize_url;
+               $headers['Content-Security-Policy'] = 'frame-ancestors ' . preg_replace( '#^(\w+://[^/]+).+?$#', '$1', $customize_url );
+               return $headers;
+       }
+
+       /**
+        * Add customize state query params to a given URL if preview is allowed.
+        *
+        * @since 4.7.0
+        * @access public
+        * @see wp_redirect()
+        * @see WP_Customize_Manager::get_allowed_url()
+        *
+        * @param string $url URL.
+        * @return string URL.
+        */
+       public function add_state_query_params( $url ) {
+               $parsed_original_url = wp_parse_url( $url );
+               $is_allowed = false;
+               foreach ( $this->get_allowed_urls() as $allowed_url ) {
+                       $parsed_allowed_url = wp_parse_url( $allowed_url );
+                       $is_allowed = (
+                               $parsed_allowed_url['scheme'] === $parsed_original_url['scheme']
+                               &&
+                               $parsed_allowed_url['host'] === $parsed_original_url['host']
+                               &&
+                               0 === strpos( $parsed_original_url['path'], $parsed_allowed_url['path'] )
+                       );
+                       if ( $is_allowed ) {
+                               break;
+                       }
+               }
+
+               if ( $is_allowed ) {
+                       $query_params = array(
+                               'customize_changeset_uuid' => $this->changeset_uuid(),
+                       );
+                       if ( ! $this->is_theme_active() ) {
+                               $query_params['customize_theme'] = $this->get_stylesheet();
+                       }
+                       if ( $this->messenger_channel ) {
+                               $query_params['customize_messenger_channel'] = $this->messenger_channel;
+                       }
+                       $url = add_query_arg( $query_params, $url );
+               }
+
+               return $url;
+       }
+
+       /**
</ins><span class="cx" style="display: block; padding: 0 10px">          * Prevent sending a 404 status when returning the response for the customize
</span><span class="cx" style="display: block; padding: 0 10px">         * preview, since it causes the jQuery Ajax to fail. Send 200 instead.
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * @since 4.0.0
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         * @deprecated 4.7.0
</ins><span class="cx" style="display: block; padding: 0 10px">          * @access public
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><span class="cx" style="display: block; padding: 0 10px">        public function customize_preview_override_404_status() {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                if ( is_404() ) {
-                       status_header( 200 );
-               }
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         _deprecated_function( __METHOD__, '4.7.0' );
</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="cx" style="display: block; padding: 0 10px">         * Print base element for preview frame.
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * @since 3.4.0
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         * @deprecated 4.7.0
</ins><span class="cx" style="display: block; padding: 0 10px">          */
</span><span class="cx" style="display: block; padding: 0 10px">        public function customize_preview_base() {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                ?><base href="<?php echo home_url( '/' ); ?>" /><?php
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         _deprecated_function( __METHOD__, '4.7.0' );
</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">@@ -809,6 +1202,14 @@
</span><span class="cx" style="display: block; padding: 0 10px">                        body.wp-customizer-unloading * {
</span><span class="cx" style="display: block; padding: 0 10px">                                pointer-events: none !important;
</span><span class="cx" style="display: block; padding: 0 10px">                        }
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                        form.customize-unpreviewable,
+                       form.customize-unpreviewable input,
+                       form.customize-unpreviewable select,
+                       form.customize-unpreviewable button,
+                       a.customize-unpreviewable,
+                       area.customize-unpreviewable {
+                               cursor: not-allowed !important;
+                       }
</ins><span class="cx" style="display: block; padding: 0 10px">                 </style><?php
</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">@@ -818,27 +1219,62 @@
</span><span class="cx" style="display: block; padding: 0 10px">         * @since 3.4.0
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><span class="cx" style="display: block; padding: 0 10px">        public function customize_preview_settings() {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                $setting_validities = $this->validate_setting_values( $this->unsanitized_post_values() );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         $post_values = $this->unsanitized_post_values( array( 'exclude_changeset' => true ) );
+               $setting_validities = $this->validate_setting_values( $post_values );
</ins><span class="cx" style="display: block; padding: 0 10px">                 $exported_setting_validities = array_map( array( $this, 'prepare_setting_validity_for_js' ), $setting_validities );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                // Note that the REQUEST_URI is not passed into home_url() since this breaks subdirectory installs.
+               $self_url = empty( $_SERVER['REQUEST_URI'] ) ? home_url( '/' ) : esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) );
+               $state_query_params = array(
+                       'customize_theme',
+                       'customize_changeset_uuid',
+                       'customize_messenger_channel',
+               );
+               $self_url = remove_query_arg( $state_query_params, $self_url );
+
+               $allowed_urls = $this->get_allowed_urls();
+               $allowed_hosts = array();
+               foreach ( $allowed_urls as $allowed_url ) {
+                       $parsed = wp_parse_url( $allowed_url );
+                       if ( empty( $parsed['host'] ) ) {
+                               continue;
+                       }
+                       $host = $parsed['host'];
+                       if ( ! empty( $parsed['port'] ) ) {
+                               $host .= ':' . $parsed['port'];
+                       }
+                       $allowed_hosts[] = $host;
+               }
</ins><span class="cx" style="display: block; padding: 0 10px">                 $settings = array(
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                        'changeset' => array(
+                               'uuid' => $this->_changeset_uuid,
+                       ),
+                       'timeouts' => array(
+                               'selectiveRefresh' => 250,
+                               'keepAliveSend' => 1000,
+                       ),
</ins><span class="cx" style="display: block; padding: 0 10px">                         'theme' => array(
</span><span class="cx" style="display: block; padding: 0 10px">                                'stylesheet' => $this->get_stylesheet(),
</span><span class="cx" style="display: block; padding: 0 10px">                                'active'     => $this->is_theme_active(),
</span><span class="cx" style="display: block; padding: 0 10px">                        ),
</span><span class="cx" style="display: block; padding: 0 10px">                        'url' => array(
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                'self' => empty( $_SERVER['REQUEST_URI'] ) ? home_url( '/' ) : esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) ),
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                         'self' => $self_url,
+                               'allowed' => array_map( 'esc_url_raw', $this->get_allowed_urls() ),
+                               'allowedHosts' => array_unique( $allowed_hosts ),
+                               'isCrossDomain' => $this->is_cross_domain(),
</ins><span class="cx" style="display: block; padding: 0 10px">                         ),
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        'channel' => wp_unslash( $_POST['customize_messenger_channel'] ),
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 'channel' => $this->messenger_channel,
</ins><span class="cx" style="display: block; padding: 0 10px">                         'activePanels' => array(),
</span><span class="cx" style="display: block; padding: 0 10px">                        'activeSections' => array(),
</span><span class="cx" style="display: block; padding: 0 10px">                        'activeControls' => array(),
</span><span class="cx" style="display: block; padding: 0 10px">                        'settingValidities' => $exported_setting_validities,
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        'nonce' => $this->get_nonces(),
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 'nonce' => current_user_can( 'customize' ) ? $this->get_nonces() : array(),
</ins><span class="cx" style="display: block; padding: 0 10px">                         'l10n' => array(
</span><span class="cx" style="display: block; padding: 0 10px">                                'shiftClickToEdit' => __( 'Shift-click to edit this element.' ),
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                                'linkUnpreviewable' => __( 'This link is not live-previewable.' ),
+                               'formUnpreviewable' => __( 'This form is not live-previewable.' ),
</ins><span class="cx" style="display: block; padding: 0 10px">                         ),
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        '_dirty' => array_keys( $this->unsanitized_post_values() ),
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 '_dirty' => array_keys( $post_values ),
</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">                foreach ( $this->panels as $panel_id => $panel ) {
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -892,21 +1328,23 @@
</span><span class="cx" style="display: block; padding: 0 10px">         * Prints a signature so we can ensure the Customizer was properly executed.
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * @since 3.4.0
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         * @deprecated 4.7.0
</ins><span class="cx" style="display: block; padding: 0 10px">          */
</span><span class="cx" style="display: block; padding: 0 10px">        public function customize_preview_signature() {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                echo 'WP_CUSTOMIZER_SIGNATURE';
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         _deprecated_function( __METHOD__, '4.7.0' );
</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="cx" style="display: block; padding: 0 10px">         * Removes the signature in case we experience a case where the Customizer was not properly executed.
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * @since 3.4.0
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         * @deprecated 4.7.0
</ins><span class="cx" style="display: block; padding: 0 10px">          *
</span><span class="cx" style="display: block; padding: 0 10px">         * @param mixed $return Value passed through for {@see 'wp_die_handler'} filter.
</span><span class="cx" style="display: block; padding: 0 10px">         * @return mixed Value passed through for {@see 'wp_die_handler'} filter.
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><span class="cx" style="display: block; padding: 0 10px">        public function remove_preview_signature( $return = null ) {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                remove_action( 'shutdown', array( $this, 'customize_preview_signature' ), 1000 );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         _deprecated_function( __METHOD__, '4.7.0' );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                return $return;
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -993,16 +1431,37 @@
</span><span class="cx" style="display: block; padding: 0 10px">         * @see WP_Customize_Setting::validate()
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * @param array $setting_values Mapping of setting IDs to values to validate and sanitize.
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         * @param array $options {
+        *     Options.
+        *
+        *     @type bool $validate_existence  Whether a setting's existence will be checked.
+        *     @type bool $validate_capability Whether the setting capability will be checked.
+        * }
</ins><span class="cx" style="display: block; padding: 0 10px">          * @return array Mapping of setting IDs to return value of validate method calls, either `true` or `WP_Error`.
</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 validate_setting_values( $setting_values ) {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ public function validate_setting_values( $setting_values, $options = array() ) {
+               $options = wp_parse_args( $options, array(
+                       'validate_capability' => false,
+                       'validate_existence' => false,
+               ) );
+
</ins><span class="cx" style="display: block; padding: 0 10px">                 $validities = array();
</span><span class="cx" style="display: block; padding: 0 10px">                foreach ( $setting_values as $setting_id => $unsanitized_value ) {
</span><span class="cx" style="display: block; padding: 0 10px">                        $setting = $this->get_setting( $setting_id );
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        if ( ! $setting || is_null( $unsanitized_value ) ) {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 if ( ! $setting ) {
+                               if ( $options['validate_existence'] ) {
+                                       $validities[ $setting_id ] = new WP_Error( 'unrecognized', __( 'Setting does not exist or is unrecognized.' ) );
+                               }
</ins><span class="cx" style="display: block; padding: 0 10px">                                 continue;
</span><span class="cx" style="display: block; padding: 0 10px">                        }
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        $validity = $setting->validate( $unsanitized_value );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 if ( is_null( $unsanitized_value ) ) {
+                               continue;
+                       }
+                       if ( $options['validate_capability'] && ! current_user_can( $setting->capability ) ) {
+                               $validity = new WP_Error( 'unauthorized', __( 'Unauthorized to modify setting due to capability.' ) );
+                       } else {
+                               $validity = $setting->validate( $unsanitized_value );
+                       }
</ins><span class="cx" style="display: block; padding: 0 10px">                         if ( ! is_wp_error( $validity ) ) {
</span><span class="cx" style="display: block; padding: 0 10px">                                /** This filter is documented in wp-includes/class-wp-customize-setting.php */
</span><span class="cx" style="display: block; padding: 0 10px">                                $late_validity = apply_filters( "customize_validate_{$setting->id}", new WP_Error(), $unsanitized_value, $setting );
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1056,11 +1515,16 @@
</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">-         * Switch the theme and trigger the save() method on each setting.
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+  * Handle customize_save WP Ajax request to save/update a changeset.
</ins><span class="cx" style="display: block; padding: 0 10px">          *
</span><span class="cx" style="display: block; padding: 0 10px">         * @since 3.4.0
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         * @since 4.7.0 The semantics of this method have changed to update a changeset, optionally to also change the status and other attributes.
</ins><span class="cx" style="display: block; padding: 0 10px">          */
</span><span class="cx" style="display: block; padding: 0 10px">        public function save() {
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                if ( ! is_user_logged_in() ) {
+                       wp_send_json_error( 'unauthenticated' );
+               }
+
</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' );
</span><span class="cx" style="display: block; padding: 0 10px">                }
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1070,7 +1534,192 @@
</span><span class="cx" style="display: block; padding: 0 10px">                        wp_send_json_error( 'invalid_nonce' );
</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">+                $changeset_post_id = $this->changeset_post_id();
+               if ( $changeset_post_id && in_array( get_post_status( $changeset_post_id ), array( 'publish', 'trash' ) ) ) {
+                       wp_send_json_error( 'changeset_already_published' );
+               }
+
+               if ( empty( $changeset_post_id ) ) {
+                       if ( ! current_user_can( get_post_type_object( 'customize_changeset' )->cap->create_posts ) ) {
+                               wp_send_json_error( 'cannot_create_changeset_post' );
+                       }
+               } else {
+                       if ( ! current_user_can( get_post_type_object( 'customize_changeset' )->cap->edit_post, $changeset_post_id ) ) {
+                               wp_send_json_error( 'cannot_edit_changeset_post' );
+                       }
+               }
+
+               if ( ! empty( $_POST['customize_changeset_data'] ) ) {
+                       $input_changeset_data = json_decode( wp_unslash( $_POST['customize_changeset_data'] ), true );
+                       if ( ! is_array( $input_changeset_data ) ) {
+                               wp_send_json_error( 'invalid_customize_changeset_data' );
+                       }
+               } else {
+                       $input_changeset_data = array();
+               }
+
+               // Validate title.
+               $changeset_title = null;
+               if ( isset( $_POST['customize_changeset_title'] ) ) {
+                       $changeset_title = sanitize_text_field( wp_unslash( $_POST['customize_changeset_title'] ) );
+               }
+
+               // Validate changeset status param.
+               $is_publish = null;
+               $changeset_status = null;
+               if ( isset( $_POST['customize_changeset_status'] ) ) {
+                       $changeset_status = wp_unslash( $_POST['customize_changeset_status'] );
+                       if ( ! get_post_status_object( $changeset_status ) || ! in_array( $changeset_status, array( 'draft', 'pending', 'publish', 'future' ), true ) ) {
+                               wp_send_json_error( 'bad_customize_changeset_status', 400 );
+                       }
+                       $is_publish = ( 'publish' === $changeset_status || 'future' === $changeset_status );
+                       if ( $is_publish ) {
+                               if ( ! current_user_can( get_post_type_object( 'customize_changeset' )->cap->publish_posts ) ) {
+                                       wp_send_json_error( 'changeset_publish_unauthorized', 403 );
+                               }
+                               if ( false === has_action( 'transition_post_status', '_wp_customize_publish_changeset' ) ) {
+                                       wp_send_json_error( 'missing_publish_callback', 500 );
+                               }
+                       }
+               }
+
+               /*
+                * Validate changeset date param. Date is assumed to be in local time for
+                * the WP if in MySQL format (YYYY-MM-DD HH:MM:SS). Otherwise, the date
+                * is parsed with strtotime() so that ISO date format may be supplied
+                * or a string like "+10 minutes".
+                */
+               $changeset_date_gmt = null;
+               if ( isset( $_POST['customize_changeset_date'] ) ) {
+                       $changeset_date = wp_unslash( $_POST['customize_changeset_date'] );
+                       if ( preg_match( '/^\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d$/', $changeset_date ) ) {
+                               $mm = substr( $changeset_date, 5, 2 );
+                               $jj = substr( $changeset_date, 8, 2 );
+                               $aa = substr( $changeset_date, 0, 4 );
+                               $valid_date = wp_checkdate( $mm, $jj, $aa, $changeset_date );
+                               if ( ! $valid_date ) {
+                                       wp_send_json_error( 'bad_customize_changeset_date', 400 );
+                               }
+                               $changeset_date_gmt = get_gmt_from_date( $changeset_date );
+                       } else {
+                               $timestamp = strtotime( $changeset_date );
+                               if ( ! $timestamp ) {
+                                       wp_send_json_error( 'bad_customize_changeset_date', 400 );
+                               }
+                               $changeset_date_gmt = gmdate( 'Y-m-d H:i:s', $timestamp );
+                       }
+                       $now = gmdate( 'Y-m-d H:i:59' );
+
+                       $is_future_dated = ( mysql2date( 'U', $changeset_date_gmt, false ) > mysql2date( 'U', $now, false ) );
+                       if ( ! $is_future_dated ) {
+                               wp_send_json_error( 'not_future_date', 400 ); // Only future dates are allowed.
+                       }
+
+                       if ( ! $this->is_theme_active() && ( 'future' === $changeset_status || $is_future_dated ) ) {
+                               wp_send_json_error( 'cannot_schedule_theme_switches', 400 ); // This should be allowed in the future, when theme is a regular setting.
+                       }
+                       $will_remain_auto_draft = ( ! $changeset_status && ( ! $changeset_post_id || 'auto-draft' === get_post_status( $changeset_post_id ) ) );
+                       if ( $changeset_date && $will_remain_auto_draft ) {
+                               wp_send_json_error( 'cannot_supply_date_for_auto_draft_changeset', 400 );
+                       }
+               }
+
+               $r = $this->save_changeset_post( array(
+                       'status' => $changeset_status,
+                       'title' => $changeset_title,
+                       'date_gmt' => $changeset_date_gmt,
+                       'data' => $input_changeset_data,
+               ) );
+               if ( is_wp_error( $r ) ) {
+                       $response = $r->get_error_data();
+               } else {
+                       $response = $r;
+
+                       // Note that if the changeset status was publish, then it will get set to trash if revisions are not supported.
+                       $response['changeset_status'] = get_post_status( $this->changeset_post_id() );
+                       if ( $is_publish && 'trash' === $response['changeset_status'] ) {
+                               $response['changeset_status'] = 'publish';
+                       }
+
+                       if ( 'publish' === $response['changeset_status'] ) {
+                               $response['next_changeset_uuid'] = wp_generate_uuid4();
+                       }
+               }
+
+               if ( isset( $response['setting_validities'] ) ) {
+                       $response['setting_validities'] = array_map( array( $this, 'prepare_setting_validity_for_js' ), $response['setting_validities'] );
+               }
+
</ins><span class="cx" style="display: block; padding: 0 10px">                 /**
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 * Filters response data for a successful customize_save Ajax request.
+                *
+                * This filter does not apply if there was a nonce or authentication failure.
+                *
+                * @since 4.2.0
+                *
+                * @param array                $response Additional information passed back to the 'saved'
+                *                                       event on `wp.customize`.
+                * @param WP_Customize_Manager $this     WP_Customize_Manager instance.
+                */
+               $response = apply_filters( 'customize_save_response', $response, $this );
+
+               if ( is_wp_error( $r ) ) {
+                       wp_send_json_error( $response );
+               } else {
+                       wp_send_json_success( $response );
+               }
+       }
+
+       /**
+        * Save the post for the loaded changeset.
+        *
+        * @since 4.7.0
+        * @access public
+        *
+        * @param array $args {
+        *     Args for changeset post.
+        *
+        *     @type array  $data     Optional additional changeset data. Values will be merged on top of any existing post values.
+        *     @type string $status   Post status. Optional. If supplied, the save will be transactional and a post revision will be allowed.
+        *     @type string $title    Post title. Optional.
+        *     @type string $date_gmt Date in GMT. Optional.
+        * }
+        *
+        * @return array|WP_Error Returns array on success and WP_Error with array data on error.
+        */
+       function save_changeset_post( $args = array() ) {
+
+               $args = array_merge(
+                       array(
+                               'status' => null,
+                               'title' => null,
+                               'data' => array(),
+                               'date_gmt' => null,
+                       ),
+                       $args
+               );
+
+               $changeset_post_id = $this->changeset_post_id();
+
+               // The request was made via wp.customize.previewer.save().
+               $update_transactionally = (bool) $args['status'];
+               $allow_revision = (bool) $args['status'];
+
+               // Amend post values with any supplied data.
+               foreach ( $args['data'] as $setting_id => $setting_params ) {
+                       if ( array_key_exists( 'value', $setting_params ) ) {
+                               $this->set_post_value( $setting_id, $setting_params['value'] ); // Add to post values so that they can be validated and sanitized.
+                       }
+               }
+
+               // Note that in addition to post data, this will include any stashed theme mods.
+               $post_values = $this->unsanitized_post_values( array(
+                       'exclude_changeset' => true,
+                       'exclude_post_data' => false,
+               ) );
+               $this->add_dynamic_settings( array_keys( $post_values ) ); // Ensure settings get created even if they lack an input value.
+
+               /**
</ins><span class="cx" style="display: block; padding: 0 10px">                  * Fires before save validation happens.
</span><span class="cx" style="display: block; padding: 0 10px">                 *
</span><span class="cx" style="display: block; padding: 0 10px">                 * Plugins can add just-in-time {@see 'customize_validate_{$this->ID}'} filters
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1084,73 +1733,384 @@
</span><span class="cx" style="display: block; padding: 0 10px">                do_action( 'customize_save_validation_before', $this );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                // Validate settings.
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                $setting_validities = $this->validate_setting_values( $this->unsanitized_post_values() );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         $setting_validities = $this->validate_setting_values( $post_values, array(
+                       'validate_capability' => true,
+                       'validate_existence' => true,
+               ) );
</ins><span class="cx" style="display: block; padding: 0 10px">                 $invalid_setting_count = count( array_filter( $setting_validities, 'is_wp_error' ) );
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                $exported_setting_validities = array_map( array( $this, 'prepare_setting_validity_for_js' ), $setting_validities );
-               if ( $invalid_setting_count > 0 ) {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
+               /*
+                * Short-circuit if there are invalid settings the update is transactional.
+                * A changeset update is transactional when a status is supplied in the request.
+                */
+               if ( $update_transactionally && $invalid_setting_count > 0 ) {
</ins><span class="cx" style="display: block; padding: 0 10px">                         $response = array(
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                'setting_validities' => $exported_setting_validities,
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                         'setting_validities' => $setting_validities,
</ins><span class="cx" style="display: block; padding: 0 10px">                                 'message' => sprintf( _n( 'There is %s invalid setting.', 'There are %s invalid settings.', $invalid_setting_count ), number_format_i18n( $invalid_setting_count ) ),
</span><span class="cx" style="display: block; padding: 0 10px">                        );
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                        return new WP_Error( 'transaction_fail', '', $response );
+               }
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        /** This filter is documented in wp-includes/class-wp-customize-manager.php */
-                       $response = apply_filters( 'customize_save_response', $response, $this );
-                       wp_send_json_error( $response );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         $response = array(
+                       'setting_validities' => $setting_validities,
+               );
+
+               // Obtain/merge data for changeset.
+               $original_changeset_data = $this->get_changeset_post_data( $changeset_post_id );
+               $data = $original_changeset_data;
+               if ( is_wp_error( $data ) ) {
+                       $data = array();
</ins><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">-                // Do we have to switch themes?
-               if ( ! $this->is_theme_active() ) {
-                       // Temporarily stop previewing the theme to allow switch_themes()
-                       // to operate properly.
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         // Ensure that all post values are included in the changeset data.
+               foreach ( $post_values as $setting_id => $post_value ) {
+                       if ( ! isset( $args['data'][ $setting_id ] ) ) {
+                               $args['data'][ $setting_id ] = array();
+                       }
+                       if ( ! isset( $args['data'][ $setting_id ]['value'] ) ) {
+                               $args['data'][ $setting_id ]['value'] = $post_value;
+                       }
+               }
+
+               foreach ( $args['data'] as $setting_id => $setting_params ) {
+                       $setting = $this->get_setting( $setting_id );
+                       if ( ! $setting || ! $setting->check_capabilities() ) {
+                               continue;
+                       }
+
+                       // Skip updating changeset for invalid setting values.
+                       if ( isset( $setting_validities[ $setting_id ] ) && is_wp_error( $setting_validities[ $setting_id ] ) ) {
+                               continue;
+                       }
+
+                       $changeset_setting_id = $setting_id;
+                       if ( 'theme_mod' === $setting->type ) {
+                               $changeset_setting_id = sprintf( '%s::%s', $this->get_stylesheet(), $setting_id );
+                       }
+
+                       if ( null === $setting_params ) {
+                               // Remove setting from changeset entirely.
+                               unset( $data[ $changeset_setting_id ] );
+                       } else {
+                               // Merge any additional setting params that have been supplied with the existing params.
+                               if ( ! isset( $data[ $changeset_setting_id ] ) ) {
+                                       $data[ $changeset_setting_id ] = array();
+                               }
+                               $data[ $changeset_setting_id ] = array_merge(
+                                       $data[ $changeset_setting_id ],
+                                       $setting_params,
+                                       array( 'type' => $setting->type )
+                               );
+                       }
+               }
+
+               $filter_context = array(
+                       'uuid' => $this->changeset_uuid(),
+                       'title' => $args['title'],
+                       'status' => $args['status'],
+                       'date_gmt' => $args['date_gmt'],
+                       'post_id' => $changeset_post_id,
+                       'previous_data' => is_wp_error( $original_changeset_data ) ? array() : $original_changeset_data,
+                       'manager' => $this,
+               );
+
+               /**
+                * Filters the settings' data that will be persisted into the changeset.
+                *
+                * Plugins may amend additional data (such as additional meta for settings) into the changeset with this filter.
+                *
+                * @since 4.7.0
+                *
+                * @param array $data Updated changeset data, mapping setting IDs to arrays containing a $value item and optionally other metadata.
+                * @param array $context {
+                *     Filter context.
+                *
+                *     @type string               $uuid          Changeset UUID.
+                *     @type string               $title         Requested title for the changeset post.
+                *     @type string               $status        Requested status for the changeset post.
+                *     @type string               $date_gmt      Requested date for the changeset post in MySQL format and GMT timezone.
+                *     @type int|false            $post_id       Post ID for the changeset, or false if it doesn't exist yet.
+                *     @type array                $previous_data Previous data contained in the changeset.
+                *     @type WP_Customize_Manager $manager       Manager instance.
+                * }
+                */
+               $data = apply_filters( 'customize_changeset_save_data', $data, $filter_context );
+
+               // Switch theme if publishing changes now.
+               if ( 'publish' === $args['status'] && ! $this->is_theme_active() ) {
+                       // Temporarily stop previewing the theme to allow switch_themes() to operate properly.
</ins><span class="cx" style="display: block; padding: 0 10px">                         $this->stop_previewing_theme();
</span><span class="cx" style="display: block; padding: 0 10px">                        switch_theme( $this->get_stylesheet() );
</span><span class="cx" style="display: block; padding: 0 10px">                        update_option( 'theme_switched_via_customizer', true );
</span><span class="cx" style="display: block; padding: 0 10px">                        $this->start_previewing_theme();
</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">+                // Gather the data for wp_insert_post()/wp_update_post().
+               $json_options = 0;
+               if ( defined( 'JSON_UNESCAPED_SLASHES' ) ) {
+                       $json_options |= JSON_UNESCAPED_SLASHES; // Introduced in PHP 5.4. This is only to improve readability as slashes needn't be escaped in storage.
+               }
+               $json_options |= JSON_PRETTY_PRINT; // Also introduced in PHP 5.4, but WP defines constant for back compat. See WP Trac #30139.
+               $post_array = array(
+                       'post_content' => wp_json_encode( $data, $json_options ),
+               );
+               if ( $args['title'] ) {
+                       $post_array['post_title'] = $args['title'];
+               }
+               if ( $changeset_post_id ) {
+                       $post_array['ID'] = $changeset_post_id;
+               } else {
+                       $post_array['post_type'] = 'customize_changeset';
+                       $post_array['post_name'] = $this->changeset_uuid();
+                       $post_array['post_status'] = 'auto-draft';
+               }
+               if ( $args['status'] ) {
+                       $post_array['post_status'] = $args['status'];
+               }
+               if ( $args['date_gmt'] ) {
+                       $post_array['post_date_gmt'] = $args['date_gmt'];
+                       $post_array['post_date'] = get_date_from_gmt( $args['date_gmt'] );
+               }
+
+               $this->store_changeset_revision = $allow_revision;
+               add_filter( 'wp_save_post_revision_post_has_changed', array( $this, '_filter_revision_post_has_changed' ), 5, 3 );
+
+               // Update the changeset post. The publish_customize_changeset action will cause the settings in the changeset to be saved via WP_Customize_Setting::save().
+               $has_kses = ( false !== has_filter( 'content_save_pre', 'wp_filter_post_kses' ) );
+               if ( $has_kses ) {
+                       kses_remove_filters(); // Prevent KSES from corrupting JSON in post_content.
+               }
+
+               // Note that updating a post with publish status will trigger WP_Customize_Manager::publish_changeset_values().
+               if ( $changeset_post_id ) {
+                       $post_array['edit_date'] = true; // Prevent date clearing.
+                       $r = wp_update_post( wp_slash( $post_array ), true );
+               } else {
+                       $r = wp_insert_post( wp_slash( $post_array ), true );
+                       if ( ! is_wp_error( $r ) ) {
+                               $this->_changeset_post_id = $r; // Update cached post ID for the loaded changeset.
+                       }
+               }
+               if ( $has_kses ) {
+                       kses_init_filters();
+               }
+               $this->_changeset_data = null; // Reset so WP_Customize_Manager::changeset_data() will re-populate with updated contents.
+
+               remove_filter( 'wp_save_post_revision_post_has_changed', array( $this, '_filter_revision_post_has_changed' ) );
+
+               if ( is_wp_error( $r ) ) {
+                       $response['changeset_post_save_failure'] = $r->get_error_code();
+                       return new WP_Error( 'changeset_post_save_failure', '', $response );
+               }
+
+               return $response;
+       }
+
+       /**
+        * Whether a changeset revision should be made.
+        *
+        * @since 4.7.0
+        * @access private
+        * @var bool
+        */
+       protected $store_changeset_revision;
+
+       /**
+        * Filters whether a changeset has changed to create a new revision.
+        *
+        * Note that this will not be called while a changeset post remains in auto-draft status.
+        *
+        * @since 4.7.0
+        * @access private
+        *
+        * @param bool    $post_has_changed Whether the post has changed.
+        * @param WP_Post $last_revision    The last revision post object.
+        * @param WP_Post $post             The post object.
+        *
+        * @return bool Whether a revision should be made.
+        */
+       public function _filter_revision_post_has_changed( $post_has_changed, $last_revision, $post ) {
+               unset( $last_revision );
+               if ( 'customize_changeset' === $post->post_type ) {
+                       $post_has_changed = $this->store_changeset_revision;
+               }
+               return $post_has_changed;
+       }
+
+       /**
+        * Publish changeset values.
+        *
+        * This will the values contained in a changeset, even changesets that do not
+        * correspond to current manager instance. This is called by
+        * `_wp_customize_publish_changeset()` when a customize_changeset post is
+        * transitioned to the `publish` status. As such, this method should not be
+        * called directly and instead `wp_publish_post()` should be used.
+        *
+        * Please note that if the settings in the changeset are for a non-activated
+        * theme, the theme must first be switched to (via `switch_theme()`) before
+        * invoking this method.
+        *
+        * @since 4.7.0
+        * @access private
+        * @see _wp_customize_publish_changeset()
+        *
+        * @param int $changeset_post_id ID for customize_changeset post. Defaults to the changeset for the current manager instance.
+        * @return true|WP_Error True or error info.
+        */
+       public function _publish_changeset_values( $changeset_post_id ) {
+               $publishing_changeset_data = $this->get_changeset_post_data( $changeset_post_id );
+               if ( is_wp_error( $publishing_changeset_data ) ) {
+                       return $publishing_changeset_data;
+               }
+
+               $changeset_post = get_post( $changeset_post_id );
+
+               /*
+                * Temporarily override the changeset context so that it will be read
+                * in calls to unsanitized_post_values() and so that it will be available
+                * on the $wp_customize object passed to hooks during the save logic.
+                */
+               $previous_changeset_post_id = $this->_changeset_post_id;
+               $this->_changeset_post_id   = $changeset_post_id;
+               $previous_changeset_uuid    = $this->_changeset_uuid;
+               $this->_changeset_uuid      = $changeset_post->post_name;
+               $previous_changeset_data    = $this->_changeset_data;
+               $this->_changeset_data      = $publishing_changeset_data;
+
+               // Ensure that other theme mods are stashed.
+               $other_theme_mod_settings = array();
+               if ( did_action( 'switch_theme' ) ) {
+                       $namespace_pattern = '/^(?P<stylesheet>.+?)::(?P<setting_id>.+)$/';
+                       $matches = array();
+                       foreach ( $this->_changeset_data as $raw_setting_id => $setting_params ) {
+                               $is_other_theme_mod = (
+                                       isset( $setting_params['value'] )
+                                       &&
+                                       isset( $setting_params['type'] )
+                                       &&
+                                       'theme_mod' === $setting_params['type']
+                                       &&
+                                       preg_match( $namespace_pattern, $raw_setting_id, $matches )
+                                       &&
+                                       $this->get_stylesheet() !== $matches['stylesheet']
+                               );
+                               if ( $is_other_theme_mod ) {
+                                       if ( ! isset( $other_theme_mod_settings[ $matches['stylesheet'] ] ) ) {
+                                               $other_theme_mod_settings[ $matches['stylesheet'] ] = array();
+                                       }
+                                       $other_theme_mod_settings[ $matches['stylesheet'] ][ $matches['setting_id'] ] = $setting_params;
+                               }
+                       }
+               }
+
+               $changeset_setting_values = $this->unsanitized_post_values( array(
+                       'exclude_post_data' => true,
+                       'exclude_changeset' => false,
+               ) );
+               $changeset_setting_ids = array_keys( $changeset_setting_values );
+               $this->add_dynamic_settings( $changeset_setting_ids );
+
</ins><span class="cx" style="display: block; padding: 0 10px">                 /**
</span><span class="cx" style="display: block; padding: 0 10px">                 * Fires once the theme has switched in the Customizer, but before settings
</span><span class="cx" style="display: block; padding: 0 10px">                 * have been saved.
</span><span class="cx" style="display: block; padding: 0 10px">                 *
</span><span class="cx" style="display: block; padding: 0 10px">                 * @since 3.4.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">-                 * @param WP_Customize_Manager $this WP_Customize_Manager instance.
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+          * @param WP_Customize_Manager $manager WP_Customize_Manager instance.
</ins><span class="cx" style="display: block; padding: 0 10px">                  */
</span><span class="cx" style="display: block; padding: 0 10px">                do_action( 'customize_save', $this );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                foreach ( $this->settings as $setting ) {
-                       $setting->save();
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         /*
+                * Ensure that all settings will allow themselves to be saved. Note that
+                * this is safe because the setting would have checked the capability
+                * when the setting value was written into the changeset. So this is why
+                * an additional capability check is not required here.
+                */
+               $original_setting_capabilities = array();
+               foreach ( $changeset_setting_ids as $setting_id ) {
+                       $setting = $this->get_setting( $setting_id );
+                       if ( $setting ) {
+                               $original_setting_capabilities[ $setting->id ] = $setting->capability;
+                               $setting->capability = 'exist';
+                       }
</ins><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">+                foreach ( $changeset_setting_ids as $setting_id ) {
+                       $setting = $this->get_setting( $setting_id );
+                       if ( $setting ) {
+                               $setting->save();
+                       }
+               }
+
+               // Update the stashed theme mod settings, removing the active theme's stashed settings, if activated.
+               if ( did_action( 'switch_theme' ) ) {
+                       $this->update_stashed_theme_mod_settings( $other_theme_mod_settings );
+               }
+
</ins><span class="cx" style="display: block; padding: 0 10px">                 /**
</span><span class="cx" style="display: block; padding: 0 10px">                 * Fires after Customize settings have been saved.
</span><span class="cx" style="display: block; padding: 0 10px">                 *
</span><span class="cx" style="display: block; padding: 0 10px">                 * @since 3.6.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">-                 * @param WP_Customize_Manager $this WP_Customize_Manager instance.
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+          * @param WP_Customize_Manager $manager WP_Customize_Manager instance.
</ins><span class="cx" style="display: block; padding: 0 10px">                  */
</span><span class="cx" style="display: block; padding: 0 10px">                do_action( 'customize_save_after', $this );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                $data = array(
-                       'setting_validities' => $exported_setting_validities,
-               );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         // Restore original capabilities.
+               foreach ( $original_setting_capabilities as $setting_id => $capability ) {
+                       $setting = $this->get_setting( $setting_id );
+                       if ( $setting ) {
+                               $setting->capability = $capability;
+                       }
+               }
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                /**
-                * Filters response data for a successful customize_save Ajax request.
-                *
-                * This filter does not apply if there was a nonce or authentication failure.
-                *
-                * @since 4.2.0
-                *
-                * @param array                $data Additional information passed back to the 'saved'
-                *                                   event on `wp.customize`.
-                * @param WP_Customize_Manager $this WP_Customize_Manager instance.
-                */
-               $response = apply_filters( 'customize_save_response', $data, $this );
-               wp_send_json_success( $response );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         // Restore original changeset data.
+               $this->_changeset_data    = $previous_changeset_data;
+               $this->_changeset_post_id = $previous_changeset_post_id;
+               $this->_changeset_uuid    = $previous_changeset_uuid;
+
+               return 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><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         * Update stashed theme mod settings.
+        *
+        * @since 4.7.0
+        * @access private
+        *
+        * @param array $inactive_theme_mod_settings Mapping of stylesheet to arrays of theme mod settings.
+        * @return array|false Returns array of updated stashed theme mods or false if the update failed or there were no changes.
+        */
+       protected function update_stashed_theme_mod_settings( $inactive_theme_mod_settings ) {
+               $stashed_theme_mod_settings = get_option( 'customize_stashed_theme_mods' );
+               if ( empty( $stashed_theme_mod_settings ) ) {
+                       $stashed_theme_mod_settings = array();
+               }
+
+               // Delete any stashed theme mods for the active theme since since they would have been loaded and saved upon activation.
+               unset( $stashed_theme_mod_settings[ $this->get_stylesheet() ] );
+
+               // Merge inactive theme mods with the stashed theme mod settings.
+               foreach ( $inactive_theme_mod_settings as $stylesheet => $theme_mod_settings ) {
+                       if ( ! isset( $stashed_theme_mod_settings[ $stylesheet ] ) ) {
+                               $stashed_theme_mod_settings[ $stylesheet ] = array();
+                       }
+
+                       $stashed_theme_mod_settings[ $stylesheet ] = array_merge(
+                               $stashed_theme_mod_settings[ $stylesheet ],
+                               $theme_mod_settings
+                       );
+               }
+
+               $autoload = false;
+               $result = update_option( 'customize_stashed_theme_mods', $stashed_theme_mod_settings, $autoload );
+               if ( ! $result ) {
+                       return false;
+               }
+               return $stashed_theme_mod_settings;
+       }
+
+       /**
</ins><span class="cx" style="display: block; padding: 0 10px">          * Refresh nonces for the current preview.
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * @since 4.2.0
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1691,6 +2651,67 @@
</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">+         * Determines whether the admin and the frontend are on different domains.
+        *
+        * @since 4.7.0
+        * @access public
+        *
+        * @return bool Whether cross-domain.
+        */
+       public function is_cross_domain() {
+               $admin_origin = wp_parse_url( admin_url() );
+               $home_origin = wp_parse_url( home_url() );
+               $cross_domain = ( strtolower( $admin_origin['host'] ) !== strtolower( $home_origin['host'] ) );
+               return $cross_domain;
+       }
+
+       /**
+        * Get URLs allowed to be previewed.
+        *
+        * If the front end and the admin are served from the same domain, load the
+        * preview over ssl if the Customizer is being loaded over ssl. This avoids
+        * insecure content warnings. This is not attempted if the admin and front end
+        * are on different domains to avoid the case where the front end doesn't have
+        * ssl certs. Domain mapping plugins can allow other urls in these conditions
+        * using the customize_allowed_urls filter.
+        *
+        * @since 4.7.0
+        * @access public
+        *
+        * @returns array Allowed URLs.
+        */
+       public function get_allowed_urls() {
+               $allowed_urls = array( home_url( '/' ) );
+
+               if ( is_ssl() && ! $this->is_cross_domain() ) {
+                       $allowed_urls[] = home_url( '/', 'https' );
+               }
+
+               /**
+                * Filters the list of URLs allowed to be clicked and followed in the Customizer preview.
+                *
+                * @since 3.4.0
+                *
+                * @param array $allowed_urls An array of allowed URLs.
+                */
+               $allowed_urls = array_unique( apply_filters( 'customize_allowed_urls', $allowed_urls ) );
+
+               return $allowed_urls;
+       }
+
+       /**
+        * Get messenger channel.
+        *
+        * @since 4.7.0
+        * @access public
+        *
+        * @return string Messenger channel.
+        */
+       public function get_messenger_channel() {
+               return $this->messenger_channel;
+       }
+
+       /**
</ins><span class="cx" style="display: block; padding: 0 10px">          * Set URL to link the user to when closing the Customizer.
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * URL is validated.
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1799,40 +2820,33 @@
</span><span class="cx" style="display: block; padding: 0 10px">         * @since 4.4.0
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><span class="cx" style="display: block; padding: 0 10px">        public function customize_pane_settings() {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                /*
-                * If the front end and the admin are served from the same domain, load the
-                * preview over ssl if the Customizer is being loaded over ssl. This avoids
-                * insecure content warnings. This is not attempted if the admin and front end
-                * are on different domains to avoid the case where the front end doesn't have
-                * ssl certs. Domain mapping plugins can allow other urls in these conditions
-                * using the customize_allowed_urls filter.
-                */
</del><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                $allowed_urls = array( home_url( '/' ) );
-               $admin_origin = parse_url( admin_url() );
-               $home_origin  = parse_url( home_url() );
-               $cross_domain = ( strtolower( $admin_origin['host'] ) !== strtolower( $home_origin['host'] ) );
-
-               if ( is_ssl() && ! $cross_domain ) {
-                       $allowed_urls[] = home_url( '/', 'https' );
-               }
-
-               /**
-                * Filters the list of URLs allowed to be clicked and followed in the Customizer preview.
-                *
-                * @since 3.4.0
-                *
-                * @param array $allowed_urls An array of allowed URLs.
-                */
-               $allowed_urls = array_unique( apply_filters( 'customize_allowed_urls', $allowed_urls ) );
-
</del><span class="cx" style="display: block; padding: 0 10px">                 $login_url = add_query_arg( array(
</span><span class="cx" style="display: block; padding: 0 10px">                        'interim-login' => 1,
</span><span class="cx" style="display: block; padding: 0 10px">                        'customize-login' => 1,
</span><span class="cx" style="display: block; padding: 0 10px">                ), wp_login_url() );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                // Ensure dirty flags are set for modified settings.
+               foreach ( array_keys( $this->unsanitized_post_values() ) as $setting_id ) {
+                       $setting = $this->get_setting( $setting_id );
+                       if ( $setting ) {
+                               $setting->dirty = true;
+                       }
+               }
+
</ins><span class="cx" style="display: block; padding: 0 10px">                 // Prepare Customizer settings to pass to JavaScript.
</span><span class="cx" style="display: block; padding: 0 10px">                $settings = array(
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                        'changeset' => array(
+                               'uuid' => $this->changeset_uuid(),
+                               'status' => $this->changeset_post_id() ? get_post_status( $this->changeset_post_id() ) : '',
+                       ),
+                       'timeouts' => array(
+                               'windowRefresh' => 250,
+                               'changesetAutoSave' => AUTOSAVE_INTERVAL * 1000,
+                               'keepAliveCheck' => 2500,
+                               'reflowPaneContents' => 100,
+                               'previewFrameSensitivity' => 2000,
+                       ),
</ins><span class="cx" style="display: block; padding: 0 10px">                         'theme'    => array(
</span><span class="cx" style="display: block; padding: 0 10px">                                'stylesheet' => $this->get_stylesheet(),
</span><span class="cx" style="display: block; padding: 0 10px">                                'active'     => $this->is_theme_active(),
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1842,8 +2856,8 @@
</span><span class="cx" style="display: block; padding: 0 10px">                                'parent'        => esc_url_raw( admin_url() ),
</span><span class="cx" style="display: block; padding: 0 10px">                                'activated'     => esc_url_raw( home_url( '/' ) ),
</span><span class="cx" style="display: block; padding: 0 10px">                                'ajax'          => esc_url_raw( admin_url( 'admin-ajax.php', 'relative' ) ),
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                'allowed'       => array_map( 'esc_url_raw', $allowed_urls ),
-                               'isCrossDomain' => $cross_domain,
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                         'allowed'       => array_map( 'esc_url_raw', $this->get_allowed_urls() ),
+                               'isCrossDomain' => $this->is_cross_domain(),
</ins><span class="cx" style="display: block; padding: 0 10px">                                 'home'          => esc_url_raw( home_url( '/' ) ),
</span><span class="cx" style="display: block; padding: 0 10px">                                'login'         => esc_url_raw( $login_url ),
</span><span class="cx" style="display: block; padding: 0 10px">                        ),
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -2337,7 +3351,8 @@
</span><span class="cx" style="display: block; padding: 0 10px">         * @see add_dynamic_settings()
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><span class="cx" style="display: block; padding: 0 10px">        public function register_dynamic_settings() {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                $this->add_dynamic_settings( array_keys( $this->unsanitized_post_values() ) );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         $setting_ids = array_keys( $this->unsanitized_post_values() );
+               $this->add_dynamic_settings( $setting_ids );
</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></pre></div>
<a id="trunksrcwpincludesclasswpcustomizenavmenusphp"></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-nav-menus.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/class-wp-customize-nav-menus.php    2016-10-17 23:53:20 UTC (rev 38809)
+++ trunk/src/wp-includes/class-wp-customize-nav-menus.php      2016-10-18 20:04:36 UTC (rev 38810)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -48,7 +48,12 @@
</span><span class="cx" style="display: block; padding: 0 10px">                $this->previewed_menus = array();
</span><span class="cx" style="display: block; padding: 0 10px">                $this->manager         = $manager;
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                // Skip useless hooks when the user can't manage nav menus anyway.
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         // See https://github.com/xwp/wp-customize-snapshots/blob/962586659688a5b1fd9ae93618b7ce2d4e7a421c/php/class-customize-snapshot-manager.php#L469-L499
+               add_action( 'customize_register', array( $this, 'customize_register' ), 11 );
+               add_filter( 'customize_dynamic_setting_args', array( $this, 'filter_dynamic_setting_args' ), 10, 2 );
+               add_filter( 'customize_dynamic_setting_class', array( $this, 'filter_dynamic_setting_class' ), 10, 3 );
+
+               // Skip remaining hooks when the user can't manage nav menus anyway.
</ins><span class="cx" style="display: block; padding: 0 10px">                 if ( ! current_user_can( 'edit_theme_options' ) ) {
</span><span class="cx" style="display: block; padding: 0 10px">                        return;
</span><span class="cx" style="display: block; padding: 0 10px">                }
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -58,9 +63,6 @@
</span><span class="cx" style="display: block; padding: 0 10px">                add_action( 'wp_ajax_search-available-menu-items-customizer', array( $this, 'ajax_search_available_items' ) );
</span><span class="cx" style="display: block; padding: 0 10px">                add_action( 'wp_ajax_customize-nav-menus-insert-auto-draft', array( $this, 'ajax_insert_auto_draft_post' ) );
</span><span class="cx" style="display: block; padding: 0 10px">                add_action( 'customize_controls_enqueue_scripts', array( $this, 'enqueue_scripts' ) );
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                add_action( 'customize_register', array( $this, 'customize_register' ), 11 );
-               add_filter( 'customize_dynamic_setting_args', array( $this, 'filter_dynamic_setting_args' ), 10, 2 );
-               add_filter( 'customize_dynamic_setting_class', array( $this, 'filter_dynamic_setting_class' ), 10, 3 );
</del><span class="cx" style="display: block; padding: 0 10px">                 add_action( 'customize_controls_print_footer_scripts', array( $this, 'print_templates' ) );
</span><span class="cx" style="display: block; padding: 0 10px">                add_action( 'customize_controls_print_footer_scripts', array( $this, 'available_items_template' ) );
</span><span class="cx" style="display: block; padding: 0 10px">                add_action( 'customize_preview_init', array( $this, 'customize_preview_init' ) );
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -486,6 +488,23 @@
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><span class="cx" style="display: block; padding: 0 10px">        public function customize_register() {
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                /*
+                * Preview settings for nav menus early so that the sections and controls will be added properly.
+                * See https://github.com/xwp/wp-customize-snapshots/blob/962586659688a5b1fd9ae93618b7ce2d4e7a421c/php/class-customize-snapshot-manager.php#L506-L543
+                */
+               $nav_menus_setting_ids = array();
+               foreach ( array_keys( $this->manager->unsanitized_post_values() ) as $setting_id ) {
+                       if ( preg_match( '/^(nav_menu_locations|nav_menu|nav_menu_item)\[/', $setting_id ) ) {
+                               $nav_menus_setting_ids[] = $setting_id;
+                       }
+               }
+               foreach ( $nav_menus_setting_ids as $setting_id ) {
+                       $setting = $this->manager->get_setting( $setting_id );
+                       if ( $setting ) {
+                               $setting->preview();
+                       }
+               }
+
</ins><span class="cx" style="display: block; padding: 0 10px">                 // Require JS-rendered control types.
</span><span class="cx" style="display: block; padding: 0 10px">                $this->manager->register_panel_type( 'WP_Customize_Nav_Menus_Panel' );
</span><span class="cx" style="display: block; padding: 0 10px">                $this->manager->register_control_type( 'WP_Customize_Nav_Menu_Control' );
</span></span></pre></div>
<a id="trunksrcwpincludesclasswpcustomizewidgetsphp"></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-widgets.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/class-wp-customize-widgets.php      2016-10-17 23:53:20 UTC (rev 38809)
+++ trunk/src/wp-includes/class-wp-customize-widgets.php        2016-10-18 20:04:36 UTC (rev 38810)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -93,16 +93,18 @@
</span><span class="cx" style="display: block; padding: 0 10px">        public function __construct( $manager ) {
</span><span class="cx" style="display: block; padding: 0 10px">                $this->manager = $manager;
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                // Skip useless hooks when the user can't manage widgets anyway.
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         // See https://github.com/xwp/wp-customize-snapshots/blob/962586659688a5b1fd9ae93618b7ce2d4e7a421c/php/class-customize-snapshot-manager.php#L420-L449
+               add_filter( 'customize_dynamic_setting_args',          array( $this, 'filter_customize_dynamic_setting_args' ), 10, 2 );
+               add_action( 'widgets_init',                            array( $this, 'register_settings' ), 95 );
+               add_action( 'customize_register',                      array( $this, 'schedule_customize_register' ), 1 );
+
+               // Skip remaining hooks when the user can't manage widgets anyway.
</ins><span class="cx" style="display: block; padding: 0 10px">                 if ( ! current_user_can( 'edit_theme_options' ) ) {
</span><span class="cx" style="display: block; padding: 0 10px">                        return;
</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">-                add_filter( 'customize_dynamic_setting_args',          array( $this, 'filter_customize_dynamic_setting_args' ), 10, 2 );
-               add_action( 'widgets_init',                            array( $this, 'register_settings' ), 95 );
</del><span class="cx" style="display: block; padding: 0 10px">                 add_action( 'wp_loaded',                               array( $this, 'override_sidebars_widgets_for_theme_switch' ) );
</span><span class="cx" style="display: block; padding: 0 10px">                add_action( 'customize_controls_init',                 array( $this, 'customize_controls_init' ) );
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                add_action( 'customize_register',                      array( $this, 'schedule_customize_register' ), 1 );
</del><span class="cx" style="display: block; padding: 0 10px">                 add_action( 'customize_controls_enqueue_scripts',      array( $this, 'enqueue_scripts' ) );
</span><span class="cx" style="display: block; padding: 0 10px">                add_action( 'customize_controls_print_styles',         array( $this, 'print_styles' ) );
</span><span class="cx" style="display: block; padding: 0 10px">                add_action( 'customize_controls_print_scripts',        array( $this, 'print_scripts' ) );
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -276,6 +278,7 @@
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                $this->old_sidebars_widgets = wp_get_sidebars_widgets();
</span><span class="cx" style="display: block; padding: 0 10px">                add_filter( 'customize_value_old_sidebars_widgets_data', array( $this, 'filter_customize_value_old_sidebars_widgets_data' ) );
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                $this->manager->set_post_value( 'old_sidebars_widgets_data', $this->old_sidebars_widgets ); // Override any value cached in changeset.
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                // retrieve_widgets() looks at the global $sidebars_widgets
</span><span class="cx" style="display: block; padding: 0 10px">                $sidebars_widgets = $this->old_sidebars_widgets;
</span></span></pre></div>
<a id="trunksrcwpincludescustomizeclasswpcustomizeselectiverefreshphp"></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/customize/class-wp-customize-selective-refresh.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/customize/class-wp-customize-selective-refresh.php  2016-10-17 23:53:20 UTC (rev 38809)
+++ trunk/src/wp-includes/customize/class-wp-customize-selective-refresh.php    2016-10-18 20:04:36 UTC (rev 38810)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -307,8 +307,6 @@
</span><span class="cx" style="display: block; padding: 0 10px">                        return;
</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">-                $this->manager->remove_preview_signature();
-
</del><span class="cx" style="display: block; padding: 0 10px">                 /*
</span><span class="cx" style="display: block; padding: 0 10px">                 * Note that is_customize_preview() returning true will entail that the
</span><span class="cx" style="display: block; padding: 0 10px">                 * user passed the 'customize' capability check and the nonce check, since
</span></span></pre></div>
<a id="trunksrcwpincludescustomizeclasswpcustomizethemecontrolphp"></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/customize/class-wp-customize-theme-control.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/customize/class-wp-customize-theme-control.php      2016-10-17 23:53:20 UTC (rev 38809)
+++ trunk/src/wp-includes/customize/class-wp-customize-theme-control.php        2016-10-18 20:04:36 UTC (rev 38810)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -63,8 +63,8 @@
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><span class="cx" style="display: block; padding: 0 10px">        public function content_template() {
</span><span class="cx" style="display: block; padding: 0 10px">                $current_url = set_url_scheme( 'http://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'] );
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                $active_url  = esc_url( remove_query_arg( 'theme', $current_url ) );
-               $preview_url = esc_url( add_query_arg( 'theme', '__THEME__', $current_url ) ); // Token because esc_url() strips curly braces.
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         $active_url  = esc_url( remove_query_arg( 'customize_theme', $current_url ) );
+               $preview_url = esc_url( add_query_arg( 'customize_theme', '__THEME__', $current_url ) ); // Token because esc_url() strips curly braces.
</ins><span class="cx" style="display: block; padding: 0 10px">                 $preview_url = str_replace( '__THEME__', '{{ data.theme.id }}', $preview_url );
</span><span class="cx" style="display: block; padding: 0 10px">                ?>
</span><span class="cx" style="display: block; padding: 0 10px">                <# if ( data.theme.isActiveTheme ) { #>
</span></span></pre></div>
<a id="trunksrcwpincludesdefaultfiltersphp"></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/default-filters.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/default-filters.php 2016-10-17 23:53:20 UTC (rev 38809)
+++ trunk/src/wp-includes/default-filters.php   2016-10-18 20:04:36 UTC (rev 38810)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -75,6 +75,7 @@
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px"> // Slugs
</span><span class="cx" style="display: block; padding: 0 10px"> add_filter( 'pre_term_slug', 'sanitize_title' );
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+add_filter( 'wp_insert_post_data', '_wp_customize_changeset_filter_insert_post_data', 10, 2 );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px"> // Keys
</span><span class="cx" style="display: block; padding: 0 10px"> foreach ( array( 'pre_post_type', 'pre_post_status', 'pre_post_comment_status', 'pre_post_ping_status' ) as $filter ) {
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -382,6 +383,7 @@
</span><span class="cx" style="display: block; padding: 0 10px"> add_action( 'wp_loaded', '_custom_header_background_just_in_time' );
</span><span class="cx" style="display: block; padding: 0 10px"> add_action( 'wp_head', '_custom_logo_header_styles' );
</span><span class="cx" style="display: block; padding: 0 10px"> add_action( 'plugins_loaded', '_wp_customize_include' );
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+add_action( 'transition_post_status', '_wp_customize_publish_changeset', 10, 3 );
</ins><span class="cx" style="display: block; padding: 0 10px"> add_action( 'admin_enqueue_scripts', '_wp_customize_loader_settings' );
</span><span class="cx" style="display: block; padding: 0 10px"> add_action( 'delete_attachment', '_delete_attachment_theme_mod' );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span></span></pre></div>
<a id="trunksrcwpincludesfunctionsphp"></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/functions.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/functions.php       2016-10-17 23:53:20 UTC (rev 38809)
+++ trunk/src/wp-includes/functions.php 2016-10-18 20:04:36 UTC (rev 38810)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -5523,3 +5523,20 @@
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        return false;
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
+/**
+ * Generate a random UUID (version 4).
+ *
+ * @since 4.7.0
+ *
+ * @return string UUID.
+ */
+function wp_generate_uuid4() {
+       return sprintf( '%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
+               mt_rand( 0, 0xffff ), mt_rand( 0, 0xffff ),
+               mt_rand( 0, 0xffff ),
+               mt_rand( 0, 0x0fff ) | 0x4000,
+               mt_rand( 0, 0x3fff ) | 0x8000,
+               mt_rand( 0, 0xffff ), mt_rand( 0, 0xffff ), mt_rand( 0, 0xffff )
+       );
+}
</ins></span></pre></div>
<a id="trunksrcwpincludesfunctionswpscriptsphp"></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/functions.wp-scripts.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/functions.wp-scripts.php    2016-10-17 23:53:20 UTC (rev 38809)
+++ trunk/src/wp-includes/functions.wp-scripts.php      2016-10-18 20:04:36 UTC (rev 38810)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -34,7 +34,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">  * @param string $function Function name.
</span><span class="cx" style="display: block; padding: 0 10px">  */
</span><span class="cx" style="display: block; padding: 0 10px"> function _wp_scripts_maybe_doing_it_wrong( $function ) {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        if ( did_action( 'init' ) ) {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ if ( did_action( 'init' ) || did_action( 'admin_enqueue_scripts' ) || did_action( 'wp_enqueue_scripts' ) || did_action( 'login_enqueue_scripts' ) ) {
</ins><span class="cx" style="display: block; padding: 0 10px">                 return;
</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="trunksrcwpincludesjscustomizebasejs"></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/customize-base.js</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/js/customize-base.js        2016-10-17 23:53:20 UTC (rev 38809)
+++ trunk/src/wp-includes/js/customize-base.js  2016-10-18 20:04:36 UTC (rev 38810)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -637,22 +637,24 @@
</span><span class="cx" style="display: block; padding: 0 10px">                /**
</span><span class="cx" style="display: block; padding: 0 10px">                 * Initialize Messenger.
</span><span class="cx" style="display: block; padding: 0 10px">                 *
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                 * @param  {object} params        Parameters to configure the messenger.
-                *         {string} .url          The URL to communicate with.
-                *         {window} .targetWindow The window instance to communicate with. Default window.parent.
-                *         {string} .channel      If provided, will send the channel with each message and only accept messages a matching channel.
-                * @param  {object} options       Extend any instance parameter or method with this object.
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+          * @param  {object} params - Parameters to configure the messenger.
+                *         {string} params.url - The URL to communicate with.
+                *         {window} params.targetWindow - The window instance to communicate with. Default window.parent.
+                *         {string} params.channel - If provided, will send the channel with each message and only accept messages a matching channel.
+                * @param  {object} options - Extend any instance parameter or method with this object.
</ins><span class="cx" style="display: block; padding: 0 10px">                  */
</span><span class="cx" style="display: block; padding: 0 10px">                initialize: function( params, options ) {
</span><span class="cx" style="display: block; padding: 0 10px">                        // Target the parent frame by default, but only if a parent frame exists.
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        var defaultTarget = window.parent == window ? null : window.parent;
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 var defaultTarget = window.parent === window ? null : window.parent;
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                        $.extend( this, options || {} );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                        this.add( 'channel', params.channel );
</span><span class="cx" style="display: block; padding: 0 10px">                        this.add( 'url', params.url || '' );
</span><span class="cx" style="display: block; padding: 0 10px">                        this.add( 'origin', this.url() ).link( this.url ).setter( function( to ) {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                return to.replace( /([^:]+:\/\/[^\/]+).*/, '$1' );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                         var urlParser = document.createElement( 'a' );
+                               urlParser.href = to;
+                               return urlParser.protocol + '//' + urlParser.hostname;
</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">                        // first add with no value
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -807,6 +809,40 @@
</span><span class="cx" style="display: block; padding: 0 10px">                return result;
</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">+        /**
+        * Utility function namespace
+        */
+       api.utils = {};
+
+       /**
+        * Parse query string.
+        *
+        * @since 4.7.0
+        * @access public
+        *
+        * @param {string} queryString Query string.
+        * @returns {object} Parsed query string.
+        */
+       api.utils.parseQueryString = function parseQueryString( queryString ) {
+               var queryParams = {};
+               _.each( queryString.split( '&' ), function( pair ) {
+                       var parts, key, value;
+                       parts = pair.split( '=', 2 );
+                       if ( ! parts[0] ) {
+                               return;
+                       }
+                       key = decodeURIComponent( parts[0].replace( /\+/g, ' ' ) );
+                       key = key.replace( / /g, '_' ); // What PHP does.
+                       if ( _.isUndefined( parts[1] ) ) {
+                               value = null;
+                       } else {
+                               value = decodeURIComponent( parts[1].replace( /\+/g, ' ' ) );
+                       }
+                       queryParams[ key ] = value;
+               } );
+               return queryParams;
+       };
+
</ins><span class="cx" style="display: block; padding: 0 10px">         // Expose the API publicly on window.wp.customize
</span><span class="cx" style="display: block; padding: 0 10px">        exports.customize = api;
</span><span class="cx" style="display: block; padding: 0 10px"> })( wp, jQuery );
</span></span></pre></div>
<a id="trunksrcwpincludesjscustomizeloaderjs"></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/customize-loader.js</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/js/customize-loader.js      2016-10-17 23:53:20 UTC (rev 38809)
+++ trunk/src/wp-includes/js/customize-loader.js        2016-10-18 20:04:36 UTC (rev 38810)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -132,6 +132,19 @@
</span><span class="cx" style="display: block; padding: 0 10px">                                targetWindow: this.iframe[0].contentWindow
</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">+                        // Expose the changeset UUID on the parent window's URL so that the customized state can survive a refresh.
+                       if ( history.replaceState ) {
+                               this.messenger.bind( 'changeset-uuid', function( changesetUuid ) {
+                                       var urlParser = document.createElement( 'a' );
+                                       urlParser.href = location.href;
+                                       urlParser.search = $.param( _.extend(
+                                               api.utils.parseQueryString( urlParser.search.substr( 1 ) ),
+                                               { changeset_uuid: changesetUuid }
+                                       ) );
+                                       history.replaceState( { customize: urlParser.href }, '', urlParser.href );
+                               } );
+                       }
+
</ins><span class="cx" style="display: block; padding: 0 10px">                         // Wait for the connection from the iframe before sending any postMessage events.
</span><span class="cx" style="display: block; padding: 0 10px">                        this.messenger.bind( 'ready', function() {
</span><span class="cx" style="display: block; padding: 0 10px">                                Loader.messenger.send( 'back' );
</span></span></pre></div>
<a id="trunksrcwpincludesjscustomizepreviewnavmenusjs"></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/customize-preview-nav-menus.js</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/js/customize-preview-nav-menus.js   2016-10-17 23:53:20 UTC (rev 38809)
+++ trunk/src/wp-includes/js/customize-preview-nav-menus.js     2016-10-18 20:04:36 UTC (rev 38810)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -106,7 +106,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">                         * @returns {boolean}
</span><span class="cx" style="display: block; padding: 0 10px">                         */
</span><span class="cx" style="display: block; padding: 0 10px">                        isRelatedSetting: function( setting, newValue, oldValue ) {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                var partial = this, navMenuLocationSetting, navMenuId, isNavMenuItemSetting;
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                         var partial = this, navMenuLocationSetting, navMenuId, isNavMenuItemSetting, _newValue, _oldValue, urlParser;
</ins><span class="cx" style="display: block; padding: 0 10px">                                 if ( _.isString( setting ) ) {
</span><span class="cx" style="display: block; padding: 0 10px">                                        setting = api( setting );
</span><span class="cx" style="display: block; padding: 0 10px">                                }
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -123,9 +123,23 @@
</span><span class="cx" style="display: block; padding: 0 10px">                                 */
</span><span class="cx" style="display: block; padding: 0 10px">                                isNavMenuItemSetting = /^nav_menu_item\[/.test( setting.id );
</span><span class="cx" style="display: block; padding: 0 10px">                                if ( isNavMenuItemSetting && _.isObject( newValue ) && _.isObject( oldValue ) ) {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                        delete newValue.type_label;
-                                       delete oldValue.type_label;
-                                       if ( _.isEqual( oldValue, newValue ) ) {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                                 _newValue = _.clone( newValue );
+                                       _oldValue = _.clone( oldValue );
+                                       delete _newValue.type_label;
+                                       delete _oldValue.type_label;
+
+                                       // Normalize URL scheme when parent frame is HTTPS to prevent selective refresh upon initial page load.
+                                       if ( 'https' === api.preview.scheme.get() ) {
+                                               urlParser = document.createElement( 'a' );
+                                               urlParser.href = _newValue.url;
+                                               urlParser.protocol = 'https:';
+                                               _newValue.url = urlParser.href;
+                                               urlParser.href = _oldValue.url;
+                                               urlParser.protocol = 'https:';
+                                               _oldValue.url = urlParser.href;
+                                       }
+
+                                       if ( _.isEqual( _oldValue, _newValue ) ) {
</ins><span class="cx" style="display: block; padding: 0 10px">                                                 return false;
</span><span class="cx" style="display: block; padding: 0 10px">                                        }
</span><span class="cx" style="display: block; padding: 0 10px">                                }
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -365,6 +379,11 @@
</span><span class="cx" style="display: block; padding: 0 10px">        self.highlightControls = function() {
</span><span class="cx" style="display: block; padding: 0 10px">                var selector = '.menu-item';
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                // Skip adding highlights if not in the customizer preview iframe.
+               if ( ! api.settings.channel ) {
+                       return;
+               }
+
</ins><span class="cx" style="display: block; padding: 0 10px">                 // Focus on the menu item control when shift+clicking the menu item.
</span><span class="cx" style="display: block; padding: 0 10px">                $( document ).on( 'click', selector, function( e ) {
</span><span class="cx" style="display: block; padding: 0 10px">                        var navMenuItemParts;
</span></span></pre></div>
<a id="trunksrcwpincludesjscustomizepreviewwidgetsjs"></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/customize-preview-widgets.js</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/js/customize-preview-widgets.js     2016-10-17 23:53:20 UTC (rev 38809)
+++ trunk/src/wp-includes/js/customize-preview-widgets.js       2016-10-18 20:04:36 UTC (rev 38810)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -572,6 +572,11 @@
</span><span class="cx" style="display: block; padding: 0 10px">                var self = this,
</span><span class="cx" style="display: block; padding: 0 10px">                        selector = this.widgetSelectors.join( ',' );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                // Skip adding highlights if not in the customizer preview iframe.
+               if ( ! api.settings.channel ) {
+                       return;
+               }
+
</ins><span class="cx" style="display: block; padding: 0 10px">                 $( selector ).attr( 'title', this.l10n.widgetTooltip );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                $( document ).on( 'mouseenter', selector, function() {
</span></span></pre></div>
<a id="trunksrcwpincludesjscustomizepreviewjs"></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/customize-preview.js</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/js/customize-preview.js     2016-10-17 23:53:20 UTC (rev 38809)
+++ trunk/src/wp-includes/js/customize-preview.js       2016-10-18 20:04:36 UTC (rev 38810)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -3,8 +3,68 @@
</span><span class="cx" style="display: block; padding: 0 10px">  */
</span><span class="cx" style="display: block; padding: 0 10px"> (function( exports, $ ){
</span><span class="cx" style="display: block; padding: 0 10px">        var api = wp.customize,
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                debounce;
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         debounce,
+               currentHistoryState = {};
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+        /*
+        * Capture the state that is passed into history.replaceState() and history.pushState()
+        * and also which is returned in the popstate event so that when the changeset_uuid
+        * gets updated when transitioning to a new changeset there the current state will
+        * be supplied in the call to history.replaceState().
+        */
+       ( function( history ) {
+               var injectUrlWithState;
+
+               if ( ! history.replaceState ) {
+                       return;
+               }
+
+               /**
+                * Amend the supplied URL with the customized state.
+                *
+                * @since 4.7.0
+                * @access private
+                *
+                * @param {string} url URL.
+                * @returns {string} URL with customized state.
+                */
+               injectUrlWithState = function( url ) {
+                       var urlParser, queryParams;
+                       urlParser = document.createElement( 'a' );
+                       urlParser.href = url;
+                       queryParams = api.utils.parseQueryString( urlParser.search.substr( 1 ) );
+
+                       queryParams.customize_changeset_uuid = api.settings.changeset.uuid;
+                       if ( ! api.settings.theme.active ) {
+                               queryParams.customize_theme = api.settings.theme.stylesheet;
+                       }
+                       if ( api.settings.theme.channel ) {
+                               queryParams.customize_messenger_channel = api.settings.channel;
+                       }
+                       urlParser.search = $.param( queryParams );
+                       return url;
+               };
+
+               history.replaceState = ( function( nativeReplaceState ) {
+                       return function historyReplaceState( data, title, url ) {
+                               currentHistoryState = data;
+                               return nativeReplaceState.call( history, data, title, injectUrlWithState( url ) );
+                       };
+               } )( history.replaceState );
+
+               history.pushState = ( function( nativePushState ) {
+                       return function historyPushState( data, title, url ) {
+                               currentHistoryState = data;
+                               return nativePushState.call( history, data, title, injectUrlWithState( url ) );
+                       };
+               } )( history.pushState );
+
+               window.addEventListener( 'popstate', function( event ) {
+                       currentHistoryState = event.state;
+               } );
+
+       }( history ) );
+
</ins><span class="cx" style="display: block; padding: 0 10px">         /**
</span><span class="cx" style="display: block; padding: 0 10px">         * Returns a debounced version of the function.
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -37,39 +97,74 @@
</span><span class="cx" style="display: block; padding: 0 10px">                 * @param {object} options - Extend any instance parameter or method with this object.
</span><span class="cx" style="display: block; padding: 0 10px">                 */
</span><span class="cx" style="display: block; padding: 0 10px">                initialize: function( params, options ) {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        var self = this;
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 var preview = this, urlParser = document.createElement( 'a' );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        api.Messenger.prototype.initialize.call( this, params, options );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 api.Messenger.prototype.initialize.call( preview, params, options );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        this.body = $( document.body );
-                       this.body.on( 'click.preview', 'a', function( event ) {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 urlParser.href = preview.origin();
+                       preview.add( 'scheme', urlParser.protocol.replace( /:$/, '' ) );
+
+                       preview.body = $( document.body );
+                       preview.body.on( 'click.preview', 'a', function( event ) {
</ins><span class="cx" style="display: block; padding: 0 10px">                                 var link, isInternalJumpLink;
</span><span class="cx" style="display: block; padding: 0 10px">                                link = $( this );
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
+                               // No-op if the anchor is not a link.
+                               if ( _.isUndefined( link.attr( 'href' ) ) ) {
+                                       return;
+                               }
+
</ins><span class="cx" style="display: block; padding: 0 10px">                                 isInternalJumpLink = ( '#' === link.attr( 'href' ).substr( 0, 1 ) );
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                event.preventDefault();
</del><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                if ( isInternalJumpLink && '#' !== link.attr( 'href' ) ) {
-                                       $( link.attr( 'href' ) ).each( function() {
-                                               this.scrollIntoView();
-                                       } );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                         // Allow internal jump links to behave normally without preventing default.
+                               if ( isInternalJumpLink ) {
+                                       return;
</ins><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 the link is not previewable, prevent the browser from navigating to it.
+                               if ( ! api.isLinkPreviewable( link[0] ) ) {
+                                       wp.a11y.speak( api.settings.l10n.linkUnpreviewable );
+                                       event.preventDefault();
+                                       return;
+                               }
+
+                               // If not in an iframe, then allow the link click to proceed normally since the state query params are added.
+                               if ( ! api.settings.channel ) {
+                                       return;
+                               }
+
+                               // Prevent initiating navigating from click and instead rely on sending url message to pane.
+                               event.preventDefault();
+
</ins><span class="cx" style="display: block; padding: 0 10px">                                 /*
</span><span class="cx" style="display: block; padding: 0 10px">                                 * Note the shift key is checked so shift+click on widgets or
</span><span class="cx" style="display: block; padding: 0 10px">                                 * nav menu items can just result on focusing on the corresponding
</span><span class="cx" style="display: block; padding: 0 10px">                                 * control instead of also navigating to the URL linked to.
</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 ( event.shiftKey || isInternalJumpLink ) {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                         if ( event.shiftKey ) {
</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">-                                self.send( 'scroll', 0 );
-                               self.send( 'url', link.prop( 'href' ) );
-                       });
</del><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        // You cannot submit forms.
-                       this.body.on( 'submit.preview', 'form', function( event ) {
-                               var urlParser;
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                         // Note: It's not relevant to send scroll because sending url message will have the same effect.
+                               preview.send( 'url', link.prop( 'href' ) );
+                       } );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                        preview.body.on( 'submit.preview', 'form', function( event ) {
+                               var urlParser = document.createElement( 'a' );
+                               urlParser.href = this.action;
+
+                               // If the link is not previewable, prevent the browser from navigating to it.
+                               if ( 'GET' !== this.method.toUpperCase() || ! api.isLinkPreviewable( urlParser ) ) {
+                                       wp.a11y.speak( api.settings.l10n.formUnpreviewable );
+                                       event.preventDefault();
+                                       return;
+                               }
+
+                               // If not in an iframe, then allow the form submission to proceed normally with the state inputs injected.
+                               if ( ! api.settings.channel ) {
+                                       return;
+                               }
+
</ins><span class="cx" style="display: block; padding: 0 10px">                                 /*
</span><span class="cx" style="display: block; padding: 0 10px">                                 * If the default wasn't prevented already (in which case the form
</span><span class="cx" style="display: block; padding: 0 10px">                                 * submission is already being handled by JS), and if it has a GET
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -81,30 +176,399 @@
</span><span class="cx" style="display: block; padding: 0 10px">                                 * a no-op, which is the same behavior as when clicking a link to an
</span><span class="cx" style="display: block; padding: 0 10px">                                 * external site in the preview.
</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 ( ! event.isDefaultPrevented() && 'GET' === this.method.toUpperCase() ) {
-                                       urlParser = document.createElement( 'a' );
-                                       urlParser.href = this.action;
-                                       if ( urlParser.search.substr( 1 ).length > 1 ) {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                         if ( ! event.isDefaultPrevented() ) {
+                                       if ( urlParser.search.length > 1 ) {
</ins><span class="cx" style="display: block; padding: 0 10px">                                                 urlParser.search += '&';
</span><span class="cx" style="display: block; padding: 0 10px">                                        }
</span><span class="cx" style="display: block; padding: 0 10px">                                        urlParser.search += $( this ).serialize();
</span><span class="cx" style="display: block; padding: 0 10px">                                        api.preview.send( 'url', urlParser.href );
</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">+                                // Prevent default since navigation should be done via sending url message or via JS submit handler.
</ins><span class="cx" style="display: block; padding: 0 10px">                                 event.preventDefault();
</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">-                        this.window = $( window );
-                       this.window.on( 'scroll.preview', debounce( function() {
-                               self.send( 'scroll', self.window.scrollTop() );
-                       }, 200 ));
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 preview.window = $( window );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        this.bind( 'scroll', function( distance ) {
-                               self.window.scrollTop( distance );
-                       });
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 if ( api.settings.channel ) {
+                               preview.window.on( 'scroll.preview', debounce( function() {
+                                       preview.send( 'scroll', preview.window.scrollTop() );
+                               }, 200 ) );
+
+                               preview.bind( 'scroll', function( distance ) {
+                                       preview.window.scrollTop( distance );
+                               });
+                       }
</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><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+        /**
+        * Inject the changeset UUID into links in the document.
+        *
+        * @since 4.7.0
+        * @access protected
+        *
+        * @access private
+        * @returns {void}
+        */
+       api.addLinkPreviewing = function addLinkPreviewing() {
+               var linkSelectors = 'a[href], area';
+
+               // Inject links into initial document.
+               $( document.body ).find( linkSelectors ).each( function() {
+                       api.prepareLinkPreview( this );
+               } );
+
+               // Inject links for new elements added to the page.
+               if ( 'undefined' !== typeof MutationObserver ) {
+                       api.mutationObserver = new MutationObserver( function( mutations ) {
+                               _.each( mutations, function( mutation ) {
+                                       $( mutation.target ).find( linkSelectors ).each( function() {
+                                               api.prepareLinkPreview( this );
+                                       } );
+                               } );
+                       } );
+                       api.mutationObserver.observe( document.documentElement, {
+                               childList: true,
+                               subtree: true
+                       } );
+               } else {
+
+                       // If mutation observers aren't available, fallback to just-in-time injection.
+                       $( document.documentElement ).on( 'click focus mouseover', linkSelectors, function() {
+                               api.prepareLinkPreview( this );
+                       } );
+               }
+       };
+
+       /**
+        * Should the supplied link is previewable.
+        *
+        * @since 4.7.0
+        * @access public
+        *
+        * @param {HTMLAnchorElement|HTMLAreaElement} element Link element.
+        * @param {string} element.search Query string.
+        * @param {string} element.pathname Path.
+        * @param {string} element.hostname Hostname.
+        * @param {object} [options]
+        * @param {object} [options.allowAdminAjax=false] Allow admin-ajax.php requests.
+        * @returns {boolean} Is appropriate for changeset link.
+        */
+       api.isLinkPreviewable = function isLinkPreviewable( element, options ) {
+               var hasMatchingHost, urlParser, args;
+
+               args = _.extend( {}, { allowAdminAjax: false }, options || {} );
+
+               if ( 'javascript:' === element.protocol ) { // jshint ignore:line
+                       return true;
+               }
+
+               // Only web URLs can be previewed.
+               if ( 'https:' !== element.protocol && 'http:' !== element.protocol ) {
+                       return false;
+               }
+
+               urlParser = document.createElement( 'a' );
+               hasMatchingHost = ! _.isUndefined( _.find( api.settings.url.allowed, function( allowedUrl ) {
+                       urlParser.href = allowedUrl;
+                       if ( urlParser.hostname === element.hostname && urlParser.protocol === element.protocol ) {
+                               return true;
+                       }
+                       return false;
+               } ) );
+               if ( ! hasMatchingHost ) {
+                       return false;
+               }
+
+               // Skip wp login and signup pages.
+               if ( /\/wp-(login|signup)\.php$/.test( element.pathname ) ) {
+                       return false;
+               }
+
+               // Allow links to admin ajax as faux frontend URLs.
+               if ( /\/wp-admin\/admin-ajax\.php$/.test( element.pathname ) ) {
+                       return args.allowAdminAjax;
+               }
+
+               // Disallow links to admin, includes, and content.
+               if ( /\/wp-(admin|includes|content)(\/|$)/.test( element.pathname ) ) {
+                       return false;
+               }
+
+               return true;
+       };
+
+       /**
+        * Inject the customize_changeset_uuid query param into links on the frontend.
+        *
+        * @since 4.7.0
+        * @access protected
+        *
+        * @param {HTMLAnchorElement|HTMLAreaElement} element Link element.
+        * @param {object} element.search Query string.
+        * @returns {void}
+        */
+       api.prepareLinkPreview = function prepareLinkPreview( element ) {
+               var queryParams;
+
+               // Skip links in admin bar.
+               if ( $( element ).closest( '#wpadminbar' ).length ) {
+                       return;
+               }
+
+               // Ignore links with href="#" or href="#id".
+               if ( '#' === $( element ).attr( 'href' ).substr( 0, 1 ) ) {
+                       return;
+               }
+
+               // Make sure links in preview use HTTPS if parent frame uses HTTPS.
+               if ( 'https' === api.preview.scheme.get() && 'http:' === element.protocol && -1 !== api.settings.url.allowedHosts.indexOf( element.hostname ) ) {
+                       element.protocol = 'https:';
+               }
+
+               if ( ! api.isLinkPreviewable( element ) ) {
+                       $( element ).addClass( 'customize-unpreviewable' );
+                       return;
+               }
+               $( element ).removeClass( 'customize-unpreviewable' );
+
+               queryParams = api.utils.parseQueryString( element.search.substring( 1 ) );
+               queryParams.customize_changeset_uuid = api.settings.changeset.uuid;
+               if ( ! api.settings.theme.active ) {
+                       queryParams.customize_theme = api.settings.theme.stylesheet;
+               }
+               if ( api.settings.channel ) {
+                       queryParams.customize_messenger_channel = api.settings.channel;
+               }
+               element.search = $.param( queryParams );
+
+               // Prevent links from breaking out of preview iframe.
+               if ( api.settings.channel ) {
+                       element.target = '_self';
+               }
+       };
+
+       /**
+        * Inject the changeset UUID into Ajax requests.
+        *
+        * @since 4.7.0
+        * @access protected
+        *
+        * @return {void}
+        */
+       api.addRequestPreviewing = function addRequestPreviewing() {
+
+               /**
+                * Rewrite Ajax requests to inject customizer state.
+                *
+                * @param {object} options Options.
+                * @param {string} options.type Type.
+                * @param {string} options.url URL.
+                * @param {object} originalOptions Original options.
+                * @param {XMLHttpRequest} xhr XHR.
+                * @returns {void}
+                */
+               var prefilterAjax = function( options, originalOptions, xhr ) {
+                       var urlParser, queryParams, requestMethod, dirtyValues = {};
+                       urlParser = document.createElement( 'a' );
+                       urlParser.href = options.url;
+
+                       // Abort if the request is not for this site.
+                       if ( ! api.isLinkPreviewable( urlParser, { allowAdminAjax: true } ) ) {
+                               return;
+                       }
+                       queryParams = api.utils.parseQueryString( urlParser.search.substring( 1 ) );
+
+                       // Note that _dirty flag will be cleared with changeset updates.
+                       api.each( function( setting ) {
+                               if ( setting._dirty ) {
+                                       dirtyValues[ setting.id ] = setting.get();
+                               }
+                       } );
+
+                       if ( ! _.isEmpty( dirtyValues ) ) {
+                               requestMethod = options.type.toUpperCase();
+
+                               // Override underlying request method to ensure unsaved changes to changeset can be included (force Backbone.emulateHTTP).
+                               if ( 'POST' !== requestMethod ) {
+                                       xhr.setRequestHeader( 'X-HTTP-Method-Override', requestMethod );
+                                       queryParams._method = requestMethod;
+                                       options.type = 'POST';
+                               }
+
+                               // Amend the post data with the customized values.
+                               if ( options.data ) {
+                                       options.data += '&';
+                               } else {
+                                       options.data = '';
+                               }
+                               options.data += $.param( {
+                                       customized: JSON.stringify( dirtyValues )
+                               } );
+                       }
+
+                       // Include customized state query params in URL.
+                       queryParams.customize_changeset_uuid = api.settings.changeset.uuid;
+                       if ( ! api.settings.theme.active ) {
+                               queryParams.customize_theme = api.settings.theme.stylesheet;
+                       }
+                       urlParser.search = $.param( queryParams );
+                       options.url = urlParser.href;
+               };
+
+               $.ajaxPrefilter( prefilterAjax );
+       };
+
+       /**
+        * Inject changeset UUID into forms, allowing preview to persist through submissions.
+        *
+        * @since 4.7.0
+        * @access protected
+        *
+        * @returns {void}
+        */
+       api.addFormPreviewing = function addFormPreviewing() {
+
+               // Inject inputs for forms in initial document.
+               $( document.body ).find( 'form' ).each( function() {
+                       api.prepareFormPreview( this );
+               } );
+
+               // Inject inputs for new forms added to the page.
+               if ( 'undefined' !== typeof MutationObserver ) {
+                       api.mutationObserver = new MutationObserver( function( mutations ) {
+                               _.each( mutations, function( mutation ) {
+                                       $( mutation.target ).find( 'form' ).each( function() {
+                                               api.prepareFormPreview( this );
+                                       } );
+                               } );
+                       } );
+                       api.mutationObserver.observe( document.documentElement, {
+                               childList: true,
+                               subtree: true
+                       } );
+               }
+       };
+
+       /**
+        * Inject changeset into form inputs.
+        *
+        * @since 4.7.0
+        * @access protected
+        *
+        * @param {HTMLFormElement} form Form.
+        * @returns {void}
+        */
+       api.prepareFormPreview = function prepareFormPreview( form ) {
+               var urlParser, stateParams = {};
+
+               if ( ! form.action ) {
+                       form.action = location.href;
+               }
+
+               urlParser = document.createElement( 'a' );
+               urlParser.href = form.action;
+
+               // Make sure forms in preview use HTTPS if parent frame uses HTTPS.
+               if ( 'https' === api.preview.scheme.get() && 'http:' === urlParser.protocol && -1 !== api.settings.url.allowedHosts.indexOf( urlParser.hostname ) ) {
+                       urlParser.protocol = 'https:';
+                       form.action = urlParser.href;
+               }
+
+               if ( 'GET' !== form.method.toUpperCase() || ! api.isLinkPreviewable( urlParser ) ) {
+                       $( form ).addClass( 'customize-unpreviewable' );
+                       return;
+               }
+               $( form ).removeClass( 'customize-unpreviewable' );
+
+               stateParams.customize_changeset_uuid = api.settings.changeset.uuid;
+               if ( ! api.settings.theme.active ) {
+                       stateParams.customize_theme = api.settings.theme.stylesheet;
+               }
+               if ( api.settings.channel ) {
+                       stateParams.customize_messenger_channel = api.settings.channel;
+               }
+
+               _.each( stateParams, function( value, name ) {
+                       var input = $( form ).find( 'input[name="' + name + '"]' );
+                       if ( input.length ) {
+                               input.val( value );
+                       } else {
+                               $( form ).prepend( $( '<input>', {
+                                       type: 'hidden',
+                                       name: name,
+                                       value: value
+                               } ) );
+                       }
+               } );
+
+               // Prevent links from breaking out of preview iframe.
+               if ( api.settings.channel ) {
+                       form.target = '_self';
+               }
+       };
+
+       /**
+        * Watch current URL and send keep-alive (heartbeat) messages to the parent.
+        *
+        * Keep the customizer pane notified that the preview is still alive
+        * and that the user hasn't navigated to a non-customized URL.
+        *
+        * @since 4.7.0
+        * @access protected
+        */
+       api.keepAliveCurrentUrl = ( function() {
+               var previousPathName = location.pathname,
+                       previousQueryString = location.search.substr( 1 ),
+                       previousQueryParams = null,
+                       stateQueryParams = [ 'customize_theme', 'customize_changeset_uuid', 'customize_messenger_channel' ];
+
+               return function keepAliveCurrentUrl() {
+                       var urlParser, currentQueryParams;
+
+                       // Short-circuit with keep-alive if previous URL is identical (as is normal case).
+                       if ( previousQueryString === location.search.substr( 1 ) && previousPathName === location.pathname ) {
+                               api.preview.send( 'keep-alive' );
+                               return;
+                       }
+
+                       urlParser = document.createElement( 'a' );
+                       if ( null === previousQueryParams ) {
+                               urlParser.search = previousQueryString;
+                               previousQueryParams = api.utils.parseQueryString( previousQueryString );
+                               _.each( stateQueryParams, function( name ) {
+                                       delete previousQueryParams[ name ];
+                               } );
+                       }
+
+                       // Determine if current URL minus customized state params and URL hash.
+                       urlParser.href = location.href;
+                       currentQueryParams = api.utils.parseQueryString( urlParser.search.substr( 1 ) );
+                       _.each( stateQueryParams, function( name ) {
+                               delete currentQueryParams[ name ];
+                       } );
+
+                       if ( previousPathName !== location.pathname || ! _.isEqual( previousQueryParams, currentQueryParams ) ) {
+                               urlParser.search = $.param( currentQueryParams );
+                               urlParser.hash = '';
+                               api.settings.url.self = urlParser.href;
+                               api.preview.send( 'ready', {
+                                       currentUrl: api.settings.url.self,
+                                       activePanels: api.settings.activePanels,
+                                       activeSections: api.settings.activeSections,
+                                       activeControls: api.settings.activeControls,
+                                       settingValidities: api.settings.settingValidities
+                               } );
+                       } else {
+                               api.preview.send( 'keep-alive' );
+                       }
+                       previousQueryParams = currentQueryParams;
+                       previousQueryString = location.search.substr( 1 );
+                       previousPathName = location.pathname;
+               };
+       } )();
+
</ins><span class="cx" style="display: block; padding: 0 10px">         $( function() {
</span><span class="cx" style="display: block; padding: 0 10px">                var bg, setValue;
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -118,6 +582,10 @@
</span><span class="cx" style="display: block; padding: 0 10px">                        channel: api.settings.channel
</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">+                api.addLinkPreviewing();
+               api.addRequestPreviewing();
+               api.addFormPreviewing();
+
</ins><span class="cx" style="display: block; padding: 0 10px">                 /**
</span><span class="cx" style="display: block; padding: 0 10px">                 * Create/update a setting value.
</span><span class="cx" style="display: block; padding: 0 10px">                 *
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -171,15 +639,47 @@
</span><span class="cx" style="display: block; padding: 0 10px">                        api.preview.send( 'nonce', api.settings.nonce );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                        api.preview.send( 'documentTitle', document.title );
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
+                       // Send scroll in case of loading via non-refresh.
+                       api.preview.send( 'scroll', $( window ).scrollTop() );
</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">                api.preview.bind( 'saved', function( response ) {
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
+                       if ( response.next_changeset_uuid ) {
+                               api.settings.changeset.uuid = response.next_changeset_uuid;
+
+                               // Update UUIDs in links and forms.
+                               $( document.body ).find( 'a[href], area' ).each( function() {
+                                       api.prepareLinkPreview( this );
+                               } );
+                               $( document.body ).find( 'form' ).each( function() {
+                                       api.prepareFormPreview( this );
+                               } );
+
+                               /*
+                                * Replace the UUID in the URL. Note that the wrapped history.replaceState()
+                                * will handle injecting the current api.settings.changeset.uuid into the URL,
+                                * so this is merely to trigger that logic.
+                                */
+                               if ( history.replaceState ) {
+                                       history.replaceState( currentHistoryState, '', location.href );
+                               }
+                       }
+
</ins><span class="cx" style="display: block; padding: 0 10px">                         api.trigger( 'saved', response );
</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">-                api.bind( 'saved', function() {
-                       api.each( function( setting ) {
-                               setting._dirty = false;
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         /*
+                * Clear dirty flag for settings when saved to changeset so that they
+                * won't be needlessly included in selective refresh or ajax requests.
+                */
+               api.preview.bind( 'changeset-saved', function( data ) {
+                       _.each( data.saved_changeset_values, function( value, settingId ) {
+                               var setting = api( settingId );
+                               if ( setting && _.isEqual( setting.get(), value ) ) {
+                                       setting._dirty = false;
+                               }
</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">@@ -192,12 +692,16 @@
</span><span class="cx" style="display: block; padding: 0 10px">                 * containers and controls are active.
</span><span class="cx" style="display: block; padding: 0 10px">                 */
</span><span class="cx" style="display: block; padding: 0 10px">                api.preview.send( 'ready', {
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                        currentUrl: api.settings.url.self,
</ins><span class="cx" style="display: block; padding: 0 10px">                         activePanels: api.settings.activePanels,
</span><span class="cx" style="display: block; padding: 0 10px">                        activeSections: api.settings.activeSections,
</span><span class="cx" style="display: block; padding: 0 10px">                        activeControls: api.settings.activeControls,
</span><span class="cx" style="display: block; padding: 0 10px">                        settingValidities: api.settings.settingValidities
</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">+                // Send ready when URL changes via JS.
+               setInterval( api.keepAliveCurrentUrl, api.settings.timeouts.keepAliveSend );
+
</ins><span class="cx" style="display: block; padding: 0 10px">                 // Display a loading indicator when preview is reloading, and remove on failure.
</span><span class="cx" style="display: block; padding: 0 10px">                api.preview.bind( 'loading-initiated', function () {
</span><span class="cx" style="display: block; padding: 0 10px">                        $( 'body' ).addClass( 'wp-customizer-unloading' );
</span></span></pre></div>
<a id="trunksrcwpincludesjscustomizeselectiverefreshjs"></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/customize-selective-refresh.js</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/js/customize-selective-refresh.js   2016-10-17 23:53:20 UTC (rev 38809)
+++ trunk/src/wp-includes/js/customize-selective-refresh.js     2016-10-18 20:04:36 UTC (rev 38810)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -11,8 +11,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">                        renderQueryVar: '',
</span><span class="cx" style="display: block; padding: 0 10px">                        l10n: {
</span><span class="cx" style="display: block; padding: 0 10px">                                shiftClickToEdit: ''
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        },
-                       refreshBuffer: 250
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 }
</ins><span class="cx" style="display: block; padding: 0 10px">                 },
</span><span class="cx" style="display: block; padding: 0 10px">                currentRequest: null
</span><span class="cx" style="display: block; padding: 0 10px">        };
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -485,8 +484,9 @@
</span><span class="cx" style="display: block; padding: 0 10px">                return {
</span><span class="cx" style="display: block; padding: 0 10px">                        wp_customize: 'on',
</span><span class="cx" style="display: block; padding: 0 10px">                        nonce: api.settings.nonce.preview,
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        theme: api.settings.theme.stylesheet,
-                       customized: JSON.stringify( dirtyCustomized )
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 customize_theme: api.settings.theme.stylesheet,
+                       customized: JSON.stringify( dirtyCustomized ),
+                       customize_changeset_uuid: api.settings.changeset.uuid
</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">@@ -668,7 +668,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">                                        self._pendingPartialRequests = {};
</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">-                        self.data.refreshBuffer
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 api.settings.timeouts.selectiveRefresh
</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">                return partialRequest.deferred.promise();
</span></span></pre></div>
<a id="trunksrcwpincludespostphp"></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/post.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/post.php    2016-10-17 23:53:20 UTC (rev 38809)
+++ trunk/src/wp-includes/post.php      2016-10-18 20:04:36 UTC (rev 38810)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -111,6 +111,51 @@
</span><span class="cx" style="display: block; padding: 0 10px">                'query_var' => 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">+        register_post_type( 'customize_changeset', array(
+               'labels' => array(
+                       'name'               => _x( 'Changesets', 'post type general name' ),
+                       'singular_name'      => _x( 'Changeset', 'post type singular name' ),
+                       'menu_name'          => _x( 'Changesets', 'admin menu' ),
+                       'name_admin_bar'     => _x( 'Changeset', 'add new on admin bar' ),
+                       'add_new'            => _x( 'Add New', 'Customize Changeset' ),
+                       'add_new_item'       => __( 'Add New Changeset' ),
+                       'new_item'           => __( 'New Changeset' ),
+                       'edit_item'          => __( 'Edit Changeset' ),
+                       'view_item'          => __( 'View Changeset' ),
+                       'all_items'          => __( 'All Changesets' ),
+                       'search_items'       => __( 'Search Changesets' ),
+                       'not_found'          => __( 'No changesets found.' ),
+                       'not_found_in_trash' => __( 'No changesets found in Trash.' ),
+               ),
+               'public' => false,
+               '_builtin' => true, /* internal use only. don't use this when registering your own post type. */
+               'map_meta_cap' => true,
+               'hierarchical' => false,
+               'rewrite' => false,
+               'query_var' => false,
+               'can_export' => false,
+               'delete_with_user' => false,
+               'supports' => array( 'title', 'author' ),
+               'capability_type' => 'customize_changeset',
+               'capabilities' => array(
+                       'create_posts' => 'customize',
+                       'delete_others_posts' => 'customize',
+                       'delete_post' => 'customize',
+                       'delete_posts' => 'customize',
+                       'delete_private_posts' => 'customize',
+                       'delete_published_posts' => 'customize',
+                       'edit_others_posts' => 'customize',
+                       'edit_post' => 'customize',
+                       'edit_posts' => 'customize',
+                       'edit_private_posts' => 'customize',
+                       'edit_published_posts' => 'do_not_allow',
+                       'publish_posts' => 'customize',
+                       'read' => 'read',
+                       'read_post' => 'customize',
+                       'read_private_posts' => 'customize',
+               ),
+       ) );
+
</ins><span class="cx" style="display: block; padding: 0 10px">         register_post_status( 'publish', array(
</span><span class="cx" style="display: block; padding: 0 10px">                'label'       => _x( 'Published', 'post status' ),
</span><span class="cx" style="display: block; padding: 0 10px">                'public'      => true,
</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   2016-10-17 23:53:20 UTC (rev 38809)
+++ trunk/src/wp-includes/script-loader.php     2016-10-18 20:04:36 UTC (rev 38810)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -450,7 +450,7 @@
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        $scripts->add( 'customize-base',     "/wp-includes/js/customize-base$suffix.js",     array( 'jquery', 'json2', 'underscore' ), false, 1 );
</span><span class="cx" style="display: block; padding: 0 10px">        $scripts->add( 'customize-loader',   "/wp-includes/js/customize-loader$suffix.js",   array( 'customize-base' ), false, 1 );
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        $scripts->add( 'customize-preview',  "/wp-includes/js/customize-preview$suffix.js",  array( 'customize-base' ), false, 1 );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ $scripts->add( 'customize-preview',  "/wp-includes/js/customize-preview$suffix.js",  array( 'wp-a11y', 'customize-base' ), false, 1 );
</ins><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><span class="cx" style="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 );
</span></span></pre></div>
<a id="trunksrcwpincludesthemephp"></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/theme.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/theme.php   2016-10-17 23:53:20 UTC (rev 38809)
+++ trunk/src/wp-includes/theme.php     2016-10-18 20:04:36 UTC (rev 38810)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -2066,9 +2066,9 @@
</span><span class="cx" style="display: block; padding: 0 10px">  * Includes and instantiates the WP_Customize_Manager class.
</span><span class="cx" style="display: block; padding: 0 10px">  *
</span><span class="cx" style="display: block; padding: 0 10px">  * Loads the Customizer at plugins_loaded when accessing the customize.php admin
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- * page or when any request includes a wp_customize=on param, either as a GET
- * query var or as POST data. This param is a signal for whether to bootstrap
- * the Customizer when WordPress is loading, especially in the Customizer preview
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ * page or when any request includes a wp_customize=on param or a customize_changeset
+ * param (a UUID). This param is a signal for whether to bootstrap the Customizer when
+ * WordPress is loading, especially in the Customizer preview
</ins><span class="cx" style="display: block; padding: 0 10px">  * or when making Customizer Ajax requests for widgets or menus.
</span><span class="cx" style="display: block; padding: 0 10px">  *
</span><span class="cx" style="display: block; padding: 0 10px">  * @since 3.4.0
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -2076,17 +2076,143 @@
</span><span class="cx" style="display: block; padding: 0 10px">  * @global WP_Customize_Manager $wp_customize
</span><span class="cx" style="display: block; padding: 0 10px">  */
</span><span class="cx" style="display: block; padding: 0 10px"> function _wp_customize_include() {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        if ( ! ( ( isset( $_REQUEST['wp_customize'] ) && 'on' == $_REQUEST['wp_customize'] )
-               || ( is_admin() && 'customize.php' == basename( $_SERVER['PHP_SELF'] ) )
-       ) ) {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
+       $is_customize_admin_page = ( is_admin() && 'customize.php' == basename( $_SERVER['PHP_SELF'] ) );
+       $should_include = (
+               $is_customize_admin_page
+               ||
+               ( isset( $_REQUEST['wp_customize'] ) && 'on' == $_REQUEST['wp_customize'] )
+               ||
+               ( ! empty( $_GET['customize_changeset_uuid'] ) || ! empty( $_POST['customize_changeset_uuid'] ) )
+       );
+
+       if ( ! $should_include ) {
</ins><span class="cx" style="display: block; padding: 0 10px">                 return;
</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">-        require_once ABSPATH . WPINC . '/class-wp-customize-manager.php'; 
-       $GLOBALS['wp_customize'] = new WP_Customize_Manager();
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ /*
+        * Note that wp_unslash() is not being used on the input vars because it is
+        * called before wp_magic_quotes() gets called. Besides this fact, none of
+        * the values should contain any characters needing slashes anyway.
+        */
+       $keys = array( 'changeset_uuid', 'customize_changeset_uuid', 'customize_theme', 'theme', 'customize_messenger_channel' );
+       $input_vars = array_merge(
+               wp_array_slice_assoc( $_GET, $keys ),
+               wp_array_slice_assoc( $_POST, $keys )
+       );
+
+       $theme = null;
+       $changeset_uuid = null;
+       $messenger_channel = null;
+
+       if ( $is_customize_admin_page && isset( $input_vars['changeset_uuid'] ) ) {
+               $changeset_uuid = sanitize_key( $input_vars['changeset_uuid'] );
+       } elseif ( ! empty( $input_vars['customize_changeset_uuid'] ) ) {
+               $changeset_uuid = sanitize_key( $input_vars['customize_changeset_uuid'] );
+       }
+
+       // Note that theme will be sanitized via WP_Theme.
+       if ( $is_customize_admin_page && isset( $input_vars['theme'] ) ) {
+               $theme = $input_vars['theme'];
+       } elseif ( isset( $input_vars['customize_theme'] ) ) {
+               $theme = $input_vars['customize_theme'];
+       }
+       if ( isset( $input_vars['customize_messenger_channel'] ) ) {
+               $messenger_channel = sanitize_key( $input_vars['customize_messenger_channel'] );
+       }
+
+       require_once ABSPATH . WPINC . '/class-wp-customize-manager.php';
+       $GLOBALS['wp_customize'] = new WP_Customize_Manager( compact( 'changeset_uuid', 'theme', 'messenger_channel' ) );
</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><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ * Publish a snapshot's changes.
+ *
+ * @param string  $new_status     New post status.
+ * @param string  $old_status     Old post status.
+ * @param WP_Post $changeset_post Changeset post object.
+ */
+function _wp_customize_publish_changeset( $new_status, $old_status, $changeset_post ) {
+       global $wp_customize;
+
+       $is_publishing_changeset = (
+               'customize_changeset' === $changeset_post->post_type
+               &&
+               'publish' === $new_status
+               &&
+               'publish' !== $old_status
+       );
+       if ( ! $is_publishing_changeset ) {
+               return;
+       }
+
+       if ( empty( $wp_customize ) ) {
+               require_once ABSPATH . WPINC . '/class-wp-customize-manager.php';
+               $wp_customize = new WP_Customize_Manager( $changeset_post->post_name );
+       }
+
+       if ( ! did_action( 'customize_register' ) ) {
+               /*
+                * When running from CLI or Cron, the customize_register action will need
+                * to be triggered in order for core, themes, and plugins to register their
+                * settings. Normally core will add_action( 'customize_register' ) at
+                * priority 10 to register the core settings, and if any themes/plugins
+                * also add_action( 'customize_register' ) at the same priority, they
+                * will have a $wp_customize with those settings registered since they
+                * call add_action() afterward, normally. However, when manually doing
+                * the customize_register action after the setup_theme, then the order
+                * will be reversed for two actions added at priority 10, resulting in
+                * the core settings no longer being available as expected to themes/plugins.
+                * So the following manually calls the method that registers the core
+                * settings up front before doing the action.
+                */
+               remove_action( 'customize_register', array( $wp_customize, 'register_controls' ) );
+               $wp_customize->register_controls();
+
+               /** This filter is documented in /wp-includes/class-wp-customize-manager.php */
+               do_action( 'customize_register', $wp_customize );
+       }
+       $wp_customize->_publish_changeset_values( $changeset_post->ID ) ;
+
+       /*
+        * Trash the changeset post if revisions are not enabled. Unpublished
+        * changesets by default get garbage collected due to the auto-draft status.
+        * When a changeset post is published, however, it would no longer get cleaned
+        * out. Ths is a problem when the changeset posts are never displayed anywhere,
+        * since they would just be endlessly piling up. So here we use the revisions
+        * feature to indicate whether or not a published changeset should get trashed
+        * and thus garbage collected.
+        */
+       if ( ! wp_revisions_enabled( $changeset_post ) ) {
+               wp_trash_post( $changeset_post->ID );
+       }
+}
+
+/**
+ * Filters changeset post data upon insert to ensure post_name is intact.
+ *
+ * This is needed to prevent the post_name from being dropped when the post is
+ * transitioned into pending status by a contributor.
+ *
+ * @since 4.7.0
+ * @see wp_insert_post()
+ *
+ * @param array $post_data          An array of slashed post data.
+ * @param array $supplied_post_data An array of sanitized, but otherwise unmodified post data.
+ * @returns array Filtered data.
+ */
+function _wp_customize_changeset_filter_insert_post_data( $post_data, $supplied_post_data ) {
+       if ( isset( $post_data['post_type'] ) && 'customize_changeset' === $post_data['post_type'] ) {
+
+               // Prevent post_name from being dropped, such as when contributor saves a changeset post as pending.
+               if ( empty( $post_data['post_name'] ) && ! empty( $supplied_post_data['post_name'] ) ) {
+                       $post_data['post_name'] = $supplied_post_data['post_name'];
+               }
+       }
+       return $post_data;
+}
+
+/**
</ins><span class="cx" style="display: block; padding: 0 10px">  * Adds settings for the customize-loader script.
</span><span class="cx" style="display: block; padding: 0 10px">  *
</span><span class="cx" style="display: block; padding: 0 10px">  * @since 3.4.0
</span></span></pre></div>
<a id="trunktestsphpunittestsadminbarphp"></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/adminbar.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/adminbar.php    2016-10-17 23:53:20 UTC (rev 38809)
+++ trunk/tests/phpunit/tests/adminbar.php      2016-10-18 20:04:36 UTC (rev 38810)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -550,4 +550,37 @@
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertNull( $node );
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
+       /**
+        * @ticket 30937
+        * @covers wp_admin_bar_customize_menu()
+        */
+       public function test_customize_link() {
+               global $wp_customize;
+               require_once ABSPATH . WPINC . '/class-wp-customize-manager.php';
+               $uuid = wp_generate_uuid4();
+               $this->go_to( home_url( "/?customize_changeset_uuid=$uuid" ) );
+               wp_set_current_user( self::$admin_id );
+
+               $this->factory()->post->create( array(
+                       'post_type' => 'customize_changeset',
+                       'post_status' => 'auto-draft',
+                       'post_name' => $uuid,
+               ) );
+               $wp_customize = new WP_Customize_Manager( array(
+                       'changeset_uuid' => $uuid,
+               ) );
+               $wp_customize->start_previewing_theme();
+
+               set_current_screen( 'front' );
+               $wp_admin_bar = $this->get_standard_admin_bar();
+               $node = $wp_admin_bar->get_node( 'customize' );
+               $this->assertNotEmpty( $node );
+
+               $parsed_url = wp_parse_url( $node->href );
+               $query_params = array();
+               wp_parse_str( $parsed_url['query'], $query_params );
+               $this->assertEquals( $uuid, $query_params['changeset_uuid'] );
+               $this->assertNotContains( 'changeset_uuid', $query_params['url'] );
+       }
</ins><span class="cx" style="display: block; padding: 0 10px"> }
</span></span></pre></div>
<a id="trunktestsphpunittestsajaxCustomizeManagerphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: 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                               (rev 0)
+++ trunk/tests/phpunit/tests/ajax/CustomizeManager.php 2016-10-18 20:04:36 UTC (rev 38810)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,310 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * Testing ajax customize manager functionality
+ *
+ * @package    WordPress
+ * @subpackage UnitTests
+ * @since      4.3.0
+ * @group      ajax
+ */
+class Tests_Ajax_CustomizeManager extends WP_Ajax_UnitTestCase {
+
+       /**
+        * Instance of WP_Customize_Manager which is reset for each test.
+        *
+        * @var WP_Customize_Manager
+        */
+       public $wp_customize;
+
+       /**
+        * Admin user ID.
+        *
+        * @var int
+        */
+       protected static $admin_user_id;
+
+       /**
+        * Subscriber user ID.
+        *
+        * @var int
+        */
+       protected static $subscriber_user_id;
+
+       /**
+        * Last response parsed.
+        *
+        * @var array|null
+        */
+       protected $_last_response_parsed;
+
+       /**
+        * Set up before class.
+        *
+        * @param WP_UnitTest_Factory $factory Factory.
+        */
+       public static function wpSetUpBeforeClass( $factory ) {
+               self::$subscriber_user_id = $factory->user->create( array( 'role' => 'subscriber' ) );
+               self::$admin_user_id = $factory->user->create( array( 'role' => 'administrator' ) );
+       }
+
+       /**
+        * Set up the test fixture.
+        */
+       public function setUp() {
+               parent::setUp();
+               require_once ABSPATH . WPINC . '/class-wp-customize-manager.php';
+       }
+
+       /**
+        * Tear down.
+        */
+       public function tearDown() {
+               parent::tearDown();
+               $_REQUEST = array();
+       }
+
+       /**
+        * Helper to keep it DRY
+        *
+        * @param string $action Action.
+        */
+       protected function make_ajax_call( $action ) {
+               $this->_last_response_parsed = null;
+               $this->_last_response = '';
+               try {
+                       $this->_handleAjax( $action );
+               } catch ( WPAjaxDieContinueException $e ) {
+                       unset( $e );
+               }
+               if ( $this->_last_response ) {
+                       $this->_last_response_parsed = json_decode( $this->_last_response, true );
+               }
+       }
+
+       /**
+        * Overridden caps for user_has_cap.
+        *
+        * @var array
+        */
+       protected $overridden_caps = array();
+
+       /**
+        * Dynamically filter a user's capabilities.
+        *
+        * @param array $allcaps An array of all the user's capabilities.
+        * @return array All caps.
+        */
+       function filter_user_has_cap( $allcaps ) {
+               $allcaps = array_merge( $allcaps, $this->overridden_caps );
+               return $allcaps;
+       }
+
+       /**
+        * Test WP_Customize_Manager::save().
+        *
+        * @ticket 30937
+        * @covers WP_Customize_Manager::save()
+        */
+       function test_save_failures() {
+               global $wp_customize;
+               $wp_customize = new WP_Customize_Manager();
+               $wp_customize->register_controls();
+               add_filter( 'user_has_cap', array( $this, 'filter_user_has_cap' ) );
+
+               // Unauthenticated.
+               wp_set_current_user( 0 );
+               $this->make_ajax_call( 'customize_save' );
+               $this->assertFalse( $this->_last_response_parsed['success'] );
+               $this->assertEquals( 'unauthenticated', $this->_last_response_parsed['data'] );
+
+               // Unauthorized.
+               wp_set_current_user( self::$subscriber_user_id );
+               $nonce = wp_create_nonce( 'save-customize_' . $wp_customize->get_stylesheet() );
+               $_POST['nonce'] = $_GET['nonce'] = $_REQUEST['nonce'] = $nonce;
+               $exception = null;
+               try {
+                       ob_start();
+                       $wp_customize->setup_theme();
+               } catch ( WPAjaxDieContinueException $e ) {
+                       $exception = $e;
+               }
+               $this->assertNotEmpty( $e );
+               $this->assertEquals( -1, $e->getMessage() );
+
+               // Not called setup_theme.
+               wp_set_current_user( self::$admin_user_id );
+               $nonce = wp_create_nonce( 'save-customize_' . $wp_customize->get_stylesheet() );
+               $_POST['nonce'] = $_GET['nonce'] = $_REQUEST['nonce'] = $nonce;
+               $this->make_ajax_call( 'customize_save' );
+               $this->assertFalse( $this->_last_response_parsed['success'] );
+               $this->assertEquals( 'not_preview', $this->_last_response_parsed['data'] );
+
+               // Bad nonce.
+               $_POST['nonce'] = $_GET['nonce'] = $_REQUEST['nonce'] = 'bad';
+               $wp_customize->setup_theme();
+               $this->make_ajax_call( 'customize_save' );
+               $this->assertFalse( $this->_last_response_parsed['success'] );
+               $this->assertEquals( 'invalid_nonce', $this->_last_response_parsed['data'] );
+
+               // User cannot create.
+               $nonce = wp_create_nonce( 'save-customize_' . $wp_customize->get_stylesheet() );
+               $_POST['nonce'] = $_GET['nonce'] = $_REQUEST['nonce'] = $nonce;
+               $post_type_obj = get_post_type_object( 'customize_changeset' );
+               $post_type_obj->cap->create_posts = 'create_customize_changesets';
+               $this->make_ajax_call( 'customize_save' );
+               $this->assertFalse( $this->_last_response_parsed['success'] );
+               $this->assertEquals( 'cannot_create_changeset_post', $this->_last_response_parsed['data'] );
+               $this->overridden_caps[ $post_type_obj->cap->create_posts ] = true;
+               $this->make_ajax_call( 'customize_save' );
+               $this->assertTrue( $this->_last_response_parsed['success'] );
+               $post_type_obj->cap->create_posts = 'customize'; // Restore.
+
+               // Changeset already published.
+               $wp_customize->set_post_value( 'blogname', 'Hello' );
+               $wp_customize->save_changeset_post( array( 'status' => 'publish' ) );
+               $this->make_ajax_call( 'customize_save' );
+               $this->assertFalse( $this->_last_response_parsed['success'] );
+               $this->assertEquals( 'changeset_already_published', $this->_last_response_parsed['data'] );
+               wp_update_post( array( 'ID' => $wp_customize->changeset_post_id(), 'post_status' => 'auto-draft' ) );
+
+               // User cannot edit.
+               $post_type_obj = get_post_type_object( 'customize_changeset' );
+               $post_type_obj->cap->edit_post = 'edit_customize_changesets';
+               $this->make_ajax_call( 'customize_save' );
+               $this->assertFalse( $this->_last_response_parsed['success'] );
+               $this->assertEquals( 'cannot_edit_changeset_post', $this->_last_response_parsed['data'] );
+               $this->overridden_caps[ $post_type_obj->cap->edit_post ] = true;
+               $this->make_ajax_call( 'customize_save' );
+               $this->assertTrue( $this->_last_response_parsed['success'] );
+               $post_type_obj->cap->edit_post = 'customize'; // Restore.
+
+               // Bad customize_changeset_data.
+               $_POST['customize_changeset_data'] = '[MALFORMED]';
+               $this->make_ajax_call( 'customize_save' );
+               $this->assertFalse( $this->_last_response_parsed['success'] );
+               $this->assertEquals( 'invalid_customize_changeset_data', $this->_last_response_parsed['data'] );
+
+               // Bad customize_changeset_status.
+               $_POST['customize_changeset_data'] = '{}';
+               $_POST['customize_changeset_status'] = 'unrecognized';
+               $this->make_ajax_call( 'customize_save' );
+               $this->assertFalse( $this->_last_response_parsed['success'] );
+               $this->assertEquals( 'bad_customize_changeset_status', $this->_last_response_parsed['data'] );
+
+               // Disallowed publish posts if not allowed.
+               $post_type_obj = get_post_type_object( 'customize_changeset' );
+               $post_type_obj->cap->publish_posts = 'publish_customize_changesets';
+               $_POST['customize_changeset_status'] = 'publish';
+               $this->make_ajax_call( 'customize_save' );
+               $this->assertFalse( $this->_last_response_parsed['success'] );
+               $this->assertEquals( 'changeset_publish_unauthorized', $this->_last_response_parsed['data'] );
+               $_POST['customize_changeset_status'] = 'future';
+               $this->make_ajax_call( 'customize_save' );
+               $this->assertFalse( $this->_last_response_parsed['success'] );
+               $this->assertEquals( 'changeset_publish_unauthorized', $this->_last_response_parsed['data'] );
+               $post_type_obj->cap->publish_posts = 'customize'; // Restore.
+
+               // Validate date.
+               $_POST['customize_changeset_status'] = 'draft';
+               $_POST['customize_changeset_date'] = 'BAD DATE';
+               $this->make_ajax_call( 'customize_save' );
+               $this->assertFalse( $this->_last_response_parsed['success'] );
+               $this->assertEquals( 'bad_customize_changeset_date', $this->_last_response_parsed['data'] );
+               $_POST['customize_changeset_date'] = '2010-01-01 00:00:00';
+               $this->make_ajax_call( 'customize_save' );
+               $this->assertFalse( $this->_last_response_parsed['success'] );
+               $this->assertEquals( 'not_future_date', $this->_last_response_parsed['data'] );
+               $_POST['customize_changeset_date'] = ( gmdate( 'Y' ) + 1 ) . '-01-01 00:00:00';
+               $this->make_ajax_call( 'customize_save' );
+               $this->assertTrue( $this->_last_response_parsed['success'] );
+               $_POST['customize_changeset_status'] = 'future';
+               $_POST['customize_changeset_date'] = '+10 minutes';
+               $this->make_ajax_call( 'customize_save' );
+               $this->assertTrue( $this->_last_response_parsed['success'] );
+               $this->assertEquals( 'future', get_post_status( $wp_customize->changeset_post_id() ) );
+               wp_update_post( array( 'ID' => $wp_customize->changeset_post_id(), 'post_status' => 'auto-draft' ) );
+
+       }
+
+       /**
+        * Set up valid user state.
+        *
+        * @param string $uuid Changeset UUID.
+        * @return WP_Customize_Manager
+        */
+       protected function set_up_valid_state( $uuid = null ) {
+               global $wp_customize;
+               wp_set_current_user( self::$admin_user_id );
+               $wp_customize = new WP_Customize_Manager( array(
+                       'changeset_uuid' => $uuid,
+               ) );
+               $wp_customize->register_controls();
+               $nonce = wp_create_nonce( 'save-customize_' . $wp_customize->get_stylesheet() );
+               $_POST['nonce'] = $_GET['nonce'] = $_REQUEST['nonce'] = $nonce;
+               $wp_customize->setup_theme();
+               return $wp_customize;
+       }
+
+       /**
+        * Test WP_Customize_Manager::save().
+        *
+        * @ticket 30937
+        * @covers WP_Customize_Manager::save()
+        */
+       function test_save_success_publish_create() {
+               $wp_customize = $this->set_up_valid_state();
+
+               // Successful future.
+               $_POST['customize_changeset_status'] = 'publish';
+               $_POST['customize_changeset_title'] = 'Success Changeset';
+               $_POST['customize_changeset_data'] = wp_json_encode( array(
+                       'blogname' => array(
+                               'value' => 'Successful Site Title',
+                       ),
+               ) );
+               $this->make_ajax_call( 'customize_save' );
+               $this->assertTrue( $this->_last_response_parsed['success'] );
+               $this->assertInternalType( 'array', $this->_last_response_parsed['data'] );
+
+               $this->assertEquals( 'publish', $this->_last_response_parsed['data']['changeset_status'] );
+               $this->assertArrayHasKey( 'next_changeset_uuid', $this->_last_response_parsed['data'] );
+               $this->assertEquals( 'Success Changeset', get_post( $wp_customize->changeset_post_id() )->post_title );
+               $this->assertEquals( 'Successful Site Title', get_option( 'blogname' ) );
+
+       }
+
+       /**
+        * Test WP_Customize_Manager::save().
+        *
+        * @ticket 30937
+        * @covers WP_Customize_Manager::save()
+        */
+       function test_save_success_publish_edit() {
+               $uuid = wp_generate_uuid4();
+
+               $post_id = $this->factory()->post->create( array(
+                       'post_name' => $uuid,
+                       'post_title' => 'Original',
+                       'post_type' => 'customize_changeset',
+                       'post_status' => 'auto-draft',
+                       'post_content' => wp_json_encode( array(
+                               'blogname' => array(
+                                       'value' => 'New Site Title',
+                               ),
+                       ) ),
+               ) );
+               $wp_customize = $this->set_up_valid_state( $uuid );
+
+               // Successful future.
+               $_POST['customize_changeset_status'] = 'publish';
+               $_POST['customize_changeset_title'] = 'Published';
+               $this->make_ajax_call( 'customize_save' );
+               $this->assertTrue( $this->_last_response_parsed['success'] );
+               $this->assertInternalType( 'array', $this->_last_response_parsed['data'] );
+
+               $this->assertEquals( 'publish', $this->_last_response_parsed['data']['changeset_status'] );
+               $this->assertArrayHasKey( 'next_changeset_uuid', $this->_last_response_parsed['data'] );
+               $this->assertEquals( 'New Site Title', get_option( 'blogname' ) );
+               $this->assertEquals( 'Published', get_post( $post_id )->post_title );
+       }
+}
</ins></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   2016-10-17 23:53:20 UTC (rev 38809)
+++ trunk/tests/phpunit/tests/customize/manager.php     2016-10-18 20:04:36 UTC (rev 38810)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -27,6 +27,30 @@
</span><span class="cx" style="display: block; padding: 0 10px">        public $undefined;
</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">+         * Admin user ID.
+        *
+        * @var int
+        */
+       protected static $admin_user_id;
+
+       /**
+        * Subscriber user ID.
+        *
+        * @var int
+        */
+       protected static $subscriber_user_id;
+
+       /**
+        * Set up before class.
+        *
+        * @param WP_UnitTest_Factory $factory Factory.
+        */
+       public static function wpSetUpBeforeClass( $factory ) {
+               self::$subscriber_user_id = $factory->user->create( array( 'role' => 'subscriber' ) );
+               self::$admin_user_id = $factory->user->create( array( 'role' => 'administrator' ) );
+       }
+
+       /**
</ins><span class="cx" style="display: block; padding: 0 10px">          * Set up test.
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><span class="cx" style="display: block; padding: 0 10px">        function setUp() {
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -42,6 +66,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">        function tearDown() {
</span><span class="cx" style="display: block; padding: 0 10px">                $this->manager = null;
</span><span class="cx" style="display: block; padding: 0 10px">                unset( $GLOBALS['wp_customize'] );
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                $_REQUEST = array();
</ins><span class="cx" style="display: block; padding: 0 10px">                 parent::tearDown();
</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">@@ -56,6 +81,608 @@
</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">+         * Test WP_Customize_Manager::__construct().
+        *
+        * @covers WP_Customize_Manager::__construct()
+        */
+       function test_constructor() {
+               $uuid = wp_generate_uuid4();
+               $theme = 'twentyfifteen';
+               $messenger_channel = 'preview-123';
+               $wp_customize = new WP_Customize_Manager( array(
+                       'changeset_uuid' => $uuid,
+                       'theme' => $theme,
+                       'messenger_channel' => $messenger_channel,
+               ) );
+               $this->assertEquals( $uuid, $wp_customize->changeset_uuid() );
+               $this->assertEquals( $theme, $wp_customize->get_stylesheet() );
+               $this->assertEquals( $messenger_channel, $wp_customize->get_messenger_channel() );
+
+               $theme = 'twentyfourteen';
+               $messenger_channel = 'preview-456';
+               $_REQUEST['theme'] = $theme;
+               $_REQUEST['customize_messenger_channel'] = $messenger_channel;
+               $wp_customize = new WP_Customize_Manager( array( 'changeset_uuid' => $uuid ) );
+               $this->assertEquals( $theme, $wp_customize->get_stylesheet() );
+               $this->assertEquals( $messenger_channel, $wp_customize->get_messenger_channel() );
+
+               $theme = 'twentyfourteen';
+               $_REQUEST['customize_theme'] = $theme;
+               $wp_customize = new WP_Customize_Manager();
+               $this->assertEquals( $theme, $wp_customize->get_stylesheet() );
+               $this->assertNotEmpty( $wp_customize->changeset_uuid() );
+       }
+
+       /**
+        * Test WP_Customize_Manager::setup_theme() for admin screen.
+        *
+        * @covers WP_Customize_Manager::setup_theme()
+        */
+       function test_setup_theme_in_customize_admin() {
+               global $pagenow, $wp_customize;
+               $pagenow = 'customize.php';
+               set_current_screen( 'customize' );
+
+               // Unauthorized.
+               $exception = null;
+               $wp_customize = new WP_Customize_Manager();
+               wp_set_current_user( self::$subscriber_user_id );
+               try {
+                       $wp_customize->setup_theme();
+               } catch ( Exception $e ) {
+                       $exception = $e;
+               }
+               $this->assertInstanceOf( 'WPDieException', $exception );
+               $this->assertContains( 'you are not allowed to customize this site', $exception->getMessage() );
+
+               // Bad changeset.
+               $exception = null;
+               wp_set_current_user( self::$admin_user_id );
+               $wp_customize = new WP_Customize_Manager( array( 'changeset_uuid' => 'bad' ) );
+               try {
+                       $wp_customize->setup_theme();
+               } catch ( Exception $e ) {
+                       $exception = $e;
+               }
+               $this->assertInstanceOf( 'WPDieException', $exception );
+               $this->assertContains( 'Invalid changeset UUID', $exception->getMessage() );
+
+               $wp_customize = new WP_Customize_Manager();
+               $wp_customize->setup_theme();
+       }
+
+       /**
+        * Test WP_Customize_Manager::setup_theme() for frontend.
+        *
+        * @covers WP_Customize_Manager::setup_theme()
+        */
+       function test_setup_theme_in_frontend() {
+               global $wp_customize, $pagenow, $show_admin_bar;
+               $pagenow = 'front';
+               set_current_screen( 'front' );
+
+               wp_set_current_user( 0 );
+               $exception = null;
+               $wp_customize = new WP_Customize_Manager();
+               wp_set_current_user( self::$subscriber_user_id );
+               try {
+                       $wp_customize->setup_theme();
+               } catch ( Exception $e ) {
+                       $exception = $e;
+               }
+               $this->assertInstanceOf( 'WPDieException', $exception );
+               $this->assertContains( 'Non-existent changeset UUID', $exception->getMessage() );
+
+               wp_set_current_user( self::$admin_user_id );
+               $wp_customize = new WP_Customize_Manager( array( 'messenger_channel' => 'preview-1' ) );
+               $wp_customize->setup_theme();
+               $this->assertFalse( $show_admin_bar );
+
+               show_admin_bar( true );
+               wp_set_current_user( self::$admin_user_id );
+               $wp_customize = new WP_Customize_Manager( array( 'messenger_channel' => null ) );
+               $wp_customize->setup_theme();
+               $this->assertTrue( $show_admin_bar );
+       }
+
+       /**
+        * Test WP_Customize_Manager::changeset_uuid().
+        *
+        * @ticket 30937
+        * @covers WP_Customize_Manager::changeset_uuid()
+        */
+       function test_changeset_uuid() {
+               $uuid = wp_generate_uuid4();
+               $wp_customize = new WP_Customize_Manager( array( 'changeset_uuid' => $uuid ) );
+               $this->assertEquals( $uuid, $wp_customize->changeset_uuid() );
+       }
+
+       /**
+        * Test WP_Customize_Manager::wp_loaded().
+        *
+        * Ensure that post values are previewed even without being in preview.
+        *
+        * @ticket 30937
+        * @covers WP_Customize_Manager::wp_loaded()
+        */
+       function test_wp_loaded() {
+               wp_set_current_user( self::$admin_user_id );
+               $wp_customize = new WP_Customize_Manager();
+               $title = 'Hello World';
+               $wp_customize->set_post_value( 'blogname', $title );
+               $this->assertNotEquals( $title, get_option( 'blogname' ) );
+               $wp_customize->wp_loaded();
+               $this->assertFalse( $wp_customize->is_preview() );
+               $this->assertEquals( $title, $wp_customize->get_setting( 'blogname' )->value() );
+               $this->assertEquals( $title, get_option( 'blogname' ) );
+       }
+
+       /**
+        * Test WP_Customize_Manager::find_changeset_post_id().
+        *
+        * @ticket 30937
+        * @covers WP_Customize_Manager::find_changeset_post_id()
+        */
+       function test_find_changeset_post_id() {
+               $uuid = wp_generate_uuid4();
+               $post_id = $this->factory()->post->create( array(
+                       'post_name' => $uuid,
+                       'post_type' => 'customize_changeset',
+                       'post_status' => 'auto-draft',
+                       'post_content' => '{}',
+               ) );
+
+               $wp_customize = new WP_Customize_Manager();
+               $this->assertNull( $wp_customize->find_changeset_post_id( wp_generate_uuid4() ) );
+               $this->assertEquals( $post_id, $wp_customize->find_changeset_post_id( $uuid ) );
+       }
+
+       /**
+        * Test WP_Customize_Manager::changeset_post_id().
+        *
+        * @ticket 30937
+        * @covers WP_Customize_Manager::changeset_post_id()
+        */
+       function test_changeset_post_id() {
+               $uuid = wp_generate_uuid4();
+               $wp_customize = new WP_Customize_Manager( array( 'changeset_uuid' => $uuid ) );
+               $this->assertNull( $wp_customize->changeset_post_id() );
+
+               $uuid = wp_generate_uuid4();
+               $wp_customize = new WP_Customize_Manager( array( 'changeset_uuid' => $uuid ) );
+               $post_id = $this->factory()->post->create( array(
+                       'post_name' => $uuid,
+                       'post_type' => 'customize_changeset',
+                       'post_status' => 'auto-draft',
+                       'post_content' => '{}',
+               ) );
+               $this->assertEquals( $post_id, $wp_customize->changeset_post_id() );
+       }
+
+       /**
+        * Test WP_Customize_Manager::changeset_data().
+        *
+        * @ticket 30937
+        * @covers WP_Customize_Manager::changeset_data()
+        */
+       function test_changeset_data() {
+               $uuid = wp_generate_uuid4();
+               $wp_customize = new WP_Customize_Manager( array( 'changeset_uuid' => $uuid ) );
+               $this->assertEquals( array(), $wp_customize->changeset_data() );
+
+               $uuid = wp_generate_uuid4();
+               $data = array( 'blogname' => array( 'value' => 'Hello World' ) );
+               $this->factory()->post->create( array(
+                       'post_name' => $uuid,
+                       'post_type' => 'customize_changeset',
+                       'post_status' => 'auto-draft',
+                       'post_content' => wp_json_encode( $data ),
+               ) );
+               $wp_customize = new WP_Customize_Manager( array( 'changeset_uuid' => $uuid ) );
+               $this->assertEquals( $data, $wp_customize->changeset_data() );
+       }
+
+       /**
+        * Test WP_Customize_Manager::customize_preview_init().
+        *
+        * @ticket 30937
+        * @covers WP_Customize_Manager::customize_preview_init()
+        */
+       function test_customize_preview_init() {
+
+               // Test authorized admin user.
+               wp_set_current_user( self::$admin_user_id );
+               $did_action_customize_preview_init = did_action( 'customize_preview_init' );
+               $wp_customize = new WP_Customize_Manager();
+               $wp_customize->customize_preview_init();
+               $this->assertEquals( $did_action_customize_preview_init + 1, did_action( 'customize_preview_init' ) );
+
+               $this->assertEquals( 10, has_action( 'wp_head', 'wp_no_robots' ) );
+               $this->assertEquals( 10, has_filter( 'wp_headers', array( $wp_customize, 'filter_iframe_security_headers' ) ) );
+               $this->assertEquals( 10, has_filter( 'wp_redirect', array( $wp_customize, 'add_state_query_params' ) ) );
+               $this->assertTrue( wp_script_is( 'customize-preview', 'enqueued' ) );
+               $this->assertEquals( 10, has_action( 'wp_head', array( $wp_customize, 'customize_preview_loading_style' ) ) );
+               $this->assertEquals( 20, has_action( 'wp_footer', array( $wp_customize, 'customize_preview_settings' ) ) );
+
+               // Test unauthorized user outside preview (no messenger_channel).
+               wp_set_current_user( self::$subscriber_user_id );
+               $wp_customize = new WP_Customize_Manager();
+               $wp_customize->register_controls();
+               $this->assertNotEmpty( $wp_customize->controls() );
+               $wp_customize->customize_preview_init();
+               $this->assertEmpty( $wp_customize->controls() );
+
+               // Test unauthorized user inside preview (with messenger_channel).
+               wp_set_current_user( self::$subscriber_user_id );
+               $wp_customize = new WP_Customize_Manager( array( 'messenger_channel' => 'preview-0' ) );
+               $exception = null;
+               try {
+                       $wp_customize->customize_preview_init();
+               } catch ( WPDieException $e ) {
+                       $exception = $e;
+               }
+               $this->assertNotNull( $exception );
+               $this->assertContains( 'Unauthorized', $exception->getMessage() );
+       }
+
+       /**
+        * Test WP_Customize_Manager::filter_iframe_security_headers().
+        *
+        * @ticket 30937
+        * @covers WP_Customize_Manager::filter_iframe_security_headers()
+        */
+       function test_filter_iframe_security_headers() {
+               $customize_url = admin_url( 'customize.php' );
+               $wp_customize = new WP_Customize_Manager();
+               $headers = $wp_customize->filter_iframe_security_headers( array() );
+               $this->assertArrayHasKey( 'X-Frame-Options', $headers );
+               $this->assertArrayHasKey( 'Content-Security-Policy', $headers );
+               $this->assertEquals( "ALLOW-FROM $customize_url", $headers['X-Frame-Options'] );
+       }
+
+       /**
+        * Test WP_Customize_Manager::add_state_query_params().
+        *
+        * @ticket 30937
+        * @covers WP_Customize_Manager::add_state_query_params()
+        */
+       function test_add_state_query_params() {
+               $uuid = wp_generate_uuid4();
+               $messenger_channel = 'preview-0';
+               $wp_customize = new WP_Customize_Manager( array(
+                       'changeset_uuid' => $uuid,
+                       'messenger_channel' => $messenger_channel,
+               ) );
+               $url = $wp_customize->add_state_query_params( home_url( '/' ) );
+               $parsed_url = wp_parse_url( $url );
+               parse_str( $parsed_url['query'], $query_params );
+               $this->assertArrayHasKey( 'customize_messenger_channel', $query_params );
+               $this->assertArrayHasKey( 'customize_changeset_uuid', $query_params );
+               $this->assertArrayNotHasKey( 'customize_theme', $query_params );
+               $this->assertEquals( $uuid, $query_params['customize_changeset_uuid'] );
+               $this->assertEquals( $messenger_channel, $query_params['customize_messenger_channel'] );
+
+               $uuid = wp_generate_uuid4();
+               $wp_customize = new WP_Customize_Manager( array(
+                       'changeset_uuid' => $uuid,
+                       'messenger_channel' => null,
+                       'theme' => 'twentyfifteen',
+               ) );
+               $url = $wp_customize->add_state_query_params( home_url( '/' ) );
+               $parsed_url = wp_parse_url( $url );
+               parse_str( $parsed_url['query'], $query_params );
+               $this->assertArrayNotHasKey( 'customize_messenger_channel', $query_params );
+               $this->assertArrayHasKey( 'customize_changeset_uuid', $query_params );
+               $this->assertArrayHasKey( 'customize_theme', $query_params );
+               $this->assertEquals( $uuid, $query_params['customize_changeset_uuid'] );
+               $this->assertEquals( 'twentyfifteen', $query_params['customize_theme'] );
+
+               $uuid = wp_generate_uuid4();
+               $wp_customize = new WP_Customize_Manager( array(
+                       'changeset_uuid' => $uuid,
+                       'messenger_channel' => null,
+                       'theme' => 'twentyfifteen',
+               ) );
+               $url = $wp_customize->add_state_query_params( 'http://not-allowed.example.com/?q=1' );
+               $parsed_url = wp_parse_url( $url );
+               parse_str( $parsed_url['query'], $query_params );
+               $this->assertArrayNotHasKey( 'customize_messenger_channel', $query_params );
+               $this->assertArrayNotHasKey( 'customize_changeset_uuid', $query_params );
+               $this->assertArrayNotHasKey( 'customize_theme', $query_params );
+       }
+
+       /**
+        * Test WP_Customize_Manager::save_changeset_post().
+        *
+        * @ticket 30937
+        * @covers WP_Customize_Manager::save_changeset_post()
+        */
+       function test_save_changeset_post_without_theme_activation() {
+               wp_set_current_user( self::$admin_user_id );
+
+               $did_action = array(
+                       'customize_save_validation_before' => did_action( 'customize_save_validation_before' ),
+                       'customize_save' => did_action( 'customize_save' ),
+                       'customize_save_after' => did_action( 'customize_save_after' ),
+               );
+               $uuid = wp_generate_uuid4();
+
+               $manager = new WP_Customize_Manager( array(
+                       'changeset_uuid' => $uuid,
+               ) );
+               $manager->register_controls();
+               $manager->set_post_value( 'blogname', 'Changeset Title' );
+               $manager->set_post_value( 'blogdescription', 'Changeset Tagline' );
+
+               $r = $manager->save_changeset_post( array(
+                       'status' => 'auto-draft',
+                       'title' => 'Auto Draft',
+                       'date_gmt' => '2010-01-01 00:00:00',
+                       'data' => array(
+                               'blogname' => array(
+                                       'value' => 'Overridden Changeset Title',
+                               ),
+                               'blogdescription' => array(
+                                       'custom' => 'something',
+                               ),
+                       ),
+               ) );
+               $this->assertInternalType( 'array', $r );
+
+               $this->assertEquals( $did_action['customize_save_validation_before'] + 1, did_action( 'customize_save_validation_before' ) );
+
+               $post_id = $manager->find_changeset_post_id( $uuid );
+               $this->assertNotNull( $post_id );
+               $saved_data = json_decode( get_post( $post_id )->post_content, true );
+               $this->assertEquals( $manager->unsanitized_post_values(), wp_list_pluck( $saved_data, 'value' ) );
+               $this->assertEquals( 'Overridden Changeset Title', $saved_data['blogname']['value'] );
+               $this->assertEquals( 'something', $saved_data['blogdescription']['custom'] );
+               $this->assertEquals( 'Auto Draft', get_post( $post_id )->post_title );
+               $this->assertEquals( 'auto-draft', get_post( $post_id )->post_status );
+               $this->assertEquals( '2010-01-01 00:00:00', get_post( $post_id )->post_date_gmt );
+               $this->assertNotEquals( 'Changeset Title', get_option( 'blogname' ) );
+               $this->assertArrayHasKey( 'setting_validities', $r );
+
+               // Test saving with invalid settings, ensuring transaction blocked.
+               $previous_saved_data = $saved_data;
+               $manager->add_setting( 'foo_unauthorized', array(
+                       'capability' => 'do_not_allow',
+               ) );
+               $manager->add_setting( 'baz_illegal', array(
+                       'validate_callback' => array( $this, 'return_illegal_error' ),
+               ) );
+               $r = $manager->save_changeset_post( array(
+                       'status' => 'auto-draft',
+                       'data' => array(
+                               'blogname' => array(
+                                       'value' => 'OK',
+                               ),
+                               'foo_unauthorized' => array(
+                                       'value' => 'No',
+                               ),
+                               'bar_unknown' => array(
+                                       'value' => 'No',
+                               ),
+                               'baz_illegal' => array(
+                                       'value' => 'No',
+                               ),
+                       ),
+               ) );
+               $this->assertInstanceOf( 'WP_Error', $r );
+               $this->assertEquals( 'transaction_fail', $r->get_error_code() );
+               $this->assertInternalType( 'array', $r->get_error_data() );
+               $this->assertArrayHasKey( 'setting_validities', $r->get_error_data() );
+               $error_data = $r->get_error_data();
+               $this->assertArrayHasKey( 'blogname', $error_data['setting_validities'] );
+               $this->assertTrue( $error_data['setting_validities']['blogname'] );
+               $this->assertArrayHasKey( 'foo_unauthorized', $error_data['setting_validities'] );
+               $this->assertInstanceOf( 'WP_Error', $error_data['setting_validities']['foo_unauthorized'] );
+               $this->assertEquals( 'unauthorized', $error_data['setting_validities']['foo_unauthorized']->get_error_code() );
+               $this->assertArrayHasKey( 'bar_unknown', $error_data['setting_validities'] );
+               $this->assertInstanceOf( 'WP_Error', $error_data['setting_validities']['bar_unknown'] );
+               $this->assertEquals( 'unrecognized', $error_data['setting_validities']['bar_unknown']->get_error_code() );
+               $this->assertArrayHasKey( 'baz_illegal', $error_data['setting_validities'] );
+               $this->assertInstanceOf( 'WP_Error', $error_data['setting_validities']['baz_illegal'] );
+               $this->assertEquals( 'illegal', $error_data['setting_validities']['baz_illegal']->get_error_code() );
+
+               // Since transactional, ensure no changes have been made.
+               $this->assertEquals( $previous_saved_data, json_decode( get_post( $post_id )->post_content, true ) );
+
+               // Attempt a non-transactional/incremental update.
+               $manager = new WP_Customize_Manager( array(
+                       'changeset_uuid' => $uuid,
+               ) );
+               $manager->register_controls(); // That is, register settings.
+               $r = $manager->save_changeset_post( array(
+                       'status' => null,
+                       'data' => array(
+                               'blogname' => array(
+                                       'value' => 'Non-Transactional \o/ <script>unsanitized</script>',
+                               ),
+                               'bar_unknown' => array(
+                                       'value' => 'No',
+                               ),
+                       ),
+               ) );
+               $this->assertInternalType( 'array', $r );
+               $this->assertArrayHasKey( 'setting_validities', $r );
+               $this->assertTrue( $r['setting_validities']['blogname'] );
+               $this->assertInstanceOf( 'WP_Error', $r['setting_validities']['bar_unknown'] );
+               $saved_data = json_decode( get_post( $post_id )->post_content, true );
+               $this->assertNotEquals( $previous_saved_data, $saved_data );
+               $this->assertEquals( 'Non-Transactional \o/ <script>unsanitized</script>', $saved_data['blogname']['value'] );
+
+               // Ensure the filter applies.
+               $customize_changeset_save_data_call_count = $this->customize_changeset_save_data_call_count;
+               add_filter( 'customize_changeset_save_data', array( $this, 'filter_customize_changeset_save_data' ), 10, 2 );
+               $manager->save_changeset_post( array(
+                       'status' => null,
+                       'data' => array(
+                               'blogname' => array(
+                                       'value' => 'Filtered',
+                               ),
+                       ),
+               ) );
+               $this->assertEquals( $customize_changeset_save_data_call_count + 1, $this->customize_changeset_save_data_call_count );
+
+               // Publish the changeset.
+               $manager = new WP_Customize_Manager( array( 'changeset_uuid' => $uuid ) );
+               $manager->register_controls();
+               $GLOBALS['wp_customize'] = $manager;
+               $r = $manager->save_changeset_post( array(
+                       'status' => 'publish',
+                       'data' => array(
+                               'blogname' => array(
+                                       'value' => 'Do it live \o/',
+                               ),
+                       ),
+               ) );
+               $this->assertInternalType( 'array', $r );
+               $this->assertEquals( 'Do it live \o/', get_option( 'blogname' ) );
+               $this->assertEquals( 'trash', get_post_status( $post_id ) ); // Auto-trashed.
+
+               // Test revisions.
+               add_post_type_support( 'customize_changeset', 'revisions' );
+               $uuid = wp_generate_uuid4();
+               $manager = new WP_Customize_Manager( array( 'changeset_uuid' => $uuid ) );
+               $manager->register_controls();
+               $GLOBALS['wp_customize'] = $manager;
+
+               $manager->set_post_value( 'blogname', 'Hello Surface' );
+               $manager->save_changeset_post( array( 'status' => 'auto-draft' ) );
+
+               $manager->set_post_value( 'blogname', 'Hello World' );
+               $manager->save_changeset_post( array( 'status' => 'draft' ) );
+               $this->assertTrue( wp_revisions_enabled( get_post( $manager->changeset_post_id() ) ) );
+
+               $manager->set_post_value( 'blogname', 'Hello Solar System' );
+               $manager->save_changeset_post( array( 'status' => 'draft' ) );
+
+               $manager->set_post_value( 'blogname', 'Hello Galaxy' );
+               $manager->save_changeset_post( array( 'status' => 'draft' ) );
+               $this->assertCount( 3, wp_get_post_revisions( $manager->changeset_post_id() ) );
+       }
+
+       /**
+        * Call count for customize_changeset_save_data filter.
+        *
+        * @var int
+        */
+       protected $customize_changeset_save_data_call_count = 0;
+
+       /**
+        * Filter customize_changeset_save_data.
+        *
+        * @param array $data    Data.
+        * @param array $context Context.
+        * @returns array Data.
+        */
+       function filter_customize_changeset_save_data( $data, $context ) {
+               $this->customize_changeset_save_data_call_count += 1;
+               $this->assertInternalType( 'array', $data );
+               $this->assertInternalType( 'array', $context );
+               $this->assertArrayHasKey( 'uuid', $context );
+               $this->assertArrayHasKey( 'title', $context );
+               $this->assertArrayHasKey( 'status', $context );
+               $this->assertArrayHasKey( 'date_gmt', $context );
+               $this->assertArrayHasKey( 'post_id', $context );
+               $this->assertArrayHasKey( 'previous_data', $context );
+               $this->assertArrayHasKey( 'manager', $context );
+               return $data;
+       }
+
+       /**
+        * Return illegal error.
+        *
+        * @return WP_Error Error.
+        */
+       function return_illegal_error() {
+               return new WP_Error( 'illegal' );
+       }
+
+       /**
+        * Test WP_Customize_Manager::save_changeset_post().
+        *
+        * @ticket 30937
+        * @covers WP_Customize_Manager::save_changeset_post()
+        * @covers WP_Customize_Manager::update_stashed_theme_mod_settings()
+        */
+       function test_save_changeset_post_with_theme_activation() {
+               wp_set_current_user( self::$admin_user_id );
+
+               $stashed_theme_mods = array(
+                       'twentyfifteen' => array(
+                               'background_color' => array(
+                                       'value' => '#123456',
+                               ),
+                       ),
+               );
+               update_option( 'customize_stashed_theme_mods', $stashed_theme_mods );
+               $uuid = wp_generate_uuid4();
+               $manager = new WP_Customize_Manager( array(
+                       'changeset_uuid' => $uuid,
+                       'theme' => 'twentyfifteen',
+               ) );
+               $manager->register_controls();
+               $GLOBALS['wp_customize'] = $manager;
+
+               $manager->set_post_value( 'blogname', 'Hello 2015' );
+               $post_values = $manager->unsanitized_post_values();
+               $manager->save_changeset_post( array( 'status' => 'publish' ) ); // Activate.
+
+               $this->assertEquals( '#123456', $post_values['background_color'] );
+               $this->assertEquals( 'twentyfifteen', get_stylesheet() );
+               $this->assertEquals( 'Hello 2015', get_option( 'blogname' ) );
+       }
+
+       /**
+        * Test WP_Customize_Manager::is_cross_domain().
+        *
+        * @ticket 30937
+        * @covers WP_Customize_Manager::is_cross_domain()
+        */
+       function test_is_cross_domain() {
+               $wp_customize = new WP_Customize_Manager();
+
+               update_option( 'home', 'http://example.com' );
+               update_option( 'siteurl', 'http://example.com' );
+               $this->assertFalse( $wp_customize->is_cross_domain() );
+
+               update_option( 'home', 'http://example.com' );
+               update_option( 'siteurl', 'https://admin.example.com' );
+               $this->assertTrue( $wp_customize->is_cross_domain() );
+       }
+
+       /**
+        * Test WP_Customize_Manager::get_allowed_urls().
+        *
+        * @ticket 30937
+        * @covers WP_Customize_Manager::get_allowed_urls()
+        */
+       function test_get_allowed_urls() {
+               $wp_customize = new WP_Customize_Manager();
+               $this->assertFalse( is_ssl() );
+               $this->assertFalse( $wp_customize->is_cross_domain() );
+               $allowed = $wp_customize->get_allowed_urls();
+               $this->assertEquals( $allowed, array( home_url( '/', 'http' ) ) );
+
+               add_filter( 'customize_allowed_urls', array( $this, 'filter_customize_allowed_urls' ) );
+               $allowed = $wp_customize->get_allowed_urls();
+               $this->assertEqualSets( $allowed, array( 'http://headless.example.com/', home_url( '/', 'http' ) ) );
+       }
+
+       /**
+        * Callback for customize_allowed_urls filter.
+        *
+        * @param array $urls URLs.
+        * @return array URLs.
+        */
+       function filter_customize_allowed_urls( $urls ) {
+               $urls[] = 'http://headless.example.com/';
+               return $urls;
+       }
+
+       /**
</ins><span class="cx" style="display: block; padding: 0 10px">          * Test WP_Customize_Manager::doing_ajax().
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * @group ajax
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -90,7 +717,8 @@
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * @ticket 30988
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        function test_unsanitized_post_values() {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ function test_unsanitized_post_values_from_input() {
+               wp_set_current_user( self::$admin_user_id );
</ins><span class="cx" style="display: block; padding: 0 10px">                 $manager = $this->manager;
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                $customized = array(
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -100,14 +728,126 @@
</span><span class="cx" style="display: block; padding: 0 10px">                $_POST['customized'] = wp_slash( wp_json_encode( $customized ) );
</span><span class="cx" style="display: block; padding: 0 10px">                $post_values = $manager->unsanitized_post_values();
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertEquals( $customized, $post_values );
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                $this->assertEmpty( $manager->unsanitized_post_values( array( 'exclude_post_data' => true ) ) );
+
+               $manager->set_post_value( 'foo', 'BAR' );
+               $post_values = $manager->unsanitized_post_values();
+               $this->assertEquals( 'BAR', $post_values['foo'] );
+               $this->assertEmpty( $manager->unsanitized_post_values( array( 'exclude_post_data' => true ) ) );
+
+               // If user is unprivileged, the post data is ignored.
+               wp_set_current_user( 0 );
+               $this->assertEmpty( $manager->unsanitized_post_values() );
</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><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         * Test WP_Customize_Manager::unsanitized_post_values().
+        *
+        * @ticket 30937
+        * @covers WP_Customize_Manager::unsanitized_post_values()
+        */
+       function test_unsanitized_post_values_with_changeset_and_stashed_theme_mods() {
+               wp_set_current_user( self::$admin_user_id );
+
+               $stashed_theme_mods = array(
+                       'twentyfifteen' => array(
+                               'background_color' => array(
+                                       'value' => '#000000',
+                               ),
+                       ),
+               );
+               $stashed_theme_mods[ get_stylesheet() ] = array(
+                       'background_color' => array(
+                               'value' => '#FFFFFF',
+                       ),
+               );
+               update_option( 'customize_stashed_theme_mods', $stashed_theme_mods );
+
+               $post_values = array(
+                       'blogdescription' => 'Post Input Tagline',
+               );
+               $_POST['customized'] = wp_slash( wp_json_encode( $post_values ) );
+
+               $uuid = wp_generate_uuid4();
+               $changeset_data = array(
+                       'blogname' => array(
+                               'value' => 'Changeset Title',
+                       ),
+                       'blogdescription' => array(
+                               'value' => 'Changeset Tagline',
+                       ),
+               );
+               $this->factory()->post->create( array(
+                       'post_type' => 'customize_changeset',
+                       'post_status' => 'auto-draft',
+                       'post_name' => $uuid,
+                       'post_content' => wp_json_encode( $changeset_data ),
+               ) );
+
+               $manager = new WP_Customize_Manager( array(
+                       'changeset_uuid' => $uuid,
+               ) );
+               $this->assertTrue( $manager->is_theme_active() );
+
+               $this->assertArrayNotHasKey( 'background_color', $manager->unsanitized_post_values() );
+
+               $this->assertEquals(
+                       array(
+                               'blogname' => 'Changeset Title',
+                               'blogdescription' => 'Post Input Tagline',
+                       ),
+                       $manager->unsanitized_post_values()
+               );
+               $this->assertEquals(
+                       array(
+                               'blogdescription' => 'Post Input Tagline',
+                       ),
+                       $manager->unsanitized_post_values( array( 'exclude_changeset' => true ) )
+               );
+
+               $manager->set_post_value( 'blogdescription', 'Post Override Tagline' );
+               $this->assertEquals(
+                       array(
+                               'blogname' => 'Changeset Title',
+                               'blogdescription' => 'Post Override Tagline',
+                       ),
+                       $manager->unsanitized_post_values()
+               );
+
+               $this->assertEquals(
+                       array(
+                               'blogname' => 'Changeset Title',
+                               'blogdescription' => 'Changeset Tagline',
+                       ),
+                       $manager->unsanitized_post_values( array( 'exclude_post_data' => true ) )
+               );
+
+               $this->assertEmpty( $manager->unsanitized_post_values( array( 'exclude_post_data' => true, 'exclude_changeset' => true ) ) );
+
+               // Test unstashing theme mods.
+               $manager = new WP_Customize_Manager( array(
+                       'changeset_uuid' => $uuid,
+                       'theme' => 'twentyfifteen',
+               ) );
+               $this->assertFalse( $manager->is_theme_active() );
+               $values = $manager->unsanitized_post_values( array( 'exclude_post_data' => true, 'exclude_changeset' => true ) );
+               $this->assertNotEmpty( $values );
+               $this->assertArrayHasKey( 'background_color', $values );
+               $this->assertEquals( '#000000', $values['background_color'] );
+
+               $values = $manager->unsanitized_post_values( array( 'exclude_post_data' => false, 'exclude_changeset' => false ) );
+               $this->assertArrayHasKey( 'background_color', $values );
+               $this->assertArrayHasKey( 'blogname', $values );
+               $this->assertArrayHasKey( 'blogdescription', $values );
+       }
+
+       /**
</ins><span class="cx" style="display: block; padding: 0 10px">          * Test the WP_Customize_Manager::post_value() method.
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * @ticket 30988
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><span class="cx" style="display: block; padding: 0 10px">        function test_post_value() {
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                wp_set_current_user( self::$admin_user_id );
</ins><span class="cx" style="display: block; padding: 0 10px">                 $posted_settings = array(
</span><span class="cx" style="display: block; padding: 0 10px">                        'foo' => 'OOF',
</span><span class="cx" style="display: block; padding: 0 10px">                );
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -131,6 +871,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">         * @ticket 34893
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><span class="cx" style="display: block; padding: 0 10px">        function test_invalid_post_value() {
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                wp_set_current_user( self::$admin_user_id );
</ins><span class="cx" style="display: block; padding: 0 10px">                 $default_value = 'foo_default';
</span><span class="cx" style="display: block; padding: 0 10px">                $setting = $this->manager->add_setting( 'foo', array(
</span><span class="cx" style="display: block; padding: 0 10px">                        'validate_callback' => array( $this, 'filter_customize_validate_foo' ),
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -196,6 +937,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">         * @ticket 37247
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><span class="cx" style="display: block; padding: 0 10px">        function test_post_value_validation_sanitization_order() {
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                wp_set_current_user( self::$admin_user_id );
</ins><span class="cx" style="display: block; padding: 0 10px">                 $default_value = '0';
</span><span class="cx" style="display: block; padding: 0 10px">                $setting = $this->manager->add_setting( 'numeric', array(
</span><span class="cx" style="display: block; padding: 0 10px">                        'validate_callback' => array( $this, 'filter_customize_validate_numeric' ),
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -240,6 +982,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">         * @see WP_Customize_Manager::validate_setting_values()
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><span class="cx" style="display: block; padding: 0 10px">        function test_validate_setting_values() {
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                wp_set_current_user( self::$admin_user_id );
</ins><span class="cx" style="display: block; padding: 0 10px">                 $setting = $this->manager->add_setting( 'foo', array(
</span><span class="cx" style="display: block; padding: 0 10px">                        'validate_callback' => array( $this, 'filter_customize_validate_foo' ),
</span><span class="cx" style="display: block; padding: 0 10px">                        'sanitize_callback' => array( $this, 'filter_customize_sanitize_foo' ),
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -305,6 +1048,40 @@
</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">+         * Test WP_Customize_Manager::validate_setting_values().
+        *
+        * @ticket 30937
+        * @covers WP_Customize_Manager::validate_setting_values()
+        */
+       function test_validate_setting_values_args() {
+               wp_set_current_user( self::$admin_user_id );
+               $this->manager->register_controls();
+
+               $validities = $this->manager->validate_setting_values( array( 'unknown' => 'X' ) );
+               $this->assertEmpty( $validities );
+
+               $validities = $this->manager->validate_setting_values( array( 'unknown' => 'X' ), array( 'validate_existence' => false ) );
+               $this->assertEmpty( $validities );
+
+               $validities = $this->manager->validate_setting_values( array( 'unknown' => 'X' ), array( 'validate_existence' => true ) );
+               $this->assertNotEmpty( $validities );
+               $this->assertArrayHasKey( 'unknown', $validities );
+               $error = $validities['unknown'];
+               $this->assertInstanceOf( 'WP_Error', $error );
+               $this->assertEquals( 'unrecognized', $error->get_error_code() );
+
+               $this->manager->get_setting( 'blogname' )->capability = 'do_not_allow';
+               $validities = $this->manager->validate_setting_values( array( 'blogname' => 'X' ), array( 'validate_capability' => false ) );
+               $this->assertArrayHasKey( 'blogname', $validities );
+               $this->assertTrue( $validities['blogname'] );
+               $validities = $this->manager->validate_setting_values( array( 'blogname' => 'X' ), array( 'validate_capability' => true ) );
+               $this->assertArrayHasKey( 'blogname', $validities );
+               $error = $validities['blogname'];
+               $this->assertInstanceOf( 'WP_Error', $error );
+               $this->assertEquals( 'unauthorized', $error->get_error_code() );
+       }
+
+       /**
</ins><span class="cx" style="display: block; padding: 0 10px">          * Add a length constraint to a setting.
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * Adds minimum-length error code if the length is less than 10.
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -328,6 +1105,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">         * @ticket 37247
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><span class="cx" style="display: block; padding: 0 10px">        function test_validate_setting_values_validation_sanitization_order() {
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                wp_set_current_user( self::$admin_user_id );
</ins><span class="cx" style="display: block; padding: 0 10px">                 $setting = $this->manager->add_setting( 'numeric', array(
</span><span class="cx" style="display: block; padding: 0 10px">                        'validate_callback' => array( $this, 'filter_customize_validate_numeric' ),
</span><span class="cx" style="display: block; padding: 0 10px">                        'sanitize_callback' => array( $this, 'filter_customize_sanitize_numeric' ),
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -369,6 +1147,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">         * @see WP_Customize_Manager::set_post_value()
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><span class="cx" style="display: block; padding: 0 10px">        function test_set_post_value() {
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                wp_set_current_user( self::$admin_user_id );
</ins><span class="cx" style="display: block; padding: 0 10px">                 $this->manager->add_setting( 'foo', array(
</span><span class="cx" style="display: block; padding: 0 10px">                        'sanitize_callback' => array( $this, 'sanitize_foo_for_test_set_post_value' ),
</span><span class="cx" style="display: block; padding: 0 10px">                ) );
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -473,7 +1252,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">                }
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertFalse( $this->manager->has_published_pages() );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                wp_set_current_user( $this->factory()->user->create( array( 'role' => 'editor' ) ) );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         wp_set_current_user( self::$admin_user_id );
</ins><span class="cx" style="display: block; padding: 0 10px">                 $this->manager->nav_menus->customize_register();
</span><span class="cx" style="display: block; padding: 0 10px">                $setting_id = 'nav_menus_created_posts';
</span><span class="cx" style="display: block; padding: 0 10px">                $setting = $this->manager->get_setting( $setting_id );
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -492,6 +1271,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">         * @ticket 30936
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><span class="cx" style="display: block; padding: 0 10px">        function test_register_dynamic_settings() {
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                wp_set_current_user( self::$admin_user_id );
</ins><span class="cx" style="display: block; padding: 0 10px">                 $posted_settings = array(
</span><span class="cx" style="display: block; padding: 0 10px">                        'foo' => 'OOF',
</span><span class="cx" style="display: block; padding: 0 10px">                        'bar' => 'RAB',
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -591,7 +1371,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">                wp_set_current_user( self::factory()->user->create( array( 'role' => 'author' ) ) );
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertEquals( home_url( '/' ), $this->manager->get_return_url() );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                wp_set_current_user( self::factory()->user->create( array( 'role' => 'administrator' ) ) );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         wp_set_current_user( self::$admin_user_id );
</ins><span class="cx" style="display: block; padding: 0 10px">                 $this->assertTrue( current_user_can( 'edit_theme_options' ) );
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertEquals( home_url( '/' ), $this->manager->get_return_url() );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -684,7 +1464,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">         * @see WP_Customize_Manager::customize_pane_settings()
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><span class="cx" style="display: block; padding: 0 10px">        function test_customize_pane_settings() {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                wp_set_current_user( self::factory()->user->create( array( 'role' => 'administrator' ) ) );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         wp_set_current_user( self::$admin_user_id );
</ins><span class="cx" style="display: block; padding: 0 10px">                 $this->manager->register_controls();
</span><span class="cx" style="display: block; padding: 0 10px">                $this->manager->prepare_controls();
</span><span class="cx" style="display: block; padding: 0 10px">                $autofocus = array( 'control' => 'blogname' );
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -706,7 +1486,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">                $data = json_decode( $json, true );
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertNotEmpty( $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">-                $this->assertEqualSets( array( 'theme', 'url', 'browser', 'panels', 'sections', 'nonce', 'autofocus', 'documentTitleTmpl', 'previewableDevices' ), array_keys( $data ) );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         $this->assertEqualSets( array( 'theme', 'url', 'browser', 'panels', 'sections', 'nonce', 'autofocus', 'documentTitleTmpl', 'previewableDevices', 'changeset', 'timeouts' ), array_keys( $data ) );
</ins><span class="cx" style="display: block; padding: 0 10px">                 $this->assertEquals( $autofocus, $data['autofocus'] );
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertArrayHasKey( 'save', $data['nonce'] );
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertArrayHasKey( 'preview', $data['nonce'] );
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -718,7 +1498,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">         * @see WP_Customize_Manager::customize_preview_settings()
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><span class="cx" style="display: block; padding: 0 10px">        function test_customize_preview_settings() {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                wp_set_current_user( self::factory()->user->create( array( 'role' => 'administrator' ) ) );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         wp_set_current_user( self::$admin_user_id );
</ins><span class="cx" style="display: block; padding: 0 10px">                 $this->manager->register_controls();
</span><span class="cx" style="display: block; padding: 0 10px">                $this->manager->prepare_controls();
</span><span class="cx" style="display: block; padding: 0 10px">                $this->manager->set_post_value( 'foo', 'bar' );
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -740,9 +1520,10 @@
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertArrayHasKey( 'settingValidities', $settings );
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertArrayHasKey( 'nonce', $settings );
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertArrayHasKey( '_dirty', $settings );
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                $this->assertArrayHasKey( 'timeouts', $settings );
+               $this->assertArrayHasKey( 'changeset', $settings );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertArrayHasKey( 'preview', $settings['nonce'] );
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                $this->assertEquals( array( 'foo' ), $settings['_dirty'] );
</del><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">@@ -814,7 +1595,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">                $manager = new WP_Customize_Manager();
</span><span class="cx" style="display: block; padding: 0 10px">                $manager->register_controls();
</span><span class="cx" style="display: block; padding: 0 10px">                $section_id = 'foo-section';
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                wp_set_current_user( self::factory()->user->create( array( 'role' => 'administrator' ) ) );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         wp_set_current_user( self::$admin_user_id );
</ins><span class="cx" style="display: block; padding: 0 10px">                 $manager->add_section( $section_id, array(
</span><span class="cx" style="display: block; padding: 0 10px">                        'title'      => 'Section',
</span><span class="cx" style="display: block; padding: 0 10px">                        'priority'   => 1,
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -845,7 +1626,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><span class="cx" style="display: block; padding: 0 10px">        function test_add_section_return_instance() {
</span><span class="cx" style="display: block; padding: 0 10px">                $manager = new WP_Customize_Manager();
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                wp_set_current_user( self::factory()->user->create( array( 'role' => 'administrator' ) ) );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         wp_set_current_user( self::$admin_user_id );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                $section_id = 'foo-section';
</span><span class="cx" style="display: block; padding: 0 10px">                $result_section = $manager->add_section( $section_id, array(
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -872,7 +1653,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><span class="cx" style="display: block; padding: 0 10px">        function test_add_setting_return_instance() {
</span><span class="cx" style="display: block; padding: 0 10px">                $manager = new WP_Customize_Manager();
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                wp_set_current_user( self::factory()->user->create( array( 'role' => 'administrator' ) ) );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         wp_set_current_user( self::$admin_user_id );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                $setting_id = 'foo-setting';
</span><span class="cx" style="display: block; padding: 0 10px">                $result_setting = $manager->add_setting( $setting_id );
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -943,7 +1724,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><span class="cx" style="display: block; padding: 0 10px">        function test_add_panel_return_instance() {
</span><span class="cx" style="display: block; padding: 0 10px">                $manager = new WP_Customize_Manager();
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                wp_set_current_user( self::factory()->user->create( array( 'role' => 'administrator' ) ) );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         wp_set_current_user( self::$admin_user_id );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                $panel_id = 'foo-panel';
</span><span class="cx" style="display: block; padding: 0 10px">                $result_panel = $manager->add_panel( $panel_id, array(
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -970,7 +1751,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">        function test_add_control_return_instance() {
</span><span class="cx" style="display: block; padding: 0 10px">                $manager = new WP_Customize_Manager();
</span><span class="cx" style="display: block; padding: 0 10px">                $section_id = 'foo-section';
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                wp_set_current_user( self::factory()->user->create( array( 'role' => 'administrator' ) ) );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         wp_set_current_user( self::$admin_user_id );
</ins><span class="cx" style="display: block; padding: 0 10px">                 $manager->add_section( $section_id, array(
</span><span class="cx" style="display: block; padding: 0 10px">                        'title'    => 'Section',
</span><span class="cx" style="display: block; padding: 0 10px">                        'priority' => 1,
</span></span></pre></div>
<a id="trunktestsphpunittestscustomizeselectiverefreshajaxphp"></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/selective-refresh-ajax.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/customize/selective-refresh-ajax.php    2016-10-17 23:53:20 UTC (rev 38809)
+++ trunk/tests/phpunit/tests/customize/selective-refresh-ajax.php      2016-10-18 20:04:36 UTC (rev 38810)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -140,25 +140,6 @@
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        /**
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-         * Make sure that the Customizer "signature" is not included in partial render responses.
-        *
-        * @see WP_Customize_Selective_Refresh::handle_render_partials_request()
-        */
-       function test_handle_render_partials_request_removes_customize_signature() {
-               $this->setup_valid_render_partials_request_environment();
-               $this->assertTrue( is_customize_preview() );
-               $this->assertEquals( 1000, has_action( 'shutdown', array( $this->wp_customize, 'customize_preview_signature' ) ) );
-               ob_start();
-               try {
-                       $this->selective_refresh->handle_render_partials_request();
-               } catch ( WPDieException $e ) {
-                       unset( $e );
-               }
-               ob_end_clean();
-               $this->assertFalse( has_action( 'shutdown', array( $this->wp_customize, 'customize_preview_signature' ) ) );
-       }
-
-       /**
</del><span class="cx" style="display: block; padding: 0 10px">          * Test WP_Customize_Selective_Refresh::handle_render_partials_request() for an unrecognized partial.
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * @see WP_Customize_Selective_Refresh::handle_render_partials_request()
</span></span></pre></div>
<a id="trunktestsphpunittestscustomizesettingphp"></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/setting.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/customize/setting.php   2016-10-17 23:53:20 UTC (rev 38809)
+++ trunk/tests/phpunit/tests/customize/setting.php     2016-10-18 20:04:36 UTC (rev 38810)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -97,6 +97,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">         * @see WP_Customize_Setting::value()
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><span class="cx" style="display: block; padding: 0 10px">        function test_preview_standard_types_non_multidimensional() {
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                wp_set_current_user( $this->factory()->user->create( array( 'role' => 'administrator' ) ) );
</ins><span class="cx" style="display: block; padding: 0 10px">                 $_POST['customized'] = wp_slash( wp_json_encode( $this->post_data_overrides ) );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                // Try non-multidimensional settings.
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -175,6 +176,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">         * @see WP_Customize_Setting::value()
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><span class="cx" style="display: block; padding: 0 10px">        function test_preview_standard_types_multidimensional() {
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                wp_set_current_user( $this->factory()->user->create( array( 'role' => 'administrator' ) ) );
</ins><span class="cx" style="display: block; padding: 0 10px">                 $_POST['customized'] = wp_slash( wp_json_encode( $this->post_data_overrides ) );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                foreach ( $this->standard_type_configs as $type => $type_options ) {
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -314,6 +316,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">         * @see WP_Customize_Setting::preview()
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><span class="cx" style="display: block; padding: 0 10px">        function test_preview_custom_type() {
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                wp_set_current_user( $this->factory()->user->create( array( 'role' => 'administrator' ) ) );
</ins><span class="cx" style="display: block; padding: 0 10px">                 $type = 'custom_type';
</span><span class="cx" style="display: block; padding: 0 10px">                $post_data_overrides = array(
</span><span class="cx" style="display: block; padding: 0 10px">                        "unset_{$type}_with_post_value" => "unset_{$type}_without_post_value\\o/",
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -478,6 +481,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">         * @ticket 31428
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><span class="cx" style="display: block; padding: 0 10px">        function test_is_current_blog_previewed() {
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                wp_set_current_user( $this->factory()->user->create( array( 'role' => 'administrator' ) ) );
</ins><span class="cx" style="display: block; padding: 0 10px">                 $type = 'option';
</span><span class="cx" style="display: block; padding: 0 10px">                $name = 'blogname';
</span><span class="cx" style="display: block; padding: 0 10px">                $post_value = __FUNCTION__;
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -502,6 +506,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">                        $this->markTestSkipped( 'Cannot test WP_Customize_Setting::is_current_blog_previewed() with switch_to_blog() if not on multisite.' );
</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">+                wp_set_current_user( self::factory()->user->create( array( 'role' => 'administrator' ) ) );
</ins><span class="cx" style="display: block; padding: 0 10px">                 $type = 'option';
</span><span class="cx" style="display: block; padding: 0 10px">                $name = 'blogdescription';
</span><span class="cx" style="display: block; padding: 0 10px">                $post_value = __FUNCTION__;
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -647,6 +652,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">         * @ticket 37294
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><span class="cx" style="display: block; padding: 0 10px">        public function test_multidimensional_value_when_previewed() {
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                wp_set_current_user( $this->factory()->user->create( array( 'role' => 'administrator' ) ) );
</ins><span class="cx" style="display: block; padding: 0 10px">                 WP_Customize_Setting::reset_aggregated_multidimensionals();
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                $initial_value = 456;
</span></span></pre></div>
<a id="trunktestsphpunittestsfunctionsphp"></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/functions.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/functions.php   2016-10-17 23:53:20 UTC (rev 38809)
+++ trunk/tests/phpunit/tests/functions.php     2016-10-18 20:04:36 UTC (rev 38810)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -880,4 +880,22 @@
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertSame( false, $raised_limit );
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertEquals( WP_MAX_MEMORY_LIMIT, $ini_limit_after );
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
+       /**
+        * Tests wp_generate_uuid4().
+        *
+        * @covers wp_generate_uuid4()
+        * @ticket 38164
+        */
+       function test_wp_generate_uuid4() {
+               $uuids = array();
+               for ( $i = 0; $i < 20; $i += 1 ) {
+                       $uuid = wp_generate_uuid4();
+                       $this->assertRegExp( '/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/', $uuid );
+                       $uuids[] = $uuid;
+               }
+
+               $unique_uuids = array_unique( $uuids );
+               $this->assertEquals( $uuids, $unique_uuids );
+       }
</ins><span class="cx" style="display: block; padding: 0 10px"> }
</span></span></pre></div>
<a id="trunktestsphpunittestspostphp"></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/post.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/post.php        2016-10-17 23:53:20 UTC (rev 38809)
+++ trunk/tests/phpunit/tests/post.php  2016-10-18 20:04:36 UTC (rev 38810)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1240,4 +1240,50 @@
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertEquals( 0, get_post( $page_id )->post_parent );
</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">+        /**
+        * Test ensuring that the post_name (UUID) is preserved when wp_insert_post()/wp_update_post() is called.
+        *
+        * @see _wp_customize_changeset_filter_insert_post_data()
+        * @ticket 30937
+        */
+       function test_wp_insert_post_for_customize_changeset_should_not_drop_post_name() {
+
+               $this->assertEquals( 10, has_filter( 'wp_insert_post_data', '_wp_customize_changeset_filter_insert_post_data' ) );
+
+               $changeset_data = array(
+                       'blogname' => array(
+                               'value' => 'Hello World',
+                       ),
+               );
+
+               wp_set_current_user( $this->factory()->user->create( array( 'role' => 'contributor' ) ) );
+
+               $uuid = wp_generate_uuid4();
+               $post_id = wp_insert_post( array(
+                       'post_type' => 'customize_changeset',
+                       'post_name' => strtoupper( $uuid ),
+                       'post_content' => wp_json_encode( $changeset_data ),
+               ) );
+               $this->assertEquals( $uuid, get_post( $post_id )->post_name, 'Expected lower-case UUID4 to be inserted.' );
+               $this->assertEquals( $changeset_data, json_decode( get_post( $post_id )->post_content, true ) );
+
+               $changeset_data['blogname']['value'] = 'Hola Mundo';
+               wp_update_post( array(
+                       'ID' => $post_id,
+                       'post_status' => 'draft',
+                       'post_content' => wp_json_encode( $changeset_data ),
+               ) );
+               $this->assertEquals( $uuid, get_post( $post_id )->post_name, 'Expected post_name to not have been dropped for drafts.' );
+               $this->assertEquals( $changeset_data, json_decode( get_post( $post_id )->post_content, true ) );
+
+               $changeset_data['blogname']['value'] = 'Hallo Welt';
+               wp_update_post( array(
+                       'ID' => $post_id,
+                       'post_status' => 'pending',
+                       'post_content' => wp_json_encode( $changeset_data ),
+               ) );
+               $this->assertEquals( $uuid, get_post( $post_id )->post_name, 'Expected post_name to not have been dropped for pending.' );
+               $this->assertEquals( $changeset_data, json_decode( get_post( $post_id )->post_content, true ) );
+       }
+
</ins><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  2016-10-17 23:53:20 UTC (rev 38809)
+++ trunk/tests/qunit/fixtures/customize-settings.js    2016-10-18 20:04:36 UTC (rev 38810)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -147,6 +147,17 @@
</span><span class="cx" style="display: block; padding: 0 10px">                'mobile': {
</span><span class="cx" style="display: block; padding: 0 10px">                        'label': 'Enter mobile preview mode'
</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: {
+               status: '',
+               uuid: '0c674ff4-c159-4e7a-beb4-cb830ae73979'
+       },
+       timeouts: {
+               windowRefresh: 250,
+               changesetAutoSave: 60000,
+               keepAliveCheck: 2500,
+               reflowPaneContents: 100,
+               previewFrameSensitivity: 2000
</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"> window._wpCustomizeControlsL10n = {};
</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      2016-10-17 23:53:20 UTC (rev 38809)
+++ trunk/tests/qunit/index.html        2016-10-18 20:04:36 UTC (rev 38810)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -316,6 +316,10 @@
</span><span class="cx" style="display: block; padding: 0 10px">                </script>
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                <div hidden>
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                        <div id="customize-preview"></div>
+               </div>
+
+               <div hidden>
</ins><span class="cx" style="display: block; padding: 0 10px">                         <div id="available-menu-items" class="accordion-container">
</span><span class="cx" style="display: block; padding: 0 10px">                        <div class="customize-section-title">
</span><span class="cx" style="display: block; padding: 0 10px">                                <button type="button" class="customize-section-back" tabindex="-1">
</span></span></pre></div>
<a id="trunktestsqunitwpadminjscustomizebasejs"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/tests/qunit/wp-admin/js/customize-base.js</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/qunit/wp-admin/js/customize-base.js   2016-10-17 23:53:20 UTC (rev 38809)
+++ trunk/tests/qunit/wp-admin/js/customize-base.js     2016-10-18 20:04:36 UTC (rev 38810)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -184,4 +184,23 @@
</span><span class="cx" style="display: block; padding: 0 10px">                assert.equal( 'error', notification.type );
</span><span class="cx" style="display: block; padding: 0 10px">                assert.equal( null, notification.data );
</span><span class="cx" style="display: block; padding: 0 10px">        } );
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
+       module( 'Customize Base: utils.parseQueryString' );
+       test( 'wp.customize.utils.parseQueryString works', function( assert ) {
+               var queryParams;
+               queryParams = wp.customize.utils.parseQueryString( 'a=1&b=2' );
+               assert.ok( _.isEqual( queryParams, { a: '1', b: '2' } ) );
+
+               queryParams = wp.customize.utils.parseQueryString( 'a+b=1&b=Hello%20World' );
+               assert.ok( _.isEqual( queryParams, { 'a_b': '1', b: 'Hello World' } ) );
+
+               queryParams = wp.customize.utils.parseQueryString( 'a%20b=1&b=Hello+World' );
+               assert.ok( _.isEqual( queryParams, { 'a_b': '1', b: 'Hello World' } ) );
+
+               queryParams = wp.customize.utils.parseQueryString( 'a=1&b' );
+               assert.ok( _.isEqual( queryParams, { 'a': '1', b: null } ) );
+
+               queryParams = wp.customize.utils.parseQueryString( 'a=1&b=' );
+               assert.ok( _.isEqual( queryParams, { 'a': '1', b: '' } ) );
+       } );
</ins><span class="cx" style="display: block; padding: 0 10px"> });
</span></span></pre></div>
<a id="trunktestsqunitwpadminjscustomizecontrolsjs"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/tests/qunit/wp-admin/js/customize-controls.js</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/qunit/wp-admin/js/customize-controls.js       2016-10-17 23:53:20 UTC (rev 38809)
+++ trunk/tests/qunit/wp-admin/js/customize-controls.js 2016-10-18 20:04:36 UTC (rev 38810)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1,4 +1,4 @@
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-/* global wp, test, ok, equal, module */
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+/* global JSON, wp, test, ok, equal, module */
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px"> wp.customize.settingConstructor.abbreviation = wp.customize.Setting.extend({
</span><span class="cx" style="display: block; padding: 0 10px">        validate: function( value ) {
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -558,4 +558,74 @@
</span><span class="cx" style="display: block; padding: 0 10px">                equal( 1, controlsForSettings['fixture-setting'].length );
</span><span class="cx" style="display: block; padding: 0 10px">                equal( wp.customize.control( controlId ), controlsForSettings['fixture-setting'][0] );
</span><span class="cx" style="display: block; padding: 0 10px">        } );
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
+       module( 'Customize Controls wp.customize.dirtyValues' );
+       test( 'dirtyValues() returns expected values', function() {
+               wp.customize.each( function( setting ) {
+                       setting._dirty = false;
+               } );
+               ok( _.isEmpty( wp.customize.dirtyValues() ) );
+               ok( _.isEmpty( wp.customize.dirtyValues( { unsaved: false } ) ) );
+
+               wp.customize( 'fixture-setting' )._dirty = true;
+               ok( ! _.isEmpty( wp.customize.dirtyValues() ) );
+               ok( _.isEmpty( wp.customize.dirtyValues( { unsaved: true } ) ) );
+
+               wp.customize( 'fixture-setting' ).set( 'Modified' );
+               ok( ! _.isEmpty( wp.customize.dirtyValues() ) );
+               ok( ! _.isEmpty( wp.customize.dirtyValues( { unsaved: true } ) ) );
+               equal( 'Modified', wp.customize.dirtyValues()['fixture-setting'] );
+       } );
+
+       module( 'Customize Controls: wp.customize.requestChangesetUpdate()' );
+       test( 'requestChangesetUpdate makes request and returns promise', function( assert ) {
+               var request, originalBeforeSetup = jQuery.ajaxSettings.beforeSend;
+
+               jQuery.ajaxSetup( {
+                       beforeSend: function( e, data ) {
+                               var queryParams, changesetData;
+                               queryParams = wp.customize.utils.parseQueryString( data.data );
+
+                               assert.equal( 'customize_save', queryParams.action );
+                               assert.ok( ! _.isUndefined( queryParams.customize_changeset_data ) );
+                               assert.ok( ! _.isUndefined( queryParams.nonce ) );
+                               assert.ok( ! _.isUndefined( queryParams.customize_theme ) );
+                               assert.equal( wp.customize.settings.changeset.uuid, queryParams.customize_changeset_uuid );
+                               assert.equal( 'on', queryParams.wp_customize );
+
+                               changesetData = JSON.parse( queryParams.customize_changeset_data );
+                               assert.ok( ! _.isUndefined( changesetData.additionalSetting ) );
+                               assert.ok( ! _.isUndefined( changesetData['fixture-setting'] ) );
+
+                               assert.equal( 'additionalValue', changesetData.additionalSetting.value );
+                               assert.equal( 'requestChangesetUpdate', changesetData['fixture-setting'].value );
+
+                               // Prevent Ajax request from completing.
+                               return false;
+                       }
+               } );
+
+               wp.customize.each( function( setting ) {
+                       setting._dirty = false;
+               } );
+
+               request = wp.customize.requestChangesetUpdate();
+               assert.equal( 'resolved', request.state());
+               request.done( function( data ) {
+                       assert.ok( _.isEqual( {}, data ) );
+               } );
+
+               wp.customize( 'fixture-setting' ).set( 'requestChangesetUpdate' );
+
+               request = wp.customize.requestChangesetUpdate( {
+                       additionalSetting: {
+                               value: 'additionalValue'
+                       }
+               } );
+
+               request.always( function( data ) {
+                       assert.equal( 'canceled', data.statusText );
+                       jQuery.ajaxSetup( { beforeSend: originalBeforeSetup } );
+               } );
+       } );
</ins><span class="cx" style="display: block; padding: 0 10px"> });
</span></span></pre>
</div>
</div>

</body>
</html>