]> BookStack Code Mirror - bookstack/blob - app/Repos/EntityRepo.php
Started refactor to merge entity repos
[bookstack] / app / Repos / EntityRepo.php
1 <?php namespace BookStack\Repos;
2
3 use BookStack\Book;
4 use BookStack\Chapter;
5 use BookStack\Entity;
6 use BookStack\Exceptions\NotFoundException;
7 use BookStack\Page;
8 use BookStack\Services\PermissionService;
9 use BookStack\Services\ViewService;
10 use Illuminate\Database\Eloquent\Builder;
11 use Illuminate\Support\Collection;
12
13 class EntityRepo
14 {
15
16     /**
17      * @var Book $book
18      */
19     public $book;
20
21     /**
22      * @var Chapter
23      */
24     public $chapter;
25
26     /**
27      * @var Page
28      */
29     public $page;
30
31     /**
32      * Base entity instances keyed by type
33      * @var []Entity
34      */
35     protected $entities;
36
37     /**
38      * @var PermissionService
39      */
40     protected $permissionService;
41
42     /**
43      * @var ViewService
44      */
45     protected $viewService;
46
47     /**
48      * Acceptable operators to be used in a query
49      * @var array
50      */
51     protected $queryOperators = ['<=', '>=', '=', '<', '>', 'like', '!='];
52
53     /**
54      * EntityService constructor.
55      */
56     public function __construct()
57     {
58         // TODO - Redo this to come via injection
59         $this->book = app(Book::class);
60         $this->chapter = app(Chapter::class);
61         $this->page = app(Page::class);
62         $this->entities = [
63             'page' => $this->page,
64             'chapter' => $this->chapter,
65             'book' => $this->book
66         ];
67         $this->viewService = app(ViewService::class);
68         $this->permissionService = app(PermissionService::class);
69     }
70
71     /**
72      * Get an entity instance via type.
73      * @param $type
74      * @return Entity
75      */
76     protected function getEntity($type)
77     {
78         return $this->entities[strtolower($type)];
79     }
80
81     /**
82      * Base query for searching entities via permission system
83      * @param string $type
84      * @param bool $allowDrafts
85      * @return \Illuminate\Database\Query\Builder
86      */
87     protected function entityQuery($type, $allowDrafts = false)
88     {
89         $q = $this->permissionService->enforceEntityRestrictions($type, $this->getEntity($type), 'view');
90         if (strtolower($type) === 'page' && !$allowDrafts) {
91             $q = $q->where('draft', '=', false);
92         }
93         return $q;
94     }
95
96     /**
97      * Check if an entity with the given id exists.
98      * @param $type
99      * @param $id
100      * @return bool
101      */
102     public function exists($type, $id)
103     {
104         return $this->entityQuery($type)->where('id', '=', $id)->exists();
105     }
106
107     /**
108      * Get an entity by ID
109      * @param string $type
110      * @param integer $id
111      * @param bool $allowDrafts
112      * @return Entity
113      */
114     public function getById($type, $id, $allowDrafts = false)
115     {
116         return $this->entityQuery($type, $allowDrafts)->findOrFail($id);
117     }
118
119     /**
120      * Get an entity by its url slug.
121      * @param string $type
122      * @param string $slug
123      * @param string|bool $bookSlug
124      * @return Entity
125      * @throws NotFoundException
126      */
127     public function getBySlug($type, $slug, $bookSlug = false)
128     {
129         $q = $this->entityQuery($type)->where('slug', '=', $slug);
130         if (strtolower($type) === 'chapter' || strtolower($type) === 'page') {
131             $q = $q->where('book_id', '=', function($query) use ($bookSlug) {
132                 $query->select('id')
133                     ->from($this->book->getTable())
134                     ->where('slug', '=', $bookSlug)->limit(1);
135             });
136         }
137         $entity = $q->first();
138         if ($entity === null) throw new NotFoundException(trans('errors.' . strtolower($type) . '_not_found'));
139         return $entity;
140     }
141
142     /**
143      * Get all entities of a type limited by count unless count if false.
144      * @param string $type
145      * @param integer|bool $count
146      * @return Collection
147      */
148     public function getAll($type, $count = 20)
149     {
150         $q = $this->entityQuery($type)->orderBy('name', 'asc');
151         if ($count !== false) $q = $q->take($count);
152         return $q->get();
153     }
154
155     /**
156      * Get all entities in a paginated format
157      * @param $type
158      * @param int $count
159      * @return \Illuminate\Contracts\Pagination\LengthAwarePaginator
160      */
161     public function getAllPaginated($type, $count = 10)
162     {
163         return $this->entityQuery($type)->orderBy('name', 'asc')->paginate($count);
164     }
165
166     /**
167      * Get the most recently created entities of the given type.
168      * @param string $type
169      * @param int $count
170      * @param int $page
171      * @param bool|callable $additionalQuery
172      */
173     public function getRecentlyCreated($type, $count = 20, $page = 0, $additionalQuery = false)
174     {
175         $query = $this->permissionService->enforceEntityRestrictions($type, $this->getEntity($type))
176             ->orderBy('created_at', 'desc');
177         if (strtolower($type) === 'page') $query = $query->where('draft', '=', false);
178         if ($additionalQuery !== false && is_callable($additionalQuery)) {
179             $additionalQuery($query);
180         }
181         return $query->skip($page * $count)->take($count)->get();
182     }
183
184     /**
185      * Get the most recently updated entities of the given type.
186      * @param string $type
187      * @param int $count
188      * @param int $page
189      * @param bool|callable $additionalQuery
190      */
191     public function getRecentlyUpdated($type, $count = 20, $page = 0, $additionalQuery = false)
192     {
193         $query = $this->permissionService->enforceEntityRestrictions($type, $this->getEntity($type))
194             ->orderBy('updated_at', 'desc');
195         if (strtolower($type) === 'page') $query = $query->where('draft', '=', false);
196         if ($additionalQuery !== false && is_callable($additionalQuery)) {
197             $additionalQuery($query);
198         }
199         return $query->skip($page * $count)->take($count)->get();
200     }
201
202     /**
203      * Get the most recently viewed entities.
204      * @param string|bool $type
205      * @param int $count
206      * @param int $page
207      * @return mixed
208      */
209     public function getRecentlyViewed($type, $count = 10, $page = 0)
210     {
211         $filter = is_bool($type) ? false : $this->getEntity($type);
212         return $this->viewService->getUserRecentlyViewed($count, $page, $filter);
213     }
214
215     /**
216      * Get the most popular entities base on all views.
217      * @param string|bool $type
218      * @param int $count
219      * @param int $page
220      * @return mixed
221      */
222     public function getPopular($type, $count = 10, $page = 0)
223     {
224         $filter = is_bool($type) ? false : $this->getEntity($type);
225         return $this->viewService->getPopular($count, $page, $filter);
226     }
227
228     /**
229      * Get draft pages owned by the current user.
230      * @param int $count
231      * @param int $page
232      */
233     public function getUserDraftPages($count = 20, $page = 0)
234     {
235         return $this->page->where('draft', '=', true)
236             ->where('created_by', '=', user()->id)
237             ->orderBy('updated_at', 'desc')
238             ->skip($count * $page)->take($count)->get();
239     }
240
241     /**
242      * Updates entity restrictions from a request
243      * @param $request
244      * @param Entity $entity
245      */
246     public function updateEntityPermissionsFromRequest($request, Entity $entity)
247     {
248         $entity->restricted = $request->has('restricted') && $request->get('restricted') === 'true';
249         $entity->permissions()->delete();
250         if ($request->has('restrictions')) {
251             foreach ($request->get('restrictions') as $roleId => $restrictions) {
252                 foreach ($restrictions as $action => $value) {
253                     $entity->permissions()->create([
254                         'role_id' => $roleId,
255                         'action'  => strtolower($action)
256                     ]);
257                 }
258             }
259         }
260         $entity->save();
261         $this->permissionService->buildJointPermissionsForEntity($entity);
262     }
263
264     /**
265      * Prepare a string of search terms by turning
266      * it into an array of terms.
267      * Keeps quoted terms together.
268      * @param $termString
269      * @return array
270      */
271     public function prepareSearchTerms($termString)
272     {
273         $termString = $this->cleanSearchTermString($termString);
274         preg_match_all('/(".*?")/', $termString, $matches);
275         $terms = [];
276         if (count($matches[1]) > 0) {
277             foreach ($matches[1] as $match) {
278                 $terms[] = $match;
279             }
280             $termString = trim(preg_replace('/"(.*?)"/', '', $termString));
281         }
282         if (!empty($termString)) $terms = array_merge($terms, explode(' ', $termString));
283         return $terms;
284     }
285
286     /**
287      * Removes any special search notation that should not
288      * be used in a full-text search.
289      * @param $termString
290      * @return mixed
291      */
292     protected function cleanSearchTermString($termString)
293     {
294         // Strip tag searches
295         $termString = preg_replace('/\[.*?\]/', '', $termString);
296         // Reduced multiple spacing into single spacing
297         $termString = preg_replace("/\s{2,}/", " ", $termString);
298         return $termString;
299     }
300
301     /**
302      * Get the available query operators as a regex escaped list.
303      * @return mixed
304      */
305     protected function getRegexEscapedOperators()
306     {
307         $escapedOperators = [];
308         foreach ($this->queryOperators as $operator) {
309             $escapedOperators[] = preg_quote($operator);
310         }
311         return join('|', $escapedOperators);
312     }
313
314     /**
315      * Parses advanced search notations and adds them to the db query.
316      * @param $query
317      * @param $termString
318      * @return mixed
319      */
320     protected function addAdvancedSearchQueries($query, $termString)
321     {
322         $escapedOperators = $this->getRegexEscapedOperators();
323         // Look for tag searches
324         preg_match_all("/\[(.*?)((${escapedOperators})(.*?))?\]/", $termString, $tags);
325         if (count($tags[0]) > 0) {
326             $this->applyTagSearches($query, $tags);
327         }
328
329         return $query;
330     }
331
332     /**
333      * Apply extracted tag search terms onto a entity query.
334      * @param $query
335      * @param $tags
336      * @return mixed
337      */
338     protected function applyTagSearches($query, $tags) {
339         $query->where(function($query) use ($tags) {
340             foreach ($tags[1] as $index => $tagName) {
341                 $query->whereHas('tags', function($query) use ($tags, $index, $tagName) {
342                     $tagOperator = $tags[3][$index];
343                     $tagValue = $tags[4][$index];
344                     if (!empty($tagOperator) && !empty($tagValue) && in_array($tagOperator, $this->queryOperators)) {
345                         if (is_numeric($tagValue) && $tagOperator !== 'like') {
346                             // We have to do a raw sql query for this since otherwise PDO will quote the value and MySQL will
347                             // search the value as a string which prevents being able to do number-based operations
348                             // on the tag values. We ensure it has a numeric value and then cast it just to be sure.
349                             $tagValue = (float) trim($query->getConnection()->getPdo()->quote($tagValue), "'");
350                             $query->where('name', '=', $tagName)->whereRaw("value ${tagOperator} ${tagValue}");
351                         } else {
352                             $query->where('name', '=', $tagName)->where('value', $tagOperator, $tagValue);
353                         }
354                     } else {
355                         $query->where('name', '=', $tagName);
356                     }
357                 });
358             }
359         });
360         return $query;
361     }
362
363     /**
364      * Alias method to update the book jointPermissions in the PermissionService.
365      * @param Collection $collection collection on entities
366      */
367     public function buildJointPermissions(Collection $collection)
368     {
369         $this->permissionService->buildJointPermissionsForEntities($collection);
370     }
371
372     /**
373      * Format a name as a url slug.
374      * @param $name
375      * @return string
376      */
377     protected function nameToSlug($name)
378     {
379         $slug = str_replace(' ', '-', strtolower($name));
380         $slug = preg_replace('/[\+\/\\\?\@\}\{\.\,\=\[\]\#\&\!\*\'\;\:\$\%]/', '', $slug);
381         if ($slug === "") $slug = substr(md5(rand(1, 500)), 0, 5);
382         return $slug;
383     }
384
385 }
386
387
388
389
390
391
392
393
394
395
396
397