From d676e1e824e0377cfcb1736dd1ff622e383d8d02 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Mon, 13 Jun 2022 17:20:21 +0100 Subject: [PATCH 01/11] Started work on hierachy conversion actions - Updates book/shelf cover image handling for easier cloning/handling. - Adds core logic for promoting books/chapters up a level. - Enables usage of book/shelf cover image via API. Related to #1087 --- app/Entities/Repos/BookRepo.php | 6 ++ app/Entities/Repos/BookshelfRepo.php | 19 ++--- app/Entities/Tools/Cloner.php | 37 ++++++---- app/Entities/Tools/HierarchyTransformer.php | 73 +++++++++++++++++++ app/Entities/Tools/TrashCan.php | 2 +- .../Controllers/Api/BookApiController.php | 30 ++++---- .../Api/BookshelfApiController.php | 35 +++++---- app/Http/Controllers/BookController.php | 14 ++-- app/Http/Controllers/BookshelfController.php | 17 +++-- tests/OpenGraphTest.php | 5 +- 10 files changed, 166 insertions(+), 72 deletions(-) create mode 100644 app/Entities/Tools/HierarchyTransformer.php diff --git a/app/Entities/Repos/BookRepo.php b/app/Entities/Repos/BookRepo.php index 7c4b280a8..0c62a13fc 100644 --- a/app/Entities/Repos/BookRepo.php +++ b/app/Entities/Repos/BookRepo.php @@ -91,6 +91,7 @@ class BookRepo { $book = new Book(); $this->baseRepo->create($book, $input); + $this->baseRepo->updateCoverImage($book, $input['image']); Activity::add(ActivityType::BOOK_CREATE, $book); return $book; @@ -102,6 +103,11 @@ class BookRepo public function update(Book $book, array $input): Book { $this->baseRepo->update($book, $input); + + if (isset($input['image'])) { + $this->baseRepo->updateCoverImage($book, $input['image'], $input['image'] === null); + } + Activity::add(ActivityType::BOOK_UPDATE, $book); return $book; diff --git a/app/Entities/Repos/BookshelfRepo.php b/app/Entities/Repos/BookshelfRepo.php index ceabba59a..03e7804d5 100644 --- a/app/Entities/Repos/BookshelfRepo.php +++ b/app/Entities/Repos/BookshelfRepo.php @@ -89,6 +89,7 @@ class BookshelfRepo { $shelf = new Bookshelf(); $this->baseRepo->create($shelf, $input); + $this->baseRepo->updateCoverImage($shelf, $input['image']); $this->updateBooks($shelf, $bookIds); Activity::add(ActivityType::BOOKSHELF_CREATE, $shelf); @@ -106,14 +107,17 @@ class BookshelfRepo $this->updateBooks($shelf, $bookIds); } + if (isset($input['image'])) { + $this->baseRepo->updateCoverImage($shelf, $input['image'], $input['image'] === null); + } + Activity::add(ActivityType::BOOKSHELF_UPDATE, $shelf); return $shelf; } /** - * Update which books are assigned to this shelf by - * syncing the given book ids. + * Update which books are assigned to this shelf by syncing the given book ids. * Function ensures the books are visible to the current user and existing. */ protected function updateBooks(Bookshelf $shelf, array $bookIds) @@ -132,17 +136,6 @@ class BookshelfRepo $shelf->books()->sync($syncData); } - /** - * Update the given shelf cover image, or clear it. - * - * @throws ImageUploadException - * @throws Exception - */ - public function updateCoverImage(Bookshelf $shelf, ?UploadedFile $coverImage, bool $removeImage = false) - { - $this->baseRepo->updateCoverImage($shelf, $coverImage, $removeImage); - } - /** * Copy down the permissions of the given shelf to all child books. */ diff --git a/app/Entities/Tools/Cloner.php b/app/Entities/Tools/Cloner.php index b4923b90a..3553a9db3 100644 --- a/app/Entities/Tools/Cloner.php +++ b/app/Entities/Tools/Cloner.php @@ -50,11 +50,8 @@ class Cloner public function clonePage(Page $original, Entity $parent, string $newName): Page { $copyPage = $this->pageRepo->getNewDraftPage($parent); - $pageData = $original->getAttributes(); - - // Update name & tags + $pageData = $this->entityToInputData($original); $pageData['name'] = $newName; - $pageData['tags'] = $this->entityTagsToInputArray($original); return $this->pageRepo->publishDraft($copyPage, $pageData); } @@ -65,9 +62,8 @@ class Cloner */ public function cloneChapter(Chapter $original, Book $parent, string $newName): Chapter { - $chapterDetails = $original->getAttributes(); + $chapterDetails = $this->entityToInputData($original); $chapterDetails['name'] = $newName; - $chapterDetails['tags'] = $this->entityTagsToInputArray($original); $copyChapter = $this->chapterRepo->create($chapterDetails, $parent); @@ -87,9 +83,8 @@ class Cloner */ public function cloneBook(Book $original, string $newName): Book { - $bookDetails = $original->getAttributes(); + $bookDetails = $this->entityToInputData($original); $bookDetails['name'] = $newName; - $bookDetails['tags'] = $this->entityTagsToInputArray($original); $copyBook = $this->bookRepo->create($bookDetails); @@ -104,16 +99,26 @@ class Cloner } } - if ($original->cover) { - try { - $tmpImgFile = tmpfile(); - $uploadedFile = $this->imageToUploadedFile($original->cover, $tmpImgFile); - $this->bookRepo->updateCoverImage($copyBook, $uploadedFile, false); - } catch (\Exception $exception) { - } + return $copyBook; + } + + /** + * Convert an entity to a raw data array of input data. + * @return array + */ + public function entityToInputData(Entity $entity): array + { + $inputData = $entity->getAttributes(); + $inputData['tags'] = $this->entityTagsToInputArray($entity); + + // Add a cover to the data if existing on the original entity + if ($entity->cover instanceof Image) { + $tmpImgFile = tmpfile(); + $uploadedFile = $this->imageToUploadedFile($entity->cover, $tmpImgFile); + $inputData['image'] = $uploadedFile; } - return $copyBook; + return $inputData; } /** diff --git a/app/Entities/Tools/HierarchyTransformer.php b/app/Entities/Tools/HierarchyTransformer.php new file mode 100644 index 000000000..17e153e05 --- /dev/null +++ b/app/Entities/Tools/HierarchyTransformer.php @@ -0,0 +1,73 @@ +cloner->entityToInputData($chapter); + $book = $this->bookRepo->create($inputData); + + // TODO - Copy permissions + + /** @var Page $page */ + foreach ($chapter->pages as $page) { + $page->chapter_id = 0; + $page->changeBook($book->id); + } + + $this->trashCan->destroyEntity($chapter); + + // TODO - Log activity for change + return $book; + } + + public function transformBookToShelf(Book $book): Bookshelf + { + // TODO - Check permissions before call + // Permissions: edit-book, delete-book, create-shelf + $inputData = $this->cloner->entityToInputData($book); + $shelf = $this->shelfRepo->create($inputData, []); + + // TODO - Copy permissions? + + $shelfBookSyncData = []; + + /** @var Chapter $chapter */ + foreach ($book->chapters as $index => $chapter) { + $newBook = $this->transformChapterToBook($chapter); + $shelfBookSyncData[$newBook->id] = ['order' => $index]; + } + + $shelf->books()->sync($shelfBookSyncData); + + if ($book->directPages->count() > 0) { + $book->name .= ' ' . trans('entities.pages'); + } else { + $this->trashCan->destroyEntity($book); + } + + // TODO - Log activity for change + return $shelf; + } +} \ No newline at end of file diff --git a/app/Entities/Tools/TrashCan.php b/app/Entities/Tools/TrashCan.php index 1e130c9e1..abec2e2d5 100644 --- a/app/Entities/Tools/TrashCan.php +++ b/app/Entities/Tools/TrashCan.php @@ -344,7 +344,7 @@ class TrashCan * * @throws Exception */ - protected function destroyEntity(Entity $entity): int + public function destroyEntity(Entity $entity): int { if ($entity instanceof Page) { return $this->destroyPage($entity); diff --git a/app/Http/Controllers/Api/BookApiController.php b/app/Http/Controllers/Api/BookApiController.php index 2b6e7a2e1..73cac6318 100644 --- a/app/Http/Controllers/Api/BookApiController.php +++ b/app/Http/Controllers/Api/BookApiController.php @@ -11,19 +11,6 @@ class BookApiController extends ApiController { protected $bookRepo; - protected $rules = [ - 'create' => [ - 'name' => ['required', 'string', 'max:255'], - 'description' => ['string', 'max:1000'], - 'tags' => ['array'], - ], - 'update' => [ - 'name' => ['string', 'min:1', 'max:255'], - 'description' => ['string', 'max:1000'], - 'tags' => ['array'], - ], - ]; - public function __construct(BookRepo $bookRepo) { $this->bookRepo = $bookRepo; @@ -97,4 +84,21 @@ class BookApiController extends ApiController return response('', 204); } + + protected function rules(): array { + return [ + 'create' => [ + 'name' => ['required', 'string', 'max:255'], + 'description' => ['string', 'max:1000'], + 'tags' => ['array'], + 'image' => array_merge(['nullable'], $this->getImageValidationRules()), + ], + 'update' => [ + 'name' => ['string', 'min:1', 'max:255'], + 'description' => ['string', 'max:1000'], + 'tags' => ['array'], + 'image' => array_merge(['nullable'], $this->getImageValidationRules()), + ], + ]; + } } diff --git a/app/Http/Controllers/Api/BookshelfApiController.php b/app/Http/Controllers/Api/BookshelfApiController.php index 2720d1db2..400dff977 100644 --- a/app/Http/Controllers/Api/BookshelfApiController.php +++ b/app/Http/Controllers/Api/BookshelfApiController.php @@ -13,21 +13,6 @@ class BookshelfApiController extends ApiController { protected BookshelfRepo $bookshelfRepo; - protected $rules = [ - 'create' => [ - 'name' => ['required', 'string', 'max:255'], - 'description' => ['string', 'max:1000'], - 'books' => ['array'], - 'tags' => ['array'], - ], - 'update' => [ - 'name' => ['string', 'min:1', 'max:255'], - 'description' => ['string', 'max:1000'], - 'books' => ['array'], - 'tags' => ['array'], - ], - ]; - /** * BookshelfApiController constructor. */ @@ -117,4 +102,24 @@ class BookshelfApiController extends ApiController return response('', 204); } + + protected function rules(): array + { + return [ + 'create' => [ + 'name' => ['required', 'string', 'max:255'], + 'description' => ['string', 'max:1000'], + 'books' => ['array'], + 'tags' => ['array'], + 'image' => array_merge(['nullable'], $this->getImageValidationRules()), + ], + 'update' => [ + 'name' => ['string', 'min:1', 'max:255'], + 'description' => ['string', 'max:1000'], + 'books' => ['array'], + 'tags' => ['array'], + 'image' => array_merge(['nullable'], $this->getImageValidationRules()), + ], + ]; + } } diff --git a/app/Http/Controllers/BookController.php b/app/Http/Controllers/BookController.php index bc403c6d0..b9dd0e799 100644 --- a/app/Http/Controllers/BookController.php +++ b/app/Http/Controllers/BookController.php @@ -100,7 +100,6 @@ class BookController extends Controller } $book = $this->bookRepo->create($request->all()); - $this->bookRepo->updateCoverImage($book, $request->file('image', null)); if ($bookshelf) { $bookshelf->appendBook($book); @@ -158,15 +157,20 @@ class BookController extends Controller { $book = $this->bookRepo->getBySlug($slug); $this->checkOwnablePermission('book-update', $book); - $this->validate($request, [ + + $validated = $this->validate($request, [ 'name' => ['required', 'string', 'max:255'], 'description' => ['string', 'max:1000'], 'image' => array_merge(['nullable'], $this->getImageValidationRules()), ]); - $book = $this->bookRepo->update($book, $request->all()); - $resetCover = $request->has('image_reset'); - $this->bookRepo->updateCoverImage($book, $request->file('image', null), $resetCover); + if ($request->has('image_reset')) { + $validated['image'] = null; + } else if (is_null($validated['image'])) { + unset($validated['image']); + } + + $book = $this->bookRepo->update($book, $validated); return redirect($book->getUrl()); } diff --git a/app/Http/Controllers/BookshelfController.php b/app/Http/Controllers/BookshelfController.php index 9a7f78a85..ce2e508c8 100644 --- a/app/Http/Controllers/BookshelfController.php +++ b/app/Http/Controllers/BookshelfController.php @@ -83,15 +83,14 @@ class BookshelfController extends Controller public function store(Request $request) { $this->checkPermission('bookshelf-create-all'); - $this->validate($request, [ + $validated = $this->validate($request, [ 'name' => ['required', 'string', 'max:255'], 'description' => ['string', 'max:1000'], 'image' => array_merge(['nullable'], $this->getImageValidationRules()), ]); $bookIds = explode(',', $request->get('books', '')); - $shelf = $this->bookshelfRepo->create($request->all(), $bookIds); - $this->bookshelfRepo->updateCoverImage($shelf, $request->file('image', null)); + $shelf = $this->bookshelfRepo->create($validated, $bookIds); return redirect($shelf->getUrl()); } @@ -160,16 +159,20 @@ class BookshelfController extends Controller { $shelf = $this->bookshelfRepo->getBySlug($slug); $this->checkOwnablePermission('bookshelf-update', $shelf); - $this->validate($request, [ + $validated = $this->validate($request, [ 'name' => ['required', 'string', 'max:255'], 'description' => ['string', 'max:1000'], 'image' => array_merge(['nullable'], $this->getImageValidationRules()), ]); + if ($request->has('image_reset')) { + $validated['image'] = null; + } else if (is_null($validated['image'])) { + unset($validated['image']); + } + $bookIds = explode(',', $request->get('books', '')); - $shelf = $this->bookshelfRepo->update($shelf, $request->all(), $bookIds); - $resetCover = $request->has('image_reset'); - $this->bookshelfRepo->updateCoverImage($shelf, $request->file('image', null), $resetCover); + $shelf = $this->bookshelfRepo->update($shelf, $validated, $bookIds); return redirect($shelf->getUrl()); } diff --git a/tests/OpenGraphTest.php b/tests/OpenGraphTest.php index 17a5aa2c5..dd99b7bef 100644 --- a/tests/OpenGraphTest.php +++ b/tests/OpenGraphTest.php @@ -6,6 +6,7 @@ use BookStack\Entities\Models\Book; use BookStack\Entities\Models\Bookshelf; use BookStack\Entities\Models\Chapter; use BookStack\Entities\Models\Page; +use BookStack\Entities\Repos\BaseRepo; use BookStack\Entities\Repos\BookRepo; use BookStack\Entities\Repos\BookshelfRepo; use Illuminate\Support\Str; @@ -69,8 +70,8 @@ class OpenGraphTest extends TestCase $this->assertArrayNotHasKey('image', $tags); // Test image set if image has cover image - $shelfRepo = app(BookshelfRepo::class); - $shelfRepo->updateCoverImage($shelf, $this->getTestImage('image.png')); + $baseRepo = app(BaseRepo::class); + $baseRepo->updateCoverImage($shelf, $this->getTestImage('image.png')); $resp = $this->asEditor()->get($shelf->getUrl()); $tags = $this->getOpenGraphTags($resp); From 90ec40691a6e523475d336a8ffb8280c05347b98 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Tue, 14 Jun 2022 15:55:44 +0100 Subject: [PATCH 02/11] Added clone of entity permissions on chapter/book promotion --- app/Entities/Tools/Cloner.php | 12 ++++++++++++ app/Entities/Tools/HierarchyTransformer.php | 11 ++--------- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/app/Entities/Tools/Cloner.php b/app/Entities/Tools/Cloner.php index 3553a9db3..91e10b9be 100644 --- a/app/Entities/Tools/Cloner.php +++ b/app/Entities/Tools/Cloner.php @@ -121,6 +121,18 @@ class Cloner return $inputData; } + /** + * Copy the permission settings from the source entity to the target entity. + */ + public function copyEntityPermissions(Entity $sourceEntity, Entity $targetEntity): void + { + $targetEntity->restricted = $sourceEntity->restricted; + $permissions = $sourceEntity->permissions()->get(['role_id', 'action'])->toArray(); + $targetEntity->permissions()->delete(); + $targetEntity->permissions()->createMany($permissions); + $targetEntity->rebuildPermissions(); + } + /** * Convert an image instance to an UploadedFile instance to mimic * a file being uploaded. diff --git a/app/Entities/Tools/HierarchyTransformer.php b/app/Entities/Tools/HierarchyTransformer.php index 17e153e05..c95d5fa53 100644 --- a/app/Entities/Tools/HierarchyTransformer.php +++ b/app/Entities/Tools/HierarchyTransformer.php @@ -16,19 +16,13 @@ class HierarchyTransformer protected Cloner $cloner; protected TrashCan $trashCan; - // TODO - Test setting book cover image from API - // Ensure we can update without resetting image accidentally - // Ensure api docs correct. - // TODO - As above but for shelves. - public function transformChapterToBook(Chapter $chapter): Book { // TODO - Check permissions before call // Permissions: edit-chapter, delete-chapter, create-book $inputData = $this->cloner->entityToInputData($chapter); $book = $this->bookRepo->create($inputData); - - // TODO - Copy permissions + $this->cloner->copyEntityPermissions($chapter, $book); /** @var Page $page */ foreach ($chapter->pages as $page) { @@ -48,8 +42,7 @@ class HierarchyTransformer // Permissions: edit-book, delete-book, create-shelf $inputData = $this->cloner->entityToInputData($book); $shelf = $this->shelfRepo->create($inputData, []); - - // TODO - Copy permissions? + $this->cloner->copyEntityPermissions($book, $shelf); $shelfBookSyncData = []; From 8da856bac3f7d76dcefbb097dd93bd6409c6ccc9 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Tue, 14 Jun 2022 16:42:29 +0100 Subject: [PATCH 03/11] Got chapter conversion to books working - Added required UI within edit view. - Added required routes and controller actions. --- app/Actions/ActivityType.php | 1 + app/Entities/Repos/BookRepo.php | 4 +-- app/Entities/Tools/HierarchyTransformer.php | 18 ++++++++++-- app/Http/Controllers/ChapterController.php | 17 +++++++++++ resources/lang/en/activities.php | 2 ++ resources/views/chapters/edit.blade.php | 32 ++++++++++++++++++++- routes/web.php | 1 + 7 files changed, 69 insertions(+), 6 deletions(-) diff --git a/app/Actions/ActivityType.php b/app/Actions/ActivityType.php index 8b5213a8b..997cc041a 100644 --- a/app/Actions/ActivityType.php +++ b/app/Actions/ActivityType.php @@ -16,6 +16,7 @@ class ActivityType const CHAPTER_MOVE = 'chapter_move'; const BOOK_CREATE = 'book_create'; + const BOOK_CREATE_FROM_CHAPTER = 'book_create_from_chapter'; const BOOK_UPDATE = 'book_update'; const BOOK_DELETE = 'book_delete'; const BOOK_SORT = 'book_sort'; diff --git a/app/Entities/Repos/BookRepo.php b/app/Entities/Repos/BookRepo.php index 0c62a13fc..b5944fd46 100644 --- a/app/Entities/Repos/BookRepo.php +++ b/app/Entities/Repos/BookRepo.php @@ -91,7 +91,7 @@ class BookRepo { $book = new Book(); $this->baseRepo->create($book, $input); - $this->baseRepo->updateCoverImage($book, $input['image']); + $this->baseRepo->updateCoverImage($book, $input['image'] ?? null); Activity::add(ActivityType::BOOK_CREATE, $book); return $book; @@ -104,7 +104,7 @@ class BookRepo { $this->baseRepo->update($book, $input); - if (isset($input['image'])) { + if (array_key_exists('image', $input)) { $this->baseRepo->updateCoverImage($book, $input['image'], $input['image'] === null); } diff --git a/app/Entities/Tools/HierarchyTransformer.php b/app/Entities/Tools/HierarchyTransformer.php index c95d5fa53..7304962b3 100644 --- a/app/Entities/Tools/HierarchyTransformer.php +++ b/app/Entities/Tools/HierarchyTransformer.php @@ -2,12 +2,14 @@ namespace BookStack\Entities\Tools; +use BookStack\Actions\ActivityType; use BookStack\Entities\Models\Book; use BookStack\Entities\Models\Bookshelf; use BookStack\Entities\Models\Chapter; use BookStack\Entities\Models\Page; use BookStack\Entities\Repos\BookRepo; use BookStack\Entities\Repos\BookshelfRepo; +use BookStack\Facades\Activity; class HierarchyTransformer { @@ -16,10 +18,20 @@ class HierarchyTransformer protected Cloner $cloner; protected TrashCan $trashCan; + public function __construct(BookRepo $bookRepo, BookshelfRepo $shelfRepo, Cloner $cloner, TrashCan $trashCan) + { + $this->bookRepo = $bookRepo; + $this->shelfRepo = $shelfRepo; + $this->cloner = $cloner; + $this->trashCan = $trashCan; + } + + /** + * Transform a chapter into a book. + * Does not check permissions, check before calling. + */ public function transformChapterToBook(Chapter $chapter): Book { - // TODO - Check permissions before call - // Permissions: edit-chapter, delete-chapter, create-book $inputData = $this->cloner->entityToInputData($chapter); $book = $this->bookRepo->create($inputData); $this->cloner->copyEntityPermissions($chapter, $book); @@ -32,7 +44,7 @@ class HierarchyTransformer $this->trashCan->destroyEntity($chapter); - // TODO - Log activity for change + Activity::add(ActivityType::BOOK_CREATE_FROM_CHAPTER); return $book; } diff --git a/app/Http/Controllers/ChapterController.php b/app/Http/Controllers/ChapterController.php index 83b9bb692..d1fe5249a 100644 --- a/app/Http/Controllers/ChapterController.php +++ b/app/Http/Controllers/ChapterController.php @@ -7,6 +7,7 @@ use BookStack\Entities\Models\Book; use BookStack\Entities\Repos\ChapterRepo; use BookStack\Entities\Tools\BookContents; use BookStack\Entities\Tools\Cloner; +use BookStack\Entities\Tools\HierarchyTransformer; use BookStack\Entities\Tools\NextPreviousContentLocator; use BookStack\Entities\Tools\PermissionsUpdater; use BookStack\Exceptions\MoveOperationException; @@ -272,4 +273,20 @@ class ChapterController extends Controller return redirect($chapter->getUrl()); } + + + /** + * Convert the chapter to a book. + */ + public function convertToBook(HierarchyTransformer $transformer, string $bookSlug, string $chapterSlug) + { + $chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug); + $this->checkOwnablePermission('chapter-update', $chapter); + $this->checkOwnablePermission('chapter-delete', $chapter); + $this->checkPermission('book-create-all'); + + $book = $transformer->transformChapterToBook($chapter); + + return redirect($book->getUrl()); + } } diff --git a/resources/lang/en/activities.php b/resources/lang/en/activities.php index 77c39b50c..0c3d2e704 100644 --- a/resources/lang/en/activities.php +++ b/resources/lang/en/activities.php @@ -28,6 +28,8 @@ return [ // Books 'book_create' => 'created book', 'book_create_notification' => 'Book successfully created', + 'book_create_from_chapter' => 'converted chapter to book', + 'book_create_from_chapter_notification' => 'Chapter successfully converted to a book', 'book_update' => 'updated book', 'book_update_notification' => 'Book successfully updated', 'book_delete' => 'deleted book', diff --git a/resources/views/chapters/edit.blade.php b/resources/views/chapters/edit.blade.php index 65c48c18d..f6fd3cc6b 100644 --- a/resources/views/chapters/edit.blade.php +++ b/resources/views/chapters/edit.blade.php @@ -15,7 +15,7 @@ ]]) -
+

{{ trans('entities.chapters_edit') }}

@@ -23,6 +23,36 @@
+{{-- TODO - Permissions--}} +
+

Convert to Book

+
+

+ You can convert this chapter to a new book with the same contents. + Any permissions set on this chapter will be copied to the new book but any inherited permissions, + from the parent book, will not be copied which could lead to a change of access control. +

+
+ +
+
+
+ @stop \ No newline at end of file diff --git a/routes/web.php b/routes/web.php index 37f59b970..dfda97253 100644 --- a/routes/web.php +++ b/routes/web.php @@ -132,6 +132,7 @@ Route::middleware('auth')->group(function () { Route::get('/books/{bookSlug}/chapter/{chapterSlug}/copy', [ChapterController::class, 'showCopy']); Route::post('/books/{bookSlug}/chapter/{chapterSlug}/copy', [ChapterController::class, 'copy']); Route::get('/books/{bookSlug}/chapter/{chapterSlug}/edit', [ChapterController::class, 'edit']); + Route::post('/books/{bookSlug}/chapter/{chapterSlug}/convert-to-book', [ChapterController::class, 'convertToBook']); Route::get('/books/{bookSlug}/chapter/{chapterSlug}/permissions', [ChapterController::class, 'showPermissions']); Route::get('/books/{bookSlug}/chapter/{chapterSlug}/export/pdf', [ChapterExportController::class, 'pdf']); Route::get('/books/{bookSlug}/chapter/{chapterSlug}/export/html', [ChapterExportController::class, 'html']); From 8c67011a1d1187f6300c19c3601ecd421fcf4f09 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Wed, 15 Jun 2022 15:05:08 +0100 Subject: [PATCH 04/11] Got book to shelf conversions working - Also extracted shelf to book view elements to own partial. - Fixed some existing logic including image param handling in update request and activity logging against correct element. --- app/Actions/ActivityType.php | 1 + app/Entities/Repos/BookshelfRepo.php | 4 +-- app/Entities/Repos/PageRepo.php | 17 --------- app/Entities/Tools/HierarchyTransformer.php | 19 ++++++---- app/Http/Controllers/BookController.php | 19 +++++++++- app/Http/Controllers/BookshelfController.php | 2 +- resources/lang/en/activities.php | 2 ++ resources/views/books/edit.blade.php | 7 +++- .../books/parts/convert-to-shelf.blade.php | 35 +++++++++++++++++++ resources/views/chapters/edit.blade.php | 32 ++--------------- .../chapters/parts/convert-to-book.blade.php | 28 +++++++++++++++ routes/web.php | 1 + 12 files changed, 110 insertions(+), 57 deletions(-) create mode 100644 resources/views/books/parts/convert-to-shelf.blade.php create mode 100644 resources/views/chapters/parts/convert-to-book.blade.php diff --git a/app/Actions/ActivityType.php b/app/Actions/ActivityType.php index 997cc041a..0ad25a5ab 100644 --- a/app/Actions/ActivityType.php +++ b/app/Actions/ActivityType.php @@ -22,6 +22,7 @@ class ActivityType const BOOK_SORT = 'book_sort'; const BOOKSHELF_CREATE = 'bookshelf_create'; + const BOOKSHELF_CREATE_FROM_BOOK = 'bookshelf_create_from_book'; const BOOKSHELF_UPDATE = 'bookshelf_update'; const BOOKSHELF_DELETE = 'bookshelf_delete'; diff --git a/app/Entities/Repos/BookshelfRepo.php b/app/Entities/Repos/BookshelfRepo.php index 03e7804d5..f37db1f06 100644 --- a/app/Entities/Repos/BookshelfRepo.php +++ b/app/Entities/Repos/BookshelfRepo.php @@ -89,7 +89,7 @@ class BookshelfRepo { $shelf = new Bookshelf(); $this->baseRepo->create($shelf, $input); - $this->baseRepo->updateCoverImage($shelf, $input['image']); + $this->baseRepo->updateCoverImage($shelf, $input['image'] ?? null); $this->updateBooks($shelf, $bookIds); Activity::add(ActivityType::BOOKSHELF_CREATE, $shelf); @@ -107,7 +107,7 @@ class BookshelfRepo $this->updateBooks($shelf, $bookIds); } - if (isset($input['image'])) { + if (array_key_exists('image', $input)) { $this->baseRepo->updateCoverImage($shelf, $input['image'], $input['image'] === null); } diff --git a/app/Entities/Repos/PageRepo.php b/app/Entities/Repos/PageRepo.php index c106d2fd3..e3c6bd17a 100644 --- a/app/Entities/Repos/PageRepo.php +++ b/app/Entities/Repos/PageRepo.php @@ -392,23 +392,6 @@ class PageRepo return $parentClass::visible()->where('id', '=', $entityId)->first(); } - /** - * Change the page's parent to the given entity. - */ - protected function changeParent(Page $page, Entity $parent) - { - $book = ($parent instanceof Chapter) ? $parent->book : $parent; - $page->chapter_id = ($parent instanceof Chapter) ? $parent->id : 0; - $page->save(); - - if ($page->book->id !== $book->id) { - $page->changeBook($book->id); - } - - $page->load('book'); - $book->rebuildPermissions(); - } - /** * Get a page revision to update for the given page. * Checks for an existing revisions before providing a fresh one. diff --git a/app/Entities/Tools/HierarchyTransformer.php b/app/Entities/Tools/HierarchyTransformer.php index 7304962b3..93c5bb9bb 100644 --- a/app/Entities/Tools/HierarchyTransformer.php +++ b/app/Entities/Tools/HierarchyTransformer.php @@ -44,14 +44,16 @@ class HierarchyTransformer $this->trashCan->destroyEntity($chapter); - Activity::add(ActivityType::BOOK_CREATE_FROM_CHAPTER); + Activity::add(ActivityType::BOOK_CREATE_FROM_CHAPTER, $book); return $book; } + /** + * Transform a book into a shelf. + * Does not check permissions, check before calling. + */ public function transformBookToShelf(Book $book): Bookshelf { - // TODO - Check permissions before call - // Permissions: edit-book, delete-book, create-shelf $inputData = $this->cloner->entityToInputData($book); $shelf = $this->shelfRepo->create($inputData, []); $this->cloner->copyEntityPermissions($book, $shelf); @@ -62,17 +64,22 @@ class HierarchyTransformer foreach ($book->chapters as $index => $chapter) { $newBook = $this->transformChapterToBook($chapter); $shelfBookSyncData[$newBook->id] = ['order' => $index]; + if (!$newBook->restricted) { + $this->cloner->copyEntityPermissions($shelf, $newBook); + } } - $shelf->books()->sync($shelfBookSyncData); - if ($book->directPages->count() > 0) { $book->name .= ' ' . trans('entities.pages'); + $shelfBookSyncData[$book->id] = ['order' => count($shelfBookSyncData) + 1]; + $book->save(); } else { $this->trashCan->destroyEntity($book); } - // TODO - Log activity for change + $shelf->books()->sync($shelfBookSyncData); + + Activity::add(ActivityType::BOOKSHELF_CREATE_FROM_BOOK, $shelf); return $shelf; } } \ No newline at end of file diff --git a/app/Http/Controllers/BookController.php b/app/Http/Controllers/BookController.php index b9dd0e799..937f7d28f 100644 --- a/app/Http/Controllers/BookController.php +++ b/app/Http/Controllers/BookController.php @@ -9,6 +9,7 @@ use BookStack\Entities\Models\Bookshelf; use BookStack\Entities\Repos\BookRepo; use BookStack\Entities\Tools\BookContents; use BookStack\Entities\Tools\Cloner; +use BookStack\Entities\Tools\HierarchyTransformer; use BookStack\Entities\Tools\PermissionsUpdater; use BookStack\Entities\Tools\ShelfContext; use BookStack\Exceptions\ImageUploadException; @@ -166,7 +167,7 @@ class BookController extends Controller if ($request->has('image_reset')) { $validated['image'] = null; - } else if (is_null($validated['image'])) { + } else if (array_key_exists('image', $validated) && is_null($validated['image'])) { unset($validated['image']); } @@ -266,4 +267,20 @@ class BookController extends Controller return redirect($bookCopy->getUrl()); } + + /** + * Convert the chapter to a book. + */ + public function convertToShelf(HierarchyTransformer $transformer, string $bookSlug) + { + $book = $this->bookRepo->getBySlug($bookSlug); + $this->checkOwnablePermission('book-update', $book); + $this->checkOwnablePermission('book-delete', $book); + $this->checkPermission('bookshelf-create-all'); + $this->checkPermission('book-create-all'); + + $shelf = $transformer->transformBookToShelf($book); + + return redirect($shelf->getUrl()); + } } diff --git a/app/Http/Controllers/BookshelfController.php b/app/Http/Controllers/BookshelfController.php index ce2e508c8..2f966beed 100644 --- a/app/Http/Controllers/BookshelfController.php +++ b/app/Http/Controllers/BookshelfController.php @@ -167,7 +167,7 @@ class BookshelfController extends Controller if ($request->has('image_reset')) { $validated['image'] = null; - } else if (is_null($validated['image'])) { + } else if (array_key_exists('image', $validated) && is_null($validated['image'])) { unset($validated['image']); } diff --git a/resources/lang/en/activities.php b/resources/lang/en/activities.php index 0c3d2e704..edddf9aeb 100644 --- a/resources/lang/en/activities.php +++ b/resources/lang/en/activities.php @@ -40,6 +40,8 @@ return [ // Bookshelves 'bookshelf_create' => 'created bookshelf', 'bookshelf_create_notification' => 'Bookshelf successfully created', + 'bookshelf_create_from_book' => 'converted book to bookshelf', + 'bookshelf_create_from_book_notification' => 'Book successfully converted to a shelf', 'bookshelf_update' => 'updated bookshelf', 'bookshelf_update_notification' => 'Bookshelf successfully updated', 'bookshelf_delete' => 'deleted bookshelf', diff --git a/resources/views/books/edit.blade.php b/resources/views/books/edit.blade.php index 403977121..180500e0a 100644 --- a/resources/views/books/edit.blade.php +++ b/resources/views/books/edit.blade.php @@ -14,12 +14,17 @@ ]]) -
+

{{ trans('entities.books_edit') }}

@include('books.parts.form', ['model' => $book, 'returnLocation' => $book->getUrl()])
+ + + @if(userCan('book-delete', $book) && userCan('book-create-all') && userCan('bookshelf-create-all')) + @include('books.parts.convert-to-shelf', ['book' => $book]) + @endif @stop \ No newline at end of file diff --git a/resources/views/books/parts/convert-to-shelf.blade.php b/resources/views/books/parts/convert-to-shelf.blade.php new file mode 100644 index 000000000..700377a19 --- /dev/null +++ b/resources/views/books/parts/convert-to-shelf.blade.php @@ -0,0 +1,35 @@ +
+

Convert to Shelf

+

+ You can convert this book to a new shelf with the same contents. + Chapters contained within this book will be converted to new books. + + If this book contains any pages, that are not in a chapter, this book will be renamed + and contain such pages, and this book will become part of the new shelf. + +

+ + Any permissions set on this book will be copied to the new shelf and to all new child books + that don't have their own permissions enforced. + + Note that permissions on shelves do not auto-cascade to content within, as they do for books. +

+
+ +
+
\ No newline at end of file diff --git a/resources/views/chapters/edit.blade.php b/resources/views/chapters/edit.blade.php index f6fd3cc6b..36058eff8 100644 --- a/resources/views/chapters/edit.blade.php +++ b/resources/views/chapters/edit.blade.php @@ -23,35 +23,9 @@
-{{-- TODO - Permissions--}} -
-

Convert to Book

-
-

- You can convert this chapter to a new book with the same contents. - Any permissions set on this chapter will be copied to the new book but any inherited permissions, - from the parent book, will not be copied which could lead to a change of access control. -

-
- -
-
-
+ @if(userCan('chapter-delete', $chapter) && userCan('book-create-all')) + @include('chapters.parts.convert-to-book') + @endif diff --git a/resources/views/chapters/parts/convert-to-book.blade.php b/resources/views/chapters/parts/convert-to-book.blade.php new file mode 100644 index 000000000..8a5d2a181 --- /dev/null +++ b/resources/views/chapters/parts/convert-to-book.blade.php @@ -0,0 +1,28 @@ +
+

Convert to Book

+
+

+ You can convert this chapter to a new book with the same contents. + Any permissions set on this chapter will be copied to the new book but any inherited permissions, + from the parent book, will not be copied which could lead to a change of access control. +

+
+ +
+
+
\ No newline at end of file diff --git a/routes/web.php b/routes/web.php index dfda97253..5e16e5333 100644 --- a/routes/web.php +++ b/routes/web.php @@ -82,6 +82,7 @@ Route::middleware('auth')->group(function () { Route::get('/books/{slug}/delete', [BookController::class, 'showDelete']); Route::get('/books/{bookSlug}/copy', [BookController::class, 'showCopy']); Route::post('/books/{bookSlug}/copy', [BookController::class, 'copy']); + Route::post('/books/{bookSlug}/convert-to-shelf', [BookController::class, 'convertToShelf']); Route::get('/books/{bookSlug}/sort', [BookSortController::class, 'show']); Route::put('/books/{bookSlug}/sort', [BookSortController::class, 'update']); Route::get('/books/{bookSlug}/export/html', [BookExportController::class, 'html']); From f145ffc93004707257b03773c70255e73909c9ed Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sun, 19 Jun 2022 16:23:18 +0100 Subject: [PATCH 05/11] Extracted conversion text to translation file --- resources/lang/en/entities.php | 12 +++++++++++ .../books/parts/convert-to-shelf.blade.php | 21 ++++++------------- .../chapters/parts/convert-to-book.blade.php | 14 ++++++------- 3 files changed, 25 insertions(+), 22 deletions(-) diff --git a/resources/lang/en/entities.php b/resources/lang/en/entities.php index bed781b61..27d67487a 100644 --- a/resources/lang/en/entities.php +++ b/resources/lang/en/entities.php @@ -355,4 +355,16 @@ return [ 'copy_consider_images' => 'Page image files will not be duplicated & the original images will retain their relation to the page they were originally uploaded to.', 'copy_consider_attachments' => 'Page attachments will not be copied.', 'copy_consider_access' => 'A change of location, owner or permissions may result in this content being accessible to those previously without access.', + + // Conversions + 'convert_to_shelf' => 'Convert to Shelf', + 'convert_to_shelf_contents_desc' => 'You can convert this book to a new shelf with the same contents. Chapters contained within this book will be converted to new books. If this book contains any pages, that are not in a chapter, this book will be renamed and contain such pages, and this book will become part of the new shelf.', + 'convert_to_shelf_permissions_desc' => 'Any permissions set on this book will be copied to the new shelf and to all new child books that don\'t have their own permissions enforced. Note that permissions on shelves do not auto-cascade to content within, as they do for books.', + 'convert_book' => 'Convert Book', + 'convert_book_confirm' => 'Are you sure you want to convert this book?', + 'convert_undo_warning' => 'This cannot be as easily undone.', + 'convert_to_book' => 'Convert to Book', + 'convert_to_book_desc' => 'You can convert this chapter to a new book with the same contents. Any permissions set on this chapter will be copied to the new book but any inherited permissions, from the parent book, will not be copied which could lead to a change of access control.', + 'convert_chapter' => 'Convert Chapter', + 'convert_chapter_confirm' => 'Are you sure you want to convert this chapter?', ]; diff --git a/resources/views/books/parts/convert-to-shelf.blade.php b/resources/views/books/parts/convert-to-shelf.blade.php index 700377a19..dde60aac0 100644 --- a/resources/views/books/parts/convert-to-shelf.blade.php +++ b/resources/views/books/parts/convert-to-shelf.blade.php @@ -1,27 +1,18 @@
-

Convert to Shelf

+

{{ trans('entities.convert_to_shelf') }}

- You can convert this book to a new shelf with the same contents. - Chapters contained within this book will be converted to new books. - - If this book contains any pages, that are not in a chapter, this book will be renamed - and contain such pages, and this book will become part of the new shelf. - + {{ trans('entities.convert_to_shelf_contents_desc') }}

- - Any permissions set on this book will be copied to the new shelf and to all new child books - that don't have their own permissions enforced. - - Note that permissions on shelves do not auto-cascade to content within, as they do for books. + {{ trans('entities.convert_to_shelf_permissions_desc') }}