mirror of
				https://github.com/firefly-iii/firefly-iii.git
				synced 2025-11-03 04:41:41 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			311 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
			
		
		
	
	
			311 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
<?php
 | 
						|
 | 
						|
/*
 | 
						|
 * UserGroupRepository.php
 | 
						|
 * Copyright (c) 2023 james@firefly-iii.org
 | 
						|
 *
 | 
						|
 * This file is part of Firefly III (https://github.com/firefly-iii).
 | 
						|
 *
 | 
						|
 * This program is free software: you can redistribute it and/or modify
 | 
						|
 * it under the terms of the GNU Affero General Public License as
 | 
						|
 * published by the Free Software Foundation, either version 3 of the
 | 
						|
 * License, or (at your option) any later version.
 | 
						|
 *
 | 
						|
 * This program is distributed in the hope that it will be useful,
 | 
						|
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
						|
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
						|
 * GNU Affero General Public License for more details.
 | 
						|
 *
 | 
						|
 * You should have received a copy of the GNU Affero General Public License
 | 
						|
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 | 
						|
 */
 | 
						|
 | 
						|
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();
 | 
						|
    }
 | 
						|
 | 
						|
    #[\Override]
 | 
						|
    public function getMembershipsFromGroupId(int $groupId): Collection
 | 
						|
    {
 | 
						|
        return $this->user->groupMemberships()->where('user_group_id', $groupId)->get();
 | 
						|
    }
 | 
						|
 | 
						|
    #[\Override]
 | 
						|
    public function getById(int $id): ?UserGroup
 | 
						|
    {
 | 
						|
        return UserGroup::find($id);
 | 
						|
    }
 | 
						|
}
 |