Page Drafts: Applied draft owner check to more endpoints

As a follow-up to e3068369a2, this applies
the draft ownership check to various other draft endpoints.

Thanks again to SVO for reporting.
This commit is contained in:
Dan Brown
2026-08-23 18:08:46 +01:00
parent 82a8cc35cf
commit 0365dcc0b1
4 changed files with 49 additions and 7 deletions
+24 -1
View File
@@ -7,6 +7,7 @@ use BookStack\Activity\Tools\CommentTree;
use BookStack\Activity\Tools\UserEntityWatchOptions;
use BookStack\Entities\Models\Book;
use BookStack\Entities\Models\Chapter;
use BookStack\Entities\Models\Page;
use BookStack\Entities\Queries\EntityQueries;
use BookStack\Entities\Queries\PageQueries;
use BookStack\Entities\Repos\PageRepo;
@@ -95,7 +96,7 @@ class PageController extends Controller
}
/**
* Show form to continue editing a draft page.
* Show a form to continue editing a draft page.
*
* @throws NotFoundException
*/
@@ -103,6 +104,7 @@ class PageController extends Controller
{
$draft = $this->queries->findVisibleByIdOrFail($pageId);
$this->checkOwnablePermission(Permission::PageCreate, $draft->getParent());
$this->ensureDraftAccess($draft);
$editorData = new PageEditorData($draft, $this->entityQueries, $request->query('editor', ''));
$this->setPageTitle(trans('entities.pages_edit_draft'));
@@ -124,6 +126,7 @@ class PageController extends Controller
$draftPage = $this->queries->findVisibleByIdOrFail($pageId);
$this->checkOwnablePermission(Permission::PageCreate, $draftPage->getParent());
$this->ensureDraftAccess($draftPage);
$page = $this->pageRepo->publishDraft($draftPage, $request->all());
@@ -235,6 +238,7 @@ class PageController extends Controller
* Save a draft update as a revision.
*
* @throws NotFoundException
* @throws PermissionsException
*/
public function saveDraft(Request $request, int $pageId)
{
@@ -245,6 +249,10 @@ class PageController extends Controller
return $this->jsonError(trans('errors.guests_cannot_save_drafts'), 500);
}
if ($page->draft) {
$this->ensureDraftAccess($page);
}
$draft = $this->pageRepo->updatePageDraft($page, $request->only(['name', 'html', 'markdown']));
$warnings = (new PageEditActivity($page))->getWarningMessagesForDraft($draft);
@@ -294,11 +302,14 @@ class PageController extends Controller
* Show the deletion page for the specified page.
*
* @throws NotFoundException
* @throws PermissionsException
*/
public function showDeleteDraft(string $bookSlug, int $pageId)
{
$page = $this->queries->findVisibleByIdOrFail($pageId);
$this->checkOwnablePermission(Permission::PageUpdate, $page);
$this->ensureDraftAccess($page);
$this->setPageTitle(trans('entities.pages_delete_draft_named', ['pageName' => $page->getShortName()]));
$usedAsTemplate =
$this->entityQueries->books->start()->where('default_template_id', '=', $page->id)->count() > 0 ||
@@ -340,7 +351,9 @@ class PageController extends Controller
$page = $this->queries->findVisibleByIdOrFail($pageId);
$book = $page->book;
$chapter = $page->chapter;
$this->checkOwnablePermission(Permission::PageUpdate, $page);
$this->ensureDraftAccess($page);
$this->pageRepo->destroy($page);
@@ -470,4 +483,14 @@ class PageController extends Controller
return redirect($pageCopy->getUrl());
}
/**
* @throws PermissionsException
*/
protected function ensureDraftAccess(Page $draft): void
{
if (!$draft->draft || $draft->created_by !== user()->id) {
throw new PermissionsException('This page is already published or does not belong to you.');
}
}
}
-4
View File
@@ -82,10 +82,6 @@ class PageRepo
*/
public function publishDraft(Page $draft, array $input): Page
{
if (!$draft->draft || $draft->created_by !== user()->id) {
throw new PermissionsException('This page is already published or does not belong to you.');
}
return (new DatabaseTransaction(function () use ($draft, $input) {
$draft->draft = false;
$draft->revision_count = 1;
+1 -1
View File
@@ -153,7 +153,7 @@ class ImageController extends Controller
}
/**
* Check related page permission and ensure type is drawio or gallery.
* Check related page permission and ensure the type is drawio or gallery.
* @throws NotifyException
*/
protected function checkImagePermission(Image $image): void
+24 -1
View File
@@ -245,7 +245,7 @@ class PageDraftTest extends TestCase
]);
}
public function test_draft_published_can_only_be_used_on_draft_pages()
public function test_draft_publish_can_only_be_used_on_draft_pages()
{
$page = $this->entities->page();
$this->withoutExceptionHandling();
@@ -263,4 +263,27 @@ class PageDraftTest extends TestCase
$page->refresh();
$this->assertNotEquals('My published drafty', $page->name);
}
public function test_destroy_draft_can_only_be_used_on_draft_pages()
{
$page = $this->entities->page();
$this->withoutExceptionHandling();
$exception = null;
try {
$this->asEditor()->delete($page->book->getUrl("/draft/{$page->id}"));
} catch (\Exception $e) {
$exception = $e;
}
$this->assertInstanceOf(PermissionsException::class, $exception);
$this->assertStringContainsString('This page is already published or does not belong to you', $exception->getMessage());
$page->refresh();
$this->assertDatabaseHas('entities', [
'id' => $page->id,
'type' => 'page',
'deleted_at' => null
]);
}
}