]> BookStack Code Mirror - bookstack/blobdiff - app/Entities/Repos/EntityRepo.php
Update maintenance.php
[bookstack] / app / Entities / Repos / EntityRepo.php
index 13a9d9a9c33ec3097a71560efb1df70e8bd97d73..a0934530f33aefa6bafc79dbe719717156e279f0 100644 (file)
@@ -1,5 +1,6 @@
 <?php namespace BookStack\Entities\Repos;
 
+use Activity;
 use BookStack\Actions\TagRepo;
 use BookStack\Actions\ViewService;
 use BookStack\Auth\Permissions\PermissionService;
@@ -15,9 +16,13 @@ use BookStack\Exceptions\NotFoundException;
 use BookStack\Exceptions\NotifyException;
 use BookStack\Uploads\AttachmentService;
 use DOMDocument;
+use DOMNode;
+use DOMXPath;
+use Illuminate\Contracts\Pagination\LengthAwarePaginator;
 use Illuminate\Database\Eloquent\Builder;
 use Illuminate\Http\Request;
 use Illuminate\Support\Collection;
+use Throwable;
 
 class EntityRepo
 {
@@ -102,7 +107,7 @@ class EntityRepo
      * @param integer $id
      * @param bool $allowDrafts
      * @param bool $ignorePermissions
-     * @return \BookStack\Entities\Entity
+     * @return Entity
      */
     public function getById($type, $id, $allowDrafts = false, $ignorePermissions = false)
     {
@@ -120,7 +125,7 @@ class EntityRepo
      * @param []int $ids
      * @param bool $allowDrafts
      * @param bool $ignorePermissions
-     * @return \Illuminate\Database\Eloquent\Builder[]|\Illuminate\Database\Eloquent\Collection|Collection
+     * @return Builder[]|\Illuminate\Database\Eloquent\Collection|Collection
      */
     public function getManyById($type, $ids, $allowDrafts = false, $ignorePermissions = false)
     {
@@ -138,7 +143,7 @@ class EntityRepo
      * @param string $type
      * @param string $slug
      * @param string|bool $bookSlug
-     * @return \BookStack\Entities\Entity
+     * @return Entity
      * @throws NotFoundException
      */
     public function getBySlug($type, $slug, $bookSlug = false)
@@ -182,15 +187,26 @@ class EntityRepo
      * @param int $count
      * @param string $sort
      * @param string $order
-     * @return \Illuminate\Contracts\Pagination\LengthAwarePaginator
+     * @param null|callable $queryAddition
+     * @return LengthAwarePaginator
      */
-    public function getAllPaginated($type, int $count = 10, string $sort = 'name', string $order = 'asc')
+    public function getAllPaginated($type, int $count = 10, string $sort = 'name', string $order = 'asc', $queryAddition = null)
     {
         $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';
@@ -282,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);
     }
 
     /**
@@ -322,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)
@@ -330,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
@@ -384,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)
@@ -396,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)
@@ -407,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)
@@ -456,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)
     {
@@ -487,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)
     {
@@ -511,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 = [])
     {
@@ -536,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)
@@ -555,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)
     {
@@ -616,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 "{{@<page_id>#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);
@@ -642,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('<body>'.$matchedPage->html.'</body>', '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 = '';
@@ -669,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.*?>.*?<\/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;
     }
 
@@ -702,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 = '';
@@ -713,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)
     {
@@ -724,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)
     {
@@ -742,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)
     {
@@ -761,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)
     {
@@ -784,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();
@@ -801,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)
     {