Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 28 additions & 13 deletions app/Auth/Permissions/PermissionService.php
Original file line number Diff line number Diff line change
Expand Up @@ -426,6 +426,9 @@ protected function getActions(Entity $entity)
if ($entity->isA('book')) {
$baseActions[] = 'chapter-create';
}
if ($entity->isA('page')) {
$baseActions[] = 'editdraft';
}
return $baseActions;
}

Expand Down Expand Up @@ -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);
}
});
});
}

Expand All @@ -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;
Expand Down
6 changes: 3 additions & 3 deletions app/Entities/Chapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

/**
Expand Down
18 changes: 16 additions & 2 deletions app/Entities/Managers/PageEditActivity.php
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<?php namespace BookStack\Entities\Managers;

use BookStack\Auth\User;
use BookStack\Entities\Page;
use BookStack\Entities\PageRevision;
use Carbon\Carbon;
Expand Down Expand Up @@ -41,12 +42,25 @@ public function activeEditingMessage(): string
}

/**
* Get the message to show when the user will be editing one of their drafts.
* Get the message to show when the user will be editing one of the drafts.
* @param PageRevision $draft
* @return string
*/
public function getEditingActiveDraftMessage(PageRevision $draft): string
public function getEditingActiveDraftMessage(PageRevision $draft, bool $sharedDrafts): string
{
if ($sharedDrafts) {
$time = $draft->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;
Expand Down
29 changes: 27 additions & 2 deletions app/Entities/Repos/PageRepo.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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 = '';
}
Expand Down Expand Up @@ -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();
}
Expand Down Expand Up @@ -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');
}
}
2 changes: 1 addition & 1 deletion app/Http/Controllers/ChapterController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
16 changes: 16 additions & 0 deletions app/Http/Controllers/Controller.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
23 changes: 12 additions & 11 deletions app/Http/Controllers/PageController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<?php

use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class AddEditDraftPermission extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
// Create new editdraft permission
$entity = 'Page';
$p = 'EditDraft';
$o = 'All';
$pu = 'Update';

// Create permission
$permId = DB::table('role_permissions')->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();
}
}
2 changes: 2 additions & 0 deletions resources/lang/en/common.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@
'create' => 'Create',
'update' => 'Update',
'edit' => 'Edit',
'drafts' => 'Drafts',
'edit_draft' => 'Edit Draft',
'sort' => 'Sort',
'move' => 'Move',
'copy' => 'Copy',
Expand Down
5 changes: 5 additions & 0 deletions resources/lang/en/entities.php
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
3 changes: 3 additions & 0 deletions resources/lang/en/settings.php
Original file line number Diff line number Diff line change
Expand Up @@ -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. <br> 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. <br> Existing drafts on the same page are overwritten, the last edited take precedence over the others.',

// Color settings
'content_colors' => 'Content Colors',
Expand Down
4 changes: 2 additions & 2 deletions resources/views/chapters/child-menu.blade.php
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
<div class="chapter-child-menu">
<button chapter-toggle type="button" aria-expanded="{{ $isOpen ? 'true' : 'false' }}"
class="text-muted @if($isOpen) open @endif">
@icon('caret-right') @icon('page') <span>{{ trans_choice('entities.x_pages', $bookChild->pages->count()) }}</span>
@icon('caret-right') @icon('page') <span>{{ trans_choice('entities.x_pages', $bookChild->getVisiblePages()->count()) }}</span>
</button>
<ul class="sub-menu inset-list @if($isOpen) open @endif" @if($isOpen) style="display: block;" @endif role="menu">
@foreach($bookChild->pages as $childPage)
@foreach($bookChild->getVisiblePages() as $childPage)
<li class="list-item-page {{ $childPage->isA('page') && $childPage->draft ? 'draft' : '' }}" role="presentation">
@include('partials.entity-list-item-basic', ['entity' => $childPage, 'classes' => $current->matches($childPage)? 'selected' : '' ])
</li>
Expand Down
Loading