<!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’ 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’ 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 & Publish' ) : __( 'Save & 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’ 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’ 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>