]> BookStack Code Mirror - bookstack/commitdiff
Sorting: Reorganised book sort code to its own directory
authorDan Brown <redacted>
Wed, 29 Jan 2025 16:40:11 +0000 (16:40 +0000)
committerDan Brown <redacted>
Wed, 29 Jan 2025 16:40:11 +0000 (16:40 +0000)
app/Entities/Tools/BookContents.php
app/Sorting/BookSortController.php [moved from app/Entities/Controllers/BookSortController.php with 88% similarity]
app/Sorting/BookSortMap.php [moved from app/Entities/Tools/BookSortMap.php with 96% similarity]
app/Sorting/BookSortMapItem.php [moved from app/Entities/Tools/BookSortMapItem.php with 94% similarity]
app/Sorting/BookSorter.php [new file with mode: 0644]
routes/web.php

index 7fa2134b7fad60627393c334781d512542133298..7dd3f3e11adade81eea09005082948c1d3c42439 100644 (file)
@@ -8,6 +8,8 @@ use BookStack\Entities\Models\Chapter;
 use BookStack\Entities\Models\Entity;
 use BookStack\Entities\Models\Page;
 use BookStack\Entities\Queries\EntityQueries;
+use BookStack\Sorting\BookSortMap;
+use BookStack\Sorting\BookSortMapItem;
 use Illuminate\Support\Collection;
 
 class BookContents
@@ -103,211 +105,4 @@ class BookContents
 
         return $query->where('book_id', '=', $this->book->id)->get();
     }
-
-    /**
-     * Sort the books content using the given sort map.
-     * Returns a list of books that were involved in the operation.
-     *
-     * @returns Book[]
-     */
-    public function sortUsingMap(BookSortMap $sortMap): array
-    {
-        // Load models into map
-        $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
-        foreach ($sortMapItems as $item) {
-            $this->applySortUpdates($item, $modelMap);
-        }
-
-        /** @var Book[] $booksInvolved */
-        $booksInvolved = array_values(array_filter($modelMap, function (string $key) {
-            return str_starts_with($key, 'book:');
-        }, ARRAY_FILTER_USE_KEY));
-
-        // 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. Changes where permissions are lacking will
-     * be skipped and not throw an error.
-     *
-     * @param array<string, Entity> $modelMap
-     */
-    protected function applySortUpdates(BookSortMapItem $sortMapItem, array $modelMap): void
-    {
-        /** @var BookChild $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;
-        }
-
-        $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($newBook->id);
-        }
-
-        if ($model instanceof Page && $chapterChanged) {
-            $model->chapter_id = $newChapter->id ?? 0;
-        }
-
-        if ($priorityChanged) {
-            $model->priority = $sortMapItem->sort;
-        }
-
-        if ($chapterChanged || $priorityChanged) {
-            $model->save();
-        }
-    }
-
-    /**
-     * 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 isSortChangePermissible(BookSortMapItem $sortMapItem, BookChild $model, ?Entity $currentParent, ?Entity $newBook, ?Entity $newChapter): bool
-    {
-        // Stop if we can't see the current parent or new book.
-        if (!$currentParent || !$newBook) {
-            return false;
-        }
-
-        $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));
-
-            if (!$hasPermission) {
-                return false;
-            }
-        }
-
-        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 instanceof Chapter && $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;
-    }
-
-    /**
-     * Load models from the database into the given sort map.
-     *
-     * @return array<string, Entity>
-     */
-    protected function loadModelsFromSortMap(BookSortMap $sortMap): array
-    {
-        $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 = $this->queries->pages->visibleForList()->whereIn('id', array_unique($ids['page']))->get();
-        /** @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;
-            }
-        }
-
-        $chapters = $this->queries->chapters->visibleForList()->whereIn('id', array_unique($ids['chapter']))->get();
-        /** @var Chapter $chapter */
-        foreach ($chapters as $chapter) {
-            $modelMap['chapter:' . $chapter->id] = $chapter;
-            $ids['book'][] = $chapter->book_id;
-        }
-
-        $books = $this->queries->books->visibleForList()->whereIn('id', array_unique($ids['book']))->get();
-        /** @var Book $book */
-        foreach ($books as $book) {
-            $modelMap['book:' . $book->id] = $book;
-        }
-
-        return $modelMap;
-    }
 }
similarity index 88%
rename from app/Entities/Controllers/BookSortController.php
rename to app/Sorting/BookSortController.php
index 5aefc583279dbdf7f97cb42122f1af52cf6489a5..feed5db4fde0694086d7c2a90b6ae61e21c96aa3 100644 (file)
@@ -1,11 +1,10 @@
 <?php
 
-namespace BookStack\Entities\Controllers;
+namespace BookStack\Sorting;
 
 use BookStack\Activity\ActivityType;
 use BookStack\Entities\Queries\BookQueries;
 use BookStack\Entities\Tools\BookContents;
-use BookStack\Entities\Tools\BookSortMap;
 use BookStack\Facades\Activity;
 use BookStack\Http\Controller;
 use Illuminate\Http\Request;
@@ -47,7 +46,7 @@ class BookSortController extends Controller
     /**
      * Sorts a book using a given mapping array.
      */
-    public function update(Request $request, string $bookSlug)
+    public function update(Request $request, BookSorter $sorter, string $bookSlug)
     {
         $book = $this->queries->findVisibleBySlugOrFail($bookSlug);
         $this->checkOwnablePermission('book-update', $book);
@@ -58,8 +57,7 @@ class BookSortController extends Controller
         }
 
         $sortMap = BookSortMap::fromJson($request->get('sort-tree'));
-        $bookContents = new BookContents($book);
-        $booksInvolved = $bookContents->sortUsingMap($sortMap);
+        $booksInvolved = $sorter->sortUsingMap($sortMap);
 
         // Rebuild permissions and add activity for involved books.
         foreach ($booksInvolved as $bookInvolved) {
similarity index 96%
rename from app/Entities/Tools/BookSortMap.php
rename to app/Sorting/BookSortMap.php
index ff1ec767f784458e58c867f4a845acd8efdfdbf5..96c9d342a27b9a419e65a20f455e5a2e1e124fee 100644 (file)
@@ -1,6 +1,6 @@
 <?php
 
-namespace BookStack\Entities\Tools;
+namespace BookStack\Sorting;
 
 class BookSortMap
 {
similarity index 94%
rename from app/Entities/Tools/BookSortMapItem.php
rename to app/Sorting/BookSortMapItem.php
index f76d87f23669001369630bc7d588f91c1923e819..8f517edd6ff5cc31bc4bb20407f67a3a27d795cb 100644 (file)
@@ -1,6 +1,6 @@
 <?php
 
-namespace BookStack\Entities\Tools;
+namespace BookStack\Sorting;
 
 class BookSortMapItem
 {
diff --git a/app/Sorting/BookSorter.php b/app/Sorting/BookSorter.php
new file mode 100644 (file)
index 0000000..7268b35
--- /dev/null
@@ -0,0 +1,226 @@
+<?php
+
+namespace BookStack\Sorting;
+
+use BookStack\Entities\Models\Book;
+use BookStack\Entities\Models\BookChild;
+use BookStack\Entities\Models\Chapter;
+use BookStack\Entities\Models\Entity;
+use BookStack\Entities\Models\Page;
+use BookStack\Entities\Queries\EntityQueries;
+
+class BookSorter
+{
+    public function __construct(
+        protected EntityQueries $queries,
+    ) {
+    }
+
+
+    /**
+     * Sort the books content using the given sort map.
+     * Returns a list of books that were involved in the operation.
+     *
+     * @returns Book[]
+     */
+    public function sortUsingMap(BookSortMap $sortMap): array
+    {
+        // Load models into map
+        $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
+        foreach ($sortMapItems as $item) {
+            $this->applySortUpdates($item, $modelMap);
+        }
+
+        /** @var Book[] $booksInvolved */
+        $booksInvolved = array_values(array_filter($modelMap, function (string $key) {
+            return str_starts_with($key, 'book:');
+        }, ARRAY_FILTER_USE_KEY));
+
+        // 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. Changes where permissions are lacking will
+     * be skipped and not throw an error.
+     *
+     * @param array<string, Entity> $modelMap
+     */
+    protected function applySortUpdates(BookSortMapItem $sortMapItem, array $modelMap): void
+    {
+        /** @var BookChild $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;
+        }
+
+        $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($newBook->id);
+        }
+
+        if ($model instanceof Page && $chapterChanged) {
+            $model->chapter_id = $newChapter->id ?? 0;
+        }
+
+        if ($priorityChanged) {
+            $model->priority = $sortMapItem->sort;
+        }
+
+        if ($chapterChanged || $priorityChanged) {
+            $model->save();
+        }
+    }
+
+    /**
+     * 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 isSortChangePermissible(BookSortMapItem $sortMapItem, BookChild $model, ?Entity $currentParent, ?Entity $newBook, ?Entity $newChapter): bool
+    {
+        // Stop if we can't see the current parent or new book.
+        if (!$currentParent || !$newBook) {
+            return false;
+        }
+
+        $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));
+
+            if (!$hasPermission) {
+                return false;
+            }
+        }
+
+        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 instanceof Chapter && $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;
+    }
+
+    /**
+     * Load models from the database into the given sort map.
+     *
+     * @return array<string, Entity>
+     */
+    protected function loadModelsFromSortMap(BookSortMap $sortMap): array
+    {
+        $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 = $this->queries->pages->visibleForList()->whereIn('id', array_unique($ids['page']))->get();
+        /** @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;
+            }
+        }
+
+        $chapters = $this->queries->chapters->visibleForList()->whereIn('id', array_unique($ids['chapter']))->get();
+        /** @var Chapter $chapter */
+        foreach ($chapters as $chapter) {
+            $modelMap['chapter:' . $chapter->id] = $chapter;
+            $ids['book'][] = $chapter->book_id;
+        }
+
+        $books = $this->queries->books->visibleForList()->whereIn('id', array_unique($ids['book']))->get();
+        /** @var Book $book */
+        foreach ($books as $book) {
+            $modelMap['book:' . $book->id] = $book;
+        }
+
+        return $modelMap;
+    }
+}
index 5bb9622e7372b96171029eb80d08854bcda3289e..e1e819dd03ec03da94896c9a5a8ddc84c05d36d7 100644 (file)
@@ -13,6 +13,7 @@ use BookStack\Permissions\PermissionsController;
 use BookStack\References\ReferenceController;
 use BookStack\Search\SearchController;
 use BookStack\Settings as SettingControllers;
+use BookStack\Sorting\BookSortController;
 use BookStack\Theming\ThemeController;
 use BookStack\Uploads\Controllers as UploadControllers;
 use BookStack\Users\Controllers as UserControllers;
@@ -66,7 +67,7 @@ Route::middleware('auth')->group(function () {
     Route::get('/books/{slug}/edit', [EntityControllers\BookController::class, 'edit']);
     Route::put('/books/{slug}', [EntityControllers\BookController::class, 'update']);
     Route::delete('/books/{id}', [EntityControllers\BookController::class, 'destroy']);
-    Route::get('/books/{slug}/sort-item', [EntityControllers\BookSortController::class, 'showItem']);
+    Route::get('/books/{slug}/sort-item', [BookSortController::class, 'showItem']);
     Route::get('/books/{slug}', [EntityControllers\BookController::class, 'show']);
     Route::get('/books/{bookSlug}/permissions', [PermissionsController::class, 'showForBook']);
     Route::put('/books/{bookSlug}/permissions', [PermissionsController::class, 'updateForBook']);
@@ -74,8 +75,8 @@ Route::middleware('auth')->group(function () {
     Route::get('/books/{bookSlug}/copy', [EntityControllers\BookController::class, 'showCopy']);
     Route::post('/books/{bookSlug}/copy', [EntityControllers\BookController::class, 'copy']);
     Route::post('/books/{bookSlug}/convert-to-shelf', [EntityControllers\BookController::class, 'convertToShelf']);
-    Route::get('/books/{bookSlug}/sort', [EntityControllers\BookSortController::class, 'show']);
-    Route::put('/books/{bookSlug}/sort', [EntityControllers\BookSortController::class, 'update']);
+    Route::get('/books/{bookSlug}/sort', [BookSortController::class, 'show']);
+    Route::put('/books/{bookSlug}/sort', [BookSortController::class, 'update']);
     Route::get('/books/{slug}/references', [ReferenceController::class, 'book']);
     Route::get('/books/{bookSlug}/export/html', [ExportControllers\BookExportController::class, 'html']);
     Route::get('/books/{bookSlug}/export/pdf', [ExportControllers\BookExportController::class, 'pdf']);