X-Git-Url: http://source.bookstackapp.com/bookstack/blobdiff_plain/0e0a17cc30fc4be84b09ab1e30d7689839ff1bae..refs/pull/1688/head:/app/Entities/Repos/EntityRepo.php diff --git a/app/Entities/Repos/EntityRepo.php b/app/Entities/Repos/EntityRepo.php index 576ed70f0..0dd0fbb0a 100644 --- a/app/Entities/Repos/EntityRepo.php +++ b/app/Entities/Repos/EntityRepo.php @@ -1,10 +1,12 @@ entityQuery($type)->where('slug', '=', $slug); + $type = strtolower($type); + $query = $this->entityQuery($type)->where('slug', '=', $slug); - if (strtolower($type) === 'chapter' || strtolower($type) === 'page') { - $q = $q->where('book_id', '=', function ($query) use ($bookSlug) { + if ($type === 'chapter' || $type === 'page') { + $query = $query->where('book_id', '=', function (QueryBuilder $query) use ($bookSlug) { $query->select('id') ->from($this->entityProvider->book->getTable()) ->where('slug', '=', $bookSlug)->limit(1); }); } - $entity = $q->first(); + + $entity = $query->first(); + if ($entity === null) { - throw new NotFoundException(trans('errors.' . strtolower($type) . '_not_found')); + throw new NotFoundException(trans('errors.' . $type . '_not_found')); } + return $entity; } @@ -179,11 +190,38 @@ class EntityRepo * Get all entities in a paginated format * @param $type * @param int $count - * @return \Illuminate\Contracts\Pagination\LengthAwarePaginator + * @param string $sort + * @param string $order + * @param null|callable $queryAddition + * @return LengthAwarePaginator */ - public function getAllPaginated($type, $count = 10) + public function getAllPaginated($type, int $count = 10, string $sort = 'name', string $order = 'asc', $queryAddition = null) { - return $this->entityQuery($type)->orderBy('name', 'asc')->paginate($count); + $query = $this->entityQuery($type); + $query = $this->addSortToQuery($query, $sort, $order); + if ($queryAddition) { + $queryAddition($query); + } + return $query->paginate($count); + } + + /** + * Add sorting operations to an entity query. + * @param Builder $query + * @param string $sort + * @param string $order + * @return Builder + */ + protected function addSortToQuery(Builder $query, string $sort = 'name', string $order = 'asc') + { + $order = ($order === 'asc') ? 'asc' : 'desc'; + $propertySorts = ['name', 'created_at', 'updated_at']; + + if (in_array($sort, $propertySorts)) { + return $query->orderBy($sort, $order); + } + + return $query; } /** @@ -265,15 +303,14 @@ class EntityRepo /** * Get the most popular entities base on all views. - * @param string|bool $type + * @param string $type * @param int $count * @param int $page * @return mixed */ - public function getPopular($type, $count = 10, $page = 0) + public function getPopular(string $type, int $count = 10, int $page = 0) { - $filter = is_bool($type) ? false : $this->entityProvider->get($type); - return $this->viewService->getPopular($count, $page, $filter); + return $this->viewService->getPopular($count, $page, $type); } /** @@ -305,7 +342,7 @@ class EntityRepo /** * Get the child items for a chapter sorted by priority but * with draft items floated to the top. - * @param \BookStack\Entities\Bookshelf $bookshelf + * @param Bookshelf $bookshelf * @return \Illuminate\Database\Eloquent\Collection|static[] */ public function getBookshelfChildren(Bookshelf $bookshelf) @@ -313,11 +350,23 @@ class EntityRepo return $this->permissionService->enforceEntityRestrictions('book', $bookshelf->books())->get(); } + /** + * Get the direct children of a book. + * @param Book $book + * @return \Illuminate\Database\Eloquent\Collection + */ + public function getBookDirectChildren(Book $book) + { + $pages = $this->permissionService->enforceEntityRestrictions('page', $book->directPages())->get(); + $chapters = $this->permissionService->enforceEntityRestrictions('chapters', $book->chapters())->get(); + return collect()->concat($pages)->concat($chapters)->sortBy('priority')->sortByDesc('draft'); + } + /** * Get all child objects of a book. * Returns a sorted collection of Pages and Chapters. * Loads the book slug onto child elements to prevent access database access for getting the slug. - * @param \BookStack\Entities\Book $book + * @param Book $book * @param bool $filterDrafts * @param bool $renderPages * @return mixed @@ -364,10 +413,21 @@ class EntityRepo return collect($tree); } + + /** + * Get the bookshelves that a book is contained in. + * @param Book $book + * @return \Illuminate\Database\Eloquent\Collection|static[] + */ + public function getBookParentShelves(Book $book) + { + return $this->permissionService->enforceEntityRestrictions('shelf', $book->shelves())->get(); + } + /** * Get the child items for a chapter sorted by priority but * with draft items floated to the top. - * @param \BookStack\Entities\Chapter $chapter + * @param Chapter $chapter * @return \Illuminate\Database\Eloquent\Collection|static[] */ public function getChapterChildren(Chapter $chapter) @@ -379,7 +439,7 @@ class EntityRepo /** * Get the next sequential priority for a new child element in the given book. - * @param \BookStack\Entities\Book $book + * @param Book $book * @return int */ public function getNewBookPriority(Book $book) @@ -390,7 +450,7 @@ class EntityRepo /** * Get a new priority for a new page to be added to the given chapter. - * @param \BookStack\Entities\Chapter $chapter + * @param Chapter $chapter * @return int */ public function getNewChapterPriority(Chapter $chapter) @@ -416,31 +476,12 @@ class EntityRepo return $slug; } - /** - * Check if a slug already exists in the database. - * @param string $type - * @param string $slug - * @param bool|integer $currentId - * @param bool|integer $bookId - * @return bool - */ - protected function slugExists($type, $slug, $currentId = false, $bookId = false) - { - $query = $this->entityProvider->get($type)->where('slug', '=', $slug); - if (strtolower($type) === 'page' || strtolower($type) === 'chapter') { - $query = $query->where('book_id', '=', $bookId); - } - if ($currentId) { - $query = $query->where('id', '!=', $currentId); - } - return $query->count() > 0; - } /** * Updates entity restrictions from a request * @param Request $request - * @param \BookStack\Entities\Entity $entity - * @throws \Throwable + * @param Entity $entity + * @throws Throwable */ public function updateEntityPermissionsFromRequest(Request $request, Entity $entity) { @@ -448,70 +489,73 @@ class EntityRepo $entity->permissions()->delete(); if ($request->filled('restrictions')) { - foreach ($request->get('restrictions') as $roleId => $restrictions) { - foreach ($restrictions as $action => $value) { - $entity->permissions()->create([ + $entityPermissionData = collect($request->get('restrictions'))->flatMap(function($restrictions, $roleId) { + return collect($restrictions)->keys()->map(function($action) use ($roleId) { + return [ 'role_id' => $roleId, - 'action' => strtolower($action) - ]); - } - } + 'action' => strtolower($action), + ] ; + }); + }); + + $entity->permissions()->createMany($entityPermissionData); } $entity->save(); - $this->permissionService->buildJointPermissionsForEntity($entity); + $entity->rebuildPermissions(); } - /** * Create a new entity from request input. * Used for books and chapters. * @param string $type * @param array $input - * @param bool|Book $book - * @return \BookStack\Entities\Entity + * @param Book|null $book + * @return Entity */ - public function createFromInput($type, $input = [], $book = false) + public function createFromInput(string $type, array $input = [], Book $book = null) { - $isChapter = strtolower($type) === 'chapter'; $entityModel = $this->entityProvider->get($type)->newInstance($input); - $entityModel->slug = $this->findSuitableSlug($type, $entityModel->name, false, $isChapter ? $book->id : false); $entityModel->created_by = user()->id; $entityModel->updated_by = user()->id; - $isChapter ? $book->chapters()->save($entityModel) : $entityModel->save(); + + if ($book) { + $entityModel->book_id = $book->id; + } + + $entityModel->refreshSlug(); + $entityModel->save(); if (isset($input['tags'])) { $this->tagRepo->saveTagsToEntity($entityModel, $input['tags']); } - $this->permissionService->buildJointPermissionsForEntity($entityModel); + $entityModel->rebuildPermissions(); $this->searchService->indexEntity($entityModel); return $entityModel; } /** * Update entity details from request input. - * Used for books and chapters - * @param string $type - * @param \BookStack\Entities\Entity $entityModel - * @param array $input - * @return \BookStack\Entities\Entity + * Used for shelves, books and chapters. */ - public function updateFromInput($type, Entity $entityModel, $input = []) + public function updateFromInput(Entity $entityModel, array $input): Entity { - if ($entityModel->name !== $input['name']) { - $entityModel->slug = $this->findSuitableSlug($type, $input['name'], $entityModel->id); - } $entityModel->fill($input); $entityModel->updated_by = user()->id; + + if ($entityModel->isDirty('name')) { + $entityModel->refreshSlug(); + } + $entityModel->save(); if (isset($input['tags'])) { $this->tagRepo->saveTagsToEntity($entityModel, $input['tags']); } - $this->permissionService->buildJointPermissionsForEntity($entityModel); + $entityModel->rebuildPermissions(); $this->searchService->indexEntity($entityModel); return $entityModel; } @@ -519,7 +563,7 @@ class EntityRepo /** * Sync the books assigned to a shelf from a comma-separated list * of book IDs. - * @param \BookStack\Entities\Bookshelf $shelf + * @param Bookshelf $shelf * @param string $books */ public function updateShelfBooks(Bookshelf $shelf, string $books) @@ -540,62 +584,24 @@ class EntityRepo /** * Change the book that an entity belongs to. - * @param string $type - * @param integer $newBookId - * @param Entity $entity - * @param bool $rebuildPermissions - * @return \BookStack\Entities\Entity */ - public function changeBook($type, $newBookId, Entity $entity, $rebuildPermissions = false) + public function changeBook(BookChild $bookChild, int $newBookId): Entity { - $entity->book_id = $newBookId; + $bookChild->book_id = $newBookId; + $bookChild->refreshSlug(); + $bookChild->save(); + // Update related activity - foreach ($entity->activity as $activity) { - $activity->book_id = $newBookId; - $activity->save(); - } - $entity->slug = $this->findSuitableSlug($type, $entity->name, $entity->id, $newBookId); - $entity->save(); + $bookChild->activity()->update(['book_id' => $newBookId]); // Update all child pages if a chapter - if (strtolower($type) === 'chapter') { - foreach ($entity->pages as $page) { - $this->changeBook('page', $newBookId, $page, false); + if ($bookChild->isA('chapter')) { + foreach ($bookChild->pages as $page) { + $this->changeBook($page, $newBookId); } } - // Update permissions if applicable - if ($rebuildPermissions) { - $entity->load('book'); - $this->permissionService->buildJointPermissionsForEntity($entity->book); - } - - return $entity; - } - - /** - * Alias method to update the book jointPermissions in the PermissionService. - * @param Book $book - */ - public function buildJointPermissionsForBook(Book $book) - { - $this->permissionService->buildJointPermissionsForEntity($book); - } - - /** - * Format a name as a url slug. - * @param $name - * @return string - */ - protected function nameToSlug($name) - { - $slug = preg_replace('/[\+\/\\\?\@\}\{\.\,\=\[\]\#\&\!\*\'\;\:\$\%]/', '', mb_strtolower($name)); - $slug = preg_replace('/\s{2,}/', ' ', $slug); - $slug = str_replace(' ', '-', $slug); - if ($slug === "") { - $slug = substr(md5(rand(1, 500)), 0, 5); - } - return $slug; + return $bookChild; } /** @@ -661,6 +667,7 @@ class EntityRepo } $doc = new DOMDocument(); + libxml_use_internal_errors(true); $doc->loadHTML(mb_convert_encoding('
'.$matchedPage->html.'', 'HTML-ENTITIES', 'UTF-8')); $matchingElem = $doc->getElementById($splitInclude[1]); if ($matchingElem === null) { @@ -676,6 +683,7 @@ class EntityRepo $innerContent .= $doc->saveHTML($childNode); } } + libxml_clear_errors(); $html = str_replace($matches[0][$index], trim($innerContent), $html); } @@ -689,13 +697,41 @@ class EntityRepo */ protected function escapeScripts(string $html) : string { - $scriptSearchRegex = '/