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