]> BookStack Code Mirror - bookstack/blob - app/Repos/PageRepo.php
Prevent duplicate slugs on sort
[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         libxml_use_internal_errors(true);
124         $doc = new \DOMDocument();
125         $doc->loadHTML($htmlText);
126
127         $container = $doc->documentElement;
128         $body = $container->childNodes->item(0);
129         $childNodes = $body->childNodes;
130
131         // Ensure no duplicate ids are used
132         $lastId = false;
133         $idArray = [];
134
135         foreach ($childNodes as $index => $childNode) {
136             /** @var \DOMElement $childNode */
137             if (get_class($childNode) !== 'DOMElement') continue;
138
139             // Overwrite id if not a bookstack custom id
140             if ($childNode->hasAttribute('id')) {
141                 $id = $childNode->getAttribute('id');
142                 if (strpos($id, 'bkmrk') === 0 && array_search($id, $idArray) === false) {
143                     $idArray[] = $id;
144                     continue;
145                 };
146             }
147
148             // Create an unique id for the element
149             do {
150                 $id = 'bkmrk-' . substr(uniqid(), -5);
151             } while ($id == $lastId);
152             $lastId = $id;
153
154             $childNode->setAttribute('id', $id);
155             $idArray[] = $id;
156         }
157
158         // Generate inner html as a string
159         $html = '';
160         foreach ($childNodes as $childNode) {
161             $html .= $doc->saveHTML($childNode);
162         }
163
164         return $html;
165     }
166
167
168     /**
169      * Gets pages by a search term.
170      * Highlights page content for showing in results.
171      * @param string      $term
172      * @param array $whereTerms
173      * @return mixed
174      */
175     public function getBySearch($term, $whereTerms = [])
176     {
177         $terms = explode(' ', preg_quote(trim($term)));
178         $pages = $this->page->fullTextSearch(['name', 'text'], $terms, $whereTerms);
179
180         // Add highlights to page text.
181         $words = join('|', $terms);
182         //lookahead/behind assertions ensures cut between words
183         $s = '\s\x00-/:-@\[-`{-~'; //character set for start/end of words
184
185         foreach ($pages as $page) {
186             preg_match_all('#(?<=[' . $s . ']).{1,30}((' . $words . ').{1,30})+(?=[' . $s . '])#uis', $page->text, $matches, PREG_SET_ORDER);
187             //delimiter between occurrences
188             $results = [];
189             foreach ($matches as $line) {
190                 $results[] = htmlspecialchars($line[0], 0, 'UTF-8');
191             }
192             $matchLimit = 6;
193             if (count($results) > $matchLimit) {
194                 $results = array_slice($results, 0, $matchLimit);
195             }
196             $result = join('... ', $results);
197
198             //highlight
199             $result = preg_replace('#' . $words . '#iu', "<span class=\"highlight\">\$0</span>", $result);
200             if (strlen($result) < 5) {
201                 $result = $page->getExcerpt(80);
202             }
203             $page->searchSnippet = $result;
204         }
205         return $pages;
206     }
207
208     /**
209      * Search for image usage.
210      * @param $imageString
211      * @return mixed
212      */
213     public function searchForImage($imageString)
214     {
215         $pages = $this->page->where('html', 'like', '%' . $imageString . '%')->get();
216         foreach ($pages as $page) {
217             $page->url = $page->getUrl();
218             $page->html = '';
219             $page->text = '';
220         }
221         return count($pages) > 0 ? $pages : false;
222     }
223
224     /**
225      * Updates a page with any fillable data and saves it into the database.
226      * @param Page   $page
227      * @param int    $book_id
228      * @param string $input
229      * @return Page
230      */
231     public function updatePage(Page $page, $book_id, $input)
232     {
233         // Save a revision before updating
234         if ($page->html !== $input['html'] || $page->name !== $input['name']) {
235             $this->saveRevision($page);
236         }
237
238         // Update with new details
239         $page->fill($input);
240         $page->slug = $this->findSuitableSlug($page->name, $book_id, $page->id);
241         $page->html = $this->formatHtml($input['html']);
242         $page->text = strip_tags($page->html);
243         $page->updated_by = auth()->user()->id;
244         $page->save();
245         return $page;
246     }
247
248     /**
249      * Restores a revision's content back into a page.
250      * @param Page $page
251      * @param Book $book
252      * @param  int $revisionId
253      * @return Page
254      */
255     public function restoreRevision(Page $page, Book $book, $revisionId)
256     {
257         $this->saveRevision($page);
258         $revision = $this->getRevisionById($revisionId);
259         $page->fill($revision->toArray());
260         $page->slug = $this->findSuitableSlug($page->name, $book->id, $page->id);
261         $page->text = strip_tags($page->html);
262         $page->updated_by = auth()->user()->id;
263         $page->save();
264         return $page;
265     }
266
267     /**
268      * Saves a page revision into the system.
269      * @param Page $page
270      * @return $this
271      */
272     private function saveRevision(Page $page)
273     {
274         $revision = $this->pageRevision->fill($page->toArray());
275         $revision->page_id = $page->id;
276         $revision->created_by = auth()->user()->id;
277         $revision->created_at = $page->updated_at;
278         $revision->save();
279         // Clear old revisions
280         if ($this->pageRevision->where('page_id', '=', $page->id)->count() > 50) {
281             $this->pageRevision->where('page_id', '=', $page->id)
282                 ->orderBy('created_at', 'desc')->skip(50)->take(5)->delete();
283         }
284         return $revision;
285     }
286
287     /**
288      * Gets a single revision via it's id.
289      * @param $id
290      * @return mixed
291      */
292     public function getRevisionById($id)
293     {
294         return $this->pageRevision->findOrFail($id);
295     }
296
297     /**
298      * Checks if a slug exists within a book already.
299      * @param            $slug
300      * @param            $bookId
301      * @param bool|false $currentId
302      * @return bool
303      */
304     public function doesSlugExist($slug, $bookId, $currentId = false)
305     {
306         $query = $this->page->where('slug', '=', $slug)->where('book_id', '=', $bookId);
307         if ($currentId) $query = $query->where('id', '!=', $currentId);
308         return $query->count() > 0;
309     }
310
311     /**
312      * Changes the related book for the specified page.
313      * Changes the book id of any relations to the page that store the book id.
314      * @param int  $bookId
315      * @param Page $page
316      * @return Page
317      */
318     public function changeBook($bookId, Page $page)
319     {
320         $page->book_id = $bookId;
321         foreach ($page->activity as $activity) {
322             $activity->book_id = $bookId;
323             $activity->save();
324         }
325         $page->slug = $this->findSuitableSlug($page->name, $bookId, $page->id);
326         $page->save();
327         return $page;
328     }
329
330     /**
331      * Gets a suitable slug for the resource
332      * @param            $name
333      * @param            $bookId
334      * @param bool|false $currentId
335      * @return string
336      */
337     public function findSuitableSlug($name, $bookId, $currentId = false)
338     {
339         $slug = Str::slug($name);
340         while ($this->doesSlugExist($slug, $bookId, $currentId)) {
341             $slug .= '-' . substr(md5(rand(1, 500)), 0, 3);
342         }
343         return $slug;
344     }
345
346     /**
347      * Destroy a given page along with its dependencies.
348      * @param $page
349      */
350     public function destroy($page)
351     {
352         Activity::removeEntity($page);
353         $page->views()->delete();
354         $page->revisions()->delete();
355         $page->delete();
356     }
357
358
359 }