]> BookStack Code Mirror - bookstack/blob - app/Users/Models/User.php
OIDC: Added testing coverage for picture fetching
[bookstack] / app / Users / Models / User.php
1 <?php
2
3 namespace BookStack\Users\Models;
4
5 use BookStack\Access\Mfa\MfaValue;
6 use BookStack\Access\Notifications\ResetPasswordNotification;
7 use BookStack\Access\SocialAccount;
8 use BookStack\Activity\Models\Favourite;
9 use BookStack\Activity\Models\Loggable;
10 use BookStack\Activity\Models\Watch;
11 use BookStack\Api\ApiToken;
12 use BookStack\App\Model;
13 use BookStack\App\Sluggable;
14 use BookStack\Entities\Tools\SlugGenerator;
15 use BookStack\Translation\LocaleDefinition;
16 use BookStack\Translation\LocaleManager;
17 use BookStack\Uploads\Image;
18 use Carbon\Carbon;
19 use Exception;
20 use Illuminate\Auth\Authenticatable;
21 use Illuminate\Auth\Passwords\CanResetPassword;
22 use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
23 use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract;
24 use Illuminate\Database\Eloquent\Builder;
25 use Illuminate\Database\Eloquent\Factories\HasFactory;
26 use Illuminate\Database\Eloquent\Relations\BelongsTo;
27 use Illuminate\Database\Eloquent\Relations\BelongsToMany;
28 use Illuminate\Database\Eloquent\Relations\HasMany;
29 use Illuminate\Notifications\Notifiable;
30 use Illuminate\Support\Collection;
31
32 /**
33  * Class User.
34  *
35  * @property int        $id
36  * @property string     $name
37  * @property string     $slug
38  * @property string     $email
39  * @property string     $password
40  * @property Carbon     $created_at
41  * @property Carbon     $updated_at
42  * @property bool       $email_confirmed
43  * @property int        $image_id
44  * @property string     $external_auth_id
45  * @property string     $system_name
46  * @property Collection $roles
47  * @property Collection $mfaValues
48  * @property ?Image     $avatar
49  */
50 class User extends Model implements AuthenticatableContract, CanResetPasswordContract, Loggable, Sluggable
51 {
52     use HasFactory;
53     use Authenticatable;
54     use CanResetPassword;
55     use Notifiable;
56
57     /**
58      * The database table used by the model.
59      *
60      * @var string
61      */
62     protected $table = 'users';
63
64     /**
65      * The attributes that are mass assignable.
66      *
67      * @var array
68      */
69     protected $fillable = ['name', 'email'];
70
71     protected $casts = ['last_activity_at' => 'datetime'];
72
73     /**
74      * The attributes excluded from the model's JSON form.
75      *
76      * @var array
77      */
78     protected $hidden = [
79         'password', 'remember_token', 'system_name', 'email_confirmed', 'external_auth_id', 'email',
80         'created_at', 'updated_at', 'image_id', 'roles', 'avatar', 'user_id', 'pivot',
81     ];
82
83     /**
84      * This holds the user's permissions when loaded.
85      */
86     protected ?Collection $permissions;
87
88     /**
89      * This holds the user's avatar URL when loaded to prevent re-calculating within the same request.
90      */
91     protected string $avatarUrl = '';
92
93     /**
94      * Returns the default public user.
95      * Fetches from the container as a singleton to effectively cache at an app level.
96      */
97     public static function getGuest(): self
98     {
99         return app()->make('users.default');
100     }
101
102     /**
103      * Check if the user is the default public user.
104      */
105     public function isGuest(): bool
106     {
107         return $this->system_name === 'public';
108     }
109
110     /**
111      * Check if the user has general access to the application.
112      */
113     public function hasAppAccess(): bool
114     {
115         return !$this->isGuest() || setting('app-public');
116     }
117
118     /**
119      * The roles that belong to the user.
120      *
121      * @return BelongsToMany
122      */
123     public function roles()
124     {
125         if ($this->id === 0) {
126             return;
127         }
128
129         return $this->belongsToMany(Role::class);
130     }
131
132     /**
133      * Check if the user has a role.
134      */
135     public function hasRole($roleId): bool
136     {
137         return $this->roles->pluck('id')->contains($roleId);
138     }
139
140     /**
141      * Check if the user has a role.
142      */
143     public function hasSystemRole(string $roleSystemName): bool
144     {
145         return $this->roles->pluck('system_name')->contains($roleSystemName);
146     }
147
148     /**
149      * Attach the default system role to this user.
150      */
151     public function attachDefaultRole(): void
152     {
153         $roleId = intval(setting('registration-role'));
154         if ($roleId && $this->roles()->where('id', '=', $roleId)->count() === 0) {
155             $this->roles()->attach($roleId);
156         }
157     }
158
159     /**
160      * Check if the user has a particular permission.
161      */
162     public function can(string $permissionName): bool
163     {
164         return $this->permissions()->contains($permissionName);
165     }
166
167     /**
168      * Get all permissions belonging to the current user.
169      */
170     protected function permissions(): Collection
171     {
172         if (isset($this->permissions)) {
173             return $this->permissions;
174         }
175
176         $this->permissions = $this->newQuery()->getConnection()->table('role_user', 'ru')
177             ->select('role_permissions.name as name')->distinct()
178             ->leftJoin('permission_role', 'ru.role_id', '=', 'permission_role.role_id')
179             ->leftJoin('role_permissions', 'permission_role.permission_id', '=', 'role_permissions.id')
180             ->where('ru.user_id', '=', $this->id)
181             ->pluck('name');
182
183         return $this->permissions;
184     }
185
186     /**
187      * Clear any cached permissions on this instance.
188      */
189     public function clearPermissionCache()
190     {
191         $this->permissions = null;
192     }
193
194     /**
195      * Attach a role to this user.
196      */
197     public function attachRole(Role $role)
198     {
199         $this->roles()->attach($role->id);
200         $this->unsetRelation('roles');
201     }
202
203     /**
204      * Get the social account associated with this user.
205      */
206     public function socialAccounts(): HasMany
207     {
208         return $this->hasMany(SocialAccount::class);
209     }
210
211     /**
212      * Check if the user has a social account,
213      * If a driver is passed it checks for that single account type.
214      *
215      * @param bool|string $socialDriver
216      *
217      * @return bool
218      */
219     public function hasSocialAccount($socialDriver = false)
220     {
221         if ($socialDriver === false) {
222             return $this->socialAccounts()->count() > 0;
223         }
224
225         return $this->socialAccounts()->where('driver', '=', $socialDriver)->exists();
226     }
227
228     /**
229      * Returns a URL to the user's avatar.
230      */
231     public function getAvatar(int $size = 50): string
232     {
233         $default = url('/user_avatar.png');
234         $imageId = $this->image_id;
235         if ($imageId === 0 || $imageId === '0' || $imageId === null) {
236             return $default;
237         }
238
239         if (!empty($this->avatarUrl)) {
240             return $this->avatarUrl;
241         }
242
243         try {
244             $avatar = $this->avatar?->getThumb($size, $size, false) ?? $default;
245         } catch (Exception $err) {
246             $avatar = $default;
247         }
248
249         $this->avatarUrl = $avatar;
250
251         return $avatar;
252     }
253
254     /**
255      * Get the avatar for the user.
256      */
257     public function avatar(): BelongsTo
258     {
259         return $this->belongsTo(Image::class, 'image_id');
260     }
261
262     /**
263      * Get the API tokens assigned to this user.
264      */
265     public function apiTokens(): HasMany
266     {
267         return $this->hasMany(ApiToken::class);
268     }
269
270     /**
271      * Get the favourite instances for this user.
272      */
273     public function favourites(): HasMany
274     {
275         return $this->hasMany(Favourite::class);
276     }
277
278     /**
279      * Get the MFA values belonging to this use.
280      */
281     public function mfaValues(): HasMany
282     {
283         return $this->hasMany(MfaValue::class);
284     }
285
286     /**
287      * Get the tracked entity watches for this user.
288      */
289     public function watches(): HasMany
290     {
291         return $this->hasMany(Watch::class);
292     }
293
294     /**
295      * Get the last activity time for this user.
296      */
297     public function scopeWithLastActivityAt(Builder $query)
298     {
299         $query->addSelect(['activities.created_at as last_activity_at'])
300             ->leftJoinSub(function (\Illuminate\Database\Query\Builder $query) {
301                 $query->from('activities')->select('user_id')
302                     ->selectRaw('max(created_at) as created_at')
303                     ->groupBy('user_id');
304             }, 'activities', 'users.id', '=', 'activities.user_id');
305     }
306
307     /**
308      * Get the url for editing this user.
309      */
310     public function getEditUrl(string $path = ''): string
311     {
312         $uri = '/settings/users/' . $this->id . '/' . trim($path, '/');
313
314         return url(rtrim($uri, '/'));
315     }
316
317     /**
318      * Get the url that links to this user's profile.
319      */
320     public function getProfileUrl(): string
321     {
322         return url('/user/' . $this->slug);
323     }
324
325     /**
326      * Get a shortened version of the user's name.
327      */
328     public function getShortName(int $chars = 8): string
329     {
330         if (mb_strlen($this->name) <= $chars) {
331             return $this->name;
332         }
333
334         $splitName = explode(' ', $this->name);
335         if (mb_strlen($splitName[0]) <= $chars) {
336             return $splitName[0];
337         }
338
339         return mb_substr($this->name, 0, max($chars - 2, 0)) . '…';
340     }
341
342     /**
343      * Get the locale for this user.
344      */
345     public function getLocale(): LocaleDefinition
346     {
347         return app()->make(LocaleManager::class)->getForUser($this);
348     }
349
350     /**
351      * Send the password reset notification.
352      *
353      * @param string $token
354      *
355      * @return void
356      */
357     public function sendPasswordResetNotification($token)
358     {
359         $this->notify(new ResetPasswordNotification($token));
360     }
361
362     /**
363      * {@inheritdoc}
364      */
365     public function logDescriptor(): string
366     {
367         return "({$this->id}) {$this->name}";
368     }
369
370     /**
371      * {@inheritdoc}
372      */
373     public function refreshSlug(): string
374     {
375         $this->slug = app()->make(SlugGenerator::class)->generate($this);
376
377         return $this->slug;
378     }
379 }