diff --git a/app/Auth/Permissions/PermissionService.php b/app/Auth/Permissions/PermissionService.php
index 97cc1ca241e..e69ac5527e7 100644
--- a/app/Auth/Permissions/PermissionService.php
+++ b/app/Auth/Permissions/PermissionService.php
@@ -426,6 +426,9 @@ protected function getActions(Entity $entity)
if ($entity->isA('book')) {
$baseActions[] = 'chapter-create';
}
+ if ($entity->isA('page')) {
+ $baseActions[] = 'editdraft';
+ }
return $baseActions;
}
@@ -656,16 +659,34 @@ public function restrictEntityQuery(Builder $query, string $ability = 'view'): B
/**
* Extend the given page query to ensure draft items are not visible
- * unless created by the given user.
+ * unless created by the given user, or editable by the given user if
+ * app-shared-drafts setting is enabled.
*/
public function enforceDraftVisiblityOnQuery(Builder $query): Builder
{
return $query->where(function (Builder $query) {
- $query->where('draft', '=', false)
- ->orWhere(function (Builder $query) {
- $query->where('draft', '=', true)
- ->where('created_by', '=', $this->currentUser()->id);
- });
+ $query->where('draft', '=', false)->orWhere(function (Builder $query) {
+ if (setting('app-shared-drafts')) {
+ $this->clean();
+ $query->where(function (Builder $parentQuery) {
+ $parentQuery->whereHas('jointPermissions', function (Builder $permissionQuery) {
+ $permissionQuery->whereIn('role_id', $this->getRoles())->where(function (Builder $query) {
+ $query->where(function (Builder $query) {
+ $query->where('action', '=', 'editdraft')->where('has_permission', '=', true);
+ })->orWhere(function (Builder $query) {
+ $query->where('action', '=', 'update')->where(function (Builder $query) {
+ $query->where('has_permission', '=', true)->orWhere(function (Builder $query) {
+ $query->where('has_permission_own', '=', true)->where('created_by', '=', $this->currentUser()->id);
+ });
+ });
+ });
+ });
+ });
+ });
+ } else {
+ $query->where('created_by', '=', $this->currentUser()->id);
+ }
+ });
});
}
@@ -680,13 +701,7 @@ public function enforceEntityRestrictions($entityType, $query, $action = 'view')
{
if (strtolower($entityType) === 'page') {
// Prevent drafts being visible to others.
- $query = $query->where(function ($query) {
- $query->where('draft', '=', false)
- ->orWhere(function ($query) {
- $query->where('draft', '=', true)
- ->where('created_by', '=', $this->currentUser()->id);
- });
- });
+ $query = $this->enforceDraftVisiblityOnQuery($query);
}
$this->currentAction = $action;
diff --git a/app/Entities/Chapter.php b/app/Entities/Chapter.php
index 848bc6448bd..338909b0e83 100644
--- a/app/Entities/Chapter.php
+++ b/app/Entities/Chapter.php
@@ -52,12 +52,12 @@ public function getExcerpt(int $length = 100)
}
/**
- * Check if this chapter has any child pages.
+ * Check if this chapter has visible child pages.
* @return bool
*/
- public function hasChildren()
+ public function hasVisibleChildren()
{
- return count($this->pages) > 0;
+ return $this->pages()->visible()->count() > 0;
}
/**
diff --git a/app/Entities/Managers/PageEditActivity.php b/app/Entities/Managers/PageEditActivity.php
index cebbf8720f1..c82a755bd54 100644
--- a/app/Entities/Managers/PageEditActivity.php
+++ b/app/Entities/Managers/PageEditActivity.php
@@ -1,5 +1,6 @@
updated_at->diffForHumans();
+ $user = user();
+ error_log('$draft->updated_by: ' . $draft->updated_by);
+ error_log('$user->id: ' . $user->id);
+ if ($draft->created_by === $user->id) {
+ return trans('entities.pages_editing_shared_draft_notification.message', ['timeDiff' => $time, 'userName' => trans('entities.pages_editing_shared_draft_notification.you')]);
+ }
+ $createdUser = User::find($draft->created_by);
+ $userName = $createdUser ? $createdUser->name : '_user'.$draft->created_by.'_';
+ return trans('entities.pages_editing_shared_draft_notification.message', ['timeDiff' => $time, 'userName' => $userName]) . "\n" .
+ trans('entities.pages_editing_shared_draft_notification.warn');
+ }
$message = trans('entities.pages_editing_draft_notification', ['timeDiff' => $draft->updated_at->diffForHumans()]);
if ($draft->page->updated_at->timestamp <= $draft->updated_at->timestamp) {
return $message;
diff --git a/app/Entities/Repos/PageRepo.php b/app/Entities/Repos/PageRepo.php
index e49eeb1ef55..3d3f31f96d6 100644
--- a/app/Entities/Repos/PageRepo.php
+++ b/app/Entities/Repos/PageRepo.php
@@ -120,6 +120,15 @@ public function getUserDraft(Page $page): ?PageRevision
return $revision;
}
+ /**
+ * Get the draft copy of the given page.
+ */
+ public function getDraft(Page $page): ?PageRevision
+ {
+ $revision = $this->getDraftQuery($page)->first();
+ return $revision;
+ }
+
/**
* Get a new draft page belonging to the given parent entity.
*/
@@ -195,7 +204,7 @@ public function update(Page $page, array $input): Page
$page->save();
// Remove all update drafts for this user & page.
- $this->getUserDraftQuery($page)->delete();
+ setting('app-shared-drafts') ? $this->getDraftQuery($page)->delete() : $this->getUserDraftQuery($page)->delete();
// Save a revision after updating
$summary = $input['summary'] ?? null;
@@ -250,6 +259,7 @@ public function updatePageDraft(Page $page, array $input)
// Otherwise save the data to a revision
$draft = $this->getPageRevisionToUpdate($page);
$draft->fill($input);
+ $draft->created_by = user()->id; // Update the user that last updated the draft, just in case of shared draft
if (setting('app-editor') !== 'markdown') {
$draft->markdown = '';
}
@@ -400,7 +410,11 @@ protected function changeParent(Page $page, Entity $parent)
*/
protected function getPageRevisionToUpdate(Page $page): PageRevision
{
- $drafts = $this->getUserDraftQuery($page)->get();
+ if (setting('app-shared-drafts')) {
+ $drafts = $this->getDraftQuery($page)->get();
+ } else {
+ $drafts = $this->getUserDraftQuery($page)->get();
+ }
if ($drafts->count() > 0) {
return $drafts->first();
}
@@ -458,4 +472,15 @@ protected function getUserDraftQuery(Page $page)
->where('page_id', '=', $page->id)
->orderBy('created_at', 'desc');
}
+
+ /**
+ * Get the query to find the draft copies of the given page.
+ */
+ protected function getDraftQuery(Page $page)
+ {
+ return PageRevision::query()
+ ->where('type', 'update_draft')
+ ->where('page_id', '=', $page->id)
+ ->orderBy('updated_at', 'desc');
+ }
}
diff --git a/app/Http/Controllers/ChapterController.php b/app/Http/Controllers/ChapterController.php
index 1355979107e..667afb27683 100644
--- a/app/Http/Controllers/ChapterController.php
+++ b/app/Http/Controllers/ChapterController.php
@@ -64,7 +64,7 @@ public function show(string $bookSlug, string $chapterSlug)
$chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
$this->checkOwnablePermission('chapter-view', $chapter);
- $sidebarTree = (new BookContents($chapter->book))->getTree();
+ $sidebarTree = (new BookContents($chapter->book))->getTree(true);
$pages = $chapter->getVisiblePages();
Views::add($chapter);
diff --git a/app/Http/Controllers/Controller.php b/app/Http/Controllers/Controller.php
index b9576f2febd..320eb8fb967 100644
--- a/app/Http/Controllers/Controller.php
+++ b/app/Http/Controllers/Controller.php
@@ -92,6 +92,22 @@ protected function checkOwnablePermission($permission, Ownable $ownable)
return $this->showPermissionError();
}
+ /**
+ * Check the current user's permissions in or against an ownable item.
+ * @param Collection $permissions
+ * @param Ownable $ownable
+ * @return bool
+ */
+ protected function checkOwnableOrPermissions($permissions, Ownable $ownable)
+ {
+ foreach ($permissions as $permission) {
+ if (userCan($permission, $ownable)) {
+ return true;
+ }
+ }
+ return $this->showPermissionError();
+ }
+
/**
* Check if a user has a permission or bypass if the callback is true.
* @param $permissionName
diff --git a/app/Http/Controllers/PageController.php b/app/Http/Controllers/PageController.php
index b216c19a8e7..616106cf113 100644
--- a/app/Http/Controllers/PageController.php
+++ b/app/Http/Controllers/PageController.php
@@ -78,7 +78,7 @@ public function createAsGuest(Request $request, string $bookSlug, string $chapte
public function editDraft(string $bookSlug, int $pageId)
{
$draft = $this->pageRepo->getById($pageId);
- $this->checkOwnablePermission('page-create', $draft->parent());
+ $this->checkOwnableOrPermissions(['page-update', 'page-editdraft'], $draft);
$this->setPageTitle(trans('entities.pages_edit_draft'));
$draftsEnabled = $this->isSignedIn();
@@ -135,7 +135,7 @@ public function show(string $bookSlug, string $pageSlug)
$pageContent = (new PageContent($page));
$page->html = $pageContent->render();
- $sidebarTree = (new BookContents($page->book))->getTree();
+ $sidebarTree = (new BookContents($page->book))->getTree(true);
$pageNav = $pageContent->getNavigation($page->html);
// Check if page comments are enabled
@@ -173,23 +173,24 @@ public function getPageAjax(int $pageId)
public function edit(string $bookSlug, string $pageSlug)
{
$page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
- $this->checkOwnablePermission('page-update', $page);
+ $this->checkOwnableOrPermissions(['page-update', 'page-editdraft'], $page);
+ $sharedDrafts = setting('app-shared-drafts');
$page->isDraft = false;
$editActivity = new PageEditActivity($page);
// Check for active editing
$warnings = [];
- if ($editActivity->hasActiveEditing()) {
+ if (!$sharedDrafts && $editActivity->hasActiveEditing()) {
$warnings[] = $editActivity->activeEditingMessage();
}
// Check for a current draft version for this user
- $userDraft = $this->pageRepo->getUserDraft($page);
- if ($userDraft !== null) {
- $page->forceFill($userDraft->only(['name', 'html', 'markdown']));
+ $draft = $sharedDrafts ? $this->pageRepo->getDraft($page) : $this->pageRepo->getUserDraft($page);
+ if ($draft !== null) {
+ $page->forceFill($draft->only(['name', 'html', 'markdown']));
$page->isDraft = true;
- $warnings[] = $editActivity->getEditingActiveDraftMessage($userDraft);
+ $warnings[] = $editActivity->getEditingActiveDraftMessage($draft, $sharedDrafts);
}
if (count($warnings) > 0) {
@@ -234,7 +235,7 @@ public function update(Request $request, string $bookSlug, string $pageSlug)
public function saveDraft(Request $request, int $pageId)
{
$page = $this->pageRepo->getById($pageId);
- $this->checkOwnablePermission('page-update', $page);
+ $this->checkOwnableOrPermissions(['page-update', 'page-editdraft'], $page);
if (!$this->isSignedIn()) {
return $this->jsonError(trans('errors.guests_cannot_save_drafts'), 500);
@@ -283,7 +284,7 @@ public function showDelete(string $bookSlug, string $pageSlug)
public function showDeleteDraft(string $bookSlug, int $pageId)
{
$page = $this->pageRepo->getById($pageId);
- $this->checkOwnablePermission('page-update', $page);
+ $this->checkOwnablePermission('page-delete', $page);
$this->setPageTitle(trans('entities.pages_delete_draft_named', ['pageName'=>$page->getShortName()]));
return view('pages.delete', [
'book' => $page->book,
@@ -323,7 +324,7 @@ public function destroyDraft(string $bookSlug, int $pageId)
$page = $this->pageRepo->getById($pageId);
$book = $page->book;
$chapter = $page->chapter;
- $this->checkOwnablePermission('page-update', $page);
+ $this->checkOwnablePermission('page-delete', $page);
$this->pageRepo->destroy($page);
diff --git a/database/migrations/2020_01_29_171400_add_edit_draft_permission.php b/database/migrations/2020_01_29_171400_add_edit_draft_permission.php
new file mode 100644
index 00000000000..793059ba7ba
--- /dev/null
+++ b/database/migrations/2020_01_29_171400_add_edit_draft_permission.php
@@ -0,0 +1,65 @@
+insertGetId([
+ 'name' => strtolower($entity) . '-' . strtolower($p) . '-' . strtolower($o),
+ 'display_name' => $p . ' ' . $o . ' ' . $entity . 's',
+ 'created_at' => \Carbon\Carbon::now()->toDateTimeString(),
+ 'updated_at' => \Carbon\Carbon::now()->toDateTimeString()
+ ]);
+
+ // Find all current roles that already have update permission
+ $roleIdsWithUdatePermission = DB::table('role_permissions')
+ ->leftJoin('permission_role', 'role_permissions.id', '=', 'permission_role.permission_id')
+ ->leftJoin('roles', 'roles.id', '=', 'permission_role.role_id')
+ ->where('role_permissions.name', '=', strtolower($entity) . '-' . strtolower($pu) . '-' . strtolower($o))->get(['roles.id'])->pluck('id');
+
+ // Generate permission_role entry
+ $rowsToInsert = $roleIdsWithUdatePermission->filter(function($roleId) {
+ return !is_null($roleId);
+ })->map(function($roleId) use ($permId) {
+ return [
+ 'role_id' => $roleId,
+ 'permission_id' => $permId
+ ];
+ })->toArray();
+
+ // Assign editdraft permission to roles
+ DB::table('permission_role')->insert($rowsToInsert);
+ }
+
+ /**
+ * Reverse the migrations.
+ *
+ * @return void
+ */
+ public function down()
+ {
+ // Delete the new editdraft permission
+ $entity = 'Page';
+ $op = 'EditDraft All';
+
+ $permissionName = strtolower($entity) . '-' . strtolower(str_replace(' ', '-', $op));
+ $permission = DB::table('role_permissions')->where('name', '=', $permissionName)->first();
+ DB::table('permission_role')->where('permission_id', '=', $permission->id)->delete();
+ DB::table('role_permissions')->where('name', '=', $permissionName)->delete();
+ }
+}
diff --git a/resources/lang/en/common.php b/resources/lang/en/common.php
index c8b4a2b223e..57c680c30ce 100644
--- a/resources/lang/en/common.php
+++ b/resources/lang/en/common.php
@@ -28,6 +28,8 @@
'create' => 'Create',
'update' => 'Update',
'edit' => 'Edit',
+ 'drafts' => 'Drafts',
+ 'edit_draft' => 'Edit Draft',
'sort' => 'Sort',
'move' => 'Move',
'copy' => 'Copy',
diff --git a/resources/lang/en/entities.php b/resources/lang/en/entities.php
index 6bbc723b0ab..ec67251cce6 100644
--- a/resources/lang/en/entities.php
+++ b/resources/lang/en/entities.php
@@ -224,6 +224,11 @@
'pages_initial_revision' => 'Initial publish',
'pages_initial_name' => 'New Page',
'pages_editing_draft_notification' => 'You are currently editing a draft that was last saved :timeDiff.',
+ 'pages_editing_shared_draft_notification' => [
+ 'message' => 'You are currently editing a draft that was last saved :timeDiff by :userName.',
+ 'you' => 'you',
+ 'warn' => 'Take care not to overwrite each other\'s updates!',
+ ],
'pages_draft_edited_notification' => 'This page has been updated by since that time. It is recommended that you discard this draft.',
'pages_draft_edit_active' => [
'start_a' => ':count users have started editing this page',
diff --git a/resources/lang/en/settings.php b/resources/lang/en/settings.php
index ab274256f20..5d41b50cb3b 100755
--- a/resources/lang/en/settings.php
+++ b/resources/lang/en/settings.php
@@ -40,6 +40,9 @@
'app_disable_comments' => 'Disable Comments',
'app_disable_comments_toggle' => 'Disable comments',
'app_disable_comments_desc' => 'Disables comments across all pages in the application.
Existing comments are not shown.',
+ 'app_shared_drafts' => 'Shared Drafts',
+ 'app_shared_drafts_toggle' => 'Enable shared drafts',
+ 'app_shared_drafts_desc' => 'Enable shared drafts across all users with edit permission on the page.
Existing drafts on the same page are overwritten, the last edited take precedence over the others.',
// Color settings
'content_colors' => 'Content Colors',
diff --git a/resources/views/chapters/child-menu.blade.php b/resources/views/chapters/child-menu.blade.php
index 6137c34e8fc..bfebc4c6f1c 100644
--- a/resources/views/chapters/child-menu.blade.php
+++ b/resources/views/chapters/child-menu.blade.php
@@ -1,10 +1,10 @@