. */ declare(strict_types=1); namespace FireflyIII\Repositories\UserGroup; use FireflyIII\Enums\UserRoleEnum; use FireflyIII\Exceptions\FireflyException; use FireflyIII\Factory\UserGroupFactory; use FireflyIII\Models\GroupMembership; use FireflyIII\Models\UserGroup; use FireflyIII\Models\UserRole; use FireflyIII\User; use Illuminate\Contracts\Auth\Authenticatable; use Illuminate\Support\Collection; /** * Class UserGroupRepository */ class UserGroupRepository implements UserGroupRepositoryInterface { private User $user; public function destroy(UserGroup $userGroup): void { app('log')->debug(sprintf('Going to destroy user group #%d ("%s").', $userGroup->id, $userGroup->title)); $memberships = $userGroup->groupMemberships()->get(); /** @var GroupMembership $membership */ foreach ($memberships as $membership) { /** @var null|User $user */ $user = $membership->user()->first(); if (null === $user) { continue; } app('log')->debug(sprintf('Processing membership #%d (user #%d "%s")', $membership->id, $user->id, $user->email)); // user has memberships of other groups? $count = $user->groupMemberships()->where('user_group_id', '!=', $userGroup->id)->count(); if (0 === $count) { app('log')->debug('User has no other memberships and needs a new user group.'); $newUserGroup = $this->createNewUserGroup($user); $user->user_group_id = $newUserGroup->id; $user->save(); app('log')->debug(sprintf('Make new group #%d ("%s")', $newUserGroup->id, $newUserGroup->title)); } // user has other memberships, select one at random and assign it to the user. if ($count > 0) { app('log')->debug('User has other memberships and will be assigned a new administration.'); /** @var GroupMembership $first */ $first = $user->groupMemberships()->where('user_group_id', '!=', $userGroup->id)->inRandomOrder()->first(); $user->user_group_id = $first->id; $user->save(); } // delete membership so group is empty after this for-loop. $membership->delete(); } // all users are now moved away from user group. // time to DESTROY all objects. // we have to do this one by one to trigger the necessary observers :( $objects = ['availableBudgets', 'bills', 'budgets', 'categories', 'currencyExchangeRates', 'objectGroups', 'recurrences', 'rules', 'ruleGroups', 'tags', 'transactionGroups', 'transactionJournals', 'piggyBanks', 'accounts', 'webhooks', ]; foreach ($objects as $object) { foreach ($userGroup->{$object}()->get() as $item) { // @phpstan-ignore-line $item->delete(); } } $userGroup->delete(); app('log')->debug('Done!'); } /** * Returns all groups the user is member in. * * {@inheritDoc} */ public function get(): Collection { $collection = new Collection(); $set = []; $memberships = $this->user->groupMemberships()->get(); /** @var GroupMembership $membership */ foreach ($memberships as $membership) { /** @var null|UserGroup $group */ $group = $membership->userGroup()->first(); if (null !== $group) { $groupId = $group->id; if (in_array($groupId, array_keys($set), true)) { continue; } $set[$groupId] = $group; } } $collection->push(...$set); return $collection; } /** * Because there is the chance that a group with this name already exists, * Firefly III runs a little loop of combinations to make sure the group name is unique. */ private function createNewUserGroup(User $user): UserGroup { $loop = 0; $groupName = $user->email; $exists = true; $existingGroup = null; while ($exists && $loop < 10) { $existingGroup = $this->findByName($groupName); if (null === $existingGroup) { $exists = false; /** @var null|UserGroup $existingGroup */ $existingGroup = $this->store(['user' => $user, 'title' => $groupName]); } if (null !== $existingGroup) { // group already exists $groupName = sprintf('%s-%s', $user->email, substr(sha1(rand(1000, 9999).microtime()), 0, 4)); } ++$loop; } return $existingGroup; } public function findByName(string $title): ?UserGroup { return UserGroup::whereTitle($title)->first(); } /** * @throws FireflyException */ public function store(array $data): UserGroup { $data['user'] = $this->user; /** @var UserGroupFactory $factory */ $factory = app(UserGroupFactory::class); return $factory->create($data); } /** * Returns all groups. * * {@inheritDoc} */ public function getAll(): Collection { return UserGroup::all(); } public function setUser(null|Authenticatable|User $user): void { app('log')->debug(sprintf('Now in %s', __METHOD__)); if ($user instanceof User) { $this->user = $user; } } public function update(UserGroup $userGroup, array $data): UserGroup { $userGroup->title = $data['title']; $userGroup->save(); return $userGroup; } /** * @SuppressWarnings(PHPMD.NPathComplexity) * * @throws FireflyException */ public function updateMembership(UserGroup $userGroup, array $data): UserGroup { $owner = UserRole::whereTitle(UserRoleEnum::OWNER)->first(); app('log')->debug('in update membership'); /** @var null|User $user */ $user = null; if (array_key_exists('id', $data)) { /** @var null|User $user */ $user = User::find($data['id']); app('log')->debug('Found user by ID'); } if (array_key_exists('email', $data) && '' !== (string)$data['email']) { /** @var null|User $user */ $user = User::whereEmail($data['email'])->first(); app('log')->debug('Found user by email'); } if (null === $user) { // should throw error, but validator already catches this. app('log')->debug('No user found'); return $userGroup; } // count the number of members in the group right now: $membershipCount = $userGroup->groupMemberships()->distinct()->count('group_memberships.user_id'); // if it's 1: if (1 === $membershipCount) { $lastUserId = $userGroup->groupMemberships()->distinct()->first(['group_memberships.user_id'])->user_id; // if this is also the user we're editing right now, and we remove all of their roles: if ($lastUserId === (int)$user->id && 0 === count($data['roles'])) { app('log')->debug('User is last in this group, refuse to act'); throw new FireflyException('You cannot remove the last member from this user group. Delete the user group instead.'); } // if this is also the user we're editing right now, and do not grant them the owner role: if ($lastUserId === (int)$user->id && count($data['roles']) > 0 && !in_array(UserRoleEnum::OWNER->value, $data['roles'], true)) { app('log')->debug('User needs to have owner role in this group, refuse to act'); throw new FireflyException('The last member in this user group must get or keep the "owner" role.'); } } if ($membershipCount > 1) { // group has multiple members. How many are owner, except the user we're editing now? $ownerCount = $userGroup->groupMemberships() ->where('user_role_id', $owner->id) ->where('user_id', '!=', $user->id)->count() ; // if there are no other owners and the current users does not get or keep the owner role, refuse. if ( 0 === $ownerCount && (0 === count($data['roles']) || (count($data['roles']) > 0 // @phpstan-ignore-line && !in_array(UserRoleEnum::OWNER->value, $data['roles'], true)))) { app('log')->debug('User needs to keep owner role in this group, refuse to act'); throw new FireflyException('The last owner in this user group must keep the "owner" role.'); } } // simplify the list of roles: $rolesSimplified = $this->simplifyListByName($data['roles']); // delete all existing roles for user: $user->groupMemberships()->where('user_group_id', $userGroup->id)->delete(); foreach ($rolesSimplified as $role) { try { $enum = UserRoleEnum::from($role); } catch (\ValueError $e) { // TODO error message continue; } $userRole = UserRole::whereTitle($enum->value)->first(); $user->groupMemberships()->create(['user_group_id' => $userGroup->id, 'user_role_id' => $userRole->id]); } return $userGroup; } private function simplifyListByName(array $roles): array { if (in_array(UserRoleEnum::OWNER->value, $roles, true)) { app('log')->debug(sprintf('List of roles is [%1$s] but this includes "%2$s", so return [%2$s]', implode(',', $roles), UserRoleEnum::OWNER->value)); return [UserRoleEnum::OWNER->value]; } if (in_array(UserRoleEnum::FULL->value, $roles, true)) { app('log')->debug(sprintf('List of roles is [%1$s] but this includes "%2$s", so return [%2$s]', implode(',', $roles), UserRoleEnum::FULL->value)); return [UserRoleEnum::FULL->value]; } return $roles; } #[\Override] public function useUserGroup(UserGroup $userGroup): void { $this->user->user_group_id = $userGroup->id; $this->user->save(); } }