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