]> BookStack Code Mirror - bookstack/blob - app/Repos/PageRepo.php
4784ad407605e2be2444378df55ef44d66be3953
[bookstack] / app / Repos / PageRepo.php
1 <?php namespace BookStack\Repos;
2
3
4 use Activity;
5 use BookStack\Book;
6 use BookStack\Exceptions\NotFoundException;
7 use Illuminate\Support\Str;
8 use BookStack\Page;
9 use BookStack\PageRevision;
10
11 class PageRepo extends EntityRepo
12 {
13     protected $pageRevision;
14
15     /**
16      * PageRepo constructor.
17      * @param PageRevision $pageRevision
18      */
19     public function __construct(PageRevision $pageRevision)
20     {
21         $this->pageRevision = $pageRevision;
22         parent::__construct();
23     }
24
25     /**
26      * Base query for getting pages, Takes restrictions into account.
27      * @return mixed
28      */
29     private function pageQuery()
30     {
31         return $this->restrictionService->enforcePageRestrictions($this->page, 'view');
32     }
33
34     /**
35      * Get a page via a specific ID.
36      * @param $id
37      * @return mixed
38      */
39     public function getById($id)
40     {
41         return $this->pageQuery()->findOrFail($id);
42     }
43
44     /**
45      * Get a page identified by the given slug.
46      * @param $slug
47      * @param $bookId
48      * @return mixed
49      * @throws NotFoundException
50      */
51     public function getBySlug($slug, $bookId)
52     {
53         $page = $this->pageQuery()->where('slug', '=', $slug)->where('book_id', '=', $bookId)->first();
54         if ($page === null) throw new NotFoundException('Page not found');
55         return $page;
56     }
57
58     /**
59      * Search through page revisions and retrieve
60      * the last page in the current book that
61      * has a slug equal to the one given.
62      * @param $pageSlug
63      * @param $bookSlug
64      * @return null | Page
65      */
66     public function findPageUsingOldSlug($pageSlug, $bookSlug)
67     {
68         $revision = $this->pageRevision->where('slug', '=', $pageSlug)
69             ->whereHas('page', function($query) {
70                 $this->restrictionService->enforcePageRestrictions($query);
71             })
72             ->where('book_slug', '=', $bookSlug)->orderBy('created_at', 'desc')
73             ->with('page')->first();
74         return $revision !== null ? $revision->page : null;
75     }
76
77     /**
78      * Get a new Page instance from the given input.
79      * @param $input
80      * @return Page
81      */
82     public function newFromInput($input)
83     {
84         $page = $this->page->fill($input);
85         return $page;
86     }
87
88     /**
89      * Count the pages with a particular slug within a book.
90      * @param $slug
91      * @param $bookId
92      * @return mixed
93      */
94     public function countBySlug($slug, $bookId)
95     {
96         return $this->page->where('slug', '=', $slug)->where('book_id', '=', $bookId)->count();
97     }
98
99     /**
100      * Save a new page into the system.
101      * Input validation must be done beforehand.
102      * @param array $input
103      * @param Book  $book
104      * @param int   $chapterId
105      * @return Page
106      */
107     public function saveNew(array $input, Book $book, $chapterId = null)
108     {
109         $page = $this->newFromInput($input);
110         $page->slug = $this->findSuitableSlug($page->name, $book->id);
111
112         if ($chapterId) $page->chapter_id = $chapterId;
113
114         $page->html = $this->formatHtml($input['html']);
115         $page->text = strip_tags($page->html);
116         $page->created_by = auth()->user()->id;
117         $page->updated_by = auth()->user()->id;
118
119         $book->pages()->save($page);
120         return $page;
121     }
122
123     /**
124      * Formats a page's html to be tagged correctly
125      * within the system.
126      * @param string $htmlText
127      * @return string
128      */
129     protected function formatHtml($htmlText)
130     {
131         if($htmlText == '') return $htmlText;
132         libxml_use_internal_errors(true);
133         $doc = new \DOMDocument();
134         $doc->loadHTML(mb_convert_encoding($htmlText, 'HTML-ENTITIES', 'UTF-8'));
135
136         $container = $doc->documentElement;
137         $body = $container->childNodes->item(0);
138         $childNodes = $body->childNodes;
139
140         // Ensure no duplicate ids are used
141         $idArray = [];
142
143         foreach ($childNodes as $index => $childNode) {
144             /** @var \DOMElement $childNode */
145             if (get_class($childNode) !== 'DOMElement') continue;
146
147             // Overwrite id if not a BookStack custom id
148             if ($childNode->hasAttribute('id')) {
149                 $id = $childNode->getAttribute('id');
150                 if (strpos($id, 'bkmrk') === 0 && array_search($id, $idArray) === false) {
151                     $idArray[] = $id;
152                     continue;
153                 };
154             }
155
156             // Create an unique id for the element
157             // Uses the content as a basis to ensure output is the same every time
158             // the same content is passed through.
159             $contentId = 'bkmrk-' . substr(strtolower(preg_replace('/\s+/', '-', trim($childNode->nodeValue))), 0, 20);
160             $newId = urlencode($contentId);
161             $loopIndex = 0;
162             while (in_array($newId, $idArray)) {
163                 $newId = urlencode($contentId . '-' . $loopIndex);
164                 $loopIndex++;
165             }
166
167             $childNode->setAttribute('id', $newId);
168             $idArray[] = $newId;
169         }
170
171         // Generate inner html as a string
172         $html = '';
173         foreach ($childNodes as $childNode) {
174             $html .= $doc->saveHTML($childNode);
175         }
176
177         return $html;
178     }
179
180
181     /**
182      * Gets pages by a search term.
183      * Highlights page content for showing in results.
184      * @param string $term
185      * @param array $whereTerms
186      * @param int $count
187      * @param array $paginationAppends
188      * @return mixed
189      */
190     public function getBySearch($term, $whereTerms = [], $count = 20, $paginationAppends = [])
191     {
192         $terms = $this->prepareSearchTerms($term);
193         $pages = $this->restrictionService->enforcePageRestrictions($this->page->fullTextSearchQuery(['name', 'text'], $terms, $whereTerms))
194             ->paginate($count)->appends($paginationAppends);
195
196         // Add highlights to page text.
197         $words = join('|', explode(' ', preg_quote(trim($term), '/')));
198         //lookahead/behind assertions ensures cut between words
199         $s = '\s\x00-/:-@\[-`{-~'; //character set for start/end of words
200
201         foreach ($pages as $page) {
202             preg_match_all('#(?<=[' . $s . ']).{1,30}((' . $words . ').{1,30})+(?=[' . $s . '])#uis', $page->text, $matches, PREG_SET_ORDER);
203             //delimiter between occurrences
204             $results = [];
205             foreach ($matches as $line) {
206                 $results[] = htmlspecialchars($line[0], 0, 'UTF-8');
207             }
208             $matchLimit = 6;
209             if (count($results) > $matchLimit) {
210                 $results = array_slice($results, 0, $matchLimit);
211             }
212             $result = join('... ', $results);
213
214             //highlight
215             $result = preg_replace('#' . $words . '#iu', "<span class=\"highlight\">\$0</span>", $result);
216             if (strlen($result) < 5) {
217                 $result = $page->getExcerpt(80);
218             }
219             $page->searchSnippet = $result;
220         }
221         return $pages;
222     }
223
224     /**
225      * Search for image usage.
226      * @param $imageString
227      * @return mixed
228      */
229     public function searchForImage($imageString)
230     {
231         $pages = $this->pageQuery()->where('html', 'like', '%' . $imageString . '%')->get();
232         foreach ($pages as $page) {
233             $page->url = $page->getUrl();
234             $page->html = '';
235             $page->text = '';
236         }
237         return count($pages) > 0 ? $pages : false;
238     }
239
240     /**
241      * Updates a page with any fillable data and saves it into the database.
242      * @param Page   $page
243      * @param int    $book_id
244      * @param string $input
245      * @return Page
246      */
247     public function updatePage(Page $page, $book_id, $input)
248     {
249         // Save a revision before updating
250         if ($page->html !== $input['html'] || $page->name !== $input['name']) {
251             $this->saveRevision($page);
252         }
253
254         // Prevent slug being updated if no name change
255         if ($page->name !== $input['name']) {
256             $page->slug = $this->findSuitableSlug($input['name'], $book_id, $page->id);
257         }
258
259         // Update with new details
260         $page->fill($input);
261         $page->html = $this->formatHtml($input['html']);
262         $page->text = strip_tags($page->html);
263         $page->updated_by = auth()->user()->id;
264         $page->save();
265         return $page;
266     }
267
268     /**
269      * Restores a revision's content back into a page.
270      * @param Page $page
271      * @param Book $book
272      * @param  int $revisionId
273      * @return Page
274      */
275     public function restoreRevision(Page $page, Book $book, $revisionId)
276     {
277         $this->saveRevision($page);
278         $revision = $this->getRevisionById($revisionId);
279         $page->fill($revision->toArray());
280         $page->slug = $this->findSuitableSlug($page->name, $book->id, $page->id);
281         $page->text = strip_tags($page->html);
282         $page->updated_by = auth()->user()->id;
283         $page->save();
284         return $page;
285     }
286
287     /**
288      * Saves a page revision into the system.
289      * @param Page $page
290      * @return $this
291      */
292     public function saveRevision(Page $page)
293     {
294         $revision = $this->pageRevision->fill($page->toArray());
295         $revision->page_id = $page->id;
296         $revision->slug = $page->slug;
297         $revision->book_slug = $page->book->slug;
298         $revision->created_by = auth()->user()->id;
299         $revision->created_at = $page->updated_at;
300         $revision->save();
301         // Clear old revisions
302         if ($this->pageRevision->where('page_id', '=', $page->id)->count() > 50) {
303             $this->pageRevision->where('page_id', '=', $page->id)
304                 ->orderBy('created_at', 'desc')->skip(50)->take(5)->delete();
305         }
306         return $revision;
307     }
308
309     /**
310      * Gets a single revision via it's id.
311      * @param $id
312      * @return mixed
313      */
314     public function getRevisionById($id)
315     {
316         return $this->pageRevision->findOrFail($id);
317     }
318
319     /**
320      * Checks if a slug exists within a book already.
321      * @param            $slug
322      * @param            $bookId
323      * @param bool|false $currentId
324      * @return bool
325      */
326     public function doesSlugExist($slug, $bookId, $currentId = false)
327     {
328         $query = $this->page->where('slug', '=', $slug)->where('book_id', '=', $bookId);
329         if ($currentId) $query = $query->where('id', '!=', $currentId);
330         return $query->count() > 0;
331     }
332
333     /**
334      * Changes the related book for the specified page.
335      * Changes the book id of any relations to the page that store the book id.
336      * @param int  $bookId
337      * @param Page $page
338      * @return Page
339      */
340     public function changeBook($bookId, Page $page)
341     {
342         $page->book_id = $bookId;
343         foreach ($page->activity as $activity) {
344             $activity->book_id = $bookId;
345             $activity->save();
346         }
347         $page->slug = $this->findSuitableSlug($page->name, $bookId, $page->id);
348         $page->save();
349         return $page;
350     }
351
352     /**
353      * Gets a suitable slug for the resource
354      * @param            $name
355      * @param            $bookId
356      * @param bool|false $currentId
357      * @return string
358      */
359     public function findSuitableSlug($name, $bookId, $currentId = false)
360     {
361         $slug = Str::slug($name);
362         while ($this->doesSlugExist($slug, $bookId, $currentId)) {
363             $slug .= '-' . substr(md5(rand(1, 500)), 0, 3);
364         }
365         return $slug;
366     }
367
368     /**
369      * Destroy a given page along with its dependencies.
370      * @param $page
371      */
372     public function destroy($page)
373     {
374         Activity::removeEntity($page);
375         $page->views()->delete();
376         $page->revisions()->delete();
377         $page->restrictions()->delete();
378         $page->delete();
379     }
380
381     /**
382      * Get the latest pages added to the system.
383      * @param $count
384      */
385     public function getRecentlyCreatedPaginated($count = 20)
386     {
387         return $this->pageQuery()->orderBy('created_at', 'desc')->paginate($count);
388     }
389
390     /**
391      * Get the latest pages added to the system.
392      * @param $count
393      */
394     public function getRecentlyUpdatedPaginated($count = 20)
395     {
396         return $this->pageQuery()->orderBy('updated_at', 'desc')->paginate($count);
397     }
398
399 }