]> BookStack Code Mirror - bookstack/blobdiff - app/Entities/Tools/BookContents.php
Added 'Sort Book' action to chapters
[bookstack] / app / Entities / Tools / BookContents.php
index ff018eda993c0d703762dfa4e9bcef58a95321b5..6f11e8cbe528f3002008f8a03d78248d0f3fdb0d 100644 (file)
@@ -116,8 +116,18 @@ class BookContents
         // 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 ($sortMap->all() as $item) {
+        foreach ($sortMapItems as $item) {
             $this->applySortUpdates($item, $modelMap);
         }
 
@@ -158,37 +168,28 @@ class BookContents
             return;
         }
 
-        $currentParentKey =  'book:' . $model->book_id;
+        $currentParentKey = 'book:' . $model->book_id;
         if ($model instanceof Page && $model->chapter_id) {
-             $currentParentKey = 'chapter:' . $model->chapter_id;
+            $currentParentKey = 'chapter:' . $model->chapter_id;
         }
 
-        $currentParent = $modelMap[$currentParentKey];
+        $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;
 
-        // Check permissions of our changes to be made
-        if (!$currentParent || !$newBook) {
-            return;
-        } else if (!userCan('chapter-update', $currentParent) && !userCan('book-update', $currentParent)) {
-            return;
-        } else if ($bookChanged && !$newChapter && !userCan('book-update', $newBook)) {
-            return;
-        } else if ($newChapter && !userCan('chapter-update', $newChapter)) {
-            return;
-        } else if (($chapterChanged || $bookChanged) && $newChapter && $newBook->id !== $newChapter->book_id) {
+        if (!$this->isSortChangePermissible($sortMapItem, $model, $currentParent, $newBook, $newChapter)) {
             return;
         }
 
         // Action the required changes
         if ($bookChanged) {
-            $model->changeBook($sortMapItem->parentBookId);
+            $model->changeBook($newBook->id);
         }
 
         if ($chapterChanged) {
-            $model->chapter_id = $sortMapItem->parentChapterId ?? 0;
+            $model->chapter_id = $newChapter->id ?? 0;
         }
 
         if ($priorityChanged) {
@@ -200,8 +201,72 @@ class BookContents
         }
     }
 
+    /**
+     * 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->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
@@ -209,8 +274,8 @@ class BookContents
         $modelMap = [];
         $ids = [
             'chapter' => [],
-            'page' => [],
-            'book' => [],
+            'page'    => [],
+            'book'    => [],
         ];
 
         foreach ($sortMap->all() as $sortMapItem) {