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