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