Customize: Add changeset locking in Customizer to prevent users from overriding each other's changes.
* Customization locking is checked when changesets are saved and when heartbeat ticks. * Lock is lifted immediately upon a user closing the Customizer. * Heartbeat is introduced into Customizer. * Changes made to user after it was locked by another user are stored as an autosave revision for restoration. * Lock notification displays link to preview the other user's changes on the frontend. * A user loading a locked Customizer changeset will be presented with an option to take over. * Autosave revisions attached to a published changeset are converted into auto-drafts so that they will be presented to users for restoration. * Focus constraining is improved in overlay notifications. * Escape key is stopped from propagating in overlay notifications, and it dismisses dismissible overlay notifications. * Introduces `changesetLocked` state which is used to disable the Save button and suppress the AYS dialog when leaving the Customizer. * Fixes bug where users could be presented with each other's autosave revisions. Props sayedwp, westonruter, melchoyce. See #31436, #31897, #39896. Fixes #42024. Built from https://develop.svn.wordpress.org/trunk@41839 git-svn-id: http://core.svn.wordpress.org/trunk@41673 1a063a9b-81f0-0310-95a4-ce76da25c4cd
This commit is contained in:
parent
f44ed386c1
commit
434e3aba82
|
@ -20,6 +20,46 @@ body {
|
|||
text-align: center;
|
||||
}
|
||||
|
||||
#customize-controls #customize-notifications-area .notice.notification-overlay.notification-changeset-locked {
|
||||
background-color: rgba( 0, 0, 0, 0.7 );
|
||||
padding: 25px;
|
||||
}
|
||||
|
||||
#customize-controls #customize-notifications-area .notice.notification-overlay.notification-changeset-locked .customize-changeset-locked-message {
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
max-width: 366px;
|
||||
min-height: 64px;
|
||||
width: auto;
|
||||
padding: 25px 109px 25px 25px;
|
||||
position: relative;
|
||||
background: #fff;
|
||||
box-shadow: 0 3px 6px rgba( 0, 0, 0, 0.3 );
|
||||
line-height: 1.5;
|
||||
overflow-y: auto;
|
||||
text-align: right;
|
||||
top: calc( 50% - 100px );
|
||||
}
|
||||
|
||||
#customize-controls #customize-notifications-area .notice.notification-overlay.notification-changeset-locked .currently-editing {
|
||||
margin-top: 0;
|
||||
}
|
||||
#customize-controls #customize-notifications-area .notice.notification-overlay.notification-changeset-locked .action-buttons {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.customize-changeset-locked-avatar {
|
||||
width: 64px;
|
||||
position: absolute;
|
||||
right: 25px;
|
||||
top: 25px;
|
||||
}
|
||||
|
||||
.wp-core-ui.wp-customizer .customize-changeset-locked-message a.button {
|
||||
margin-left: 10px;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
#customize-controls .description {
|
||||
color: #555d66;
|
||||
}
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -20,6 +20,46 @@ body {
|
|||
text-align: center;
|
||||
}
|
||||
|
||||
#customize-controls #customize-notifications-area .notice.notification-overlay.notification-changeset-locked {
|
||||
background-color: rgba( 0, 0, 0, 0.7 );
|
||||
padding: 25px;
|
||||
}
|
||||
|
||||
#customize-controls #customize-notifications-area .notice.notification-overlay.notification-changeset-locked .customize-changeset-locked-message {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
max-width: 366px;
|
||||
min-height: 64px;
|
||||
width: auto;
|
||||
padding: 25px 25px 25px 109px;
|
||||
position: relative;
|
||||
background: #fff;
|
||||
box-shadow: 0 3px 6px rgba( 0, 0, 0, 0.3 );
|
||||
line-height: 1.5;
|
||||
overflow-y: auto;
|
||||
text-align: left;
|
||||
top: calc( 50% - 100px );
|
||||
}
|
||||
|
||||
#customize-controls #customize-notifications-area .notice.notification-overlay.notification-changeset-locked .currently-editing {
|
||||
margin-top: 0;
|
||||
}
|
||||
#customize-controls #customize-notifications-area .notice.notification-overlay.notification-changeset-locked .action-buttons {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.customize-changeset-locked-avatar {
|
||||
width: 64px;
|
||||
position: absolute;
|
||||
left: 25px;
|
||||
top: 25px;
|
||||
}
|
||||
|
||||
.wp-core-ui.wp-customizer .customize-changeset-locked-message a.button {
|
||||
margin-right: 10px;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
#customize-controls .description {
|
||||
color: #555d66;
|
||||
}
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -87,6 +87,7 @@ add_action( 'customize_controls_print_styles', 'print_admin_styles', 20
|
|||
*/
|
||||
do_action( 'customize_controls_init' );
|
||||
|
||||
wp_enqueue_script( 'heartbeat' );
|
||||
wp_enqueue_script( 'customize-controls' );
|
||||
wp_enqueue_style( 'customize-controls' );
|
||||
|
||||
|
|
|
@ -34,6 +34,37 @@
|
|||
if ( notification.loading ) {
|
||||
notification.containerClasses += ' notification-loading';
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Render notification.
|
||||
*
|
||||
* @since 4.9.0
|
||||
*
|
||||
* @return {jQuery} Notification container.
|
||||
*/
|
||||
render: function() {
|
||||
var li = api.Notification.prototype.render.call( this );
|
||||
li.on( 'keydown', _.bind( this.handleEscape, this ) );
|
||||
return li;
|
||||
},
|
||||
|
||||
/**
|
||||
* Stop propagation on escape key presses, but also dismiss notification if it is dismissible.
|
||||
*
|
||||
* @since 4.9.0
|
||||
*
|
||||
* @param {jQuery.Event} event - Event.
|
||||
* @returns {void}
|
||||
*/
|
||||
handleEscape: function( event ) {
|
||||
var notification = this;
|
||||
if ( 27 === event.which ) {
|
||||
event.stopPropagation();
|
||||
if ( notification.dismissible && notification.parent ) {
|
||||
notification.parent.remove( notification.code );
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -282,11 +313,30 @@
|
|||
* @returns {void}
|
||||
*/
|
||||
constrainFocus: function constrainFocus( event ) {
|
||||
var collection = this;
|
||||
if ( ! collection.focusContainer || collection.focusContainer.is( event.target ) || $.contains( collection.focusContainer[0], event.target[0] ) ) {
|
||||
var collection = this, focusableElements;
|
||||
|
||||
// Prevent keys from escaping.
|
||||
event.stopPropagation();
|
||||
|
||||
if ( 9 !== event.which ) { // Tab key.
|
||||
return;
|
||||
}
|
||||
collection.focusContainer.focus();
|
||||
|
||||
focusableElements = collection.focusContainer.find( ':focusable' );
|
||||
if ( 0 === focusableElements.length ) {
|
||||
focusableElements = collection.focusContainer;
|
||||
}
|
||||
|
||||
if ( ! $.contains( collection.focusContainer[0], event.target ) || ! $.contains( collection.focusContainer[0], document.activeElement ) ) {
|
||||
event.preventDefault();
|
||||
focusableElements.first().focus();
|
||||
} else if ( focusableElements.last().is( event.target ) && ! event.shiftKey ) {
|
||||
event.preventDefault();
|
||||
focusableElements.first().focus();
|
||||
} else if ( focusableElements.first().is( event.target ) && event.shiftKey ) {
|
||||
event.preventDefault();
|
||||
focusableElements.last().focus();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -6737,7 +6787,8 @@
|
|||
'selectedChangesetStatus',
|
||||
'remainingTimeToPublish',
|
||||
'previewerAlive',
|
||||
'editShortcutVisibility'
|
||||
'editShortcutVisibility',
|
||||
'changesetLocked'
|
||||
], function( name ) {
|
||||
api.state.create( name );
|
||||
});
|
||||
|
@ -7184,14 +7235,14 @@
|
|||
} else if ( response.code ) {
|
||||
if ( 'not_future_date' === response.code && api.section.has( 'publish_settings' ) && api.section( 'publish_settings' ).active.get() && api.control.has( 'changeset_scheduled_date' ) ) {
|
||||
api.control( 'changeset_scheduled_date' ).toggleFutureDateNotification( true ).focus();
|
||||
} else {
|
||||
} else if ( 'changeset_locked' !== response.code ) {
|
||||
notification = new api.Notification( response.code, _.extend( notificationArgs, {
|
||||
message: response.message
|
||||
} ) );
|
||||
}
|
||||
} else {
|
||||
notification = new api.Notification( 'unknown_error', _.extend( notificationArgs, {
|
||||
message: api.l10n.serverSaveError
|
||||
message: api.l10n.unknownRequestFail
|
||||
} ) );
|
||||
}
|
||||
|
||||
|
@ -7497,6 +7548,7 @@
|
|||
selectedChangesetDate = state.instance( 'selectedChangesetDate' ),
|
||||
previewerAlive = state.instance( 'previewerAlive' ),
|
||||
editShortcutVisibility = state.instance( 'editShortcutVisibility' ),
|
||||
changesetLocked = state.instance( 'changesetLocked' ),
|
||||
populateChangesetUuidParam;
|
||||
|
||||
state.bind( 'change', function() {
|
||||
|
@ -7547,7 +7599,7 @@
|
|||
* 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() && ! trashing() && ( ! activated() || ! saved() || ( changesetStatus() !== selectedChangesetStatus() && '' !== changesetStatus() ) || ( 'future' === selectedChangesetStatus() && changesetDate.get() !== selectedChangesetDate.get() ) );
|
||||
canSave = ! saving() && ! trashing() && ! changesetLocked() && ( ! activated() || ! saved() || ( changesetStatus() !== selectedChangesetStatus() && '' !== changesetStatus() ) || ( 'future' === selectedChangesetStatus() && changesetDate.get() !== selectedChangesetDate.get() ) );
|
||||
|
||||
saveBtn.prop( 'disabled', ! canSave );
|
||||
});
|
||||
|
@ -7561,6 +7613,7 @@
|
|||
|
||||
// Set default states.
|
||||
changesetStatus( api.settings.changeset.status );
|
||||
changesetLocked( Boolean( api.settings.changeset.lockUser ) );
|
||||
changesetDate( api.settings.changeset.publishDate );
|
||||
selectedChangesetDate( api.settings.changeset.publishDate );
|
||||
selectedChangesetStatus( '' === api.settings.changeset.status || 'auto-draft' === api.settings.changeset.status ? 'publish' : api.settings.changeset.status );
|
||||
|
@ -7660,6 +7713,185 @@
|
|||
}
|
||||
}( api.state ) );
|
||||
|
||||
/**
|
||||
* Handles lock notice and take over request.
|
||||
*
|
||||
* @since 4.9.0
|
||||
*/
|
||||
( function checkAndDisplayLockNotice() {
|
||||
|
||||
/**
|
||||
* A notification that is displayed in a full-screen overlay with information about the locked changeset.
|
||||
*
|
||||
* @since 4.9.0
|
||||
* @class
|
||||
* @augments wp.customize.Notification
|
||||
* @augments wp.customize.OverlayNotification
|
||||
*/
|
||||
var LockedNotification = api.OverlayNotification.extend({
|
||||
|
||||
/**
|
||||
* Template ID.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
templateId: 'customize-changeset-locked-notification',
|
||||
|
||||
/**
|
||||
* Lock user.
|
||||
*
|
||||
* @type {object}
|
||||
*/
|
||||
lockUser: null,
|
||||
|
||||
/**
|
||||
* Initialize.
|
||||
*
|
||||
* @since 4.9.0
|
||||
*
|
||||
* @param {string} [code] - Code.
|
||||
* @param {object} [params] - Params.
|
||||
*/
|
||||
initialize: function( code, params ) {
|
||||
var notification = this, _code, _params;
|
||||
_code = code || 'changeset_locked';
|
||||
_params = _.extend(
|
||||
{
|
||||
type: 'warning',
|
||||
containerClasses: '',
|
||||
lockUser: {}
|
||||
},
|
||||
params
|
||||
);
|
||||
_params.containerClasses += ' notification-changeset-locked';
|
||||
api.OverlayNotification.prototype.initialize.call( notification, _code, _params );
|
||||
},
|
||||
|
||||
/**
|
||||
* Render notification.
|
||||
*
|
||||
* @since 4.9.0
|
||||
*
|
||||
* @return {jQuery} Notification container.
|
||||
*/
|
||||
render: function() {
|
||||
var notification = this, li, data, takeOverButton, request;
|
||||
data = _.extend(
|
||||
{
|
||||
allowOverride: false,
|
||||
returnUrl: api.settings.url['return'],
|
||||
previewUrl: api.previewer.previewUrl.get(),
|
||||
frontendPreviewUrl: api.previewer.getFrontendPreviewUrl()
|
||||
},
|
||||
this
|
||||
);
|
||||
|
||||
li = api.OverlayNotification.prototype.render.call( data );
|
||||
|
||||
// Try to autosave the changeset now.
|
||||
api.requestChangesetUpdate( {}, { autosave: true } ).fail( function( response ) {
|
||||
if ( ! response.autosaved ) {
|
||||
li.find( '.notice-error' ).prop( 'hidden', false ).text( response.message || api.l10n.unknownRequestFail );
|
||||
}
|
||||
} );
|
||||
|
||||
takeOverButton = li.find( '.customize-notice-take-over-button' );
|
||||
takeOverButton.on( 'click', function( event ) {
|
||||
event.preventDefault();
|
||||
if ( request ) {
|
||||
return;
|
||||
}
|
||||
|
||||
takeOverButton.addClass( 'disabled' );
|
||||
request = wp.ajax.post( 'customize_override_changeset_lock', {
|
||||
wp_customize: 'on',
|
||||
customize_theme: api.settings.theme.stylesheet,
|
||||
customize_changeset_uuid: api.settings.changeset.uuid,
|
||||
nonce: api.settings.nonce.override_lock
|
||||
} );
|
||||
|
||||
request.done( function() {
|
||||
api.notifications.remove( notification.code ); // Remove self.
|
||||
api.state( 'changesetLocked' ).set( false );
|
||||
} );
|
||||
|
||||
request.fail( function( response ) {
|
||||
var message = response.message || api.l10n.unknownRequestFail;
|
||||
li.find( '.notice-error' ).prop( 'hidden', false ).text( message );
|
||||
|
||||
request.always( function() {
|
||||
takeOverButton.removeClass( 'disabled' );
|
||||
} );
|
||||
} );
|
||||
|
||||
request.always( function() {
|
||||
request = null;
|
||||
} );
|
||||
} );
|
||||
|
||||
return li;
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Start lock.
|
||||
*
|
||||
* @since 4.9.0
|
||||
*
|
||||
* @param {object} [args] - Args.
|
||||
* @param {object} [args.lockUser] - Lock user data.
|
||||
* @param {boolean} [args.allowOverride=false] - Whether override is allowed.
|
||||
* @returns {void}
|
||||
*/
|
||||
function startLock( args ) {
|
||||
if ( args && args.lockUser ) {
|
||||
api.settings.changeset.lockUser = args.lockUser;
|
||||
}
|
||||
api.state( 'changesetLocked' ).set( true );
|
||||
api.notifications.add( new LockedNotification( 'changeset_locked', {
|
||||
lockUser: api.settings.changeset.lockUser,
|
||||
allowOverride: Boolean( args && args.allowOverride )
|
||||
} ) );
|
||||
}
|
||||
|
||||
// Show initial notification.
|
||||
if ( api.settings.changeset.lockUser ) {
|
||||
startLock( { allowOverride: true } );
|
||||
}
|
||||
|
||||
// Check for lock when sending heartbeat requests.
|
||||
$( document ).on( 'heartbeat-send.update_lock_notice', function( event, data ) {
|
||||
data.check_changeset_lock = true;
|
||||
} );
|
||||
|
||||
// Handle heartbeat ticks.
|
||||
$( document ).on( 'heartbeat-tick.update_lock_notice', function( event, data ) {
|
||||
var notification, code = 'changeset_locked';
|
||||
if ( ! data.customize_changeset_lock_user ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Update notification when a different user takes over.
|
||||
notification = api.notifications( code );
|
||||
if ( notification && notification.lockUser.id !== api.settings.changeset.lockUser.id ) {
|
||||
api.notifications.remove( code );
|
||||
}
|
||||
|
||||
startLock( {
|
||||
lockUser: data.customize_changeset_lock_user
|
||||
} );
|
||||
} );
|
||||
|
||||
// Handle locking in response to changeset save errors.
|
||||
api.bind( 'error', function( response ) {
|
||||
if ( 'changeset_locked' === response.code && response.lock_user ) {
|
||||
startLock( {
|
||||
lockUser: response.lock_user
|
||||
} );
|
||||
}
|
||||
} );
|
||||
} )();
|
||||
|
||||
// Set up initial notifications.
|
||||
(function() {
|
||||
|
||||
|
@ -7733,11 +7965,12 @@
|
|||
|
||||
// Handle dismissal of notice.
|
||||
li.find( '.notice-dismiss' ).on( 'click', function() {
|
||||
wp.ajax.post( 'customize_dismiss_autosave', {
|
||||
wp.ajax.post( 'customize_dismiss_autosave_or_lock', {
|
||||
wp_customize: 'on',
|
||||
customize_theme: api.settings.theme.stylesheet,
|
||||
customize_changeset_uuid: api.settings.changeset.uuid,
|
||||
nonce: api.settings.nonce.dismiss_autosave
|
||||
nonce: api.settings.nonce.dismiss_autosave_or_lock,
|
||||
dismiss_autosave: true
|
||||
} );
|
||||
} );
|
||||
|
||||
|
@ -8167,7 +8400,7 @@
|
|||
|
||||
// Prompt user with AYS dialog if leaving the Customizer with unsaved changes
|
||||
$( window ).on( 'beforeunload.customize-confirm', function() {
|
||||
if ( ! isCleanState() ) {
|
||||
if ( ! isCleanState() && ! api.state( 'changesetLocked' ).get() ) {
|
||||
setTimeout( function() {
|
||||
overlay.removeClass( 'customize-loading' );
|
||||
}, 1 );
|
||||
|
@ -8178,11 +8411,14 @@
|
|||
api.bind( 'change', startPromptingBeforeUnload );
|
||||
|
||||
function requestClose() {
|
||||
var clearedToClose = $.Deferred();
|
||||
var clearedToClose = $.Deferred(), dismissAutoSave = false, dismissLock = false;
|
||||
|
||||
if ( isCleanState() ) {
|
||||
clearedToClose.resolve();
|
||||
dismissLock = true;
|
||||
} else if ( confirm( api.l10n.saveAlert ) ) {
|
||||
|
||||
dismissLock = true;
|
||||
|
||||
// Mark all settings as clean to prevent another call to requestChangesetUpdate.
|
||||
api.each( function( setting ) {
|
||||
setting._dirty = false;
|
||||
|
@ -8191,24 +8427,29 @@
|
|||
$( window ).off( 'beforeunload.wp-customize-changeset-update' );
|
||||
|
||||
closeBtn.css( 'cursor', 'progress' );
|
||||
if ( '' === api.state( 'changesetStatus' ).get() ) {
|
||||
clearedToClose.resolve();
|
||||
if ( '' !== api.state( 'changesetStatus' ).get() ) {
|
||||
dismissAutoSave = true;
|
||||
}
|
||||
} else {
|
||||
wp.ajax.send( 'customize_dismiss_autosave', {
|
||||
clearedToClose.reject();
|
||||
}
|
||||
|
||||
if ( dismissLock || dismissAutoSave ) {
|
||||
wp.ajax.send( 'customize_dismiss_autosave_or_lock', {
|
||||
timeout: 500, // Don't wait too long.
|
||||
data: {
|
||||
wp_customize: 'on',
|
||||
customize_theme: api.settings.theme.stylesheet,
|
||||
customize_changeset_uuid: api.settings.changeset.uuid,
|
||||
nonce: api.settings.nonce.dismiss_autosave
|
||||
nonce: api.settings.nonce.dismiss_autosave_or_lock,
|
||||
dismiss_autosave: dismissAutoSave,
|
||||
dismiss_lock: dismissLock
|
||||
}
|
||||
} ).always( function() {
|
||||
clearedToClose.resolve();
|
||||
} );
|
||||
}
|
||||
} else {
|
||||
clearedToClose.reject();
|
||||
}
|
||||
|
||||
return clearedToClose.promise();
|
||||
}
|
||||
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -174,7 +174,7 @@ final class WP_Customize_Manager {
|
|||
protected $messenger_channel;
|
||||
|
||||
/**
|
||||
* Whether the autosave revision of the changeset should should be loaded.
|
||||
* Whether the autosave revision of the changeset should be loaded.
|
||||
*
|
||||
* @since 4.9.0
|
||||
* @var bool
|
||||
|
@ -377,7 +377,10 @@ final class WP_Customize_Manager {
|
|||
add_action( 'wp_ajax_customize_trash', array( $this, 'handle_changeset_trash_request' ) );
|
||||
add_action( 'wp_ajax_customize_refresh_nonces', array( $this, 'refresh_nonces' ) );
|
||||
add_action( 'wp_ajax_customize_load_themes', array( $this, 'handle_load_themes_request' ) );
|
||||
add_action( 'wp_ajax_customize_dismiss_autosave', array( $this, 'handle_dismiss_autosave_request' ) );
|
||||
add_filter( 'heartbeat_settings', array( $this, 'add_customize_screen_to_heartbeat_settings' ) );
|
||||
add_filter( 'heartbeat_received', array( $this, 'check_changeset_lock_with_heartbeat' ), 10, 3 );
|
||||
add_action( 'wp_ajax_customize_override_changeset_lock', array( $this, 'handle_override_changeset_lock_request' ) );
|
||||
add_action( 'wp_ajax_customize_dismiss_autosave_or_lock', array( $this, 'handle_dismiss_autosave_or_lock_request' ) );
|
||||
|
||||
add_action( 'customize_register', array( $this, 'register_controls' ) );
|
||||
add_action( 'customize_register', array( $this, 'register_dynamic_settings' ), 11 ); // allow code to create settings first
|
||||
|
@ -629,6 +632,8 @@ final class WP_Customize_Manager {
|
|||
|
||||
$this->_changeset_uuid = $changeset_uuid;
|
||||
}
|
||||
|
||||
$this->set_changeset_lock( $this->changeset_post_id() );
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1106,7 +1111,7 @@ final class WP_Customize_Manager {
|
|||
$this->_changeset_data = array();
|
||||
} else {
|
||||
if ( $this->autosaved() ) {
|
||||
$autosave_post = wp_get_post_autosave( $changeset_post_id );
|
||||
$autosave_post = wp_get_post_autosave( $changeset_post_id, get_current_user_id() );
|
||||
if ( $autosave_post ) {
|
||||
$data = $this->get_changeset_post_data( $autosave_post->ID );
|
||||
if ( ! is_wp_error( $data ) ) {
|
||||
|
@ -2376,11 +2381,24 @@ final class WP_Customize_Manager {
|
|||
}
|
||||
}
|
||||
|
||||
$lock_user_id = null;
|
||||
$autosave = ! empty( $_POST['customize_changeset_autosave'] );
|
||||
if ( ! $is_new_changeset ) {
|
||||
$lock_user_id = wp_check_post_lock( $this->changeset_post_id() );
|
||||
}
|
||||
|
||||
// Force request to autosave when changeset is locked.
|
||||
if ( $lock_user_id && ! $autosave ) {
|
||||
$autosave = true;
|
||||
$changeset_status = null;
|
||||
$changeset_date_gmt = null;
|
||||
}
|
||||
|
||||
if ( $autosave && ! defined( 'DOING_AUTOSAVE' ) ) { // Back-compat.
|
||||
define( 'DOING_AUTOSAVE', true );
|
||||
}
|
||||
|
||||
$autosaved = false;
|
||||
$r = $this->save_changeset_post( array(
|
||||
'status' => $changeset_status,
|
||||
'title' => $changeset_title,
|
||||
|
@ -2388,6 +2406,21 @@ final class WP_Customize_Manager {
|
|||
'data' => $input_changeset_data,
|
||||
'autosave' => $autosave,
|
||||
) );
|
||||
if ( $autosave && ! is_wp_error( $r ) ) {
|
||||
$autosaved = true;
|
||||
}
|
||||
|
||||
// If the changeset was locked and an autosave request wasn't itself an error, then now explicitly return with a failure.
|
||||
if ( $lock_user_id && ! is_wp_error( $r ) ) {
|
||||
$r = new WP_Error(
|
||||
'changeset_locked',
|
||||
__( 'Changeset is being edited by other user.' ),
|
||||
array(
|
||||
'lock_user' => $this->get_lock_user_data( $lock_user_id ),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if ( is_wp_error( $r ) ) {
|
||||
$response = array(
|
||||
'message' => $r->get_error_message(),
|
||||
|
@ -2413,6 +2446,10 @@ final class WP_Customize_Manager {
|
|||
$response['changeset_status'] = 'publish';
|
||||
}
|
||||
|
||||
if ( 'publish' !== $response['changeset_status'] ) {
|
||||
$this->set_changeset_lock( $changeset_post->ID );
|
||||
}
|
||||
|
||||
if ( 'future' === $response['changeset_status'] ) {
|
||||
$response['changeset_date'] = $changeset_post->post_date;
|
||||
}
|
||||
|
@ -2422,6 +2459,10 @@ final class WP_Customize_Manager {
|
|||
}
|
||||
}
|
||||
|
||||
if ( $autosave ) {
|
||||
$response['autosaved'] = $autosaved;
|
||||
}
|
||||
|
||||
if ( isset( $response['setting_validities'] ) ) {
|
||||
$response['setting_validities'] = array_map( array( $this, 'prepare_setting_validity_for_js' ), $response['setting_validities'] );
|
||||
}
|
||||
|
@ -2684,6 +2725,7 @@ final class WP_Customize_Manager {
|
|||
array(
|
||||
'type' => $setting->type,
|
||||
'user_id' => $args['user_id'],
|
||||
'date_modified_gmt' => current_time( 'mysql', true ),
|
||||
)
|
||||
);
|
||||
|
||||
|
@ -2798,7 +2840,7 @@ final class WP_Customize_Manager {
|
|||
$r = wp_update_post( wp_slash( $post_array ), true );
|
||||
|
||||
// Delete autosave revision when the changeset is updated.
|
||||
$autosave_draft = wp_get_post_autosave( $changeset_post_id );
|
||||
$autosave_draft = wp_get_post_autosave( $changeset_post_id, get_current_user_id() );
|
||||
if ( $autosave_draft ) {
|
||||
wp_delete_post( $autosave_draft->ID, true );
|
||||
}
|
||||
|
@ -2989,6 +3031,157 @@ final class WP_Customize_Manager {
|
|||
return $caps;
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks the changeset post as being currently edited by the current user.
|
||||
*
|
||||
* @since 4.9.0
|
||||
*
|
||||
* @param int $changeset_post_id Changeset post id.
|
||||
* @param bool $take_over Take over the changeset, default is false.
|
||||
*/
|
||||
public function set_changeset_lock( $changeset_post_id, $take_over = false ) {
|
||||
if ( $changeset_post_id ) {
|
||||
$can_override = ! (bool) get_post_meta( $changeset_post_id, '_edit_lock', true );
|
||||
|
||||
if ( $take_over ) {
|
||||
$can_override = true;
|
||||
}
|
||||
|
||||
if ( $can_override ) {
|
||||
$lock = sprintf( '%s:%s', time(), get_current_user_id() );
|
||||
update_post_meta( $changeset_post_id, '_edit_lock', $lock );
|
||||
} else {
|
||||
$this->refresh_changeset_lock( $changeset_post_id );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes changeset lock with the current time if current user edited the changeset before.
|
||||
*
|
||||
* @since 4.9.0
|
||||
*
|
||||
* @param int $changeset_post_id Changeset post id.
|
||||
*/
|
||||
public function refresh_changeset_lock( $changeset_post_id ) {
|
||||
if ( ! $changeset_post_id ) {
|
||||
return;
|
||||
}
|
||||
$lock = get_post_meta( $changeset_post_id, '_edit_lock', true );
|
||||
$lock = explode( ':', $lock );
|
||||
|
||||
if ( $lock && ! empty( $lock[1] ) ) {
|
||||
$user_id = intval( $lock[1] );
|
||||
$current_user_id = get_current_user_id();
|
||||
if ( $user_id === $current_user_id ) {
|
||||
$lock = sprintf( '%s:%s', time(), $user_id );
|
||||
update_post_meta( $changeset_post_id, '_edit_lock', $lock );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter heartbeat settings for the Customizer.
|
||||
*
|
||||
* @since 4.9.0
|
||||
* @param array $settings Current settings to filter.
|
||||
* @return array Heartbeat settings.
|
||||
*/
|
||||
public function add_customize_screen_to_heartbeat_settings( $settings ) {
|
||||
global $pagenow;
|
||||
if ( 'customize.php' === $pagenow ) {
|
||||
$settings['screenId'] = 'customize';
|
||||
}
|
||||
return $settings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get lock user data.
|
||||
*
|
||||
* @since 4.9.0
|
||||
*
|
||||
* @param int $user_id User ID.
|
||||
* @return array|null User data formatted for client.
|
||||
*/
|
||||
protected function get_lock_user_data( $user_id ) {
|
||||
if ( ! $user_id ) {
|
||||
return null;
|
||||
}
|
||||
$lock_user = get_userdata( $user_id );
|
||||
if ( ! $lock_user ) {
|
||||
return null;
|
||||
}
|
||||
return array(
|
||||
'id' => $lock_user->ID,
|
||||
'name' => $lock_user->display_name,
|
||||
'avatar' => get_avatar_url( $lock_user->ID, array( 'size' => 128 ) ),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check locked changeset with heartbeat API.
|
||||
*
|
||||
* @since 4.9.0
|
||||
*
|
||||
* @param array $response The Heartbeat response.
|
||||
* @param array $data The $_POST data sent.
|
||||
* @param string $screen_id The screen id.
|
||||
* @return array The Heartbeat response.
|
||||
*/
|
||||
public function check_changeset_lock_with_heartbeat( $response, $data, $screen_id ) {
|
||||
if ( array_key_exists( 'check_changeset_lock', $data ) && 'customize' === $screen_id && current_user_can( 'customize' ) && $this->changeset_post_id() ) {
|
||||
$lock_user_id = wp_check_post_lock( $this->changeset_post_id() );
|
||||
|
||||
if ( $lock_user_id ) {
|
||||
$response['customize_changeset_lock_user'] = $this->get_lock_user_data( $lock_user_id );
|
||||
} else {
|
||||
|
||||
// Refreshing time will ensure that the user is sitting on customizer and has not closed the customizer tab.
|
||||
$this->refresh_changeset_lock( $this->changeset_post_id() );
|
||||
}
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes changeset lock when take over request is sent via Ajax.
|
||||
*
|
||||
* @since 4.9.0
|
||||
*/
|
||||
public function handle_override_changeset_lock_request() {
|
||||
if ( ! $this->is_preview() ) {
|
||||
wp_send_json_error( 'not_preview', 400 );
|
||||
}
|
||||
|
||||
if ( ! check_ajax_referer( 'customize_override_changeset_lock', 'nonce', false ) ) {
|
||||
wp_send_json_error( array(
|
||||
'code' => 'invalid_nonce',
|
||||
'message' => __( 'Security check failed.' ),
|
||||
) );
|
||||
}
|
||||
|
||||
$changeset_post_id = $this->changeset_post_id();
|
||||
|
||||
if ( empty( $changeset_post_id ) ) {
|
||||
wp_send_json_error( array(
|
||||
'code' => 'no_changeset_found_to_take_over',
|
||||
'message' => __( 'No changeset found to take over' ),
|
||||
) );
|
||||
}
|
||||
|
||||
if ( ! current_user_can( get_post_type_object( 'customize_changeset' )->cap->edit_post, $changeset_post_id ) ) {
|
||||
wp_send_json_error( array(
|
||||
'code' => 'cannot_remove_changeset_lock',
|
||||
'message' => __( 'Sorry you are not allowed to take over.' ),
|
||||
) );
|
||||
}
|
||||
|
||||
$this->set_changeset_lock( $changeset_post_id, true );
|
||||
|
||||
wp_send_json_success( 'changeset_taken_over' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a changeset revision should be made.
|
||||
*
|
||||
|
@ -3033,11 +3226,14 @@ final class WP_Customize_Manager {
|
|||
*
|
||||
* @since 4.7.0
|
||||
* @see _wp_customize_publish_changeset()
|
||||
* @global wpdb $wpdb
|
||||
*
|
||||
* @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 ) {
|
||||
global $wpdb;
|
||||
|
||||
$publishing_changeset_data = $this->get_changeset_post_data( $changeset_post_id );
|
||||
if ( is_wp_error( $publishing_changeset_data ) ) {
|
||||
return $publishing_changeset_data;
|
||||
|
@ -3175,6 +3371,30 @@ final class WP_Customize_Manager {
|
|||
$this->_changeset_post_id = $previous_changeset_post_id;
|
||||
$this->_changeset_uuid = $previous_changeset_uuid;
|
||||
|
||||
/*
|
||||
* Convert all autosave revisions into their own auto-drafts so that users can be prompted to
|
||||
* restore them when a changeset is published, but they had been locked out from including
|
||||
* their changes in the changeset.
|
||||
*/
|
||||
$revisions = wp_get_post_revisions( $changeset_post_id, array( 'check_enabled' => false ) );
|
||||
foreach ( $revisions as $revision ) {
|
||||
if ( false !== strpos( $revision->post_name, "{$changeset_post_id}-autosave" ) ) {
|
||||
$wpdb->update(
|
||||
$wpdb->posts,
|
||||
array(
|
||||
'post_status' => 'auto-draft',
|
||||
'post_type' => 'customize_changeset',
|
||||
'post_name' => wp_generate_uuid4(),
|
||||
'post_parent' => 0,
|
||||
),
|
||||
array(
|
||||
'ID' => $revision->ID,
|
||||
)
|
||||
);
|
||||
clean_post_cache( $revision->ID );
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -3229,21 +3449,39 @@ final class WP_Customize_Manager {
|
|||
}
|
||||
|
||||
/**
|
||||
* Delete a given auto-draft changeset or the autosave revision for a given changeset.
|
||||
* Delete a given auto-draft changeset or the autosave revision for a given changeset or delete changeset lock.
|
||||
*
|
||||
* @since 4.9.0
|
||||
*/
|
||||
public function handle_dismiss_autosave_request() {
|
||||
public function handle_dismiss_autosave_or_lock_request() {
|
||||
if ( ! $this->is_preview() ) {
|
||||
wp_send_json_error( 'not_preview', 400 );
|
||||
}
|
||||
|
||||
if ( ! check_ajax_referer( 'customize_dismiss_autosave', 'nonce', false ) ) {
|
||||
if ( ! check_ajax_referer( 'customize_dismiss_autosave_or_lock', 'nonce', false ) ) {
|
||||
wp_send_json_error( 'invalid_nonce', 403 );
|
||||
}
|
||||
|
||||
$changeset_post_id = $this->changeset_post_id();
|
||||
$dismiss_lock = ! empty( $_POST['dismiss_lock'] );
|
||||
$dismiss_autosave = ! empty( $_POST['dismiss_autosave'] );
|
||||
|
||||
if ( $dismiss_lock ) {
|
||||
if ( empty( $changeset_post_id ) && ! $dismiss_autosave ) {
|
||||
wp_send_json_error( 'no_changeset_to_dismiss_lock', 404 );
|
||||
}
|
||||
if ( ! current_user_can( get_post_type_object( 'customize_changeset' )->cap->edit_post, $changeset_post_id ) && ! $dismiss_autosave ) {
|
||||
wp_send_json_error( 'cannot_remove_changeset_lock', 403 );
|
||||
}
|
||||
|
||||
delete_post_meta( $changeset_post_id, '_edit_lock' );
|
||||
|
||||
if ( ! $dismiss_autosave ) {
|
||||
wp_send_json_success( 'changeset_lock_dismissed' );
|
||||
}
|
||||
}
|
||||
|
||||
if ( $dismiss_autosave ) {
|
||||
if ( empty( $changeset_post_id ) || 'auto-draft' === get_post_status( $changeset_post_id ) ) {
|
||||
$dismissed = $this->dismiss_user_auto_draft_changesets();
|
||||
if ( $dismissed > 0 ) {
|
||||
|
@ -3252,7 +3490,7 @@ final class WP_Customize_Manager {
|
|||
wp_send_json_error( 'no_auto_draft_to_delete', 404 );
|
||||
}
|
||||
} else {
|
||||
$revision = wp_get_post_autosave( $changeset_post_id );
|
||||
$revision = wp_get_post_autosave( $changeset_post_id, get_current_user_id() );
|
||||
|
||||
if ( $revision ) {
|
||||
if ( ! current_user_can( get_post_type_object( 'customize_changeset' )->cap->delete_post, $changeset_post_id ) ) {
|
||||
|
@ -3268,6 +3506,8 @@ final class WP_Customize_Manager {
|
|||
wp_send_json_error( 'no_autosave_revision_to_delete', 404 );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
wp_send_json_error( 'unknown_error', 500 );
|
||||
}
|
||||
|
||||
|
@ -3817,6 +4057,39 @@ final class WP_Customize_Manager {
|
|||
</li>
|
||||
</script>
|
||||
|
||||
<script type="text/html" id="tmpl-customize-changeset-locked-notification">
|
||||
<li class="notice notice-{{ data.type || 'info' }} {{ data.containerClasses || '' }}" data-code="{{ data.code }}" data-type="{{ data.type }}">
|
||||
<div class="notification-message customize-changeset-locked-message">
|
||||
<img class="customize-changeset-locked-avatar" src="{{ data.lockUser.avatar }}" alt="{{ data.lockUser.name }}">
|
||||
<p class="currently-editing">
|
||||
<# if ( data.message ) { #>
|
||||
{{{ data.message }}}
|
||||
<# } else if ( data.allowOverride ) { #>
|
||||
<?php
|
||||
/* translators: %s: User who is customizing the changeset in customizer. */
|
||||
printf( __( '%s is already customizing this site. Do you want to take over?' ), '{{ data.lockUser.name }}' );
|
||||
?>
|
||||
<# } else { #>
|
||||
<?php
|
||||
/* translators: %s: User who is customizing the changeset in customizer. */
|
||||
printf( __( '%s is already customizing this site. Please wait until they are done to try customizing. Your latest changes have been autosaved.' ), '{{ data.lockUser.name }}' );
|
||||
?>
|
||||
<# } #>
|
||||
</p>
|
||||
<p class="notice notice-error notice-alt" hidden></p>
|
||||
<p class="action-buttons">
|
||||
<# if ( data.returnUrl !== data.previewUrl ) { #>
|
||||
<a class="button customize-notice-go-back-button" href="{{ data.returnUrl }}"><?php _e( 'Go back' ); ?></a>
|
||||
<# } #>
|
||||
<a class="button customize-notice-preview-button" href="{{ data.frontendPreviewUrl }}"><?php _e( 'Preview' ); ?></a>
|
||||
<# if ( data.allowOverride ) { #>
|
||||
<button class="button button-primary wp-tab-last customize-notice-take-over-button"><?php _e( 'Take over' ); ?></button>
|
||||
<# } #>
|
||||
</p>
|
||||
</div>
|
||||
</li>
|
||||
</script>
|
||||
|
||||
<?php
|
||||
/* The following template is obsolete in core but retained for plugins. */
|
||||
?>
|
||||
|
@ -4188,7 +4461,8 @@ final class WP_Customize_Manager {
|
|||
'save' => wp_create_nonce( 'save-customize_' . $this->get_stylesheet() ),
|
||||
'preview' => wp_create_nonce( 'preview-customize_' . $this->get_stylesheet() ),
|
||||
'switch_themes' => wp_create_nonce( 'switch_themes' ),
|
||||
'dismiss_autosave' => wp_create_nonce( 'customize_dismiss_autosave' ),
|
||||
'dismiss_autosave_or_lock' => wp_create_nonce( 'customize_dismiss_autosave_or_lock' ),
|
||||
'override_lock' => wp_create_nonce( 'customize_override_changeset_lock' ),
|
||||
'trash' => wp_create_nonce( 'trash_customize_changeset' ),
|
||||
);
|
||||
|
||||
|
@ -4231,7 +4505,7 @@ final class WP_Customize_Manager {
|
|||
$changeset_post_id = $this->changeset_post_id();
|
||||
if ( ! $this->saved_starter_content_changeset && ! $this->autosaved() ) {
|
||||
if ( $changeset_post_id ) {
|
||||
$autosave_revision_post = wp_get_post_autosave( $changeset_post_id );
|
||||
$autosave_revision_post = wp_get_post_autosave( $changeset_post_id, get_current_user_id() );
|
||||
} else {
|
||||
$autosave_autodraft_posts = $this->get_changeset_posts( array(
|
||||
'posts_per_page' => 1,
|
||||
|
@ -4277,6 +4551,11 @@ final class WP_Customize_Manager {
|
|||
$initial_date = current_time( 'mysql', false );
|
||||
}
|
||||
|
||||
$lock_user_id = false;
|
||||
if ( $this->changeset_post_id() ) {
|
||||
$lock_user_id = wp_check_post_lock( $this->changeset_post_id() );
|
||||
}
|
||||
|
||||
$settings = array(
|
||||
'changeset' => array(
|
||||
'uuid' => $this->changeset_uuid(),
|
||||
|
@ -4288,6 +4567,7 @@ final class WP_Customize_Manager {
|
|||
'currentUserCanPublish' => $current_user_can_publish,
|
||||
'publishDate' => $initial_date,
|
||||
'statusChoices' => $status_choices,
|
||||
'lockUser' => $lock_user_id ? $this->get_lock_user_data( $lock_user_id ) : null,
|
||||
),
|
||||
'initialServerDate' => current_time( 'mysql', false ),
|
||||
'dateFormat' => get_option( 'date_format' ),
|
||||
|
|
|
@ -367,6 +367,10 @@
|
|||
has_focus: settings.hasFocus
|
||||
};
|
||||
|
||||
if ( 'customize' === settings.screenId ) {
|
||||
ajaxData.wp_customize = 'on';
|
||||
}
|
||||
|
||||
settings.connecting = true;
|
||||
settings.xhr = $.ajax({
|
||||
url: settings.url,
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -547,7 +547,7 @@ function wp_default_scripts( &$scripts ) {
|
|||
$scripts->add( 'customize-preview', "/wp-includes/js/customize-preview$suffix.js", array( 'wp-a11y', 'customize-base' ), false, 1 );
|
||||
$scripts->add( 'customize-models', "/wp-includes/js/customize-models.js", array( 'underscore', 'backbone' ), false, 1 );
|
||||
$scripts->add( 'customize-views', "/wp-includes/js/customize-views.js", array( 'jquery', 'underscore', 'imgareaselect', 'customize-models', 'media-editor', 'media-views' ), false, 1 );
|
||||
$scripts->add( 'customize-controls', "/wp-admin/js/customize-controls$suffix.js", array( 'customize-base', 'wp-a11y', 'wp-util' ), false, 1 );
|
||||
$scripts->add( 'customize-controls', "/wp-admin/js/customize-controls$suffix.js", array( 'customize-base', 'wp-a11y', 'wp-util', 'jquery-ui-core' ), false, 1 );
|
||||
did_action( 'init' ) && $scripts->localize( 'customize-controls', '_wpCustomizeControlsL10n', array(
|
||||
'activate' => __( 'Activate & Publish' ),
|
||||
'save' => __( 'Save & Publish' ), // @todo Remove as not required.
|
||||
|
@ -574,11 +574,13 @@ function wp_default_scripts( &$scripts ) {
|
|||
'collapseSidebar' => _x( 'Hide Controls', 'label for hide controls button without length constraints' ),
|
||||
'expandSidebar' => _x( 'Show Controls', 'label for hide controls button without length constraints' ),
|
||||
'untitledBlogName' => __( '(Untitled)' ),
|
||||
'serverSaveError' => __( 'Failed connecting to the server. Please try saving again.' ),
|
||||
'unknownRequestFail' => __( 'Looks like something’s gone wrong. Wait a couple seconds, and then try again.' ),
|
||||
'themeDownloading' => __( 'Downloading your new theme…' ),
|
||||
'themePreviewWait' => __( 'Setting up your live preview. This may take a bit.' ),
|
||||
'revertingChanges' => __( 'Reverting unpublished changes…' ),
|
||||
'trashConfirm' => __( 'Are you sure you’d like to discard your unpublished changes?' ),
|
||||
/* translators: %s: Display name of the user who has taken over the changeset in customizer. */
|
||||
'takenOverMessage' => __( '%s has taken over and is currently customizing.' ),
|
||||
/* translators: %s: URL to the Customizer to load the autosaved version */
|
||||
'autosaveNotice' => __( 'There is a more recent autosave of your changes than the one you are previewing. <a href="%s">Restore the autosave</a>' ),
|
||||
'videoHeaderNotice' => __( 'This theme doesn’t support video headers on this page. Navigate to the front page or another page that supports video headers.' ),
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
*
|
||||
* @global string $wp_version
|
||||
*/
|
||||
$wp_version = '4.9-beta1-41838';
|
||||
$wp_version = '4.9-beta1-41839';
|
||||
|
||||
/**
|
||||
* Holds the WordPress DB revision, increments when changes are made to the WordPress DB schema.
|
||||
|
|
Loading…
Reference in New Issue