X-Git-Url: http://source.bookstackapp.com/bookstack/blobdiff_plain/89de328439729a70b7faafc72c38c08489a436a3..refs/pull/1462/head:/app/Entities/Repos/EntityRepo.php diff --git a/app/Entities/Repos/EntityRepo.php b/app/Entities/Repos/EntityRepo.php index 44fb9ad12..a0934530f 100644 --- a/app/Entities/Repos/EntityRepo.php +++ b/app/Entities/Repos/EntityRepo.php @@ -1,5 +1,6 @@ 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 +298,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 +337,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 +345,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 @@ -367,7 +411,7 @@ class EntityRepo /** * 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 +423,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 +434,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) @@ -439,8 +483,8 @@ class EntityRepo /** * 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) { @@ -470,7 +514,7 @@ class EntityRepo * @param string $type * @param array $input * @param bool|Book $book - * @return \BookStack\Entities\Entity + * @return Entity */ public function createFromInput($type, $input = [], $book = false) { @@ -494,9 +538,9 @@ class EntityRepo * Update entity details from request input. * Used for books and chapters * @param string $type - * @param \BookStack\Entities\Entity $entityModel + * @param Entity $entityModel * @param array $input - * @return \BookStack\Entities\Entity + * @return Entity */ public function updateFromInput($type, Entity $entityModel, $input = []) { @@ -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) @@ -538,13 +582,28 @@ 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 + * @return Entity */ public function changeBook($type, $newBookId, Entity $entity, $rebuildPermissions = false) { @@ -599,24 +658,48 @@ class EntityRepo } /** - * Render the page for viewing, Parsing and performing features such as page transclusion. + * Render the page for viewing * @param Page $page - * @param bool $ignorePermissions - * @return mixed|string + * @param bool $blankIncludes + * @return string */ - public function renderPage(Page $page, $ignorePermissions = false) + public function renderPage(Page $page, bool $blankIncludes = false) : string { $content = $page->html; + if (!config('app.allow_content_scripts')) { $content = $this->escapeScripts($content); } - $matches = []; - preg_match_all("/{{@\s?([0-9].*?)}}/", $content, $matches); - if (count($matches[0]) === 0) { - return $content; + if ($blankIncludes) { + $content = $this->blankPageIncludes($content); + } else { + $content = $this->parsePageIncludes($content); } + return $content; + } + + /** + * Remove any page include tags within the given HTML. + * @param string $html + * @return string + */ + protected function blankPageIncludes(string $html) : string + { + return preg_replace("/{{@\s?([0-9].*?)}}/", '', $html); + } + + /** + * Parse any include tags "{{@#section}}" to be part of the page. + * @param string $html + * @return mixed|string + */ + protected function parsePageIncludes(string $html) : string + { + $matches = []; + preg_match_all("/{{@\s?([0-9].*?)}}/", $html, $matches); + $topLevelTags = ['table', 'ul', 'ol']; foreach ($matches[1] as $index => $includeId) { $splitInclude = explode('#', $includeId, 2); @@ -625,22 +708,23 @@ class EntityRepo continue; } - $matchedPage = $this->getById('page', $pageId, false, $ignorePermissions); + $matchedPage = $this->getById('page', $pageId); if ($matchedPage === null) { - $content = str_replace($matches[0][$index], '', $content); + $html = str_replace($matches[0][$index], '', $html); continue; } if (count($splitInclude) === 1) { - $content = str_replace($matches[0][$index], $matchedPage->html, $content); + $html = str_replace($matches[0][$index], $matchedPage->html, $html); continue; } $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) { - $content = str_replace($matches[0][$index], '', $content); + $html = str_replace($matches[0][$index], '', $html); continue; } $innerContent = ''; @@ -652,29 +736,49 @@ class EntityRepo $innerContent .= $doc->saveHTML($childNode); } } - $content = str_replace($matches[0][$index], trim($innerContent), $content); + libxml_clear_errors(); + $html = str_replace($matches[0][$index], trim($innerContent), $html); } - return $content; + return $html; } /** * Escape script tags within HTML content. * @param string $html - * @return mixed + * @return string */ - protected function escapeScripts(string $html) + protected function escapeScripts(string $html) : string { - $scriptSearchRegex = '/.*?<\/script>/ms'; - $matches = []; - preg_match_all($scriptSearchRegex, $html, $matches); - if (count($matches) === 0) { + if ($html == '') { return $html; } - foreach ($matches[0] as $match) { - $html = str_replace($match, htmlentities($match), $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('//body//*//script'); + foreach ($scriptElems as $scriptElem) { + $scriptElem->parentNode->removeChild($scriptElem); + } + + // Remove 'on*' attributes + $onAttributes = $xPath->query('//body//*/@*[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; } @@ -685,7 +789,7 @@ class EntityRepo */ public function searchForImage($imageString) { - $pages = $this->entityQuery('page')->where('html', 'like', '%' . $imageString . '%')->get(); + $pages = $this->entityQuery('page')->where('html', 'like', '%' . $imageString . '%')->get(['id', 'name', 'slug', 'book_id']); foreach ($pages as $page) { $page->url = $page->getUrl(); $page->html = ''; @@ -696,8 +800,8 @@ class EntityRepo /** * Destroy a bookshelf instance - * @param \BookStack\Entities\Bookshelf $shelf - * @throws \Throwable + * @param Bookshelf $shelf + * @throws Throwable */ public function destroyBookshelf(Bookshelf $shelf) { @@ -707,9 +811,9 @@ class EntityRepo /** * Destroy the provided book and all its child entities. - * @param \BookStack\Entities\Book $book + * @param Book $book * @throws NotifyException - * @throws \Throwable + * @throws Throwable */ public function destroyBook(Book $book) { @@ -725,8 +829,8 @@ class EntityRepo /** * Destroy a chapter and its relations. - * @param \BookStack\Entities\Chapter $chapter - * @throws \Throwable + * @param Chapter $chapter + * @throws Throwable */ public function destroyChapter(Chapter $chapter) { @@ -744,7 +848,7 @@ class EntityRepo * Destroy a given page along with its dependencies. * @param Page $page * @throws NotifyException - * @throws \Throwable + * @throws Throwable */ public function destroyPage(Page $page) { @@ -767,12 +871,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(); @@ -784,9 +888,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) {