]> BookStack Code Mirror - bookstack/blob - app/Entities/Models/Entity.php
Minor capitalisation fix for Estonian
[bookstack] / app / Entities / Models / Entity.php
1 <?php
2
3 namespace BookStack\Entities\Models;
4
5 use BookStack\Actions\Activity;
6 use BookStack\Actions\Comment;
7 use BookStack\Actions\Favourite;
8 use BookStack\Actions\Tag;
9 use BookStack\Actions\View;
10 use BookStack\Auth\Permissions\EntityPermission;
11 use BookStack\Auth\Permissions\JointPermission;
12 use BookStack\Entities\Tools\SearchIndex;
13 use BookStack\Entities\Tools\SlugGenerator;
14 use BookStack\Facades\Permissions;
15 use BookStack\Interfaces\Favouritable;
16 use BookStack\Interfaces\Sluggable;
17 use BookStack\Interfaces\Viewable;
18 use BookStack\Model;
19 use BookStack\Traits\HasCreatorAndUpdater;
20 use BookStack\Traits\HasOwner;
21 use Carbon\Carbon;
22 use Illuminate\Database\Eloquent\Builder;
23 use Illuminate\Database\Eloquent\Collection;
24 use Illuminate\Database\Eloquent\Relations\MorphMany;
25 use Illuminate\Database\Eloquent\SoftDeletes;
26
27 /**
28  * Class Entity
29  * The base class for book-like items such as pages, chapters & books.
30  * This is not a database model in itself but extended.
31  *
32  * @property int        $id
33  * @property string     $name
34  * @property string     $slug
35  * @property Carbon     $created_at
36  * @property Carbon     $updated_at
37  * @property int        $created_by
38  * @property int        $updated_by
39  * @property bool       $restricted
40  * @property Collection $tags
41  *
42  * @method static Entity|Builder visible()
43  * @method static Entity|Builder hasPermission(string $permission)
44  * @method static Builder withLastView()
45  * @method static Builder withViewCount()
46  */
47 abstract class Entity extends Model implements Sluggable, Favouritable, Viewable
48 {
49     use SoftDeletes;
50     use HasCreatorAndUpdater;
51     use HasOwner;
52
53     /**
54      * @var string - Name of property where the main text content is found
55      */
56     public $textField = 'description';
57
58     /**
59      * @var float - Multiplier for search indexing.
60      */
61     public $searchFactor = 1.0;
62
63     /**
64      * Get the entities that are visible to the current user.
65      */
66     public function scopeVisible(Builder $query): Builder
67     {
68         return $this->scopeHasPermission($query, 'view');
69     }
70
71     /**
72      * Scope the query to those entities that the current user has the given permission for.
73      */
74     public function scopeHasPermission(Builder $query, string $permission)
75     {
76         return Permissions::restrictEntityQuery($query, $permission);
77     }
78
79     /**
80      * Query scope to get the last view from the current user.
81      */
82     public function scopeWithLastView(Builder $query)
83     {
84         $viewedAtQuery = View::query()->select('updated_at')
85             ->whereColumn('viewable_id', '=', $this->getTable() . '.id')
86             ->where('viewable_type', '=', $this->getMorphClass())
87             ->where('user_id', '=', user()->id)
88             ->take(1);
89
90         return $query->addSelect(['last_viewed_at' => $viewedAtQuery]);
91     }
92
93     /**
94      * Query scope to get the total view count of the entities.
95      */
96     public function scopeWithViewCount(Builder $query)
97     {
98         $viewCountQuery = View::query()->selectRaw('SUM(views) as view_count')
99             ->whereColumn('viewable_id', '=', $this->getTable() . '.id')
100             ->where('viewable_type', '=', $this->getMorphClass())->take(1);
101
102         $query->addSelect(['view_count' => $viewCountQuery]);
103     }
104
105     /**
106      * Compares this entity to another given entity.
107      * Matches by comparing class and id.
108      */
109     public function matches(Entity $entity): bool
110     {
111         return [get_class($this), $this->id] === [get_class($entity), $entity->id];
112     }
113
114     /**
115      * Checks if the current entity matches or contains the given.
116      */
117     public function matchesOrContains(Entity $entity): bool
118     {
119         if ($this->matches($entity)) {
120             return true;
121         }
122
123         if (($entity->isA('chapter') || $entity->isA('page')) && $this->isA('book')) {
124             return $entity->book_id === $this->id;
125         }
126
127         if ($entity->isA('page') && $this->isA('chapter')) {
128             return $entity->chapter_id === $this->id;
129         }
130
131         return false;
132     }
133
134     /**
135      * Gets the activity objects for this entity.
136      */
137     public function activity(): MorphMany
138     {
139         return $this->morphMany(Activity::class, 'entity')
140             ->orderBy('created_at', 'desc');
141     }
142
143     /**
144      * Get View objects for this entity.
145      */
146     public function views(): MorphMany
147     {
148         return $this->morphMany(View::class, 'viewable');
149     }
150
151     /**
152      * Get the Tag models that have been user assigned to this entity.
153      */
154     public function tags(): MorphMany
155     {
156         return $this->morphMany(Tag::class, 'entity')->orderBy('order', 'asc');
157     }
158
159     /**
160      * Get the comments for an entity.
161      */
162     public function comments(bool $orderByCreated = true): MorphMany
163     {
164         $query = $this->morphMany(Comment::class, 'entity');
165
166         return $orderByCreated ? $query->orderBy('created_at', 'asc') : $query;
167     }
168
169     /**
170      * Get the related search terms.
171      */
172     public function searchTerms(): MorphMany
173     {
174         return $this->morphMany(SearchTerm::class, 'entity');
175     }
176
177     /**
178      * Get this entities restrictions.
179      */
180     public function permissions(): MorphMany
181     {
182         return $this->morphMany(EntityPermission::class, 'restrictable');
183     }
184
185     /**
186      * Check if this entity has a specific restriction set against it.
187      */
188     public function hasRestriction(int $role_id, string $action): bool
189     {
190         return $this->permissions()->where('role_id', '=', $role_id)
191             ->where('action', '=', $action)->count() > 0;
192     }
193
194     /**
195      * Get the entity jointPermissions this is connected to.
196      */
197     public function jointPermissions(): MorphMany
198     {
199         return $this->morphMany(JointPermission::class, 'entity');
200     }
201
202     /**
203      * Get the related delete records for this entity.
204      */
205     public function deletions(): MorphMany
206     {
207         return $this->morphMany(Deletion::class, 'deletable');
208     }
209
210     /**
211      * Check if this instance or class is a certain type of entity.
212      * Examples of $type are 'page', 'book', 'chapter'.
213      */
214     public static function isA(string $type): bool
215     {
216         return static::getType() === strtolower($type);
217     }
218
219     /**
220      * Get the entity type as a simple lowercase word.
221      */
222     public static function getType(): string
223     {
224         $className = array_slice(explode('\\', static::class), -1, 1)[0];
225
226         return strtolower($className);
227     }
228
229     /**
230      * Gets a limited-length version of the entities name.
231      */
232     public function getShortName(int $length = 25): string
233     {
234         if (mb_strlen($this->name) <= $length) {
235             return $this->name;
236         }
237
238         return mb_substr($this->name, 0, $length - 3) . '...';
239     }
240
241     /**
242      * Get the body text of this entity.
243      */
244     public function getText(): string
245     {
246         return $this->{$this->textField} ?? '';
247     }
248
249     /**
250      * Get an excerpt of this entity's descriptive content to the specified length.
251      */
252     public function getExcerpt(int $length = 100): string
253     {
254         $text = $this->getText();
255
256         if (mb_strlen($text) > $length) {
257             $text = mb_substr($text, 0, $length - 3) . '...';
258         }
259
260         return trim($text);
261     }
262
263     /**
264      * Get the url of this entity.
265      */
266     abstract public function getUrl(string $path = '/'): string;
267
268     /**
269      * Get the parent entity if existing.
270      * This is the "static" parent and does not include dynamic
271      * relations such as shelves to books.
272      */
273     public function getParent(): ?Entity
274     {
275         if ($this instanceof Page) {
276             return $this->chapter_id ? $this->chapter()->withTrashed()->first() : $this->book()->withTrashed()->first();
277         }
278         if ($this instanceof Chapter) {
279             return $this->book()->withTrashed()->first();
280         }
281
282         return null;
283     }
284
285     /**
286      * Rebuild the permissions for this entity.
287      */
288     public function rebuildPermissions()
289     {
290         /** @noinspection PhpUnhandledExceptionInspection */
291         Permissions::buildJointPermissionsForEntity(clone $this);
292     }
293
294     /**
295      * Index the current entity for search.
296      */
297     public function indexForSearch()
298     {
299         app(SearchIndex::class)->indexEntity(clone $this);
300     }
301
302     /**
303      * @inheritdoc
304      */
305     public function refreshSlug(): string
306     {
307         $this->slug = app(SlugGenerator::class)->generate($this);
308
309         return $this->slug;
310     }
311
312     /**
313      * @inheritdoc
314      */
315     public function favourites(): MorphMany
316     {
317         return $this->morphMany(Favourite::class, 'favouritable');
318     }
319
320     /**
321      * Check if the entity is a favourite of the current user.
322      */
323     public function isFavourite(): bool
324     {
325         return $this->favourites()
326             ->where('user_id', '=', user()->id)
327             ->exists();
328     }
329 }