diff --git a/app/Auth/UserRepo.php b/app/Auth/UserRepo.php index 6b7de3259..6fb5dfa0f 100644 --- a/app/Auth/UserRepo.php +++ b/app/Auth/UserRepo.php @@ -1,6 +1,7 @@ <?php namespace BookStack\Auth; use Activity; +use BookStack\Entities\EntityProvider; use BookStack\Entities\Models\Book; use BookStack\Entities\Models\Bookshelf; use BookStack\Entities\Models\Chapter; @@ -169,7 +170,7 @@ class UserRepo * Remove the given user from storage, Delete all related content. * @throws Exception */ - public function destroy(User $user) + public function destroy(User $user, ?int $newOwnerId = null) { $user->socialAccounts()->delete(); $user->apiTokens()->delete(); @@ -183,6 +184,25 @@ class UserRepo foreach ($profileImages as $image) { Images::destroy($image); } + + if (!empty($newOwnerId)) { + $newOwner = User::query()->find($newOwnerId); + if (!is_null($newOwner)) { + $this->migrateOwnership($user, $newOwner); + } + } + } + + /** + * Migrate ownership of items in the system from one user to another. + */ + protected function migrateOwnership(User $fromUser, User $toUser) + { + $entities = (new EntityProvider)->all(); + foreach ($entities as $instance) { + $instance->newQuery()->where('owned_by', '=', $fromUser->id) + ->update(['owned_by' => $toUser->id]); + } } /** diff --git a/app/Entities/EntityProvider.php b/app/Entities/EntityProvider.php index ef1935a0f..c77a57d61 100644 --- a/app/Entities/EntityProvider.php +++ b/app/Entities/EntityProvider.php @@ -55,7 +55,7 @@ class EntityProvider /** * Fetch all core entity types as an associated array * with their basic names as the keys. - * @return [string => Entity] + * @return array<Entity> */ public function all(): array { diff --git a/app/Http/Controllers/UserController.php b/app/Http/Controllers/UserController.php index 8d688ed84..852d507c1 100644 --- a/app/Http/Controllers/UserController.php +++ b/app/Http/Controllers/UserController.php @@ -217,12 +217,13 @@ class UserController extends Controller * Remove the specified user from storage. * @throws \Exception */ - public function destroy(int $id) + public function destroy(Request $request, int $id) { $this->preventAccessInDemoMode(); $this->checkPermissionOrCurrentUser('users-manage', $id); $user = $this->userRepo->getById($id); + $newOwnerId = $request->get('new_owner_id', null); if ($this->userRepo->isOnlyAdmin($user)) { $this->showErrorNotification(trans('errors.users_cannot_delete_only_admin')); @@ -234,7 +235,7 @@ class UserController extends Controller return redirect($user->getEditUrl()); } - $this->userRepo->destroy($user); + $this->userRepo->destroy($user, $newOwnerId); $this->showSuccessNotification(trans('settings.users_delete_success')); $this->logActivity(ActivityType::USER_DELETE, $user); diff --git a/resources/lang/en/settings.php b/resources/lang/en/settings.php index 3e043e3c6..fe7ebc612 100755 --- a/resources/lang/en/settings.php +++ b/resources/lang/en/settings.php @@ -175,7 +175,10 @@ return [ 'users_delete_named' => 'Delete user :userName', 'users_delete_warning' => 'This will fully delete this user with the name \':userName\' from the system.', 'users_delete_confirm' => 'Are you sure you want to delete this user?', - 'users_delete_success' => 'Users successfully removed', + 'users_migrate_ownership' => 'Migrate Ownership', + 'users_migrate_ownership_desc' => 'Select a user here if you want another user to become the owner of all items currently owned by this user.', + 'users_none_selected' => 'No user selected', + 'users_delete_success' => 'User successfully removed', 'users_edit' => 'Edit User', 'users_edit_profile' => 'Edit Profile', 'users_edit_success' => 'User successfully updated', diff --git a/resources/views/components/user-select.blade.php b/resources/views/components/user-select.blade.php index c6a30f53d..2a07f0bde 100644 --- a/resources/views/components/user-select.blade.php +++ b/resources/views/components/user-select.blade.php @@ -1,13 +1,17 @@ <div class="dropdown-search custom-select-input" components="dropdown dropdown-search user-select" option:dropdown-search:url="/search/users/select" > - <input refs="user-select@input" type="hidden" name="{{ $name }}" value="{{ $user->id }}"> + <input refs="user-select@input" type="hidden" name="{{ $name }}" value="{{ $user->id ?? '' }}"> <div refs="dropdown@toggle" class="dropdown-search-toggle flex-container-row items-center" aria-haspopup="true" aria-expanded="false" tabindex="0"> <div refs="user-select@user-info" class="flex-container-row items-center px-s"> - <img class="avatar mr-m" src="{{ $user->getAvatar(30) }}" alt="{{ $user->name }}"> - <span>{{ $user->name }}</span> + @if($user) + <img class="avatar mr-m" src="{{ $user->getAvatar(30) }}" alt="{{ $user->name }}"> + <span>{{ $user->name }}</span> + @else + <span>{{ trans('settings.users_none_selected') }}</span> + @endif </div> <span style="font-size: 1.5rem; margin-left: auto;"> @icon('caret-down') diff --git a/resources/views/users/delete.blade.php b/resources/views/users/delete.blade.php index d3349c2f3..aba6f5cc1 100644 --- a/resources/views/users/delete.blade.php +++ b/resources/views/users/delete.blade.php @@ -12,6 +12,20 @@ <p>{{ trans('settings.users_delete_warning', ['userName' => $user->name]) }}</p> + <hr class="my-l"> + + <div class="grid half gap-xl v-center"> + <div> + <label class="setting-list-label">{{ trans('settings.users_migrate_ownership') }}</label> + <p class="small">{{ trans('settings.users_migrate_ownership_desc') }}</p> + </div> + <div> + @include('components.user-select', ['name' => 'new_owner_id', 'user' => null]) + </div> + </div> + + <hr class="my-l"> + <div class="grid half"> <p class="text-neg"><strong>{{ trans('settings.users_delete_confirm') }}</strong></p> <div>