use Activity;
use BookStack\Repos\UserRepo;
use Illuminate\Http\Request;
-use Illuminate\Support\Facades\Auth;
use BookStack\Http\Requests;
use BookStack\Repos\BookRepo;
use BookStack\Repos\ChapterRepo;
return redirect($book->getUrl());
}
- $sortedBooks = [];
// Sort pages and chapters
+ $sortedBooks = [];
+ $updatedModels = collect();
$sortMap = json_decode($request->get('sort-tree'));
$defaultBookId = $book->id;
- foreach ($sortMap as $index => $bookChild) {
- $id = $bookChild->id;
+
+ // Loop through contents of provided map and update entities accordingly
+ foreach ($sortMap as $bookChild) {
+ $priority = $bookChild->sort;
+ $id = intval($bookChild->id);
$isPage = $bookChild->type == 'page';
- $bookId = $this->bookRepo->exists($bookChild->book) ? $bookChild->book : $defaultBookId;
+ $bookId = $this->bookRepo->exists($bookChild->book) ? intval($bookChild->book) : $defaultBookId;
+ $chapterId = ($isPage && $bookChild->parentChapter === false) ? 0 : intval($bookChild->parentChapter);
$model = $isPage ? $this->pageRepo->getById($id) : $this->chapterRepo->getById($id);
- $isPage ? $this->pageRepo->changeBook($bookId, $model) : $this->chapterRepo->changeBook($bookId, $model);
- $model->priority = $index;
- if ($isPage) {
- $model->chapter_id = ($bookChild->parentChapter === false) ? 0 : $bookChild->parentChapter;
+
+ // Update models only if there's a change in parent chain or ordering.
+ if ($model->priority !== $priority || $model->book_id !== $bookId || ($isPage && $model->chapter_id !== $chapterId)) {
+ $isPage ? $this->pageRepo->changeBook($bookId, $model) : $this->chapterRepo->changeBook($bookId, $model);
+ $model->priority = $priority;
+ if ($isPage) $model->chapter_id = $chapterId;
+ $model->save();
+ $updatedModels->push($model);
}
- $model->save();
+
+ // Store involved books to be sorted later
if (!in_array($bookId, $sortedBooks)) {
$sortedBooks[] = $bookId;
}
// Add activity for books
foreach ($sortedBooks as $bookId) {
$updatedBook = $this->bookRepo->getById($bookId);
- $this->bookRepo->updateBookPermissions($updatedBook);
Activity::add($updatedBook, 'book_sort', $updatedBook->id);
}
+ // Update permissions on changed models
+ $this->bookRepo->buildJointPermissions($updatedModels);
+
return redirect($book->getUrl());
}
return redirect()->back();
}
- $this->chapterRepo->changeBook($parent->id, $chapter);
+ $this->chapterRepo->changeBook($parent->id, $chapter, true);
Activity::add($chapter, 'chapter_move', $chapter->book->id);
session()->flash('success', sprintf('Chapter moved to "%s"', $parent->name));
use Alpha\B;
use BookStack\Exceptions\NotFoundException;
+use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Str;
use BookStack\Book;
use Views;
$book->delete();
}
- /**
- * Alias method to update the book jointPermissions in the PermissionService.
- * @param Book $book
- */
- public function updateBookPermissions(Book $book)
- {
- $this->permissionService->buildJointPermissionsForEntity($book);
- }
-
/**
* Get the next child element priority.
* @param Book $book
/**
* Changes the book relation of this chapter.
- * @param $bookId
+ * @param $bookId
* @param Chapter $chapter
+ * @param bool $rebuildPermissions
* @return Chapter
*/
- public function changeBook($bookId, Chapter $chapter)
+ public function changeBook($bookId, Chapter $chapter, $rebuildPermissions = false)
{
$chapter->book_id = $bookId;
// Update related activity
foreach ($chapter->pages as $page) {
$this->pageRepo->changeBook($bookId, $page);
}
- // Update permissions
- $chapter->load('book');
- $this->permissionService->buildJointPermissionsForEntity($chapter->book);
+
+ // Update permissions if applicable
+ if ($rebuildPermissions) {
+ $chapter->load('book');
+ $this->permissionService->buildJointPermissionsForEntity($chapter->book);
+ }
return $chapter;
}
use BookStack\Page;
use BookStack\Services\PermissionService;
use BookStack\User;
+use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
class EntityRepo
return $query;
}
+ /**
+ * Alias method to update the book jointPermissions in the PermissionService.
+ * @param Collection $collection collection on entities
+ */
+ public function buildJointPermissions(Collection $collection)
+ {
+ $this->permissionService->buildJointPermissionsForEntities($collection);
+ }
+
}
use BookStack\Page;
use BookStack\Role;
use BookStack\User;
-use Illuminate\Database\Eloquent\Collection;
+use Illuminate\Support\Collection;
class PermissionService
{
protected $jointPermission;
protected $role;
+ protected $entityCache;
+
/**
* PermissionService constructor.
* @param JointPermission $jointPermission
$this->page = $page;
}
+ /**
+ * Prepare the local entity cache and ensure it's empty
+ */
+ protected function readyEntityCache()
+ {
+ $this->entityCache = [
+ 'books' => collect(),
+ 'chapters' => collect()
+ ];
+ }
+
+ /**
+ * Get a book via ID, Checks local cache
+ * @param $bookId
+ * @return Book
+ */
+ protected function getBook($bookId)
+ {
+ if (isset($this->entityCache['books']) && $this->entityCache['books']->has($bookId)) {
+ return $this->entityCache['books']->get($bookId);
+ }
+
+ $book = $this->book->find($bookId);
+ if ($book === null) $book = false;
+ if (isset($this->entityCache['books'])) {
+ $this->entityCache['books']->put($bookId, $book);
+ }
+
+ return $book;
+ }
+
+ /**
+ * Get a chapter via ID, Checks local cache
+ * @param $chapterId
+ * @return Book
+ */
+ protected function getChapter($chapterId)
+ {
+ if (isset($this->entityCache['chapters']) && $this->entityCache['chapters']->has($chapterId)) {
+ return $this->entityCache['chapters']->get($chapterId);
+ }
+
+ $chapter = $this->chapter->find($chapterId);
+ if ($chapter === null) $chapter = false;
+ if (isset($this->entityCache['chapters'])) {
+ $this->entityCache['chapters']->put($chapterId, $chapter);
+ }
+
+ return $chapter;
+ }
+
/**
* Get the roles for the current user;
* @return array|bool
public function buildJointPermissions()
{
$this->jointPermission->truncate();
+ $this->readyEntityCache();
// Get all roles (Should be the most limited dimension)
$roles = $this->role->with('permissions')->get();
}
/**
- * Create the entity jointPermissions for a particular entity.
+ * Rebuild the entity jointPermissions for a particular entity.
* @param Entity $entity
*/
public function buildJointPermissionsForEntity(Entity $entity)
$this->createManyJointPermissions($entities, $roles);
}
+ /**
+ * Rebuild the entity jointPermissions for a collection of entities.
+ * @param Collection $entities
+ */
+ public function buildJointPermissionsForEntities(Collection $entities)
+ {
+ $roles = $this->role->with('jointPermissions')->get();
+ $this->deleteManyJointPermissionsForEntities($entities);
+ $this->createManyJointPermissions($entities, $roles);
+ }
+
/**
* Build the entity jointPermissions for a particular role.
* @param Role $role
*/
protected function deleteManyJointPermissionsForEntities($entities)
{
+ $query = $this->jointPermission->newQuery();
foreach ($entities as $entity) {
- $entity->jointPermissions()->delete();
+ $query->orWhere(function($query) use ($entity) {
+ $query->where('entity_id', '=', $entity->id)
+ ->where('entity_type', '=', $entity->getMorphClass());
+ });
}
+ $query->delete();
}
/**
*/
protected function createManyJointPermissions($entities, $roles)
{
+ $this->readyEntityCache();
$jointPermissions = [];
foreach ($entities as $entity) {
foreach ($roles as $role) {
} elseif ($entity->isA('chapter')) {
if (!$entity->restricted) {
- $hasExplicitAccessToBook = $entity->book->hasActiveRestriction($role->id, $restrictionAction);
- $hasPermissiveAccessToBook = !$entity->book->restricted;
+ $book = $this->getBook($entity->book_id);
+ $hasExplicitAccessToBook = $book->hasActiveRestriction($role->id, $restrictionAction);
+ $hasPermissiveAccessToBook = !$book->restricted;
return $this->createJointPermissionDataArray($entity, $role, $action,
($hasExplicitAccessToBook || ($roleHasPermission && $hasPermissiveAccessToBook)),
($hasExplicitAccessToBook || ($roleHasPermissionOwn && $hasPermissiveAccessToBook)));
} elseif ($entity->isA('page')) {
if (!$entity->restricted) {
- $hasExplicitAccessToBook = $entity->book->hasActiveRestriction($role->id, $restrictionAction);
- $hasPermissiveAccessToBook = !$entity->book->restricted;
- $hasExplicitAccessToChapter = $entity->chapter && $entity->chapter->hasActiveRestriction($role->id, $restrictionAction);
- $hasPermissiveAccessToChapter = $entity->chapter && !$entity->chapter->restricted;
- $acknowledgeChapter = ($entity->chapter && $entity->chapter->restricted);
+ $book = $this->getBook($entity->book_id);
+ $hasExplicitAccessToBook = $book->hasActiveRestriction($role->id, $restrictionAction);
+ $hasPermissiveAccessToBook = !$book->restricted;
+
+ $chapter = $this->getChapter($entity->chapter_id);
+ $hasExplicitAccessToChapter = $chapter && $chapter->hasActiveRestriction($role->id, $restrictionAction);
+ $hasPermissiveAccessToChapter = $chapter && !$chapter->restricted;
+ $acknowledgeChapter = ($chapter && $chapter->restricted);
$hasExplicitAccessToParents = $acknowledgeChapter ? $hasExplicitAccessToChapter : $hasExplicitAccessToBook;
$hasPermissiveAccessToParents = $acknowledgeChapter ? $hasPermissiveAccessToChapter : $hasPermissiveAccessToBook;
var sortableOptions = {
group: 'serialization',
onDrop: function($item, container, _super) {
- var pageMap = buildPageMap();
+ var pageMap = buildEntityMap();
$('#sort-tree-input').val(JSON.stringify(pageMap));
_super($item, container);
},
$link.remove();
});
- function buildPageMap() {
- var pageMap = [];
+ /**
+ * Build up a mapping of entities with their ordering and nesting.
+ * @returns {Array}
+ */
+ function buildEntityMap() {
+ var entityMap = [];
var $lists = $('.sort-list');
$lists.each(function(listIndex) {
var list = $(this);
var bookId = list.closest('[data-type="book"]').attr('data-id');
- var $childElements = list.find('[data-type="page"], [data-type="chapter"]');
- $childElements.each(function(childIndex) {
+ var $directChildren = list.find('> [data-type="page"], > [data-type="chapter"]');
+ $directChildren.each(function(directChildIndex) {
var $childElem = $(this);
var type = $childElem.attr('data-type');
var parentChapter = false;
- if(type === 'page' && $childElem.closest('[data-type="chapter"]').length === 1) {
- parentChapter = $childElem.closest('[data-type="chapter"]').attr('data-id');
- }
- pageMap.push({
- id: $childElem.attr('data-id'),
+ var childId = $childElem.attr('data-id');
+ entityMap.push({
+ id: childId,
+ sort: directChildIndex,
parentChapter: parentChapter,
type: type,
book: bookId
});
+ $chapterChildren = $childElem.find('[data-type="page"]').each(function(pageIndex) {
+ var $chapterChild = $(this);
+ entityMap.push({
+ id: $chapterChild.attr('data-id'),
+ sort: pageIndex,
+ parentChapter: childId,
+ type: 'page',
+ book: bookId
+ });
+ });
});
});
- return pageMap;
+ return entityMap;
}
});