X-Git-Url: http://source.bookstackapp.com/bookstack/blobdiff_plain/8c190324ac5f8debdb5ee643a38a36667d977bfd..refs/pull/1688/head:/app/Entities/Repos/EntityRepo.php diff --git a/app/Entities/Repos/EntityRepo.php b/app/Entities/Repos/EntityRepo.php index 1cff46da1..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; } @@ -183,7 +193,7 @@ class EntityRepo * @param string $sort * @param string $order * @param null|callable $queryAddition - * @return \Illuminate\Contracts\Pagination\LengthAwarePaginator + * @return LengthAwarePaginator */ public function getAllPaginated($type, int $count = 10, string $sort = 'name', string $order = 'asc', $queryAddition = null) { @@ -332,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) @@ -356,7 +366,7 @@ class EntityRepo * 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 @@ -403,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) @@ -418,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) @@ -429,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) @@ -455,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) { @@ -487,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; } @@ -558,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) @@ -577,79 +582,26 @@ class EntityRepo $shelf->books()->sync($syncData); } - /** - * Append a Book to a BookShelf. - * @param Bookshelf $shelf - * @param Book $book - */ - public function appendBookToShelf(Bookshelf $shelf, Book $book) - { - if ($shelf->contains($book)) { - return; - } - - $maxOrder = $shelf->books()->max('order'); - $shelf->books()->attach($book->id, ['order' => $maxOrder + 1]); - } - /** * 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; } /** @@ -745,13 +697,41 @@ class EntityRepo */ protected function escapeScripts(string $html) : string { - $scriptSearchRegex = '/.*?<\/script>/ms'; - $matches = []; - preg_match_all($scriptSearchRegex, $html, $matches); + if ($html == '') { + return $html; + } + + libxml_use_internal_errors(true); + $doc = new DOMDocument(); + $doc->loadHTML(mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8')); + $xPath = new DOMXPath($doc); + + // Remove standard script tags + $scriptElems = $xPath->query('//script'); + foreach ($scriptElems as $scriptElem) { + $scriptElem->parentNode->removeChild($scriptElem); + } + + // Remove data or JavaScript iFrames + $badIframes = $xPath->query('//*[contains(@src, \'data:\')] | //*[contains(@src, \'javascript:\')] | //*[@srcdoc]'); + foreach ($badIframes as $badIframe) { + $badIframe->parentNode->removeChild($badIframe); + } - foreach ($matches[0] as $match) { - $html = str_replace($match, htmlentities($match), $html); + // Remove 'on*' attributes + $onAttributes = $xPath->query('//@*[starts-with(name(), \'on\')]'); + foreach ($onAttributes as $attr) { + /** @var \DOMAttr $attr*/ + $attrName = $attr->nodeName; + $attr->parentNode->removeAttribute($attrName); } + + $html = ''; + $topElems = $doc->documentElement->childNodes->item(0)->childNodes; + foreach ($topElems as $child) { + $html .= $doc->saveHTML($child); + } + return $html; } @@ -773,8 +753,8 @@ class EntityRepo /** * Destroy a bookshelf instance - * @param \BookStack\Entities\Bookshelf $shelf - * @throws \Throwable + * @param Bookshelf $shelf + * @throws Throwable */ public function destroyBookshelf(Bookshelf $shelf) { @@ -782,28 +762,10 @@ class EntityRepo $shelf->delete(); } - /** - * Destroy the provided book and all its child entities. - * @param \BookStack\Entities\Book $book - * @throws NotifyException - * @throws \Throwable - */ - public function destroyBook(Book $book) - { - foreach ($book->pages as $page) { - $this->destroyPage($page); - } - foreach ($book->chapters as $chapter) { - $this->destroyChapter($chapter); - } - $this->destroyEntityCommonRelations($book); - $book->delete(); - } - /** * Destroy a chapter and its relations. - * @param \BookStack\Entities\Chapter $chapter - * @throws \Throwable + * @param Chapter $chapter + * @throws Throwable */ public function destroyChapter(Chapter $chapter) { @@ -821,14 +783,17 @@ class EntityRepo * Destroy a given page along with its dependencies. * @param Page $page * @throws NotifyException - * @throws \Throwable + * @throws Throwable */ public function destroyPage(Page $page) { - // Check if set as custom homepage + // Check if set as custom homepage & remove setting if not used or throw error if active $customHome = setting('app-homepage', '0:'); if (intval($page->id) === intval(explode(':', $customHome)[0])) { - throw new NotifyException(trans('errors.page_custom_home_deletion'), $page->getUrl()); + if (setting('app-homepage-type') === 'page') { + throw new NotifyException(trans('errors.page_custom_home_deletion'), $page->getUrl()); + } + setting()->remove('app-homepage'); } $this->destroyEntityCommonRelations($page); @@ -844,12 +809,12 @@ class EntityRepo /** * Destroy or handle the common relations connected to an entity. - * @param \BookStack\Entities\Entity $entity - * @throws \Throwable + * @param Entity $entity + * @throws Throwable */ protected function destroyEntityCommonRelations(Entity $entity) { - \Activity::removeEntity($entity); + Activity::removeEntity($entity); $entity->views()->delete(); $entity->permissions()->delete(); $entity->tags()->delete(); @@ -861,9 +826,9 @@ class EntityRepo /** * Copy the permissions of a bookshelf to all child books. * Returns the number of books that had permissions updated. - * @param \BookStack\Entities\Bookshelf $bookshelf + * @param Bookshelf $bookshelf * @return int - * @throws \Throwable + * @throws Throwable */ public function copyBookshelfPermissions(Bookshelf $bookshelf) { @@ -871,6 +836,7 @@ class EntityRepo $shelfBooks = $bookshelf->books()->get(); $updatedBookCount = 0; + /** @var Book $book */ foreach ($shelfBooks as $book) { if (!userCan('restrictions-manage', $book)) { continue; @@ -879,7 +845,7 @@ class EntityRepo $book->restricted = $bookshelf->restricted; $book->permissions()->createMany($shelfPermissions); $book->save(); - $this->permissionService->buildJointPermissionsForEntity($book); + $book->rebuildPermissions(); $updatedBookCount++; }