0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-02-10 23:36:14 -05:00

Fixed potential unexpected unpublish and infinite loop when leaving editor (#18816)

closes https://github.com/TryGhost/Product/issues/4059

- modified `saveTask` so when it has the `leavingEditor` option it doesn't attempt to set a new post status
- save when leaving the editor is never a publish/unpublish-related event, rather it's a convenience autosave and a forced revision creation so modifying the post status should never happen for those saves
- updated the `willTransition` handling to avoid repeated saves
- sets a property on the controller whenever we attempt a save+transition retry on leaving the editor then skips any further attempted saves on transitions
- although it would be preferable we can't use `try/catch` on these saves because our save task always catches and doesn't re-throw, adjusting that would be a much larger change than we want to make for this fix
This commit is contained in:
Kevin Ansfield 2023-11-01 11:59:29 +00:00 committed by GitHub
parent b9c2989325
commit 541a1fae5e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23

View file

@ -131,6 +131,7 @@ export default class LexicalEditorController extends Controller {
/* private properties ----------------------------------------------------*/
_leaveConfirmed = false;
_saveOnLeavePerformed = false;
_previousTagNames = null; // set by setPost and _postSaved, used in hasDirtyAttributes
/* computed properties ---------------------------------------------------*/
@ -433,7 +434,6 @@ export default class LexicalEditorController extends Controller {
*saveTask(options = {}) {
let prevStatus = this.get('post.status');
let isNew = this.get('post.isNew');
let status;
const adapterOptions = {};
this.cancelAutosave();
@ -442,28 +442,39 @@ export default class LexicalEditorController extends Controller {
return;
}
if (options.backgroundSave) {
// do not allow a post's status to be set to published by a background save
status = 'draft';
// leaving the editor should never result in a status change, we only save on editor
// leave to trigger a revision save
if (options.leavingEditor) {
// ensure we always have a status present otherwise we'll error when saving
if (!this.post.status) {
this.post.status = 'draft';
}
} else {
if (this.get('post.pastScheduledTime')) {
status = (!this.willSchedule && !this.willPublish) ? 'draft' : 'published';
let status;
if (options.backgroundSave) {
// do not allow a post's status to be set to published by a background save
status = 'draft';
} else {
if (this.willPublish && !this.get('post.isScheduled')) {
status = 'published';
} else if (this.willSchedule && !this.get('post.isPublished')) {
status = 'scheduled';
} else if (this.get('post.isSent')) {
status = 'sent';
if (this.get('post.pastScheduledTime')) {
status = (!this.willSchedule && !this.willPublish) ? 'draft' : 'published';
} else {
status = 'draft';
if (this.willPublish && !this.get('post.isScheduled')) {
status = 'published';
} else if (this.willSchedule && !this.get('post.isPublished')) {
status = 'scheduled';
} else if (this.get('post.isSent')) {
status = 'sent';
} else {
status = 'draft';
}
}
}
}
// set manually here instead of in beforeSaveTask because the
// new publishing flow sets the post status manually on publish
this.set('post.status', status);
// set manually here instead of in beforeSaveTask because the
// new publishing flow sets the post status manually on publish
this.set('post.status', status);
}
const explicitSave = !options.backgroundSave;
const leavingEditor = options.leavingEditor;
@ -496,8 +507,7 @@ export default class LexicalEditorController extends Controller {
yield this.modals.open(ReAuthenticateModal);
if (this.session.isAuthenticated) {
this.saveTask.perform(options);
return;
return this.saveTask.perform(options);
}
}
@ -959,6 +969,10 @@ export default class LexicalEditorController extends Controller {
return transition.retry();
}
const fromNewToEdit = this.router.currentRouteName === 'lexical-editor.new'
&& transition.targetName === 'lexical-editor.edit'
&& transition.intent.contexts?.[0]?.id === post.id;
// clean up blank cards when leaving the editor if we have a draft post
// - blank cards could be left around due to autosave triggering whilst
// a blank card is present then the user attempting to leave
@ -968,31 +982,30 @@ export default class LexicalEditorController extends Controller {
// this._koenig.cleanup();
// }
// if we need to save when leaving the editor, abort the transition, save,
// then retry. If a previous transition already performed a save, skip to
// avoid potential infinite transition+save loops
let hasDirtyAttributes = this.hasDirtyAttributes;
let state = post.getProperties('isDeleted', 'isSaving', 'hasDirtyAttributes', 'isNew');
// Check if anything has changed since the last revision
let postRevisions = post.get('postRevisions').toArray();
let latestRevision = postRevisions[postRevisions.length - 1];
let hasChangedSinceLastRevision = !post.isNew && post.lexical.replaceAll(this.config.blogUrl, '') !== latestRevision.lexical.replaceAll(this.config.blogUrl, '');
let fromNewToEdit = this.router.currentRouteName === 'lexical-editor.new'
&& transition.targetName === 'lexical-editor.edit'
&& transition.intent.contexts
&& transition.intent.contexts[0]
&& transition.intent.contexts[0].id === post.id;
let hasChangedSinceLastRevision = !latestRevision || (!post.isNew && post.lexical.replaceAll(this.config.blogUrl, '') !== latestRevision.lexical.replaceAll(this.config.blogUrl, ''));
let deletedWithoutChanges = state.isDeleted
&& (state.isSaving || !state.hasDirtyAttributes);
&& (state.isSaving || !state.hasDirtyAttributes);
// If leaving the editor and the post has changed since we last saved a revision, always save a new revision
if (hasChangedSinceLastRevision) {
if (!this._saveOnLeavePerformed && hasChangedSinceLastRevision) {
transition.abort();
if (this._autosaveRunning) {
this.cancelAutosave();
this.autosaveTask.cancelAll();
}
await this.autosaveTask.perform({leavingEditor: true, backgroundSave: false});
this._saveOnLeavePerformed = true;
return transition.retry();
}
@ -1013,7 +1026,10 @@ export default class LexicalEditorController extends Controller {
this.autosaveTask.cancelAll();
// If leaving the editor, always save a revision
await this.autosaveTask.perform({leavingEditor: true});
if (!this._saveOnLeavePerformed) {
await this.autosaveTask.perform({leavingEditor: true});
this._saveOnLeavePerformed = true;
}
return transition.retry();
}
@ -1063,6 +1079,7 @@ export default class LexicalEditorController extends Controller {
this._previousTagNames = [];
this._leaveConfirmed = false;
this._saveOnLeavePerformed = false;
this.set('post', null);
this.set('hasDirtyAttributes', false);