- Added testing to cover warning cases.
- Refactored logic to be simpler and move much of the business out of
the controller.
- Added new message that's more suitable to the case this was handling.
- For detecting an outdated draft, checked the draft created_at time
instead of updated_at to better fit the scenario being checked.
- Updated some method types to align with those potentially being used
in the logic of the code.
- Added a cache of shown messages on the front-end to prevent them
re-showing on every save during the session, even if dismissed.
use BookStack\Auth\User;
use BookStack\Model;
use Carbon\Carbon;
+use Illuminate\Database\Eloquent\Relations\BelongsTo;
/**
* Class PageRevision.
* @property string $book_slug
* @property int $created_by
* @property Carbon $created_at
+ * @property Carbon $updated_at
* @property string $type
* @property string $summary
* @property string $markdown
* @property string $html
* @property int $revision_number
+ * @property Page $page
*/
class PageRevision extends Model
{
/**
* Get the user that created the page revision.
- *
- * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
- public function createdBy()
+ public function createdBy(): BelongsTo
{
return $this->belongsTo(User::class, 'created_by');
}
/**
* Get the page this revision originates from.
- *
- * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
- public function page()
+ public function page(): BelongsTo
{
return $this->belongsTo(Page::class);
}
/**
* Check if there's active editing being performed on this page.
- *
- * @return bool
*/
public function hasActiveEditing(): bool
{
return trans('entities.pages_draft_edit_active.message', ['start' => $userMessage, 'time' => $timeMessage]);
}
+ /**
+ * Get any editor clash warning messages to show for the given draft revision.
+ * @param PageRevision|Page $draft
+ * @return string[]
+ */
+ public function getWarningMessagesForDraft($draft): array
+ {
+ $warnings = [];
+
+ if ($this->hasActiveEditing()) {
+ $warnings[] = $this->activeEditingMessage();
+ }
+
+ if ($draft instanceof PageRevision && $this->hasPageBeenUpdatedSinceDraftCreated($draft)) {
+ $warnings[] = trans('entities.pages_draft_page_changed_since_creation');
+ }
+
+ return $warnings;
+ }
+
/**
* Check if the page has been updated since the draft has been saved.
- *
- * @return bool
*/
- public function hasPageBeenUpdatedSinceDraftSaved(PageRevision $draft): bool
+ protected function hasPageBeenUpdatedSinceDraftCreated(PageRevision $draft): bool
{
- return $draft->page->updated_at->timestamp >= $draft->updated_at->timestamp;
+ return $draft->page->updated_at->timestamp > $draft->created_at->timestamp;
}
/**
* Get the message to show when the user will be editing one of their drafts.
- *
- * @param PageRevision $draft
- *
- * @return string
*/
public function getEditingActiveDraftMessage(PageRevision $draft): string
{
use BookStack\Actions\View;
use BookStack\Entities\Models\Page;
+use BookStack\Entities\Models\PageRevision;
use BookStack\Entities\Repos\PageRepo;
use BookStack\Entities\Tools\BookContents;
use BookStack\Entities\Tools\NextPreviousContentLocator;
return $this->jsonError(trans('errors.guests_cannot_save_drafts'), 500);
}
- // Check for active editing or time conflict
- $warnings = [];
- $jsonResponseWarning = '';
- $editActivity = new PageEditActivity($page);
- if ($editActivity->hasActiveEditing()) {
- $warnings[] = $editActivity->activeEditingMessage();
- }
- $userDraft = $this->pageRepo->getUserDraft($page);
- if ($userDraft !== null) {
- if ($editActivity->hasPageBeenUpdatedSinceDraftSaved($userDraft)) {
- $warnings[] = $editActivity->getEditingActiveDraftMessage($userDraft);
- }
- }
- if (count($warnings) > 0) {
- $jsonResponseWarning = implode("\n", $warnings);
- }
-
$draft = $this->pageRepo->updatePageDraft($page, $request->only(['name', 'html', 'markdown']));
-
- $updateTime = $draft->updated_at->timestamp;
+ $warnings = (new PageEditActivity($page))->getWarningMessagesForDraft($draft);
return response()->json([
'status' => 'success',
'message' => trans('entities.pages_edit_draft_save_at'),
- 'warning' => $jsonResponseWarning,
- 'timestamp' => $updateTime,
+ 'warning' => implode("\n", $warnings),
+ 'timestamp' => $draft->updated_at->timestamp,
]);
}
frequency: 30000,
last: 0,
};
+ this.shownWarningsCache = new Set();
if (this.pageId !== 0 && this.draftsEnabled) {
window.setTimeout(() => {
}
this.draftNotifyChange(`${resp.data.message} ${Dates.utcTimeStampToLocalTime(resp.data.timestamp)}`);
this.autoSave.last = Date.now();
- if (resp.data.warning.length > 0) {
+ if (resp.data.warning && !this.shownWarningsCache.has(resp.data.warning)) {
window.$events.emit('warning', resp.data.warning);
+ this.shownWarningsCache.add(resp.data.warning);
}
} catch (err) {
// Save the editor content in LocalStorage as a last resort, just in case.
'pages_initial_name' => 'New Page',
'pages_editing_draft_notification' => 'You are currently editing a draft that was last saved :timeDiff.',
'pages_draft_edited_notification' => 'This page has been updated by since that time. It is recommended that you discard this draft.',
+ 'pages_draft_page_changed_since_creation' => 'This page has been updated since this draft was created. It is recommended that you discard this draft or take care not to overwrite any page changes.',
'pages_draft_edit_active' => [
'start_a' => ':count users have started editing this page',
'start_b' => ':userName has started editing this page',
use BookStack\Entities\Models\Book;
use BookStack\Entities\Models\Page;
+use BookStack\Entities\Models\PageRevision;
use BookStack\Entities\Repos\PageRepo;
use Tests\TestCase;
->assertElementNotContains('.notification', 'Admin has started editing this page');
}
+ public function test_draft_save_shows_alert_if_draft_older_than_last_page_update()
+ {
+ $admin = $this->getAdmin();
+ $editor = $this->getEditor();
+ /** @var Page $page */
+ $page = Page::query()->first();
+
+ $this->actingAs($editor)->put('/ajax/page/' . $page->id . '/save-draft', [
+ 'name' => $page->name,
+ 'html' => '<p>updated draft</p>',
+ ]);
+
+ /** @var PageRevision $draft */
+ $draft = $page->allRevisions()
+ ->where('type', '=', 'update_draft')
+ ->where('created_by', '=', $editor->id)
+ ->first();
+ $draft->created_at = now()->subMinute(1);
+ $draft->save();
+
+ $this->actingAs($admin)->put($page->refresh()->getUrl(), [
+ 'name' => $page->name,
+ 'html' => '<p>admin update</p>',
+ ]);
+
+ $resp = $this->actingAs($editor)->put('/ajax/page/' . $page->id . '/save-draft', [
+ 'name' => $page->name,
+ 'html' => '<p>updated draft again</p>',
+ ]);
+
+ $resp->assertJson([
+ 'warning' => 'This page has been updated since this draft was created. It is recommended that you discard this draft or take care not to overwrite any page changes.',
+ ]);
+ }
+
+ public function test_draft_save_shows_alert_if_draft_edit_started_by_someone_else()
+ {
+ $admin = $this->getAdmin();
+ $editor = $this->getEditor();
+ /** @var Page $page */
+ $page = Page::query()->first();
+
+ $this->actingAs($admin)->put('/ajax/page/' . $page->id . '/save-draft', [
+ 'name' => $page->name,
+ 'html' => '<p>updated draft</p>',
+ ]);
+
+ $resp = $this->actingAs($editor)->put('/ajax/page/' . $page->id . '/save-draft', [
+ 'name' => $page->name,
+ 'html' => '<p>updated draft again</p>',
+ ]);
+
+ $resp->assertJson([
+ 'warning' => 'Admin has started editing this page in the last 60 minutes. Take care not to overwrite each other\'s updates!',
+ ]);
+ }
+
+
+
public function test_draft_pages_show_on_homepage()
{
/** @var Book $book */