diff --git a/app/Entities/Repos/ChapterRepo.php b/app/Entities/Repos/ChapterRepo.php index 87f9e9e40cc..2b81891af63 100644 --- a/app/Entities/Repos/ChapterRepo.php +++ b/app/Entities/Repos/ChapterRepo.php @@ -10,6 +10,7 @@ use BookStack\Entities\Tools\TrashCan; use BookStack\Exceptions\MoveOperationException; use BookStack\Exceptions\NotFoundException; +use BookStack\Exceptions\PermissionsException; use BookStack\Facades\Activity; use Exception; @@ -85,15 +86,19 @@ public function destroy(Chapter $chapter) * 'book:' (book:5). * * @throws MoveOperationException + * @throws PermissionsException */ public function move(Chapter $chapter, string $parentIdentifier): Book { - /** @var Book $parent */ $parent = $this->findParentByIdentifier($parentIdentifier); if (is_null($parent)) { throw new MoveOperationException('Book to move chapter into not found'); } + if (!userCan('chapter-create', $parent)) { + throw new PermissionsException('User does not have permission to create a chapter within the chosen book'); + } + $chapter->changeBook($parent->id); $chapter->rebuildPermissions(); Activity::add(ActivityType::CHAPTER_MOVE, $chapter); diff --git a/app/Entities/Repos/PageRepo.php b/app/Entities/Repos/PageRepo.php index 99294646145..828c4572fd1 100644 --- a/app/Entities/Repos/PageRepo.php +++ b/app/Entities/Repos/PageRepo.php @@ -328,7 +328,7 @@ public function restoreRevision(Page $page, int $revisionId): Page public function move(Page $page, string $parentIdentifier): Entity { $parent = $this->findParentByIdentifier($parentIdentifier); - if ($parent === null) { + if (is_null($parent)) { throw new MoveOperationException('Book or chapter to move page into not found'); } diff --git a/app/Entities/Tools/BookContents.php b/app/Entities/Tools/BookContents.php index 9b2190ca23e..99602de4140 100644 --- a/app/Entities/Tools/BookContents.php +++ b/app/Entities/Tools/BookContents.php @@ -7,7 +7,6 @@ use BookStack\Entities\Models\Chapter; use BookStack\Entities\Models\Entity; use BookStack\Entities\Models\Page; -use BookStack\Exceptions\SortOperationException; use Illuminate\Support\Collection; class BookContents @@ -107,111 +106,207 @@ protected function getPages(bool $showDrafts = false, bool $getPageContent = fal } /** - * Sort the books content using the given map. - * The map is a single-dimension collection of objects in the following format: - * { - * +"id": "294" (ID of item) - * +"sort": 1 (Sort order index) - * +"parentChapter": false (ID of parent chapter, as string, or false) - * +"type": "page" (Entity type of item) - * +"book": "1" (Id of book to place item in) - * }. - * + * Sort the books content using the given sort map. * Returns a list of books that were involved in the operation. * - * @throws SortOperationException + * @returns Book[] */ - public function sortUsingMap(Collection $sortMap): Collection + public function sortUsingMap(BookSortMap $sortMap): array { // Load models into map - $this->loadModelsIntoSortMap($sortMap); - $booksInvolved = $this->getBooksInvolvedInSort($sortMap); + $modelMap = $this->loadModelsFromSortMap($sortMap); + + // Sort our changes from our map to be chapters first + // Since they need to be process to ensure book alignment for child page changes. + $sortMapItems = $sortMap->all(); + usort($sortMapItems, function(BookSortMapItem $itemA, BookSortMapItem $itemB) { + $aScore = $itemA->type === 'page' ? 2 : 1; + $bScore = $itemB->type === 'page' ? 2 : 1; + return $aScore - $bScore; + }); // Perform the sort - $sortMap->each(function ($mapItem) { - $this->applySortUpdates($mapItem); - }); + foreach ($sortMapItems as $item) { + $this->applySortUpdates($item, $modelMap); + } + + /** @var Book[] $booksInvolved */ + $booksInvolved = array_values(array_filter($modelMap, function (string $key) { + return strpos($key, 'book:') === 0; + }, ARRAY_FILTER_USE_KEY)); - // Update permissions and activity. - $booksInvolved->each(function (Book $book) { + // Update permissions of books involved + foreach ($booksInvolved as $book) { $book->rebuildPermissions(); - }); + } return $booksInvolved; } /** * Using the given sort map item, detect changes for the related model - * and update it if required. + * and update it if required. Changes where permissions are lacking will + * be skipped and not throw an error. + * + * @param array $modelMap */ - protected function applySortUpdates(\stdClass $sortMapItem) + protected function applySortUpdates(BookSortMapItem $sortMapItem, array $modelMap): void { /** @var BookChild $model */ - $model = $sortMapItem->model; + $model = $modelMap[$sortMapItem->type . ':' . $sortMapItem->id] ?? null; + if (!$model) { + return; + } + + $priorityChanged = $model->priority !== $sortMapItem->sort; + $bookChanged = $model->book_id !== $sortMapItem->parentBookId; + $chapterChanged = ($model instanceof Page) && $model->chapter_id !== $sortMapItem->parentChapterId; + + // Stop if there's no change + if (!$priorityChanged && !$bookChanged && !$chapterChanged) { + return; + } + + $currentParentKey = 'book:' . $model->book_id; + if ($model instanceof Page && $model->chapter_id) { + $currentParentKey = 'chapter:' . $model->chapter_id; + } - $priorityChanged = intval($model->priority) !== intval($sortMapItem->sort); - $bookChanged = intval($model->book_id) !== intval($sortMapItem->book); - $chapterChanged = ($model instanceof Page) && intval($model->chapter_id) !== $sortMapItem->parentChapter; + $currentParent = $modelMap[$currentParentKey] ?? null; + /** @var Book $newBook */ + $newBook = $modelMap['book:' . $sortMapItem->parentBookId] ?? null; + /** @var ?Chapter $newChapter */ + $newChapter = $sortMapItem->parentChapterId ? ($modelMap['chapter:' . $sortMapItem->parentChapterId] ?? null) : null; + if (!$this->isSortChangePermissible($sortMapItem, $model, $currentParent, $newBook, $newChapter)) { + return; + } + + // Action the required changes if ($bookChanged) { - $model->changeBook($sortMapItem->book); + $model->changeBook($newBook->id); } if ($chapterChanged) { - $model->chapter_id = intval($sortMapItem->parentChapter); - $model->save(); + $model->chapter_id = $newChapter->id ?? 0; } if ($priorityChanged) { - $model->priority = intval($sortMapItem->sort); + $model->priority = $sortMapItem->sort; + } + + if ($chapterChanged || $priorityChanged) { $model->save(); } } /** - * Load models from the database into the given sort map. + * Check if the current user has permissions to apply the given sorting change. + * Is quite complex since items can gain a different parent change. Acts as a: + * - Update of old parent element (Change of content/order). + * - Update of sorted/moved element. + * - Deletion of element (Relative to parent upon move). + * - Creation of element within parent (Upon move to new parent). */ - protected function loadModelsIntoSortMap(Collection $sortMap): void + protected function isSortChangePermissible(BookSortMapItem $sortMapItem, BookChild $model, ?Entity $currentParent, ?Entity $newBook, ?Entity $newChapter): bool { - $keyMap = $sortMap->keyBy(function (\stdClass $sortMapItem) { - return $sortMapItem->type . ':' . $sortMapItem->id; - }); - $pageIds = $sortMap->where('type', '=', 'page')->pluck('id'); - $chapterIds = $sortMap->where('type', '=', 'chapter')->pluck('id'); + // Stop if we can't see the current parent or new book. + if (!$currentParent || !$newBook) { + return false; + } - $pages = Page::visible()->whereIn('id', $pageIds)->get(); - $chapters = Chapter::visible()->whereIn('id', $chapterIds)->get(); + $hasNewParent = $newBook->id !== $model->book_id || ($model instanceof Page && $model->chapter_id !== ($sortMapItem->parentChapterId ?? 0)); + if ($model instanceof Chapter) { + $hasPermission = userCan('book-update', $currentParent) + && userCan('book-update', $newBook) + && userCan('chapter-update', $model) + && (!$hasNewParent || userCan('chapter-create', $newBook)) + && (!$hasNewParent || userCan('chapter-delete', $model)); - foreach ($pages as $page) { - $sortItem = $keyMap->get('page:' . $page->id); - $sortItem->model = $page; + if (!$hasPermission) { + return false; + } } - foreach ($chapters as $chapter) { - $sortItem = $keyMap->get('chapter:' . $chapter->id); - $sortItem->model = $chapter; + if ($model instanceof Page) { + $parentPermission = ($currentParent instanceof Chapter) ? 'chapter-update' : 'book-update'; + $hasCurrentParentPermission = userCan($parentPermission, $currentParent); + + // This needs to check if there was an intended chapter location in the original sort map + // rather than inferring from the $newChapter since that variable may be null + // due to other reasons (Visibility). + $newParent = $sortMapItem->parentChapterId ? $newChapter : $newBook; + if (!$newParent) { + return false; + } + + $hasPageEditPermission = userCan('page-update', $model); + $newParentInRightLocation = ($newParent instanceof Book || $newParent->book_id === $newBook->id); + $newParentPermission = ($newParent instanceof Chapter) ? 'chapter-update' : 'book-update'; + $hasNewParentPermission = userCan($newParentPermission, $newParent); + + $hasDeletePermissionIfMoving = (!$hasNewParent || userCan('page-delete', $model)); + $hasCreatePermissionIfMoving = (!$hasNewParent || userCan('page-create', $newParent)); + + $hasPermission = $hasCurrentParentPermission + && $newParentInRightLocation + && $hasNewParentPermission + && $hasPageEditPermission + && $hasDeletePermissionIfMoving + && $hasCreatePermissionIfMoving; + + if (!$hasPermission) { + return false; + } } + + return true; } /** - * Get the books involved in a sort. - * The given sort map should have its models loaded first. - * - * @throws SortOperationException + * Load models from the database into the given sort map. + * @return array */ - protected function getBooksInvolvedInSort(Collection $sortMap): Collection + protected function loadModelsFromSortMap(BookSortMap $sortMap): array { - $bookIdsInvolved = collect([$this->book->id]); - $bookIdsInvolved = $bookIdsInvolved->concat($sortMap->pluck('book')); - $bookIdsInvolved = $bookIdsInvolved->concat($sortMap->pluck('model.book_id')); - $bookIdsInvolved = $bookIdsInvolved->unique()->toArray(); + $modelMap = []; + $ids = [ + 'chapter' => [], + 'page' => [], + 'book' => [], + ]; + + foreach ($sortMap->all() as $sortMapItem) { + $ids[$sortMapItem->type][] = $sortMapItem->id; + $ids['book'][] = $sortMapItem->parentBookId; + if ($sortMapItem->parentChapterId) { + $ids['chapter'][] = $sortMapItem->parentChapterId; + } + } + + $pages = Page::visible()->whereIn('id', array_unique($ids['page']))->get(Page::$listAttributes); + /** @var Page $page */ + foreach ($pages as $page) { + $modelMap['page:' . $page->id] = $page; + $ids['book'][] = $page->book_id; + if ($page->chapter_id) { + $ids['chapter'][] = $page->chapter_id; + } + } - $books = Book::hasPermission('update')->whereIn('id', $bookIdsInvolved)->get(); + $chapters = Chapter::visible()->whereIn('id', array_unique($ids['chapter']))->get(); + /** @var Chapter $chapter */ + foreach ($chapters as $chapter) { + $modelMap['chapter:' . $chapter->id] = $chapter; + $ids['book'][] = $chapter->book_id; + } - if (count($books) !== count($bookIdsInvolved)) { - throw new SortOperationException('Could not find all books requested in sort operation'); + $books = Book::visible()->whereIn('id', array_unique($ids['book']))->get(); + /** @var Book $book */ + foreach ($books as $book) { + $modelMap['book:' . $book->id] = $book; } - return $books; + return $modelMap; } } diff --git a/app/Entities/Tools/BookSortMap.php b/app/Entities/Tools/BookSortMap.php new file mode 100644 index 00000000000..1ce4905f7ba --- /dev/null +++ b/app/Entities/Tools/BookSortMap.php @@ -0,0 +1,45 @@ +mapData[] = $mapItem; + } + + /** + * @return BookSortMapItem[] + */ + public function all(): array + { + return $this->mapData; + } + + public static function fromJson(string $json): self + { + $map = new static(); + $mapData = json_decode($json); + + foreach ($mapData as $mapDataItem) { + $item = new BookSortMapItem( + intval($mapDataItem->id), + intval($mapDataItem->sort), + $mapDataItem->parentChapter ? intval($mapDataItem->parentChapter) : null, + $mapDataItem->type, + intval($mapDataItem->book) + ); + + $map->addItem($item); + } + + return $map; + } + +} \ No newline at end of file diff --git a/app/Entities/Tools/BookSortMapItem.php b/app/Entities/Tools/BookSortMapItem.php new file mode 100644 index 00000000000..7fb9a1db561 --- /dev/null +++ b/app/Entities/Tools/BookSortMapItem.php @@ -0,0 +1,44 @@ +id = $id; + $this->sort = $sort; + $this->parentChapterId = $parentChapterId; + $this->type = $type; + $this->parentBookId = $parentBookId; + } + + +} \ No newline at end of file diff --git a/app/Exceptions/SortOperationException.php b/app/Exceptions/SortOperationException.php deleted file mode 100644 index ade9e47d216..00000000000 --- a/app/Exceptions/SortOperationException.php +++ /dev/null @@ -1,9 +0,0 @@ -getUrl()); } - $sortMap = collect(json_decode($request->get('sort-tree'))); + $sortMap = BookSortMap::fromJson($request->get('sort-tree')); $bookContents = new BookContents($book); - $booksInvolved = collect(); - - try { - $booksInvolved = $bookContents->sortUsingMap($sortMap); - } catch (SortOperationException $exception) { - $this->showPermissionError(); - } + $booksInvolved = $bookContents->sortUsingMap($sortMap); // Rebuild permissions and add activity for involved books. - $booksInvolved->each(function (Book $book) { - Activity::add(ActivityType::BOOK_SORT, $book); - }); + foreach ($booksInvolved as $bookInvolved) { + Activity::add(ActivityType::BOOK_SORT, $bookInvolved); + } return redirect($book->getUrl()); } diff --git a/app/Http/Controllers/ChapterController.php b/app/Http/Controllers/ChapterController.php index 7541ad0dbd9..83b9bb692da 100644 --- a/app/Http/Controllers/ChapterController.php +++ b/app/Http/Controllers/ChapterController.php @@ -11,6 +11,7 @@ use BookStack\Entities\Tools\PermissionsUpdater; use BookStack\Exceptions\MoveOperationException; use BookStack\Exceptions\NotFoundException; +use BookStack\Exceptions\PermissionsException; use Illuminate\Http\Request; use Illuminate\Validation\ValidationException; use Throwable; @@ -180,6 +181,8 @@ public function move(Request $request, string $bookSlug, string $chapterSlug) try { $newBook = $this->chapterRepo->move($chapter, $entitySelection); + } catch (PermissionsException $exception) { + $this->showPermissionError(); } catch (MoveOperationException $exception) { $this->showErrorNotification(trans('errors.selected_book_not_found')); diff --git a/app/Http/Controllers/PageController.php b/app/Http/Controllers/PageController.php index 3e57657da71..fc4b463e15f 100644 --- a/app/Http/Controllers/PageController.php +++ b/app/Http/Controllers/PageController.php @@ -412,11 +412,9 @@ public function move(Request $request, string $bookSlug, string $pageSlug) try { $parent = $this->pageRepo->move($page, $entitySelection); + } catch (PermissionsException $exception) { + $this->showPermissionError(); } catch (Exception $exception) { - if ($exception instanceof PermissionsException) { - $this->showPermissionError(); - } - $this->showErrorNotification(trans('errors.selected_book_chapter_not_found')); return redirect()->back(); diff --git a/tests/Entity/SortTest.php b/tests/Entity/SortTest.php index 89279bfcf90..9ff75e7000c 100644 --- a/tests/Entity/SortTest.php +++ b/tests/Entity/SortTest.php @@ -33,9 +33,9 @@ public function test_drafts_do_not_show_up() public function test_page_move_into_book() { - $page = Page::first(); + $page = Page::query()->first(); $currentBook = $page->book; - $newBook = Book::where('id', '!=', $currentBook->id)->first(); + $newBook = Book::query()->where('id', '!=', $currentBook->id)->first(); $resp = $this->asEditor()->get($page->getUrl('/move')); $resp->assertSee('Move Page'); @@ -43,7 +43,7 @@ public function test_page_move_into_book() $movePageResp = $this->put($page->getUrl('/move'), [ 'entity_selection' => 'book:' . $newBook->id, ]); - $page = Page::find($page->id); + $page = Page::query()->find($page->id); $movePageResp->assertRedirect($page->getUrl()); $this->assertTrue($page->book->id == $newBook->id, 'Page book is now the new book'); @@ -55,15 +55,15 @@ public function test_page_move_into_book() public function test_page_move_into_chapter() { - $page = Page::first(); + $page = Page::query()->first(); $currentBook = $page->book; - $newBook = Book::where('id', '!=', $currentBook->id)->first(); + $newBook = Book::query()->where('id', '!=', $currentBook->id)->first(); $newChapter = $newBook->chapters()->first(); $movePageResp = $this->actingAs($this->getEditor())->put($page->getUrl('/move'), [ 'entity_selection' => 'chapter:' . $newChapter->id, ]); - $page = Page::find($page->id); + $page = Page::query()->find($page->id); $movePageResp->assertRedirect($page->getUrl()); $this->assertTrue($page->book->id == $newBook->id, 'Page parent is now the new chapter'); @@ -74,9 +74,9 @@ public function test_page_move_into_chapter() public function test_page_move_from_chapter_to_book() { - $oldChapter = Chapter::first(); + $oldChapter = Chapter::query()->first(); $page = $oldChapter->pages()->first(); - $newBook = Book::where('id', '!=', $oldChapter->book_id)->first(); + $newBook = Book::query()->where('id', '!=', $oldChapter->book_id)->first(); $movePageResp = $this->actingAs($this->getEditor())->put($page->getUrl('/move'), [ 'entity_selection' => 'book:' . $newBook->id, @@ -110,7 +110,7 @@ public function test_page_move_requires_create_permissions_on_parent() 'entity_selection' => 'book:' . $newBook->id, ]); - $page = Page::find($page->id); + $page = Page::query()->find($page->id); $movePageResp->assertRedirect($page->getUrl()); $this->assertTrue($page->book->id == $newBook->id, 'Page book is now the new book'); @@ -118,9 +118,9 @@ public function test_page_move_requires_create_permissions_on_parent() public function test_page_move_requires_delete_permissions() { - $page = Page::first(); + $page = Page::query()->first(); $currentBook = $page->book; - $newBook = Book::where('id', '!=', $currentBook->id)->first(); + $newBook = Book::query()->where('id', '!=', $currentBook->id)->first(); $editor = $this->getEditor(); $this->setEntityRestrictions($newBook, ['view', 'update', 'create', 'delete'], $editor->roles->all()); @@ -138,17 +138,17 @@ public function test_page_move_requires_delete_permissions() 'entity_selection' => 'book:' . $newBook->id, ]); - $page = Page::find($page->id); + $page = Page::query()->find($page->id); $movePageResp->assertRedirect($page->getUrl()); $this->assertTrue($page->book->id == $newBook->id, 'Page book is now the new book'); } public function test_chapter_move() { - $chapter = Chapter::first(); + $chapter = Chapter::query()->first(); $currentBook = $chapter->book; $pageToCheck = $chapter->pages->first(); - $newBook = Book::where('id', '!=', $currentBook->id)->first(); + $newBook = Book::query()->where('id', '!=', $currentBook->id)->first(); $chapterMoveResp = $this->asEditor()->get($chapter->getUrl('/move')); $chapterMoveResp->assertSee('Move Chapter'); @@ -157,7 +157,7 @@ public function test_chapter_move() 'entity_selection' => 'book:' . $newBook->id, ]); - $chapter = Chapter::find($chapter->id); + $chapter = Chapter::query()->find($chapter->id); $moveChapterResp->assertRedirect($chapter->getUrl()); $this->assertTrue($chapter->book->id === $newBook->id, 'Chapter Book is now the new book'); @@ -165,7 +165,7 @@ public function test_chapter_move() $newBookResp->assertSee('moved chapter'); $newBookResp->assertSee($chapter->name); - $pageToCheck = Page::find($pageToCheck->id); + $pageToCheck = Page::query()->find($pageToCheck->id); $this->assertTrue($pageToCheck->book_id === $newBook->id, 'Chapter child page\'s book id has changed to the new book'); $pageCheckResp = $this->get($pageToCheck->getUrl()); $pageCheckResp->assertSee($newBook->name); @@ -173,9 +173,9 @@ public function test_chapter_move() public function test_chapter_move_requires_delete_permissions() { - $chapter = Chapter::first(); + $chapter = Chapter::query()->first(); $currentBook = $chapter->book; - $newBook = Book::where('id', '!=', $currentBook->id)->first(); + $newBook = Book::query()->where('id', '!=', $currentBook->id)->first(); $editor = $this->getEditor(); $this->setEntityRestrictions($newBook, ['view', 'update', 'create', 'delete'], $editor->roles->all()); @@ -193,7 +193,32 @@ public function test_chapter_move_requires_delete_permissions() 'entity_selection' => 'book:' . $newBook->id, ]); - $chapter = Chapter::find($chapter->id); + $chapter = Chapter::query()->find($chapter->id); + $moveChapterResp->assertRedirect($chapter->getUrl()); + $this->assertTrue($chapter->book->id == $newBook->id, 'Page book is now the new book'); + } + + public function test_chapter_move_requires_create_permissions_in_new_book() + { + $chapter = Chapter::query()->first(); + $currentBook = $chapter->book; + $newBook = Book::query()->where('id', '!=', $currentBook->id)->first(); + $editor = $this->getEditor(); + + $this->setEntityRestrictions($newBook, ['view', 'update', 'delete'], [$editor->roles->first()]); + $this->setEntityRestrictions($chapter, ['view', 'update', 'create', 'delete'], [$editor->roles->first()]); + + $moveChapterResp = $this->actingAs($editor)->put($chapter->getUrl('/move'), [ + 'entity_selection' => 'book:' . $newBook->id, + ]); + $this->assertPermissionError($moveChapterResp); + + $this->setEntityRestrictions($newBook, ['view', 'update', 'create', 'delete'], [$editor->roles->first()]); + $moveChapterResp = $this->put($chapter->getUrl('/move'), [ + 'entity_selection' => 'book:' . $newBook->id, + ]); + + $chapter = Chapter::query()->find($chapter->id); $moveChapterResp->assertRedirect($chapter->getUrl()); $this->assertTrue($chapter->book->id == $newBook->id, 'Page book is now the new book'); } @@ -239,20 +264,20 @@ public function test_book_sort() // Create request data $reqData = [ [ - 'id' => $chapterToMove->id, - 'sort' => 0, + 'id' => $chapterToMove->id, + 'sort' => 0, 'parentChapter' => false, - 'type' => 'chapter', - 'book' => $newBook->id, + 'type' => 'chapter', + 'book' => $newBook->id, ], ]; foreach ($pagesToMove as $index => $page) { $reqData[] = [ - 'id' => $page->id, - 'sort' => $index, + 'id' => $page->id, + 'sort' => $index, 'parentChapter' => $index === count($pagesToMove) - 1 ? $chapterToMove->id : false, - 'type' => 'page', - 'book' => $newBook->id, + 'type' => 'page', + 'book' => $newBook->id, ]; } @@ -260,18 +285,153 @@ public function test_book_sort() $sortResp->assertRedirect($newBook->getUrl()); $sortResp->assertStatus(302); $this->assertDatabaseHas('chapters', [ - 'id' => $chapterToMove->id, - 'book_id' => $newBook->id, + 'id' => $chapterToMove->id, + 'book_id' => $newBook->id, 'priority' => 0, ]); $this->assertTrue($newBook->chapters()->count() === 1); $this->assertTrue($newBook->chapters()->first()->pages()->count() === 1); $checkPage = $pagesToMove[1]; - $checkResp = $this->get(Page::find($checkPage->id)->getUrl()); + $checkResp = $this->get($checkPage->refresh()->getUrl()); $checkResp->assertSee($newBook->name); } + public function test_book_sort_makes_no_changes_if_new_chapter_does_not_align_with_new_book() + { + /** @var Page $page */ + $page = Page::query()->where('chapter_id', '!=', 0)->first(); + $otherChapter = Chapter::query()->where('book_id', '!=', $page->book_id)->first(); + + $sortData = [ + 'id' => $page->id, + 'sort' => 0, + 'parentChapter' => $otherChapter->id, + 'type' => 'page', + 'book' => $page->book_id, + ]; + $this->asEditor()->put($page->book->getUrl('/sort'), ['sort-tree' => json_encode([$sortData])])->assertRedirect(); + + $this->assertDatabaseHas('pages', [ + 'id' => $page->id, 'chapter_id' => $page->chapter_id, 'book_id' => $page->book_id, + ]); + } + + public function test_book_sort_makes_no_changes_if_no_view_permissions_on_new_chapter() + { + /** @var Page $page */ + $page = Page::query()->where('chapter_id', '!=', 0)->first(); + /** @var Chapter $otherChapter */ + $otherChapter = Chapter::query()->where('book_id', '!=', $page->book_id)->first(); + $this->setEntityRestrictions($otherChapter); + + $sortData = [ + 'id' => $page->id, + 'sort' => 0, + 'parentChapter' => $otherChapter->id, + 'type' => 'page', + 'book' => $otherChapter->book_id, + ]; + $this->asEditor()->put($page->book->getUrl('/sort'), ['sort-tree' => json_encode([$sortData])])->assertRedirect(); + + $this->assertDatabaseHas('pages', [ + 'id' => $page->id, 'chapter_id' => $page->chapter_id, 'book_id' => $page->book_id, + ]); + } + + public function test_book_sort_makes_no_changes_if_no_view_permissions_on_new_book() + { + /** @var Page $page */ + $page = Page::query()->where('chapter_id', '!=', 0)->first(); + /** @var Chapter $otherChapter */ + $otherChapter = Chapter::query()->where('book_id', '!=', $page->book_id)->first(); + $editor = $this->getEditor(); + $this->setEntityRestrictions($otherChapter->book, ['update', 'delete'], [$editor->roles()->first()]); + + $sortData = [ + 'id' => $page->id, + 'sort' => 0, + 'parentChapter' => $otherChapter->id, + 'type' => 'page', + 'book' => $otherChapter->book_id, + ]; + $this->actingAs($editor)->put($page->book->getUrl('/sort'), ['sort-tree' => json_encode([$sortData])])->assertRedirect(); + + $this->assertDatabaseHas('pages', [ + 'id' => $page->id, 'chapter_id' => $page->chapter_id, 'book_id' => $page->book_id, + ]); + } + + public function test_book_sort_makes_no_changes_if_no_update_or_create_permissions_on_new_chapter() + { + /** @var Page $page */ + $page = Page::query()->where('chapter_id', '!=', 0)->first(); + /** @var Chapter $otherChapter */ + $otherChapter = Chapter::query()->where('book_id', '!=', $page->book_id)->first(); + $editor = $this->getEditor(); + $this->setEntityRestrictions($otherChapter, ['view', 'delete'], [$editor->roles()->first()]); + + $sortData = [ + 'id' => $page->id, + 'sort' => 0, + 'parentChapter' => $otherChapter->id, + 'type' => 'page', + 'book' => $otherChapter->book_id, + ]; + $this->actingAs($editor)->put($page->book->getUrl('/sort'), ['sort-tree' => json_encode([$sortData])])->assertRedirect(); + + $this->assertDatabaseHas('pages', [ + 'id' => $page->id, 'chapter_id' => $page->chapter_id, 'book_id' => $page->book_id, + ]); + } + + public function test_book_sort_makes_no_changes_if_no_update_permissions_on_moved_item() + { + /** @var Page $page */ + $page = Page::query()->where('chapter_id', '!=', 0)->first(); + /** @var Chapter $otherChapter */ + $otherChapter = Chapter::query()->where('book_id', '!=', $page->book_id)->first(); + $editor = $this->getEditor(); + $this->setEntityRestrictions($page, ['view', 'delete'], [$editor->roles()->first()]); + + $sortData = [ + 'id' => $page->id, + 'sort' => 0, + 'parentChapter' => $otherChapter->id, + 'type' => 'page', + 'book' => $otherChapter->book_id, + ]; + $this->actingAs($editor)->put($page->book->getUrl('/sort'), ['sort-tree' => json_encode([$sortData])])->assertRedirect(); + + $this->assertDatabaseHas('pages', [ + 'id' => $page->id, 'chapter_id' => $page->chapter_id, 'book_id' => $page->book_id, + ]); + } + + public function test_book_sort_makes_no_changes_if_no_delete_permissions_on_moved_item() + { + /** @var Page $page */ + $page = Page::query()->where('chapter_id', '!=', 0)->first(); + /** @var Chapter $otherChapter */ + $otherChapter = Chapter::query()->where('book_id', '!=', $page->book_id)->first(); + $editor = $this->getEditor(); + $this->setEntityRestrictions($page, ['view', 'update'], [$editor->roles()->first()]); + + $sortData = [ + 'id' => $page->id, + 'sort' => 0, + 'parentChapter' => $otherChapter->id, + 'type' => 'page', + 'book' => $otherChapter->book_id, + ]; + $this->actingAs($editor)->put($page->book->getUrl('/sort'), ['sort-tree' => json_encode([$sortData])])->assertRedirect(); + + $this->assertDatabaseHas('pages', [ + 'id' => $page->id, 'chapter_id' => $page->chapter_id, 'book_id' => $page->book_id, + ]); + } + + public function test_book_sort_item_returns_book_content() { $books = Book::all(); diff --git a/tests/Permissions/EntityPermissionsTest.php b/tests/Permissions/EntityPermissionsTest.php index 96d4792b9c6..abd5065f50a 100644 --- a/tests/Permissions/EntityPermissionsTest.php +++ b/tests/Permissions/EntityPermissionsTest.php @@ -670,51 +670,6 @@ public function test_book_sort_view_permission() $this->actingAs($this->user)->get($firstBook->getUrl('/sort')); } - public function test_book_sort_permission() - { - /** @var Book $firstBook */ - $firstBook = Book::query()->first(); - /** @var Book $secondBook */ - $secondBook = Book::query()->find(2); - - $this->setRestrictionsForTestRoles($firstBook, ['view', 'update']); - $this->setRestrictionsForTestRoles($secondBook, ['view']); - - $firstBookChapter = $this->newChapter(['name' => 'first book chapter'], $firstBook); - $secondBookChapter = $this->newChapter(['name' => 'second book chapter'], $secondBook); - - // Create request data - $reqData = [ - [ - 'id' => $firstBookChapter->id, - 'sort' => 0, - 'parentChapter' => false, - 'type' => 'chapter', - 'book' => $secondBook->id, - ], - ]; - - // Move chapter from first book to a second book - $this->actingAs($this->user)->put($firstBook->getUrl() . '/sort', ['sort-tree' => json_encode($reqData)]) - ->assertRedirect('/'); - $this->get('/')->assertSee('You do not have permission'); - - $reqData = [ - [ - 'id' => $secondBookChapter->id, - 'sort' => 0, - 'parentChapter' => false, - 'type' => 'chapter', - 'book' => $firstBook->id, - ], - ]; - - // Move chapter from second book to first book - $this->actingAs($this->user)->put($firstBook->getUrl() . '/sort', ['sort-tree' => json_encode($reqData)]) - ->assertRedirect('/'); - $this->get('/')->assertSee('You do not have permission'); - } - public function test_can_create_page_if_chapter_has_permissions_when_book_not_visible() { /** @var Book $book */