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