]> BookStack Code Mirror - bookstack/blob - app/Users/UserRepo.php
Slugs: Added slug recording at points of generation
[bookstack] / app / Users / UserRepo.php
1 <?php
2
3 namespace BookStack\Users;
4
5 use BookStack\Access\UserInviteException;
6 use BookStack\Access\UserInviteService;
7 use BookStack\Activity\ActivityType;
8 use BookStack\Entities\Tools\SlugGenerator;
9 use BookStack\Exceptions\NotifyException;
10 use BookStack\Exceptions\UserUpdateException;
11 use BookStack\Facades\Activity;
12 use BookStack\Uploads\UserAvatars;
13 use BookStack\Users\Models\Role;
14 use BookStack\Users\Models\User;
15 use DB;
16 use Exception;
17 use Illuminate\Support\Facades\Hash;
18 use Illuminate\Support\Facades\Log;
19 use Illuminate\Support\Str;
20
21 class UserRepo
22 {
23     public function __construct(
24         protected UserAvatars $userAvatar,
25         protected UserInviteService $inviteService,
26         protected SlugGenerator $slugGenerator,
27     ) {
28     }
29
30     /**
31      * Get a user by their email address.
32      */
33     public function getByEmail(string $email): ?User
34     {
35         return User::query()->where('email', '=', $email)->first();
36     }
37
38     /**
39      * Get a user by their ID.
40      */
41     public function getById(int $id): User
42     {
43         return User::query()->findOrFail($id);
44     }
45
46     /**
47      * Get a user by their slug.
48      */
49     public function getBySlug(string $slug): User
50     {
51         return User::query()->where('slug', '=', $slug)->firstOrFail();
52     }
53
54     /**
55      * Create a new basic instance of user with the given pre-validated data.
56      *
57      * @param array{name: string, email: string, password: ?string, external_auth_id: ?string, language: ?string, roles: ?array} $data
58      */
59     public function createWithoutActivity(array $data, bool $emailConfirmed = false): User
60     {
61         $user = new User();
62         $user->name = $data['name'];
63         $user->email = $data['email'];
64         $user->password = Hash::make(empty($data['password']) ? Str::random(32) : $data['password']);
65         $user->email_confirmed = $emailConfirmed;
66         $user->external_auth_id = $data['external_auth_id'] ?? '';
67
68         $this->slugGenerator->regenerateForUser($user);
69         $user->save();
70
71         if (!empty($data['language'])) {
72             setting()->putUser($user, 'language', $data['language']);
73         }
74
75         if (isset($data['roles'])) {
76             $this->setUserRoles($user, $data['roles']);
77         }
78
79         $this->downloadAndAssignUserAvatar($user);
80
81         return $user;
82     }
83
84     /**
85      * As per "createWithoutActivity" but records a "create" activity.
86      *
87      * @param array{name: string, email: string, password: ?string, external_auth_id: ?string, language: ?string, roles: ?array} $data
88      * @throws UserInviteException
89      */
90     public function create(array $data, bool $sendInvite = false): User
91     {
92         $user = $this->createWithoutActivity($data, true);
93
94         if ($sendInvite) {
95             $this->inviteService->sendInvitation($user);
96         }
97
98         Activity::add(ActivityType::USER_CREATE, $user);
99
100         return $user;
101     }
102
103     /**
104      * Update the given user with the given data, but do not create an activity.
105      *
106      * @param array{name: ?string, email: ?string, external_auth_id: ?string, password: ?string, roles: ?array<int>, language: ?string} $data
107      *
108      * @throws UserUpdateException
109      */
110     public function updateWithoutActivity(User $user, array $data, bool $manageUsersAllowed): User
111     {
112         if (!empty($data['name'])) {
113             $user->name = $data['name'];
114             $this->slugGenerator->regenerateForUser($user);
115         }
116
117         if (!empty($data['email']) && $manageUsersAllowed) {
118             $user->email = $data['email'];
119         }
120
121         if (!empty($data['external_auth_id']) && $manageUsersAllowed) {
122             $user->external_auth_id = $data['external_auth_id'];
123         }
124
125         if (isset($data['roles']) && $manageUsersAllowed) {
126             $this->setUserRoles($user, $data['roles']);
127         }
128
129         if (!empty($data['password'])) {
130             $user->password = Hash::make($data['password']);
131         }
132
133         if (!empty($data['language'])) {
134             setting()->putUser($user, 'language', $data['language']);
135         }
136
137         $user->save();
138
139         return $user;
140     }
141
142     /**
143      * Update the given user with the given data.
144      *
145      * @param array{name: ?string, email: ?string, external_auth_id: ?string, password: ?string, roles: ?array<int>, language: ?string} $data
146      *
147      * @throws UserUpdateException
148      */
149     public function update(User $user, array $data, bool $manageUsersAllowed): User
150     {
151         $user = $this->updateWithoutActivity($user, $data, $manageUsersAllowed);
152
153         Activity::add(ActivityType::USER_UPDATE, $user);
154
155         return $user;
156     }
157
158     /**
159      * Remove the given user from storage, Delete all related content.
160      *
161      * @throws Exception
162      */
163     public function destroy(User $user, ?int $newOwnerId = null): void
164     {
165         $this->ensureDeletable($user);
166
167         $this->removeUserDependantRelations($user);
168         $this->nullifyUserNonDependantRelations($user);
169         $user->delete();
170
171         // Delete user profile images
172         $this->userAvatar->destroyAllForUser($user);
173
174         // Delete related activities
175         setting()->deleteUserSettings($user->id);
176
177         // Migrate or nullify ownership
178         $newOwner = null;
179         if (!empty($newOwnerId)) {
180             $newOwner = User::query()->find($newOwnerId);
181         }
182         $this->migrateOwnership($user, $newOwner);
183
184         Activity::add(ActivityType::USER_DELETE, $user);
185     }
186
187     protected function removeUserDependantRelations(User $user): void
188     {
189         $user->apiTokens()->delete();
190         $user->socialAccounts()->delete();
191         $user->favourites()->delete();
192         $user->mfaValues()->delete();
193         $user->watches()->delete();
194
195         $tables = ['email_confirmations', 'user_invites', 'views'];
196         foreach ($tables as $table) {
197             DB::table($table)->where('user_id', '=', $user->id)->delete();
198         }
199     }
200     protected function nullifyUserNonDependantRelations(User $user): void
201     {
202         $toNullify = [
203             'attachments' => ['created_by', 'updated_by'],
204             'comments' => ['created_by', 'updated_by'],
205             'deletions' => ['deleted_by'],
206             'entities' => ['created_by', 'updated_by'],
207             'images' => ['created_by', 'updated_by'],
208             'imports' => ['created_by'],
209             'joint_permissions' => ['owner_id'],
210             'page_revisions' => ['created_by'],
211             'sessions' => ['user_id'],
212         ];
213
214         foreach ($toNullify as $table => $columns) {
215             foreach ($columns as $column) {
216                 DB::table($table)
217                     ->where($column, '=', $user->id)
218                     ->update([$column => null]);
219             }
220         }
221     }
222
223     /**
224      * @throws NotifyException
225      */
226     protected function ensureDeletable(User $user): void
227     {
228         if ($this->isOnlyAdmin($user)) {
229             throw new NotifyException(trans('errors.users_cannot_delete_only_admin'), $user->getEditUrl());
230         }
231
232         if ($user->system_name === 'public') {
233             throw new NotifyException(trans('errors.users_cannot_delete_guest'), $user->getEditUrl());
234         }
235     }
236
237     /**
238      * Migrate ownership of items in the system from one user to another.
239      */
240     protected function migrateOwnership(User $fromUser, User|null $toUser): void
241     {
242         $newOwnerValue = $toUser ? $toUser->id : null;
243         DB::table('entities')
244             ->where('owned_by', '=', $fromUser->id)
245             ->update(['owned_by' => $newOwnerValue]);
246     }
247
248     /**
249      * Get an avatar image for a user and set it as their avatar.
250      * Returns early if avatars disabled or not set in config.
251      */
252     protected function downloadAndAssignUserAvatar(User $user): void
253     {
254         try {
255             $this->userAvatar->fetchAndAssignToUser($user);
256         } catch (Exception $e) {
257             Log::error('Failed to save user avatar image');
258         }
259     }
260
261     /**
262      * Checks if the give user is the only admin.
263      */
264     protected function isOnlyAdmin(User $user): bool
265     {
266         if (!$user->hasSystemRole('admin')) {
267             return false;
268         }
269
270         $adminRole = Role::getSystemRole('admin');
271         if ($adminRole->users()->count() > 1) {
272             return false;
273         }
274
275         return true;
276     }
277
278     /**
279      * Set the assigned user roles via an array of role IDs.
280      *
281      * @throws UserUpdateException
282      */
283     protected function setUserRoles(User $user, array $roles): void
284     {
285         $roles = array_filter(array_values($roles));
286
287         if ($this->demotingLastAdmin($user, $roles)) {
288             throw new UserUpdateException(trans('errors.role_cannot_remove_only_admin'), $user->getEditUrl());
289         }
290
291         $user->roles()->sync($roles);
292     }
293
294     /**
295      * Check if the given user is the last admin and their new roles no longer
296      * contain the admin role.
297      */
298     protected function demotingLastAdmin(User $user, array $newRoles): bool
299     {
300         if ($this->isOnlyAdmin($user)) {
301             $adminRole = Role::getSystemRole('admin');
302             if (!in_array(strval($adminRole->id), $newRoles)) {
303                 return true;
304             }
305         }
306
307         return false;
308     }
309 }