From f0d67913663803e53f1b421fb4a7219190d0bee0 Mon Sep 17 00:00:00 2001 From: Maxence Lange Date: Tue, 25 Feb 2025 14:16:58 -0100 Subject: [PATCH] first throw --- appinfo/info.xml | 5 +- lib/Api/v2/ITeamEntityOperation.php | 21 + lib/Api/v2/ITeamMemberOperation.php | 13 + lib/Api/v2/ITeamMembershipOperation.php | 22 + lib/Api/v2/ITeamOperation.php | 31 ++ lib/Api/v2/ITeamSession.php | 49 +++ lib/Api/v2/ITeamSuperOperation.php | 13 + lib/Api/v2/Operation/CoreOperation.php | 80 ++++ lib/Api/v2/Operation/TeamEntityOperation.php | 45 ++ lib/Api/v2/Operation/TeamMemberOperation.php | 53 +++ .../v2/Operation/TeamMembershipOperation.php | 28 ++ lib/Api/v2/Operation/TeamOperation.php | 66 +++ lib/Api/v2/Operation/TeamSuperOperation.php | 47 ++ lib/Api/v2/TeamSession.php | 172 ++++++++ lib/Command/Test.php | 146 +++++++ lib/Controller/LocalController.php | 28 ++ lib/Db/CoreMapper.php | 58 +++ lib/Db/TeamEntityMapper.php | 104 +++++ lib/Db/TeamMapper.php | 93 ++++ lib/Db/TeamMemberMapper.php | 92 ++++ lib/Db/TeamMembershipMapper.php | 71 +++ lib/Enum/TeamApi.php | 15 + lib/Enum/TeamEntityType.php | 21 + lib/Enum/TeamMemberLevel.php | 20 + .../TeamEntityNotFoundException.php | 12 + ...TeamEntityOverwritePermissionException.php | 12 + .../TeamMembershipNotFoundException.php | 12 + lib/Exceptions/TeamNotFoundException.php | 12 + lib/Exceptions/TeamOwnerNotFoundException.php | 12 + lib/FederatedItems/TeamCreate.php | 94 ++++ lib/Managers/CoreManager.php | 64 +++ lib/Managers/TeamEntityManager.php | 92 ++++ lib/Managers/TeamManager.php | 81 ++++ lib/Managers/TeamMemberManager.php | 104 +++++ lib/Managers/TeamMembershipManager.php | 81 ++++ .../Version3200Date20250115140201.php | 273 ++++++++++++ lib/Model/Federated/FederatedEvent.php | 24 +- lib/Model/Team.php | 140 ++++++ lib/Model/TeamCore.php | 59 +++ lib/Model/TeamEntity.php | 114 +++++ lib/Model/TeamMember.php | 163 +++++++ lib/Model/TeamMembership.php | 99 +++++ lib/Service/TeamEntityService.php | 132 ++++++ lib/Service/TeamMemberService.php | 37 ++ lib/Service/TeamMembershipService.php | 405 ++++++++++++++++++ lib/Service/TeamService.php | 90 ++++ lib/Service/ToolsService.php | 27 ++ 47 files changed, 3425 insertions(+), 7 deletions(-) create mode 100644 lib/Api/v2/ITeamEntityOperation.php create mode 100644 lib/Api/v2/ITeamMemberOperation.php create mode 100644 lib/Api/v2/ITeamMembershipOperation.php create mode 100644 lib/Api/v2/ITeamOperation.php create mode 100644 lib/Api/v2/ITeamSession.php create mode 100644 lib/Api/v2/ITeamSuperOperation.php create mode 100644 lib/Api/v2/Operation/CoreOperation.php create mode 100644 lib/Api/v2/Operation/TeamEntityOperation.php create mode 100644 lib/Api/v2/Operation/TeamMemberOperation.php create mode 100644 lib/Api/v2/Operation/TeamMembershipOperation.php create mode 100644 lib/Api/v2/Operation/TeamOperation.php create mode 100644 lib/Api/v2/Operation/TeamSuperOperation.php create mode 100644 lib/Api/v2/TeamSession.php create mode 100644 lib/Command/Test.php create mode 100644 lib/Db/CoreMapper.php create mode 100644 lib/Db/TeamEntityMapper.php create mode 100644 lib/Db/TeamMapper.php create mode 100644 lib/Db/TeamMemberMapper.php create mode 100644 lib/Db/TeamMembershipMapper.php create mode 100644 lib/Enum/TeamApi.php create mode 100644 lib/Enum/TeamEntityType.php create mode 100644 lib/Enum/TeamMemberLevel.php create mode 100644 lib/Exceptions/TeamEntityNotFoundException.php create mode 100644 lib/Exceptions/TeamEntityOverwritePermissionException.php create mode 100644 lib/Exceptions/TeamMembershipNotFoundException.php create mode 100644 lib/Exceptions/TeamNotFoundException.php create mode 100644 lib/Exceptions/TeamOwnerNotFoundException.php create mode 100644 lib/FederatedItems/TeamCreate.php create mode 100644 lib/Managers/CoreManager.php create mode 100644 lib/Managers/TeamEntityManager.php create mode 100644 lib/Managers/TeamManager.php create mode 100644 lib/Managers/TeamMemberManager.php create mode 100644 lib/Managers/TeamMembershipManager.php create mode 100644 lib/Migration/Version3200Date20250115140201.php create mode 100644 lib/Model/Team.php create mode 100644 lib/Model/TeamCore.php create mode 100644 lib/Model/TeamEntity.php create mode 100644 lib/Model/TeamMember.php create mode 100644 lib/Model/TeamMembership.php create mode 100644 lib/Service/TeamEntityService.php create mode 100644 lib/Service/TeamMemberService.php create mode 100644 lib/Service/TeamMembershipService.php create mode 100644 lib/Service/TeamService.php create mode 100644 lib/Service/ToolsService.php diff --git a/appinfo/info.xml b/appinfo/info.xml index 2498a2620..75b1ba5e6 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -12,7 +12,7 @@ Those groups of people can then be used by any other app for sharing purpose. ]]> - 32.0.0-dev.0 + 31.0.0-dev.1 agpl Maxence Lange @@ -31,7 +31,7 @@ Those groups of people can then be used by any other app for sharing purpose. https://github.com/nextcloud/circles.git https://raw.githubusercontent.com/nextcloud/circles/master/screenshots/0.12.0.png - + @@ -45,6 +45,7 @@ Those groups of people can then be used by any other app for sharing purpose. + OCA\Circles\Command\Test OCA\Circles\Command\CirclesCreate OCA\Circles\Command\MembersAdd OCA\Circles\Command\CirclesJoin diff --git a/lib/Api/v2/ITeamEntityOperation.php b/lib/Api/v2/ITeamEntityOperation.php new file mode 100644 index 000000000..dd9edad24 --- /dev/null +++ b/lib/Api/v2/ITeamEntityOperation.php @@ -0,0 +1,21 @@ + + * @copyright 2017 + * @license GNU AGPL version 3 or any later version + * + * 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 . + * + */ + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\Circles\Api\v2; + +use OCA\Circles\Model\TeamEntity; +use OCP\IUser; + +interface ITeamSession { + public function sessionAsCurrentUser(): self; + public function sessionAsUser(IUser $user): self; + public function sessionAsLocalUser(string $userId): self; + public function sessionAsApp(string $appId): self; + public function sessionAsSuperAdmin(): self; + public function sessionAsEntity(TeamEntity $entity): self; + public function hasEntity(): bool; + public function getEntity(): TeamEntity; + public function performTeamOperation(): ITeamOperation; + public function performTeamMemberOperation(): ITeamMemberOperation; +} diff --git a/lib/Api/v2/ITeamSuperOperation.php b/lib/Api/v2/ITeamSuperOperation.php new file mode 100644 index 000000000..1ee95ec4a --- /dev/null +++ b/lib/Api/v2/ITeamSuperOperation.php @@ -0,0 +1,13 @@ +session !== null) { + throw new \Exception('cannot overwrite session'); // TODO specific Exception + } + if (!$session->hasEntity()) { + throw new \Exception('entity must be set'); // TODO: specific exception + } + $this->session = $session; + } + + final public function getSession(): TeamSession { + return $this->session; + } + + final public function hasEntity(): bool { + return $this->session->hasEntity(); + } + + final public function getEntity(): TeamEntity { + return $this->session->getEntity(); + } +// +// +// final public function asEntity(TeamEntity $entity): void { +// if ($this->entity !== null) { +// throw new \Exception('cannot overwrite initiator'); // TODO specific Exception +// } +// $this->entity = $entity; +// } + + /** + * @param TeamEntityType|TeamEntityType[] $limitToEntities + */ + final public function confirmSessionInitialized(TeamEntityType|array $limitToEntities = []): void { + if ($this->session === null) { + throw new \Exception('session/entity not initialized'); // TODO specific Exception + } + + if (!empty($limitToEntities) + && !in_array($this->getEntity()->getTeamEntityType(), $limitToEntities)) { + throw new \Exception('entity cannot execute this operation'); // TODO specific Exception + } + + } + + /** + * @param TeamEntityType|TeamEntityType[] $limitToEntities + */ + final protected function lowPriorityProcess(TeamEntityType|array $limitToEntities = []): void { + $this->confirmSessionInitialized($limitToEntities); + // TODO: detect type of process and filters it +// if (!in_array( +// $this->entity->getTeamEntityType(), +// TeamEntityType::OCC, +// TeamEntityType::CRON, +// TeamEntityType::ASYNC, +// )) { +// throw new \Exception('cannot be executed on main process'); +// } + } + +} diff --git a/lib/Api/v2/Operation/TeamEntityOperation.php b/lib/Api/v2/Operation/TeamEntityOperation.php new file mode 100644 index 000000000..bfee5f9a5 --- /dev/null +++ b/lib/Api/v2/Operation/TeamEntityOperation.php @@ -0,0 +1,45 @@ +teamEntityService->generateTeamEntityFromLocalUser($userId); + } + + public function getFromTeam(string $singleId): TeamEntity { + return $this->teamEntityService->generateTeamEntityFromTeam($singleId); + } + + public function getFromApp(string $appId): TeamEntity { + return $this->teamEntityService->generateTeamEntityFromApp($appId); + } + + public function getFromOcc(): TeamEntity { + return $this->teamEntityService->generateTeamEntityFromOcc(); + } + + public function getFromSuperAdmin(): TeamEntity { + return $this->teamEntityService->generateTeamEntityFromSuperAdmin(); + } + + public function getFromUser(IUser $user): TeamEntity { + return $this->teamEntityService->generateTeamEntityFromUser($user); + } +} diff --git a/lib/Api/v2/Operation/TeamMemberOperation.php b/lib/Api/v2/Operation/TeamMemberOperation.php new file mode 100644 index 000000000..c628fc720 --- /dev/null +++ b/lib/Api/v2/Operation/TeamMemberOperation.php @@ -0,0 +1,53 @@ +confirmSessionInitialized(); + return $this->teamMemberManager->getMembersFromTeam($this->getEntity(), $singleId); + } + + public function addMember(Team|string $team, TeamEntity|string $entity): TeamMember { + $this->confirmSessionInitialized(); + + if (!($team instanceof Team)) { + $team = $this->teamService->getTeam($this->getEntity(), $team); + } + if (!($entity instanceof TeamEntity)) { + $entity = $this->teamEntityService->getTeamEntity($entity); + } + + return $this->teamMemberService->addMember($this->getEntity(), $team, $entity); + } +} diff --git a/lib/Api/v2/Operation/TeamMembershipOperation.php b/lib/Api/v2/Operation/TeamMembershipOperation.php new file mode 100644 index 000000000..40191fbf9 --- /dev/null +++ b/lib/Api/v2/Operation/TeamMembershipOperation.php @@ -0,0 +1,28 @@ +lowPriorityProcess(); + $this->teamMembershipService->syncTeamMemberships($this->getSession(), $singleId); + } + + +} diff --git a/lib/Api/v2/Operation/TeamOperation.php b/lib/Api/v2/Operation/TeamOperation.php new file mode 100644 index 000000000..e5e486040 --- /dev/null +++ b/lib/Api/v2/Operation/TeamOperation.php @@ -0,0 +1,66 @@ +confirmSessionInitialized(); + + // only APP, OCC and SUPER_ADMIN can create a team under the name of someone else + if (!in_array($this->getEntity()->getTeamEntityType(), [ + TeamEntityType::APP, + TeamEntityType::OCC, + TeamEntityType::SUPER_ADMIN, + ],true)) { + $realOwner = null; + } + + return $this->teamService->create($this->getEntity(), $teamName, $realOwner ?? null); + } + + /** + * @throws TeamNotFoundException + */ + public function getTeam(string $singleId): Team { + $this->confirmSessionInitialized(); + return $this->teamManager->getTeam($singleId); + } + + /** + * @return TeamMember[] + */ + public function getTeams(): array { + $this->confirmSessionInitialized(); + return []; + } + + public function getAvailableTeams(): array { + $this->confirmSessionInitialized(); + return []; + } +} diff --git a/lib/Api/v2/Operation/TeamSuperOperation.php b/lib/Api/v2/Operation/TeamSuperOperation.php new file mode 100644 index 000000000..a26e01843 --- /dev/null +++ b/lib/Api/v2/Operation/TeamSuperOperation.php @@ -0,0 +1,47 @@ +lowPriorityProcess([TeamEntityType::SUPER_ADMIN]); + + $this->teamManager->dropAll(); + $this->teamEntityManager->dropAll(); + $this->teamMemberManager->dropAll(); + $this->teamMembershipManager->dropAll(); + + $this->userConfig->deleteKey(Application::APP_ID, 'teamSingleId'); + $this->appConfig->deleteKey(Application::APP_ID, 'occSingleId'); + $this->appConfig->deleteKey(Application::APP_ID, 'superAdminSingleId'); + foreach($this->appConfig->searchValues('teamSingleId') as $appId => $value) { + $this->appConfig->deleteKey($appId, 'teamSingleId'); + } + } +} diff --git a/lib/Api/v2/TeamSession.php b/lib/Api/v2/TeamSession.php new file mode 100644 index 000000000..61b7cb9a2 --- /dev/null +++ b/lib/Api/v2/TeamSession.php @@ -0,0 +1,172 @@ + + * @copyright 2017 + * @license GNU AGPL version 3 or any later version + * + * 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 . + * + */ + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\Circles\Api\v2; + +use OCA\Circles\Api\v2\Operation\TeamEntityOperation; +use OCA\Circles\Api\v2\Operation\TeamMemberOperation; +use OCA\Circles\Api\v2\Operation\TeamMembershipOperation; +use OCA\Circles\Api\v2\Operation\TeamOperation; +use OCA\Circles\Api\v2\Operation\TeamSuperOperation; +use OCA\Circles\Model\TeamEntity; +use OCA\Circles\Service\TeamEntityService; +use OCP\IUser; +use OCP\IUserSession; + +// API/endpoint/occ => TeamSession -> Operation => Service -> (FederatedItem) -> Manager -> Mapper + +class TeamSession implements ITeamSession { + private ?TeamEntity $entity = null; + private ?ITeamOperation $initiatedTeamOperation = null; + private ?ITeamEntityOperation $initiatedTeamEntityOperation = null; + private ?ITeamMemberOperation $initiatedTeamMemberOperation = null; + private ?ITeamMembershipOperation $initiatedTeamMembershipOperation = null; + private ?ITeamSuperOperation $initiatedTeamSuperOperation = null; + + public function __construct( + private IUserSession $userSession, + private readonly TeamEntityService $teamEntityService, + private TeamOperation $teamOperation, + private TeamEntityOperation $teamEntityOperation, + private TeamMemberOperation $teamMemberOperation, + private TeamMembershipOperation $teamMembershipOperation, + private TeamSuperOperation $teamSuperOperation, + ) { + // by default, and if available, ITeamSession uses current user session + $user = $this->userSession?->getUser(); + if ($user !== null) { + $this->entity = $this->teamEntityService->generateTeamEntityFromUser($user); + } else if (\OC::$CLI) { + $this->entity = $this->teamEntityService->generateTeamEntityFromOcc(); + } + } + + public function sessionAsCurrentUser(): self { + $user = $this->userSession?->getUser(); + if ($user === null) { + return $this; + } + + return $this->sessionAsEntity($this->teamEntityService->generateTeamEntityFromUser($user)); + } + + public function sessionAsUser(IUser $user): self { + return $this->sessionAsEntity($this->teamEntityService->generateTeamEntityFromUser($user)); + } + + public function sessionAsLocalUser(string $userId): self { + return $this->sessionAsEntity($this->teamEntityService->generateTeamEntityFromLocalUser($userId)); + } + + public function sessionAsApp(string $appId): self { + return $this->sessionAsEntity($this->teamEntityService->generateTeamEntityFromApp($appId)); + } + + public function sessionAsOcc(): self { + return $this->sessionAsEntity($this->teamEntityService->generateTeamEntityFromOcc()); + } + + public function sessionAsSuperAdmin(): self { + return $this->sessionAsEntity($this->teamEntityService->generateTeamEntityFromSuperAdmin()); + } + + public function sessionAsEntity(TeamEntity $entity): self { + $session = clone $this; + $session->entity = $entity; + $this->initiatedTeamOperation = null; + $this->initiatedTeamEntityOperation = null; + $this->initiatedTeamMemberOperation = null; + $this->initiatedTeamMembershipOperation = null; + return $session; + } + + public function hasEntity(): bool { + return ($this->entity !== null); + } + + public function getEntity(): TeamEntity { + return $this->entity; + } + + public function performTeamOperation(): ITeamOperation { + if ($this->initiatedTeamOperation === null) { + $operation = clone $this->teamOperation; + $operation->fromSession($this); + $this->initiatedTeamOperation = $operation; + } + + return $this->initiatedTeamOperation; + } + + public function performTeamEntityOperation(): ITeamEntityOperation { + if ($this->initiatedTeamEntityOperation === null) { + if ($this->entity === null) { + return $this->teamEntityOperation; + } + $operation = clone $this->teamEntityOperation; + $operation->fromSession($this); + $this->initiatedTeamEntityOperation = $operation; + } + + return $this->initiatedTeamEntityOperation; + } + + public function performTeamMemberOperation(): ITeamMemberOperation { + if ($this->initiatedTeamMemberOperation === null) { + $operation = clone $this->teamMemberOperation; + $operation->fromSession($this); + $this->initiatedTeamMemberOperation = $operation; + } + + return $this->initiatedTeamMemberOperation; + } + + public function performTeamMembershipOperation(): ITeamMembershipOperation { + if ($this->initiatedTeamMembershipOperation === null) { + $operation = clone $this->teamMembershipOperation; + $operation->fromSession($this); + $this->initiatedTeamMembershipOperation = $operation; + } + + return $this->initiatedTeamMembershipOperation; + } + + public function performSuperOperation(): ITeamSuperOperation { + if ($this->initiatedTeamSuperOperation === null) { + $operation = clone $this->teamSuperOperation; + $operation->fromSession($this); + $this->initiatedTeamSuperOperation = $operation; + } + + return $this->initiatedTeamSuperOperation; + } +} diff --git a/lib/Command/Test.php b/lib/Command/Test.php new file mode 100644 index 000000000..17a237254 --- /dev/null +++ b/lib/Command/Test.php @@ -0,0 +1,146 @@ +setName('teams:test') + ->setDescription('create a new circle') +// ->addArgument('owner', InputArgument::REQUIRED, 'owner of the circle') +// ->addArgument('name', InputArgument::REQUIRED, 'name of the circle') + ->addOption('reset', '', InputOption::VALUE_NONE, 'reset all data'); +// ->addOption('local', '', InputOption::VALUE_NONE, 'create a local circle') +// ->addOption('status-code', '', InputOption::VALUE_NONE, 'display status code on exception') +// ->addOption( +// 'type', '', InputOption::VALUE_REQUIRED, 'type of the owner', +// Member::$TYPE[Member::TYPE_SINGLE] +// ); + } + + protected function execute(InputInterface $input, OutputInterface $output): int { +// $team = $this->teamSession->performTeamOperation()->getTeam('KRyQQW3d5OinYfT'); + + if ($input->getOption('reset')) { + $session = $this->teamSession->sessionAsSuperAdmin(); + $session->performSuperOperation()->dropAllData(); + $output->writeln('reset'); + return 0; + } +// + $entityOperation = $this->teamSession->performTeamEntityOperation(); + $test3 = $entityOperation->getFromLocalUser('test3'); + + $entityOperation = $this->teamSession->performTeamEntityOperation(); + $team1 = $this->teamSession->performTeamOperation()->createTeam( + 'this is a test 001', + $entityOperation->getFromLocalUser('test3') + ); + $team2 = $this->teamSession->performTeamOperation()->createTeam( + 'this is a test 002', + $team1->asEntity() + ); + $team3 = $this->teamSession->performTeamOperation()->createTeam( + 'this is a test 003', + $team2->asEntity() + ); + $team4 = $this->teamSession->performTeamOperation()->createTeam( + 'this is a test 001', + $team3->asEntity() + ); +// +// echo json_encode($team1, JSON_PRETTY_PRINT) ."\n"; +// echo json_encode($team2, JSON_PRETTY_PRINT) ."\n"; +// echo json_encode($team3, JSON_PRETTY_PRINT) ."\n"; +// echo json_encode($team4, JSON_PRETTY_PRINT) ."\n"; + +// $teamOperation = $this->teamSession->performTeamOperation(); +// $teams = $teamOperation->getTeams(); + +// return 0; + + $this->teamSession->performTeamMemberOperation()->addMember( + $team4, + $entityOperation->getFromLocalUser('test1') + ); + + + echo json_encode($this->teamSession->performTeamOperation()->getTeam($team2->getSingleId()), JSON_PRETTY_PRINT) . "\n"; + $session = $this->teamSession->sessionAsSuperAdmin(); + $membershipOperation = $session->performTeamMembershipOperation(); + $membershipOperation->syncTeamMemberships($team1->getSingleId()); + + //echo json_encode($teams, JSON_PRETTY_PRINT) . "\n"; + + return 0; + + $ownerId = $input->getArgument('owner'); + $name = $input->getArgument('name'); + + try { + $this->federatedUserService->bypassCurrentUserCondition(true); + + $type = Member::parseTypeString($input->getOption('type')); + + $owner = $this->federatedUserService->getFederatedUser($ownerId, $type); + $outcome = $this->circleService->create( + $name, + $owner, + $input->getOption('personal'), + $input->getOption('local') + ); + } catch (FederatedItemException $e) { + if ($input->getOption('status-code')) { + throw new FederatedItemException( + ' [' . get_class($e) . ', ' . $e->getStatus() . ']' . "\n" . $e->getMessage() + ); + } + + throw $e; + } + + if (strtolower($input->getOption('output')) === 'json') { + $output->writeln(json_encode($outcome, JSON_PRETTY_PRINT)); + } elseif (strtolower($input->getOption('output')) !== 'none') { + /** @var Circle $circle */ + $circle = $this->deserialize($outcome, Circle::class); + $output->writeln('Id: ' . $circle->getSingleId() . ''); + $output->writeln('Name: ' . $circle->getDisplayName() . ''); + $output->writeln('Owner: ' . $circle->getOwner()->getDisplayName() . ''); + } + + return 0; + } +} diff --git a/lib/Controller/LocalController.php b/lib/Controller/LocalController.php index dc8e4a096..fce3d1471 100644 --- a/lib/Controller/LocalController.php +++ b/lib/Controller/LocalController.php @@ -423,6 +423,34 @@ public function circles(int $limit = -1, int $offset = 0): DataResponse { } + + /** + * @NoAdminRequired + * + * @param int $limit + * @param int $offset + * + * @return DataResponse + * @throws OCSException + */ + public function currentCircles(int $limit = -1, int $offset = 0): DataResponse { + try { + $this->setCurrentFederatedUser(); + + $probe = new CircleProbe(); + $probe->filterHiddenCircles() + ->filterBackendCircles() + ->setItemsLimit($limit) + ->setItemsOffset($offset); + + return new DataResponse($this->serializeArray($this->circleService->probeCircles($probe))); + } catch (Exception $e) { + $this->e($e); + throw new OCSException($e->getMessage(), (int)$e->getCode()); + } + } + + /** * @NoAdminRequired * diff --git a/lib/Db/CoreMapper.php b/lib/Db/CoreMapper.php new file mode 100644 index 000000000..a911cf017 --- /dev/null +++ b/lib/Db/CoreMapper.php @@ -0,0 +1,58 @@ +> [class => list] */ + private array $fields = []; + + /** + * returns the list of database table field names from an Entity + * + * @return string[] + */ + final protected function getFields(string $entityClass): array { + if (!array_key_exists($entityClass, $this->fields)) { + $entity = new $entityClass(); + if ($entity instanceof Entity) { + $this->fields[$entityClass] = array_map(static function(string $property) use ($entity): string { + return $entity->propertyToColumn($property); + }, array_keys($entity->getFieldTypes())); + } else { + Server::get(LoggerInterface::class)->notice('calling CoreMapper::getFields() with a non-Entity model', ['entityClass' => $entityClass]); + $this->fields[$entityClass] = []; + } + } + + return $this->fields[$entityClass]; + } + + /** + * Setting $useMemberships to FALSE will make your request heavier ! + * + * @param TeamEntity|null $initiator + * @param IQueryBuilder $qb + * @param bool $useMemberships set to FALSE to bypass memberships structured caching, meaning making heavier request + */ + protected function limitToInitiator(?TeamEntity $initiator, IQueryBuilder $qb, bool $useMemberships = true): void { + if ($initiator === null) { + return; + } + + // TODO finish, based on useMemberships + } +} diff --git a/lib/Db/TeamEntityMapper.php b/lib/Db/TeamEntityMapper.php new file mode 100644 index 000000000..021a54094 --- /dev/null +++ b/lib/Db/TeamEntityMapper.php @@ -0,0 +1,104 @@ + + */ +class TeamEntityMapper extends CoreMapper { + public const TABLE = 'teams_entities'; + private array $fields = []; + + public function __construct( + IDBConnection $db, + private readonly LoggerInterface $logger, + ) { + parent::__construct($db, self::TABLE, TeamEntity::class); + } + + public function getBySingleId(string $singleId) { + $qb = $this->db->getQueryBuilder(); + $qb->select('*') + ->from($this->getTableName()) + ->where($qb->expr()->eq('single_id', $qb->createNamedParameter($singleId))); + + try { + return $this->findEntity($qb); + } catch (DoesNotExistException) { + throw new TeamEntityNotFoundException('no team entity found'); + } + } + + public function getByOrigId(TeamEntityType $type, string $origId) { + $qb = $this->db->getQueryBuilder(); + $qb->select('*') + ->from($this->getTableName()) + ->where($qb->expr()->eq('single_id', $qb->createNamedParameter($type->value))) + ->where($qb->expr()->eq('orig_id', $qb->createNamedParameter($origId))); + + try { + return $this->findEntity($qb); + } catch (DoesNotExistException) { + throw new TeamEntityNotFoundException('no team entity found'); + } + } + + public function joinTeamEntity( + IQueryBuilder $qb, + string $aliasSingleId, + string $fieldSingleId, + string $aliasDetails = 'te', + bool $leftJoin = false, + ): void { + foreach ($this->getFields(TeamEntity::class) as $field) { + $qb->selectAlias($aliasDetails . '.' . $field, $aliasDetails . '_' . $field); + } + + $join = ($leftJoin) ? 'leftJoin' : 'innerJoin'; + $qb->$join( + $aliasSingleId, + self::TABLE, + $aliasDetails, + $qb->expr()->eq($aliasSingleId . '.' . $fieldSingleId, $aliasDetails . '.single_id') + ); + } + + /** + * @throws TeamEntityNotFoundException + */ + public function createTeamEntityFromRow(array $row, string $alias = ''): TeamEntity { + $new = []; + foreach($this->getFields(TeamEntity::class) as $field) { + $new[$field] = $row[$alias . $field]; + } + + if ($new['id'] === null) { + throw new TeamEntityNotFoundException(); + } + + return $this->mapRowToEntity($new); + } + + public function emptyTable() { + $qb = $this->db->getQueryBuilder(); + $qb->delete(self::TABLE); + $qb->executeStatement(); + } +} diff --git a/lib/Db/TeamMapper.php b/lib/Db/TeamMapper.php new file mode 100644 index 000000000..bd9374d7f --- /dev/null +++ b/lib/Db/TeamMapper.php @@ -0,0 +1,93 @@ + + */ +class TeamMapper extends CoreMapper { + public const TABLE = 'teams'; + private array $fields = []; + + public function __construct( + IDBConnection $db, + private readonly LoggerInterface $logger, + ) { + parent::__construct($db, self::TABLE, Team::class); + } + + public function getBySingleId(string $singleId): Team { + $qb = $this->db->getQueryBuilder(); + $qb->select('*') + ->from($this->getTableName()) + ->where($qb->expr()->eq('single_id', $qb->createNamedParameter($singleId))); + + try { + return $this->findEntity($qb); + } catch (DoesNotExistException) { + throw new TeamNotFoundException('no team found'); + } + } + + public function joinTeam( + IQueryBuilder $qb, + string $aliasSingleId, + string $fieldSingleId, + string $aliasDetails = 't', + bool $leftJoin = false + ): void { + foreach ($this->getFields(Team::class) as $field) { + $qb->selectAlias($aliasDetails . '.' . $field, $aliasDetails . '_' . $field); + } + + $join = ($leftJoin) ? 'leftJoin' : 'innerJoin'; + $qb->$join( + $aliasSingleId, + self::TABLE, + $aliasDetails, + $qb->expr()->eq($aliasSingleId . '.' . $fieldSingleId, $aliasDetails . '.single_id') + ); + } + + /** + * @throws TeamNotFoundException + */ + public function createTeamFromRow(array $row, string $alias = ''): Team { + $new = []; + foreach($this->getFields(Team::class) as $field) { + $new[$field] = $row[$alias . $field]; + } + + if ($new['id'] === null) { + throw new TeamNotFoundException(); + } + + return $this->mapRowToEntity($new); + } + + public function emptyTable() { + $qb = $this->db->getQueryBuilder(); + $qb->delete(self::TABLE); + $qb->executeStatement(); + } +} diff --git a/lib/Db/TeamMemberMapper.php b/lib/Db/TeamMemberMapper.php new file mode 100644 index 000000000..1c84a40cf --- /dev/null +++ b/lib/Db/TeamMemberMapper.php @@ -0,0 +1,92 @@ + + */ +class TeamMemberMapper extends CoreMapper { + public const TABLE = 'teams_members'; + + private array $fields = []; + + public function __construct( + IDBConnection $db, + private readonly TeamMapper $teamMapper, + private readonly TeamEntityMapper $teamEntityMapper, + private readonly LoggerInterface $logger, + ) { + parent::__construct($db, self::TABLE, TeamMember::class); + } + + /** + * @return TeamMember[] + */ + public function getTeamMembers(?TeamEntity $initiator, string $teamSingleId): array { + $qb = $this->db->getQueryBuilder(); + $qb->select('tm.*') + ->from($this->getTableName(), 'tm') + ->where($qb->expr()->eq('team_single_id', $qb->createNamedParameter($teamSingleId))); + + $this->limitToInitiator($initiator, $qb, false); + $this->teamEntityMapper->joinTeamEntity($qb, 'tm', 'member_single_id', 'te'); + + $result = $qb->executeQuery(); + $teamMembers = []; + while ($row = $result->fetch()) { + $teamMember = $this->createTeamMemberFromRow($row); + $teamMember->setEntity($this->teamEntityMapper->createTeamEntityFromRow($row, 'te_')); + $teamMembers[] = $teamMember; + } + + return $teamMembers; + } + + + public function getEntityTeams(?TeamEntity $initiator, string $singleId): array { + $qb = $this->db->getQueryBuilder(); + $qb->select('tm.*') + ->from($this->getTableName(), 'tm') + ->where($qb->expr()->eq('member_single_id', $qb->createNamedParameter($singleId))); + + $this->limitToInitiator($initiator, $qb, false); + $this->teamMapper->joinTeam($qb, 'tm', 'team_single_id', 't'); + + $result = $qb->executeQuery(); + $teamMembers = []; + while ($row = $result->fetch()) { + $teamMember = $this->createTeamMemberFromRow($row); + $teamMember->setTeam($this->teamMapper->createTeamFromRow($row, 't_')); + $teamMembers[] = $teamMember; + } + + return $teamMembers; + } + + public function createTeamMemberFromRow(array $row, string $alias = ''): TeamMember { + $new = []; + foreach($this->getFields(TeamMember::class) as $field) { + $new[$field] = $row[$alias . $field]; + } + + return $this->mapRowToEntity($new); + } + + public function emptyTable() { + $qb = $this->db->getQueryBuilder(); + $qb->delete(self::TABLE); + $qb->executeStatement(); + } +} diff --git a/lib/Db/TeamMembershipMapper.php b/lib/Db/TeamMembershipMapper.php new file mode 100644 index 000000000..13aacf796 --- /dev/null +++ b/lib/Db/TeamMembershipMapper.php @@ -0,0 +1,71 @@ + + */ +class TeamMembershipMapper extends QBMapper { + public const TABLE = 'teams_memberships'; + + private array $fields = []; + + public function __construct( + IDBConnection $db, + private readonly LoggerInterface $logger, + ) { + parent::__construct($db, self::TABLE, TeamMembership::class); + } + + public function getMembershipsRelatedToEntity(string $singleId): array { + $qb = $this->db->getQueryBuilder(); + $qb->select('*') + ->from($this->getTableName()) + ->where($qb->expr()->eq('single_id', $qb->createNamedParameter($singleId))); + + return $this->findEntities($qb); + } + + public function getByTeamAndEntity(string $teamSingleId, string $singleId): TeamMembership { + $qb = $this->db->getQueryBuilder(); + $qb->select('*') + ->from($this->getTableName()) + ->where($qb->expr()->eq('single_id', $qb->createNamedParameter($singleId))) + ->andWhere($qb->expr()->eq('team_single_id', $qb->createNamedParameter($teamSingleId))); + + try { + return $this->findEntity($qb); + } catch (DoesNotExistException) { + throw new TeamMembershipNotFoundException('no team membership found'); + } + } + + public function removeMembership(string $teamSingleId, string $singleId): void { + $qb = $this->db->getQueryBuilder(); + $qb->delete($this->getTableName()) + ->where($qb->expr()->eq('single_id', $qb->createNamedParameter($singleId))) + ->andWhere($qb->expr()->eq('team_single_id', $qb->createNamedParameter($teamSingleId))); + + $qb->executeStatement(); + } + + public function emptyTable() { + $qb = $this->db->getQueryBuilder(); + $qb->delete(self::TABLE); + $qb->executeStatement(); + } +} diff --git a/lib/Enum/TeamApi.php b/lib/Enum/TeamApi.php new file mode 100644 index 000000000..9cfc35c95 --- /dev/null +++ b/lib/Enum/TeamApi.php @@ -0,0 +1,15 @@ +getTeam(); + + $event->setOutcome($team->jsonSerialize()); + } + + /** + * @param FederatedEvent $event + * + * @throws FederatedEventDSyncException + * @throws InvalidIdException + * @throws RequestBuilderException + */ + public function manage(FederatedEvent $event): void { + $team = $event->getTeam(); + $owner = $team->getOwner(); + // + // + // + // + // + try { + $this->teamManager->getTeam($team->getSingleId()); + throw new FederatedEventDSyncException('Circle already exist'); + } catch (CircleNotFoundException $e) { + } + + $this->teamManager->confirmNaming($team); + + try { + $this->teamEntityManager->getTeamEntity($owner->getMemberSingleId()); + throw new FederatedEventDSyncException('Owner already exist'); + } catch (MemberNotFoundException $e) { + } + + if ($owner->hasInvitedBy()) { + $owner->setNoteObj('invitedBy', $owner->getInvitedBy()); + } + + $this->circleRequest->save($circle); + $this->memberRequest->save($owner); + + $this->membershipService->onUpdate($owner->getSingleId()); + $this->membershipService->updatePopulation($circle); + + $this->eventService->circleCreating($event); + } + + /** + * @param FederatedEvent $event + * @param array $results + */ + public function result(FederatedEvent $event, array $results): void { + $this->eventService->circleCreated($event, $results); + } +} diff --git a/lib/Managers/CoreManager.php b/lib/Managers/CoreManager.php new file mode 100644 index 000000000..e2aac1656 --- /dev/null +++ b/lib/Managers/CoreManager.php @@ -0,0 +1,64 @@ +getEntity(); + } + + return $initiator; + } + + /** + * returns NULL if initiator have super permissions (occ, super_admin) + */ + final protected function filterInitiator(TeamSession|TeamEntity $initiator): ?TeamEntity { + $initiator = $this->extractTeamEntity($initiator); + + if (in_array($initiator->getTeamEntityType(), [ + TeamEntityType::OCC, + TeamEntityType::SUPER_ADMIN, + ], true)) { + return null; + } + + return $initiator; + } + + /** + * @throws TeamEntityOverwritePermissionException + * @throws TeamMembershipNotFoundException + */ + final protected function getInitiatorMembership(TeamEntity|TeamSession $initiator, string $teamSingleId): TeamMembership { + $initiatorEntity = $this->filterInitiator($initiator); + if ($initiatorEntity === null) { + throw new TeamEntityOverwritePermissionException('initiator is above everyone, bypassing membership check'); + } + + return Server::get(TeamMembershipMapper::class)->getByTeamAndEntity($teamSingleId, $initiatorEntity->getSingleId()); + } + +} diff --git a/lib/Managers/TeamEntityManager.php b/lib/Managers/TeamEntityManager.php new file mode 100644 index 000000000..30e6ae84a --- /dev/null +++ b/lib/Managers/TeamEntityManager.php @@ -0,0 +1,92 @@ + + * @copyright 2017 + * @license GNU AGPL version 3 or any later version + * + * 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 . + * + */ + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\Circles\Managers; + +use OCA\Circles\Db\TeamEntityMapper; +use OCA\Circles\Enum\TeamEntityType; +use OCA\Circles\Exceptions\TeamEntityNotFoundException; +use OCA\Circles\Model\TeamEntity; +use OCA\Circles\Service\ToolsService; + +class TeamEntityManager { + /** @var TeamEntity[] */ + private array $entities = []; + + public function __construct( + private readonly ToolsService $toolsService, + private readonly TeamEntityMapper $teamEntityMapper, + ) { + } + + /** + * @throws TeamEntityNotFoundException + */ + public function getTeamEntity(string $singleId): TeamEntity { + if (!array_key_exists($singleId, $this->entities)) { + $this->entities[$singleId] = $this->teamEntityMapper->getBySingleId($singleId); + } + + return $this->entities[$singleId]; + } + + /** + * @throws TeamEntityNotFoundException + */ + public function searchTeamEntity(TeamEntityType $type, string $origId): TeamEntity { + return $this->teamEntityMapper->getByOrigId($type, $origId); + } + + public function createTeamEntity( + TeamEntityType $type, + string $origId, + string $displayName, + ?string $singleId = null + ): TeamEntity { + $teamEntity = new TeamEntity(); + $teamEntity->setSingleId($singleId ?? $this->toolsService->generateSingleId()); + $teamEntity->setTeamEntityType($type); + $teamEntity->setOrigId($origId); + $teamEntity->setDisplayName($displayName); + $this->teamEntityMapper->insert($teamEntity); + + return $teamEntity; + } + + public function save(TeamEntity $teamEntity): void { + // TODO: do not store teamentity if type === TEAM + } + + public function dropAll() { + $this->teamEntityMapper->emptyTable(); + } +} diff --git a/lib/Managers/TeamManager.php b/lib/Managers/TeamManager.php new file mode 100644 index 000000000..c57e5f23b --- /dev/null +++ b/lib/Managers/TeamManager.php @@ -0,0 +1,81 @@ + + * @copyright 2017 + * @license GNU AGPL version 3 or any later version + * + * 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 . + * + */ + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\Circles\Managers; + + +use OCA\Circles\Db\TeamMapper; +use OCA\Circles\Exceptions\TeamNotFoundException; +use OCA\Circles\Model\Circle; +use OCA\Circles\Model\Team; + +class TeamManager { + public function __construct( + private TeamMapper $teamMapper, + ) { + } + + /** + * @throws TeamNotFoundException + */ + public function getTeam(string $singleId): Team { + return $this->teamMapper->getBySingleId($singleId); + } + + public function confirmNaming(Team $team): void { + if ($team->isConfig(Circle::CFG_SYSTEM) + || $team->isConfig(Circle::CFG_SINGLE)) { + return; + } + +// $this->confirmDisplayName($circle); +// $this->generateSanitizedName($circle); + } + +// public function createTeam( +// TeamEntityType $type, +// string $origId, +// string $displayName, +// ): TeamEntity { +// $teamEntity = new TeamEntity(); +// $teamEntity->setTeamEntityType($type); +// $teamEntity->setOrigId($origId); +// $teamEntity->setDisplayName($displayName); +// $this->teamEntityMapper->insert($teamEntity); +// +// return $teamEntity; +// } + + public function dropAll() { + $this->teamMapper->emptyTable(); + } +} diff --git a/lib/Managers/TeamMemberManager.php b/lib/Managers/TeamMemberManager.php new file mode 100644 index 000000000..d909bbb3c --- /dev/null +++ b/lib/Managers/TeamMemberManager.php @@ -0,0 +1,104 @@ + + * @copyright 2017 + * @license GNU AGPL version 3 or any later version + * + * 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 . + * + */ + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\Circles\Managers; + +use OCA\Circles\Api\v2\TeamSession; +use OCA\Circles\Db\TeamEntityMapper; +use OCA\Circles\Db\TeamMemberMapper; +use OCA\Circles\Db\TeamMembershipMapper; +use OCA\Circles\Enum\TeamEntityType; +use OCA\Circles\Enum\TeamMemberLevel; +use OCA\Circles\Exceptions\TeamEntityNotFoundException; +use OCA\Circles\Exceptions\TeamEntityOverwritePermissionException; +use OCA\Circles\Exceptions\TeamMembershipNotFoundException; +use OCA\Circles\Model\TeamEntity; +use OCA\Circles\Model\TeamMember; +use OCA\Circles\Model\TeamMembership; +use OCA\Circles\Service\ToolsService; + +class TeamMemberManager extends CoreManager { + public function __construct( + private readonly ToolsService $toolsService, + private readonly TeamMemberMapper $teamMemberMapper, + private readonly TeamMembershipMapper $teamMembershipMapper, + ) { + } + + public function getTeamMember(TeamSession|TeamEntity $initiator, string $teamSingleId, string $memberSingleId): TeamMember { + // return $this->teamMemberMapper->getByTeamAndMember($teamSingleId, $memberSingleId); + } + + + public function addMember(TeamSession|TeamEntity $initiator, string $teamSingleId, string $singleId): TeamMember { + try { + $initiatorMembership = $this->getInitiatorMembership($initiator, $teamSingleId); + if ($initiatorMembership->getLevel() < TeamMemberLevel::MODERATOR->value) { + throw new \Exception('not enough permissions'); // TODO: real exception + } + } catch (TeamEntityOverwritePermissionException) { + } + + $teamMember = new TeamMember(); + $teamMember->setTeamSingleId($teamSingleId); + $teamMember->setMemberSingleId($singleId); + $teamMember->setTeamMemberLevel(TeamMemberLevel::MEMBER); + $teamMember->setInvitedBy($this->extractTeamEntity($initiator)); + $this->teamMemberMapper->insert($teamMember); + + return $teamMember; + } + + /** + * @return TeamMember[] + */ + public function getMembersFromTeam(TeamSession|TeamEntity $initiator, string $teamSingleId): array { + return $this->teamMemberMapper->getTeamMembers( + $this->filterInitiator($initiator), + $teamSingleId + ); + } + + /** + * @return TeamMember[] + */ + public function getTeamsContainingEntity(TeamSession|TeamEntity $initiator, $singleId) { + return $this->teamMemberMapper->getEntityTeams( + $this->filterInitiator($initiator), + $singleId + ); + } + + public function dropAll() { + $this->teamMemberMapper->emptyTable(); + } +} diff --git a/lib/Managers/TeamMembershipManager.php b/lib/Managers/TeamMembershipManager.php new file mode 100644 index 000000000..c9b7f8dd2 --- /dev/null +++ b/lib/Managers/TeamMembershipManager.php @@ -0,0 +1,81 @@ + + * @copyright 2017 + * @license GNU AGPL version 3 or any later version + * + * 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 . + * + */ + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\Circles\Managers; + +use OCA\Circles\Db\TeamEntityMapper; +use OCA\Circles\Db\TeamMemberMapper; +use OCA\Circles\Db\TeamMembershipMapper; +use OCA\Circles\Enum\TeamEntityType; +use OCA\Circles\Exceptions\TeamEntityNotFoundException; +use OCA\Circles\Model\TeamEntity; +use OCA\Circles\Model\TeamMember; +use OCA\Circles\Model\TeamMembership; +use OCA\Circles\Service\ToolsService; + +class TeamMembershipManager { + public function __construct( + private readonly ToolsService $toolsService, + private readonly TeamMembershipMapper $teamMembershipMapper, + ) { + } + + /** + * @return TeamMembership[] + */ + public function getMemberships(string $singleId): array { + return $this->teamMembershipMapper->getMembershipsRelatedToEntity($singleId); + } + + public function insertMembership(TeamMembership $teamMembership): void { + $this->teamMembershipMapper->insert($teamMembership); + } + + public function overwriteMembership(int $id, TeamMembership $teamMembership): void { + $teamMembership->setId($id); + $this->teamMembershipMapper->update($teamMembership); + } + + public function removeMembership(TeamMembership $teamMembership): void { + $this->teamMembershipMapper->removeMembership($teamMembership->getSingleId(), $teamMembership->getTeamSingleId()); + } + + public function removeSingleId(string $singleId): void { + //$this->teamMembershipMapper->remove + } + + public function dropAll() { + $this->teamMembershipMapper->emptyTable(); + } + + +} diff --git a/lib/Migration/Version3200Date20250115140201.php b/lib/Migration/Version3200Date20250115140201.php new file mode 100644 index 000000000..486c46abe --- /dev/null +++ b/lib/Migration/Version3200Date20250115140201.php @@ -0,0 +1,273 @@ +hasTable('teams')) { + $table = $schema->createTable('teams'); + $table->addColumn( + 'id', Types::INTEGER, [ + 'autoincrement' => true, + 'notnull' => true, + 'length' => 4, + 'unsigned' => true, + ] + ); + $table->addColumn( + 'single_id', Types::STRING, [ + 'notnull' => true, + 'length' => 31, + ] + ); + $table->addColumn( + 'display_name', Types::STRING, [ + 'notnull' => false, + 'default' => '', + 'length' => 255 + ] + ); + $table->addColumn( + 'sanitized_name', Types::STRING, [ + 'notnull' => false, + 'default' => '', + 'length' => 127 + ] + ); + $table->addColumn( + 'config', Types::INTEGER, [ + 'notnull' => false, + 'length' => 11, + 'default' => 0, + 'unsigned' => true + ] + ); + $table->addColumn( + 'settings', Types::TEXT, [ + 'notnull' => false, + 'default' => '[]', + ] + ); + $table->addColumn( + 'metadata', Types::TEXT, [ + 'notnull' => false, + 'default' => '[]', + ] + ); + $table->addColumn( + 'creation', Types::INTEGER, [ + 'notnull' => false, + 'default' => 0, + ] + ); + + $table->setPrimaryKey(['id']); + $table->addUniqueIndex(['single_id']); + $table->addIndex(['display_name']); + $table->addUniqueIndex(['sanitized_name']); + $table->addIndex(['config']); + $table->addIndex(['creation']); + + $modified = true; + } + + if (!$schema->hasTable('teams_entities')) { + $table = $schema->createTable('teams_entities'); + $table->addColumn( + 'id', Types::INTEGER, [ + 'autoincrement' => true, + 'notnull' => true, + 'length' => 4, + 'unsigned' => true, + ] + ); + $table->addColumn( + 'single_id', Types::STRING, [ + 'notnull' => true, + 'length' => 31, + ] + ); + $table->addColumn( + 'type', Types::INTEGER, [ + 'notnull' => false, + 'length' => 4, + 'default' => 0, + 'unsigned' => true, + ] + ); + $table->addColumn( + 'orig_id', Types::STRING, [ + 'notnull' => false, + 'default' => '', + 'length' => 127, + ] + ); + $table->addColumn( + 'display_name', Types::STRING, [ + 'notnull' => false, + 'default' => '', + 'length' => 255 + ] + ); + $table->setPrimaryKey(['id']); + $table->addUniqueIndex(['single_id']); + $table->addUniqueIndex(['type', 'orig_id']); + + $modified = true; + } + + if (!$schema->hasTable('teams_members')) { + $table = $schema->createTable('teams_members'); + $table->addColumn( + 'id', Types::INTEGER, [ + 'autoincrement' => true, + 'notnull' => true, + 'length' => 4, + 'unsigned' => true, + ] + ); + $table->addColumn( + 'team_single_id', Types::STRING, [ + 'notnull' => true, + 'length' => 31, + ] + ); + $table->addColumn( + 'member_single_id', Types::STRING, [ + 'notnull' => true, + 'length' => 31, + ] + ); + $table->addColumn( + 'invited_by_single_id', Types::STRING, [ + 'notnull' => true, + 'length' => 31, + ] + ); + $table->addColumn( + 'level', Types::INTEGER, [ + 'notnull' => false, + 'default' => 0, + 'length' => 2 + ] + ); + $table->addColumn( + 'metadata', Types::TEXT, [ + 'notnull' => false, + 'default' => '[]', + ] + ); + $table->addColumn( + 'creation', Types::INTEGER, [ + 'notnull' => false, + 'default' => 0, + ] + ); + + $table->setPrimaryKey(['id']); + $table->addUniqueIndex(['team_single_id', 'member_single_id']); + $table->addIndex(['level', 'creation']); + + $modified = true; + } + + if (!$schema->hasTable('teams_memberships')) { + $table = $schema->createTable('teams_memberships'); + $table->addColumn( + 'id', Types::INTEGER, [ + 'autoincrement' => true, + 'notnull' => true, + 'length' => 4, + 'unsigned' => true, + ] + ); + $table->addColumn( + 'single_id', Types::STRING, [ + 'notnull' => true, + 'length' => 31, + ] + ); + $table->addColumn( + 'team_single_id', Types::STRING, [ + 'notnull' => true, + 'length' => 31, + ] + ); + //inheritance_path + $table->addColumn( + 'level', Types::INTEGER, [ + 'notnull' => false, + 'default' => 0, + 'length' => 2 + ] + ); + $table->addColumn( + 'inheritance_first', Types::STRING, [ + 'notnull' => true, + 'length' => 31, + 'default' => '', + ] + ); + $table->addColumn( + 'inheritance_last', Types::STRING, [ + 'notnull' => true, + 'length' => 31, + 'default' => '', + ] + ); + $table->addColumn( + 'inheritance_depth', Types::INTEGER, [ + 'notnull' => true, + 'length' => 2, + 'unsigned' => true, + 'default' => 0, + ] + ); + $table->addColumn( + 'inheritance_path', Types::TEXT, [ + 'notnull' => true, + 'default' => '[]', + ] + ); + + $table->setPrimaryKey(['id']); + $table->addUniqueIndex(['single_id', 'team_single_id']); + $table->addUniqueIndex(['inheritance_first', 'inheritance_last', 'team_single_id'], 'teams_mbs_iit'); + $table->addIndex(['single_id']); + $table->addIndex(['team_single_id']); + + $modified = true; + } + + + + if (!$modified) { + return null; + } + + return $schema; + } +} diff --git a/lib/Model/Federated/FederatedEvent.php b/lib/Model/Federated/FederatedEvent.php index 0a3370613..8f492c62f 100644 --- a/lib/Model/Federated/FederatedEvent.php +++ b/lib/Model/Federated/FederatedEvent.php @@ -12,8 +12,10 @@ namespace OCA\Circles\Model\Federated; use JsonSerializable; +use OCA\Circles\Exceptions\TeamNotFoundException; use OCA\Circles\Model\Circle; use OCA\Circles\Model\Member; +use OCA\Circles\Model\Team; use OCA\Circles\Tools\Exceptions\InvalidItemException; use OCA\Circles\Tools\Model\SimpleDataStore; use OCA\Circles\Tools\Traits\TArrayTools; @@ -96,12 +98,8 @@ class FederatedEvent implements JsonSerializable { private bool $forceSync = false; + private ?Team $team = null; - /** - * FederatedEvent constructor. - * - * @param string $class - */ public function __construct(string $class = '') { $this->class = $class; $this->params = new SimpleDataStore(); @@ -275,6 +273,22 @@ public function getCircle(): Circle { } + + public function setTeam(Team $team): self { + $this->team = $team; + + return $this; + } + + public function getTeam(): Team { + if ($this->team === null) { + throw new TeamNotFoundException('Team not found in FederatedEvent'); + } + + return $this->team; + } + + /** * @param string $itemId * diff --git a/lib/Model/Team.php b/lib/Model/Team.php new file mode 100644 index 000000000..7db4b96db --- /dev/null +++ b/lib/Model/Team.php @@ -0,0 +1,140 @@ +addType('singleId', 'string'); + $this->addType('displayName', 'string'); + $this->addType('sanitizedName', 'string'); + $this->addType('config', 'integer'); + $this->addType('settings', 'json'); + $this->addType('metadata', 'json'); + $this->addType('creation', 'integer'); + } + + public function getOwner(): TeamEntity { + if ($this->owner === null) { + $owner = new TeamEntity(); + $owner->import($this->metadata['_owner'] ?? []); + if ($owner->isValid()) { + $this->owner = $owner; + } + } + + if ($this->owner === null) { + throw new TeamOwnerNotFoundException(); + } + + return $this->owner; + } + + public function setOwner(TeamEntity $owner): void { + $this->owner = $owner; + $this->setMetaValue('_owner', $owner->jsonSerialize()); + } + + /** + * only returns metadata that does not start with underscore '_' + */ + public function getFilteredMetadata(): array { + return array_filter($this->metadata, static fn(string $k): bool => !str_starts_with($k, '_'), ARRAY_FILTER_USE_KEY); + } + + public function setMetaValue(string $key, string|int|float|bool|array $value): void { + $metadata = $this->metadata; + $metadata[$key] = $value; + $this->setter('metadata', [$metadata]); + } + + public function getMembers(null|TeamSession|TeamEntity $initiator = null): ?array { + $initiator ??= $this->lazyLoadingEntity; + if ($initiator === null) { + return null; + } + + return $this->getTeamMemberManager()->getMembersFromTeam($initiator, $this->getSingleId()); + } + + public function asEntity(): TeamEntity { + return new TeamEntity($this); + } + + /** + * @return array + */ + public function jsonSerialize(): array { + if ($this->isApiVersion(TeamApi::V2)) { + return [ + 'singleId' => $this->getSingleId(), + 'displayName' => $this->getDisplayName(), + 'sanitizedName' => $this->getSanitizedName(), + 'config' => $this->getConfig(), + 'settings' => $this->getSettings(), + 'metadata' => $this->getFilteredMetadata(), + 'members' => $this->getMembers(), + 'creation' => $this->getCreation(), + 'owner' => $this->metadata['_owner'] ?? [] + ]; + } + + if ($this->isApiVersion(TeamApi::V1)) { + return []; + } + + return [ + 'singleId' => $this->getSingleId(), + 'displayName' => $this->getDisplayName(), + 'sanitizedName' => $this->getSanitizedName(), + 'config' => $this->getConfig(), + 'settings' => $this->getSettings(), + 'metadata' => $this->getMetadata(), + 'members' => $this->getMembers(), + 'creation' => $this->getCreation(), + 'owner' => $this->metadata['_owner'] ?? [] + ]; + } +} diff --git a/lib/Model/TeamCore.php b/lib/Model/TeamCore.php new file mode 100644 index 000000000..a601a39ac --- /dev/null +++ b/lib/Model/TeamCore.php @@ -0,0 +1,59 @@ +lazyLoadingEntity = $entity; + } + + public function getTeamEntityManager(): ?TeamEntityManager { + if ($this->teamEntityManager === null) { + $this->teamEntityManager = Server::get(TeamEntityManager::class); + } + + return $this->teamEntityManager; + } + + public function getTeamMemberManager(): TeamMemberManager { + if ($this->teamMemberManager === null) { + $this->teamMemberManager = Server::get(TeamMemberManager::class); + } + + return $this->teamMemberManager; + } + + protected function isApiVersion(TeamApi $api): bool { + return ($api === $this->getApiVersion()); + } + + protected function getApiVersion(): TeamApi { + if (!defined('USING_TEAMS_API')) { + return self::DEFAULT_TEAMS_API; + } + + return USING_TEAMS_API; + } +} diff --git a/lib/Model/TeamEntity.php b/lib/Model/TeamEntity.php new file mode 100644 index 000000000..d7ed7e088 --- /dev/null +++ b/lib/Model/TeamEntity.php @@ -0,0 +1,114 @@ +addType('singleId', 'string'); + $this->addType('type', 'integer'); + $this->addType('origId', 'string'); + $this->addType('displayName', 'string'); + + if ($orig === null) { + return; + } + + if ($orig instanceof Team) { + $this->setSingleId($orig->getSingleId()); + $this->setTeamEntityType(TeamEntityType::TEAM); + $this->setOrigId($orig->getSingleId()); + $this->setDisplayName($orig->getDisplayName()); + } + } + + public function getTeamEntityType(): TeamEntityType { + return TeamEntityType::from($this->getType()); + } + + public function setTeamEntityType(TeamEntityType $type): void { + $this->setType($type->value); + } + + public function getDisplayName(): string { + if ($this->displayName === '' && $this->singleId !== '') { + try { + $lazyTeamEntity = $this->getTeamEntityManager()?->getTeamEntity($this->singleId); + $this->displayName = $lazyTeamEntity?->getDisplayName() ?? ''; + } catch (TeamEntityNotFoundException) { + } + } + + return $this->displayName; + } + + public function isTeam(): bool { + return ($this->getTeamEntityType() === TeamEntityType::TEAM); + } + + public function asTeam(): Team { + // TODO: generate a team from current entity; + } + + public function import(array $data): void { + $this->setSingleId($data['singleId']); + $this->setType($data['type']); + $this->setOrigId($data['origId']); + $this->setDisplayName($data['displayName']); + } + + public function isValid(): bool { + return true; // TODO + } + + /** + * @return array + */ + public function jsonSerialize(bool $ignoreApi = false): array { + + if (!$ignoreApi && $this->isApiVersion(TeamApi::V2)) { + return [ + 'singleId' => $this->getSingleId(), + 'displayName' => $this->getDisplayName(), + 'type' => $this->getType(), + ]; + } + + if (!$ignoreApi && $this->isApiVersion(TeamAPI::V1)) { + return []; // TODO: old API compat + } + + return [ + 'singleId' => $this->getSingleId(), + 'type' => $this->getType(), + 'origId' => $this->getOrigId(), + 'displayName' => $this->getDisplayName(), + ]; + } +} diff --git a/lib/Model/TeamMember.php b/lib/Model/TeamMember.php new file mode 100644 index 000000000..b7b6e7c42 --- /dev/null +++ b/lib/Model/TeamMember.php @@ -0,0 +1,163 @@ +addType('teamSingleId', 'string'); + $this->addType('memberSingleId', 'string'); + $this->addType('level', 'integer'); + $this->addType('invitedBySingleId', 'string'); + $this->addType('metadata', 'json'); + $this->addType('creation', 'integer'); + } + + public function getTeamMemberLevel(): TeamMemberLevel { + return TeamMemberLevel::from($this->getLevel()); + } + + public function setTeamMemberLevel(TeamMemberLevel $level): void { + $this->setLevel($level->value); + } + + public function getTeam(): ?Team { + // TODO: team can be null, build from meta (needed ?) + return $this->team; + } + + public function setTeam(Team $team): void { + $this->team = $team; + $this->setTeamSingleId($team->getSingleId()); + } + + public function getEntity(): ?TeamEntity { + // TODO: member can be null, build from meta (needed ?) + return $this->entity; + } + + public function setEntity(TeamEntity $entity): void { + $this->entity = $entity; + $this->setMemberSingleId($entity->getSingleId()); + } + + /** + * @throws TeamEntityNotFoundException + */ + public function getInvitedBy(): TeamEntity { + if ($this->invitedBy === null) { + $invitedBy = new TeamEntity(); + $invitedBy->import($this->metadata['_invitedBy'] ?? []); + if ($invitedBy->isValid()) { + $this->invitedBy = $invitedBy; + } + } + + if ($this->invitedBy === null) { + throw new TeamEntityNotFoundException(); + } + + return $this->invitedBy; + } + + public function setInvitedBy(TeamEntity $invitedBy): void { + $this->invitedBy = $invitedBy; + $this->setInvitedBySingleId($invitedBy->getSingleId()); + $this->setMetaValue('_invitedBy', $invitedBy->jsonSerialize(true)); + } + + public function setMetaValue(string $key, string|int|float|bool|array $value): void { + $metadata = $this->metadata; + $metadata[$key] = $value; + $this->setter('metadata', [$metadata]); + } + + /** + * only returns metadata that does not start with underscore '_' + */ + public function getFilteredMetadata(): array { + return array_filter($this->metadata, static fn(string $k): bool => !str_starts_with($k, '_'), ARRAY_FILTER_USE_KEY); + } + + public function import(array $data): void { + $this->setTeamSingleId($data['teamSingleId'] ?? ''); + $this->setMemberSingleId($data['memberSingleId'] ?? ''); + $this->setLevel($data['level'] = 0); + $this->setMetadata($data['metadata'] ?? []); + $this->setCreation($data['creation'] ?? 0); + } + + public function isValid(): bool { + return ($this->getTeamSingleId() !== '' && $this->getMemberSingleId() !== ''); + } + + /** + * @return array + */ + public function jsonSerialize(): array { + if ($this->isApiVersion(TeamApi::V2)) { + return [ + 'teamId' => $this->getTeamSingleId(), + 'singleId' => $this->getMemberSingleId(), + 'displayName' => $this->getEntity()?->getDisplayName() ?? '', + 'level' => $this->getLevel(), + 'metadata' => $this->getFilteredMetadata(), + 'invitedBy' => $this->getInvitedBy(), + 'creation' => $this->getCreation() + ]; + } + + if ($this->isApiVersion(TeamAPI::V1)) { + return []; // TODO: old API compat + } + + return [ + 'teamSingleId' => $this->getTeamSingleId(), + 'memberSingleId' => $this->getMemberSingleId(), + 'team' => $this->getTeam(), + 'member' => $this->getEntity(), + 'level' => $this->getLevel(), + 'metadata' => $this->getMetadata(), + 'creation' => $this->getCreation(), + 'invitedBy' => $this->metadata['_invitedBy'] ?? [], + ]; + } +} diff --git a/lib/Model/TeamMembership.php b/lib/Model/TeamMembership.php new file mode 100644 index 000000000..b5194c5e7 --- /dev/null +++ b/lib/Model/TeamMembership.php @@ -0,0 +1,99 @@ +addType('singleId', 'string'); + $this->addType('teamSingleId', 'string'); + $this->addType('level', 'integer'); + $this->addType('inheritanceFirst', 'string'); + $this->addType('inheritanceLast', 'string'); + $this->addType('inheritanceDepth', 'integer'); + $this->addType('inheritancePath', 'json'); + } + + public function setPath(array $path): void { + array_shift($path); + $path = array_reverse($path); + $this->setInheritancePath($path); + + $depth = count($path); + $this->setInheritanceDepth($depth); + if ($depth === 0) { + return; + } + + $this->setInheritanceFirst($path[0]); + $this->setInheritanceLast(array_pop($path)); + } + + /** + * @return array + */ + public function jsonSerialize(): array { + if ($this->isApiVersion(TeamApi::V2)) { + return [ + 'singleId' => $this->getSingleId(), + 'teamSingleId' => $this->getTeamSingleId(), + 'level' => $this->getLevel(), + 'inheritanceFirst' => $this->getInheritanceFirst(), + 'inheritanceLast' => $this->getInheritanceLast(), + 'inheritanceDepth' => $this->getInheritanceDepth(), + 'inheritancePath' => $this->getInheritancePath() + ]; + } + + if ($this->isApiVersion(TeamAPI::V1)) { + return []; // TODO: old API compat + } + + return [ + 'singleId' => $this->getSingleId(), + 'teamSingleId' => $this->getTeamSingleId(), + 'level' => $this->getLevel(), + 'inheritanceFirst' => $this->getInheritanceFirst(), + 'inheritanceLast' => $this->getInheritanceLast(), + 'inheritanceDepth' => $this->getInheritanceDepth(), + 'inheritancePath' => $this->getInheritancePath() + ]; + } +} diff --git a/lib/Service/TeamEntityService.php b/lib/Service/TeamEntityService.php new file mode 100644 index 000000000..eb9566bf2 --- /dev/null +++ b/lib/Service/TeamEntityService.php @@ -0,0 +1,132 @@ +teamEntityManager->getTeamEntity($singleId); + } + + public function generateTeamEntityFromLocalUser(string $userId): TeamEntity { + $singleId = $this->userConfig->getValueString($userId, Application::APP_ID, 'teamSingleId'); + if ($singleId !== '') { + return $this->generateTeamEntity($singleId, TeamEntityType::LOCAL_USER, $userId); + } + + $user = $this->userManager->get($userId); + if ($user === null) { + throw new TeamEntityNotFoundException('local user not found'); + } + return $this->generateTeamEntityFromUser($user); + } + + /** + * @throws \OCA\Circles\Exceptions\TeamNotFoundException + */ + public function generateTeamEntityFromTeam(string $singleId): TeamEntity { + return $this->teamManager->getTeam($singleId)->asEntity(); + } + + public function generateTeamEntityFromApp(string $appId): TeamEntity { + $singleId = $this->appConfig->getValueString($appId, 'teamSingleId'); + if ($singleId !== '') { + return $this->generateTeamEntity($singleId, TeamEntityType::APP, $appId, $appId); + } + + try { + $teamEntity = $this->teamEntityManager->searchTeamEntity(TeamEntityType::APP, $appId); + } catch (TeamEntityNotFoundException) { + $teamEntity = $this->teamEntityManager->createTeamEntity(TeamEntityType::APP, $appId, $appId); + } + $this->appConfig->setValueString($appId, 'teamSingleId', $teamEntity->getSingleId()); + + return $teamEntity; + } + + public function generateTeamEntityFromOcc(): TeamEntity { + $singleId = $this->appConfig->getValueString(Application::APP_ID, 'occSingleId'); + if ($singleId !== '') { + return $this->generateTeamEntity($singleId, TeamEntityType::OCC, 'occ', 'Occ Command'); + } + + try { + $teamEntity = $this->teamEntityManager->searchTeamEntity(TeamEntityType::OCC, 'occ'); + } catch (TeamEntityNotFoundException) { + $teamEntity = $this->teamEntityManager->createTeamEntity(TeamEntityType::OCC, 'occ', 'Occ Command'); + } + $this->appConfig->setValueString(Application::APP_ID, 'occSingleId', $teamEntity->getSingleId()); + + return $teamEntity; + } + + public function generateTeamEntityFromSuperAdmin(): TeamEntity { + $singleId = $this->appConfig->getValueString(Application::APP_ID, 'superAdminSingleId'); + if ($singleId !== '') { + return $this->generateTeamEntity($singleId, TeamEntityType::SUPER_ADMIN, 'superAdmin', 'Super Admin Script'); + } + + try { + $teamEntity = $this->teamEntityManager->searchTeamEntity(TeamEntityType::SUPER_ADMIN, 'superAdmin'); + } catch (TeamEntityNotFoundException) { + $teamEntity = $this->teamEntityManager->createTeamEntity(TeamEntityType::SUPER_ADMIN, 'superAdmin', 'Super Admin Script'); + } + $this->appConfig->setValueString(Application::APP_ID, 'superAdminSingleId', $teamEntity->getSingleId()); + + return $teamEntity; + } + + public function generateTeamEntityFromUser(IUser $user): TeamEntity { + $singleId = $this->userConfig->getValueString($user->getUID(), Application::APP_ID, 'teamSingleId'); + if ($singleId === '') { + try { + $teamEntity = $this->teamEntityManager->searchTeamEntity(TeamEntityType::LOCAL_USER, $user->getUID()); + } catch (TeamEntityNotFoundException) { + $teamEntity = $this->teamEntityManager->createTeamEntity(TeamEntityType::LOCAL_USER, $user->getUID(), $user->getDisplayName()); + } + $this->userConfig->setValueString($user->getUID(), Application::APP_ID, 'teamSingleId', $teamEntity->getSingleId()); + return $teamEntity; + } + + return $this->generateTeamEntity($singleId, TeamEntityType::LOCAL_USER, $user->getUID(), $user->getDisplayName()); + } + + private function generateTeamEntity(string $singleId, TeamEntityType $type, string $origId, string $displayName = ''): TeamEntity { + $entity = new TeamEntity(); + $entity->setSingleId($singleId); + $entity->setTeamEntityType($type); + $entity->setOrigId($origId); + $entity->setDisplayName($displayName); + return $entity; + } + +} diff --git a/lib/Service/TeamMemberService.php b/lib/Service/TeamMemberService.php new file mode 100644 index 000000000..c4c6ba1ab --- /dev/null +++ b/lib/Service/TeamMemberService.php @@ -0,0 +1,37 @@ +teamMemberManager->addMember($initiator, $team->getSingleId(), $entity->getSingleId()); + } + +} diff --git a/lib/Service/TeamMembershipService.php b/lib/Service/TeamMembershipService.php new file mode 100644 index 000000000..302444984 --- /dev/null +++ b/lib/Service/TeamMembershipService.php @@ -0,0 +1,405 @@ +getEntity()->getTeamEntityType() !== TeamEntityType::SUPER_ADMIN) { + throw new \Exception('must be ran as super admin'); // TODO: distinct exception + } + + if ($singleId === '') { + return; + } + + try { + $this->teamEntityManager->getTeamEntity($singleId); + } catch (TeamEntityNotFoundException) { + $this->teamMembershipManager->removeSingleId($singleId); + } + + foreach ($this->getDescendantsSingleIds($teamSession, $singleId) as $entry) { + $this->manageMemberships($teamSession, $entry); + } + } + + /** + * recursively obtains the list of all singleIds descendant of one singleId. + * + * @return list + */ + private function getDescendantsSingleIds(TeamSession $session, string $singleId): array { + $singleIds = [$singleId]; + $members = $this->teamMemberManager->getMembersFromTeam($session, $singleId); + foreach ($members as $member) { + if ($member->getTeamMemberLevel() === TeamMemberLevel::INVITED) { + continue; // only real members + } + $singleIds[] = $member->getMemberSingleId(); + if ($member->getEntity()?->getTeamEntityType() === TeamEntityType::TEAM) { + $this->teamMemberManager->getMembersFromTeam($session, $member->getMemberSingleId()); + } + } + + return array_unique($singleIds); + } + + /** + * @return int number of modified rows from the database + */ + private function manageMemberships(TeamSession $session, string $singleId): int { + /** @var array $memberships */ + $memberships = []; + $this->generateMemberships($session, $singleId, memberships: $memberships); + return $this->updateMembershipsDatabase($singleId, $memberships); + } + +// /** +// * @param string $circleId +// * @param string $singleId +// * @param bool $detailed +// * +// * @return Membership +// * @throws MembershipNotFoundException +// * @throws RequestBuilderException +// */ +// public function getMembership(string $circleId, string $singleId, bool $detailed = false): Membership { +// $membership = $this->membershipRequest->getMembership($circleId, $singleId); +// if ($detailed) { +// $details = $this->circleRequest->getCirclesByIds($membership->getInheritancePath()); +// $membership->setInheritanceDetails($details); +// } +// +// return $membership; +// } +// +// /** +// * @param string $singleId +// * @param bool $all +// */ +// public function resetMemberships(string $singleId = '', bool $all = false) { +// $this->membershipRequest->removeBySingleId($singleId, $all); +// } + + /** + * @param FederatedUser $federatedUser + */ + public function deleteFederatedUser(FederatedUser $federatedUser) { + $this->membershipRequest->deleteFederatedUser($federatedUser); + } + + /** + * Return the list of freshly generated Memberships from a singleId + * directly based on oc_teams_members + * + * @param list $path + * @param array $memberships + */ + private function generateMemberships( + TeamSession $session, + string $singleId, + string $teamId = '', + array $path = [], + array &$memberships = [], + ): void { + $teamId = ($teamId === '') ? $singleId : $teamId; + $path[] = $teamId; + + $members = $this->teamMemberManager->getTeamsContainingEntity($session, $teamId); + foreach ($members as $member) { + if ($member->getTeamMemberLevel() === TeamMemberLevel::INVITED) { + continue; // we do not care about invited people + } + + $membership = new TeamMembership(); + $membership->setSingleId($singleId); + $membership->setTeamSingleId($member->getTeamSingleId()); + $membership->setLevel($member->getLevel()); + $membership->setPath($path); + + /** + * if we already have a memberships between entity and team, we check the depth of inheritance + * and the final level assigned to the membership. + * The idea is to: + * - avoid duplicate memberships + * - keep the membership with higher level + * - keep the membership with the shortest inheritance path + */ + $known = $memberships[$member->getTeamSingleId()]; + if ($known === null + || $known->getLevel() < $membership->getLevel() + || ($known->getLevel() === $membership->getLevel() + && $known->getInheritanceDepth() > $membership->getInheritanceDepth())) { + $memberships[$member->getTeamSingleId()] = $membership; + } + + $this->generateMemberships( + $session, + $singleId, + $member->getTeamSingleId(), + $path, + $memberships, + ); + } + } + + /** + * @param array $memberships + * + * @return int number of entries from the database modified during the process + */ + private function updateMembershipsDatabase(string $singleId, array $memberships): int { + $known = $this->teamMembershipManager->getMemberships($singleId); + + $indexed = []; + foreach($known as $entry) { + $indexed[$entry->getTeamSingleId()] = $entry; + } + + $deprecated = $this->removeDeprecatedMemberships($memberships, $indexed); + + +// if (!empty($deprecated)) { +// $this->eventService->membershipsRemoved($deprecated); +// } +// + $updated = $this->updateMemberships($memberships, $indexed); + $new = $this->createMemberships($memberships, $indexed); + + // if (!empty($new)) { +// $this->eventService->membershipsCreated($new); +// } + + // TODO: implement and confirm feature + // for each entries in the arrays ? + // // clearing the getSharedWith() cache for singleId related to the membership + // $this->shareWrapperService->clearCache($membership->getSingleId()); + + return count($deprecated) + count($new) + count($updated); + } + + /** + * @param string $id + * @param array $knownIds + * + * @return array + * @throws RequestBuilderException + */ + private function getChildrenMembers( string $id, array &$knownIds = []): array { + $singleIds = array_map( + function(Member $item): string { + return $item->getSingleId(); + }, $this->memberRequest->getMembers($id) + ); + + foreach ($singleIds as $singleId) { + if ($singleId !== $id && !in_array($singleId, $knownIds)) { + $knownIds[] = $singleId; + $singleIds = array_merge($singleIds, $this->getChildrenMembers($singleId, $knownIds)); + } + } + + return array_unique($singleIds); + } + + /** + * @param string $id + * @param array $knownIds + * + * @return array + */ + private + function getChildrenMemberships( + string $id, array &$knownIds = [] + ): array { + $singleIds = array_map( + function(Membership $item): string { + return $item->getSingleId(); + }, $this->membershipRequest->getInherited($id) + ); + + foreach ($singleIds as $singleId) { + if ($singleId !== $id && !in_array($singleId, $knownIds)) { + $knownIds[] = $singleId; + $singleIds = array_merge($singleIds, $this->getChildrenMemberships($singleId, $knownIds)); + } + } + + return array_unique($singleIds); + } + + /** + * Add the new membership if unknown or Update known membership if: + * - new membership comes with more power + * - level is the same, but inheritance is shorter + * + * @param Membership $membership + * @param Membership[] $memberships + */ + private function fillMemberships( + Membership $membership, array &$memberships + ) { + foreach ($memberships as $known) { + if ($known->getCircleId() === $membership->getCircleId()) { + if ($known->getLevel() < $membership->getLevel() + || ($known->getLevel() === $membership->getLevel() + && $known->getInheritanceDepth() > $membership->getInheritanceDepth()) + ) { + $known->setLevel($membership->getLevel()); + $known->setInheritanceFirst($membership->getInheritanceFirst()); + $known->setInheritanceLast($membership->getInheritanceLast()); + $known->setInheritanceDepth($membership->getInheritanceDepth()); + $known->setInheritancePath($membership->getInheritancePath()); + } + + return; + } + } + + $memberships[$membership->getCircleId()] = $membership; + } + + /** + * @param list $memberships + * @param list $known + * + * @return TeamMembership[] + */ + private function removeDeprecatedMemberships(array $memberships, array $known): array { + $teamSingleIds = array_keys($memberships); + + $deprecated = []; + foreach ($known as $singleId => $item) { + if (!in_array($singleId, $teamSingleIds, true)) { + $deprecated[] = $item; + $this->teamMembershipManager->removeMembership($item); + } + } + + return $deprecated; + } + + /** + * @param list $memberships + * @param list $known + * + * @return TeamMembership[] + */ + private function updateMemberships(array $memberships, array $known): array { + $updated = []; + + foreach ($memberships as $singleId => $membership) { + if (!array_key_exists($singleId, $known)) { + continue; + } + + $item = $known[$singleId]; + if ($item->getLevel() !== $membership->getLevel() + || $item->getInheritanceDepth() !== $membership->getInheritanceDepth()) { + $this->teamMembershipManager->overwriteMembership($item->getId(), $membership); + $updated[] = $membership; + } elseif ($item->getInheritancePath() !== $membership->getInheritancePath()) { + $this->teamMembershipManager->overwriteMembership($item->getId(), $membership); + } + } + + return $updated; + } + + /** + * @param list $memberships + * @param list $known + * + * @return TeamMembership[] + */ + private function createMemberships(array $memberships, array $known): array { + $new = []; + + foreach ($memberships as $singleId => $membership) { + if (array_key_exists($singleId, $known)) { + continue; + } + $this->teamMembershipManager->insertMembership($membership); + $new[] = $membership; + } + + return $new; + } + + /** + * @param Membership[] $list + * @param string $circleId + * + * @return Membership + * @throws ItemNotFoundException + */ + private + function getMembershipsFromList( + array $list, string $circleId + ): Membership { + foreach ($list as $item) { + if ($item->getCircleId() === $circleId) { + return $item; + } + } + + throw new ItemNotFoundException(); + } + + /** + * @param Circle $circle + */ + public + function updatePopulation( + Circle $circle + ): void { + $local = $inherited = 0; + $memberships = $this->membershipRequest->getInherited($circle->getSingleId(), Member::LEVEL_MEMBER); + foreach ($memberships as $membership) { + $inherited++; + if ($membership->getCircleId() === $circle->getSingleId()) { + $local++; + } + } + + $settings = $circle->getSettings(); + $settings['population'] = $local; + $settings['populationInherited'] = $inherited; + $this->circleRequest->updateSettings($circle->setSettings($settings)); + } +} diff --git a/lib/Service/TeamService.php b/lib/Service/TeamService.php new file mode 100644 index 000000000..cc7356017 --- /dev/null +++ b/lib/Service/TeamService.php @@ -0,0 +1,90 @@ +toolsService->generateSingleId(); + + // todo: check team_single_id does not exist in oc_teams not oc_teams_members + +// $owner = new TeamMember(); +// $owner->setTeamSingleId($teamSingleId); +// $owner->setMemberSingleId($ownerSingleId); +// $owner->setTeamMemberLevel(TeamMemberLevel::OWNER); +// $owner->setCreation(time()); +// $this->teamMemberMapper->insert($owner); + +// $owner = $this->teamEntityManager->getTeamEntity($ownerSingleId); +// +// $owner = new TeamEntity(); +// $owner->setTeamEntityType(TeamEntityType::LOCAL_USER); +// $owner->setDisplayName('test_test_owner'); +// $owner->setSingleId('asde'); +// $owner->setOrigId('admin'); +// $owner->enableLazyLoading(); +// $realOwner?->enableLazyLoading(); + + $team = new Team(); + $team->enableLazyloading($owner); + $team->setSingleId($teamSingleId); + $team->setDisplayName($name); + $team->setSanitizedName($this->toolsService->generateSingleId(6)); // TODO + $team->setOwner($realOwner ?? $owner); + $this->teamMapper->insert($team); + + $member = new TeamMember(); + $member->setInvitedBy($owner); + $member->setTeamMemberLevel(TeamMemberLevel::OWNER); + $member->setTeam($team); + $member->setEntity($team->getOwner()); + $this->teamMemberMapper->insert($member); + + $this->teamEntityManager->createTeamEntity( + TeamEntityType::TEAM, + $team->getSingleId(), + $team->getDisplayName(), + $team->getSingleId() + ); + +// $team->setMembers([$member]); + +// $mem + + return $team; + } + + public function getTeam(TeamEntity $initiator, string $teamSingleId): Team { + $this->teamMapper->getBySingleId($teamSingleId); + } + + private function generateMetadata(Team $team): void { + } +} diff --git a/lib/Service/ToolsService.php b/lib/Service/ToolsService.php new file mode 100644 index 000000000..879001843 --- /dev/null +++ b/lib/Service/ToolsService.php @@ -0,0 +1,27 @@ +