Skip to content

Commit ac0cd99

Browse files
committed
Sorting: Reorganised book sort code to its own directory
1 parent 786a434 commit ac0cd99

File tree

6 files changed

+237
-217
lines changed

6 files changed

+237
-217
lines changed

app/Entities/Tools/BookContents.php

Lines changed: 2 additions & 207 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
use BookStack\Entities\Models\Entity;
99
use BookStack\Entities\Models\Page;
1010
use BookStack\Entities\Queries\EntityQueries;
11+
use BookStack\Sorting\BookSortMap;
12+
use BookStack\Sorting\BookSortMapItem;
1113
use Illuminate\Support\Collection;
1214

1315
class BookContents
@@ -103,211 +105,4 @@ protected function getPages(bool $showDrafts = false, bool $getPageContent = fal
103105

104106
return $query->where('book_id', '=', $this->book->id)->get();
105107
}
106-
107-
/**
108-
* Sort the books content using the given sort map.
109-
* Returns a list of books that were involved in the operation.
110-
*
111-
* @returns Book[]
112-
*/
113-
public function sortUsingMap(BookSortMap $sortMap): array
114-
{
115-
// Load models into map
116-
$modelMap = $this->loadModelsFromSortMap($sortMap);
117-
118-
// Sort our changes from our map to be chapters first
119-
// Since they need to be process to ensure book alignment for child page changes.
120-
$sortMapItems = $sortMap->all();
121-
usort($sortMapItems, function (BookSortMapItem $itemA, BookSortMapItem $itemB) {
122-
$aScore = $itemA->type === 'page' ? 2 : 1;
123-
$bScore = $itemB->type === 'page' ? 2 : 1;
124-
125-
return $aScore - $bScore;
126-
});
127-
128-
// Perform the sort
129-
foreach ($sortMapItems as $item) {
130-
$this->applySortUpdates($item, $modelMap);
131-
}
132-
133-
/** @var Book[] $booksInvolved */
134-
$booksInvolved = array_values(array_filter($modelMap, function (string $key) {
135-
return str_starts_with($key, 'book:');
136-
}, ARRAY_FILTER_USE_KEY));
137-
138-
// Update permissions of books involved
139-
foreach ($booksInvolved as $book) {
140-
$book->rebuildPermissions();
141-
}
142-
143-
return $booksInvolved;
144-
}
145-
146-
/**
147-
* Using the given sort map item, detect changes for the related model
148-
* and update it if required. Changes where permissions are lacking will
149-
* be skipped and not throw an error.
150-
*
151-
* @param array<string, Entity> $modelMap
152-
*/
153-
protected function applySortUpdates(BookSortMapItem $sortMapItem, array $modelMap): void
154-
{
155-
/** @var BookChild $model */
156-
$model = $modelMap[$sortMapItem->type . ':' . $sortMapItem->id] ?? null;
157-
if (!$model) {
158-
return;
159-
}
160-
161-
$priorityChanged = $model->priority !== $sortMapItem->sort;
162-
$bookChanged = $model->book_id !== $sortMapItem->parentBookId;
163-
$chapterChanged = ($model instanceof Page) && $model->chapter_id !== $sortMapItem->parentChapterId;
164-
165-
// Stop if there's no change
166-
if (!$priorityChanged && !$bookChanged && !$chapterChanged) {
167-
return;
168-
}
169-
170-
$currentParentKey = 'book:' . $model->book_id;
171-
if ($model instanceof Page && $model->chapter_id) {
172-
$currentParentKey = 'chapter:' . $model->chapter_id;
173-
}
174-
175-
$currentParent = $modelMap[$currentParentKey] ?? null;
176-
/** @var Book $newBook */
177-
$newBook = $modelMap['book:' . $sortMapItem->parentBookId] ?? null;
178-
/** @var ?Chapter $newChapter */
179-
$newChapter = $sortMapItem->parentChapterId ? ($modelMap['chapter:' . $sortMapItem->parentChapterId] ?? null) : null;
180-
181-
if (!$this->isSortChangePermissible($sortMapItem, $model, $currentParent, $newBook, $newChapter)) {
182-
return;
183-
}
184-
185-
// Action the required changes
186-
if ($bookChanged) {
187-
$model->changeBook($newBook->id);
188-
}
189-
190-
if ($model instanceof Page && $chapterChanged) {
191-
$model->chapter_id = $newChapter->id ?? 0;
192-
}
193-
194-
if ($priorityChanged) {
195-
$model->priority = $sortMapItem->sort;
196-
}
197-
198-
if ($chapterChanged || $priorityChanged) {
199-
$model->save();
200-
}
201-
}
202-
203-
/**
204-
* Check if the current user has permissions to apply the given sorting change.
205-
* Is quite complex since items can gain a different parent change. Acts as a:
206-
* - Update of old parent element (Change of content/order).
207-
* - Update of sorted/moved element.
208-
* - Deletion of element (Relative to parent upon move).
209-
* - Creation of element within parent (Upon move to new parent).
210-
*/
211-
protected function isSortChangePermissible(BookSortMapItem $sortMapItem, BookChild $model, ?Entity $currentParent, ?Entity $newBook, ?Entity $newChapter): bool
212-
{
213-
// Stop if we can't see the current parent or new book.
214-
if (!$currentParent || !$newBook) {
215-
return false;
216-
}
217-
218-
$hasNewParent = $newBook->id !== $model->book_id || ($model instanceof Page && $model->chapter_id !== ($sortMapItem->parentChapterId ?? 0));
219-
if ($model instanceof Chapter) {
220-
$hasPermission = userCan('book-update', $currentParent)
221-
&& userCan('book-update', $newBook)
222-
&& userCan('chapter-update', $model)
223-
&& (!$hasNewParent || userCan('chapter-create', $newBook))
224-
&& (!$hasNewParent || userCan('chapter-delete', $model));
225-
226-
if (!$hasPermission) {
227-
return false;
228-
}
229-
}
230-
231-
if ($model instanceof Page) {
232-
$parentPermission = ($currentParent instanceof Chapter) ? 'chapter-update' : 'book-update';
233-
$hasCurrentParentPermission = userCan($parentPermission, $currentParent);
234-
235-
// This needs to check if there was an intended chapter location in the original sort map
236-
// rather than inferring from the $newChapter since that variable may be null
237-
// due to other reasons (Visibility).
238-
$newParent = $sortMapItem->parentChapterId ? $newChapter : $newBook;
239-
if (!$newParent) {
240-
return false;
241-
}
242-
243-
$hasPageEditPermission = userCan('page-update', $model);
244-
$newParentInRightLocation = ($newParent instanceof Book || ($newParent instanceof Chapter && $newParent->book_id === $newBook->id));
245-
$newParentPermission = ($newParent instanceof Chapter) ? 'chapter-update' : 'book-update';
246-
$hasNewParentPermission = userCan($newParentPermission, $newParent);
247-
248-
$hasDeletePermissionIfMoving = (!$hasNewParent || userCan('page-delete', $model));
249-
$hasCreatePermissionIfMoving = (!$hasNewParent || userCan('page-create', $newParent));
250-
251-
$hasPermission = $hasCurrentParentPermission
252-
&& $newParentInRightLocation
253-
&& $hasNewParentPermission
254-
&& $hasPageEditPermission
255-
&& $hasDeletePermissionIfMoving
256-
&& $hasCreatePermissionIfMoving;
257-
258-
if (!$hasPermission) {
259-
return false;
260-
}
261-
}
262-
263-
return true;
264-
}
265-
266-
/**
267-
* Load models from the database into the given sort map.
268-
*
269-
* @return array<string, Entity>
270-
*/
271-
protected function loadModelsFromSortMap(BookSortMap $sortMap): array
272-
{
273-
$modelMap = [];
274-
$ids = [
275-
'chapter' => [],
276-
'page' => [],
277-
'book' => [],
278-
];
279-
280-
foreach ($sortMap->all() as $sortMapItem) {
281-
$ids[$sortMapItem->type][] = $sortMapItem->id;
282-
$ids['book'][] = $sortMapItem->parentBookId;
283-
if ($sortMapItem->parentChapterId) {
284-
$ids['chapter'][] = $sortMapItem->parentChapterId;
285-
}
286-
}
287-
288-
$pages = $this->queries->pages->visibleForList()->whereIn('id', array_unique($ids['page']))->get();
289-
/** @var Page $page */
290-
foreach ($pages as $page) {
291-
$modelMap['page:' . $page->id] = $page;
292-
$ids['book'][] = $page->book_id;
293-
if ($page->chapter_id) {
294-
$ids['chapter'][] = $page->chapter_id;
295-
}
296-
}
297-
298-
$chapters = $this->queries->chapters->visibleForList()->whereIn('id', array_unique($ids['chapter']))->get();
299-
/** @var Chapter $chapter */
300-
foreach ($chapters as $chapter) {
301-
$modelMap['chapter:' . $chapter->id] = $chapter;
302-
$ids['book'][] = $chapter->book_id;
303-
}
304-
305-
$books = $this->queries->books->visibleForList()->whereIn('id', array_unique($ids['book']))->get();
306-
/** @var Book $book */
307-
foreach ($books as $book) {
308-
$modelMap['book:' . $book->id] = $book;
309-
}
310-
311-
return $modelMap;
312-
}
313108
}

app/Entities/Controllers/BookSortController.php renamed to app/Sorting/BookSortController.php

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
<?php
22

3-
namespace BookStack\Entities\Controllers;
3+
namespace BookStack\Sorting;
44

55
use BookStack\Activity\ActivityType;
66
use BookStack\Entities\Queries\BookQueries;
77
use BookStack\Entities\Tools\BookContents;
8-
use BookStack\Entities\Tools\BookSortMap;
98
use BookStack\Facades\Activity;
109
use BookStack\Http\Controller;
1110
use Illuminate\Http\Request;
@@ -47,7 +46,7 @@ public function showItem(string $bookSlug)
4746
/**
4847
* Sorts a book using a given mapping array.
4948
*/
50-
public function update(Request $request, string $bookSlug)
49+
public function update(Request $request, BookSorter $sorter, string $bookSlug)
5150
{
5251
$book = $this->queries->findVisibleBySlugOrFail($bookSlug);
5352
$this->checkOwnablePermission('book-update', $book);
@@ -58,8 +57,7 @@ public function update(Request $request, string $bookSlug)
5857
}
5958

6059
$sortMap = BookSortMap::fromJson($request->get('sort-tree'));
61-
$bookContents = new BookContents($book);
62-
$booksInvolved = $bookContents->sortUsingMap($sortMap);
60+
$booksInvolved = $sorter->sortUsingMap($sortMap);
6361

6462
// Rebuild permissions and add activity for involved books.
6563
foreach ($booksInvolved as $bookInvolved) {
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<?php
22

3-
namespace BookStack\Entities\Tools;
3+
namespace BookStack\Sorting;
44

55
class BookSortMap
66
{
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<?php
22

3-
namespace BookStack\Entities\Tools;
3+
namespace BookStack\Sorting;
44

55
class BookSortMapItem
66
{

0 commit comments

Comments
 (0)