diff --git a/appinfo/routes.php b/appinfo/routes.php index fb346ddff..8836768f6 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -38,6 +38,10 @@ ['name' => 'Local#editName', 'url' => '/circles/{circleId}/name', 'verb' => 'PUT'], ['name' => 'Local#editDescription', 'url' => '/circles/{circleId}/description', 'verb' => 'PUT'], ['name' => 'Local#editSetting', 'url' => '/circles/{circleId}/setting', 'verb' => 'PUT'], + ['name' => 'Local#createInvitation', 'url' => '/circles/{circleId}/invitation', 'verb' => 'PUT'], + ['name' => 'Local#revokeInvitation', 'url' => '/circles/{circleId}/invitation', 'verb' => 'DELETE'], + ['name' => 'Local#getInvitation', 'url' => '/invitations/{invitationCode}', 'verb' => 'GET'], + ['name' => 'Local#joinInvitation', 'url' => '/invitations/{invitationCode}', 'verb' => 'POST'], ['name' => 'Local#editConfig', 'url' => '/circles/{circleId}/config', 'verb' => 'PUT'], ['name' => 'Local#link', 'url' => '/link/{circleId}/{singleId}', 'verb' => 'GET'], diff --git a/lib/Controller/LocalController.php b/lib/Controller/LocalController.php index 8a1e8f08f..3621c05a6 100644 --- a/lib/Controller/LocalController.php +++ b/lib/Controller/LocalController.php @@ -32,6 +32,9 @@ use OCA\Circles\Service\SearchService; use OCA\Circles\Tools\Traits\TDeserialize; use OCA\Circles\Tools\Traits\TNCLogger; +use OCP\AppFramework\Http; +use OCP\AppFramework\Http\Attribute\NoAdminRequired; +use OCP\AppFramework\Http\Attribute\UserRateLimit; use OCP\AppFramework\Http\DataResponse; use OCP\AppFramework\OCS\OCSException; use OCP\AppFramework\OCSController; @@ -590,6 +593,120 @@ public function link(string $circleId, string $singleId): DataResponse { } } + /** + * @param string $circleId + * + * @return DataResponse + * @throws OCSException + */ + #[NoAdminRequired] + public function createInvitation(string $circleId): DataResponse { + try { + $this->setCurrentFederatedUser(); + + $outcome = $this->circleService->createInvitation($circleId); + + return new DataResponse($this->serializeArray($outcome)); + } catch (\Exception $e) { + $this->e($e, ['circleId' => $circleId]); + throw new OCSException($e->getMessage(), (int)$e->getCode(), $e); + } + } + + /** + * @param string $circleId + * + * @return DataResponse + * @throws OCSException + */ + #[NoAdminRequired] + public function revokeInvitation(string $circleId): DataResponse { + try { + $this->setCurrentFederatedUser(); + + $outcome = $this->circleService->revokeInvitation($circleId); + + return new DataResponse($this->serializeArray($outcome)); + } catch (\Exception $e) { + $this->e($e, ['circleId' => $circleId]); + throw new OCSException($e->getMessage(), (int)$e->getCode(), $e); + } + } + + /** + * @param string $invitationCode + * + * @return DataResponse + * @throws OCSException + */ + #[NoAdminRequired] + #[UserRateLimit(limit: 10, period: 3600)] + public function getInvitation(string $invitationCode): DataResponse { + try { + $this->setCurrentFederatedUser(); + + $circleProbe = (new CircleProbe()) + ->includeSystemCircles() + ->includeHiddenCircles() + ->filterByInvitationCode($invitationCode); + + $circles = $this->circleService->getCircles($circleProbe); + if (empty($circles)) { + return new DataResponse([], Http::STATUS_NOT_FOUND); + } + $circle = reset($circles); + + $membershipStatus = 'NOT_A_MEMBER'; + if ($circle->hasInitiator()) { + if ($circle->getInitiator()->getLevel() > Member::LEVEL_NONE) { + $membershipStatus = 'MEMBER'; + } elseif ($circle->getInitiator()->getStatus() === Member::STATUS_REQUEST) { + $membershipStatus = 'REQUESTED_MEMBERSHIP'; + } + } + + return new DataResponse([ + 'circleId' => $circle->getSingleId(), + 'circleName' => $circle->getName(), + 'membershipStatus' => $membershipStatus, + ]); + } catch (\Exception $e) { + $this->e($e, ['circleId' => $invitationCode]); + throw new OCSException($e->getMessage(), (int)$e->getCode(), $e); + } + } + + /** + * @param string $invitationCode + * + * @return DataResponse + * @throws OCSException + */ + #[NoAdminRequired] + #[UserRateLimit(limit: 10, period: 3600)] + public function joinInvitation(string $invitationCode): DataResponse { + try { + $this->setCurrentFederatedUser(); + + $circleProbe = (new CircleProbe()) + ->includeSystemCircles() + ->includeHiddenCircles() + ->filterByInvitationCode($invitationCode); + + $circles = $this->circleService->getCircles($circleProbe); + if (empty($circles)) { + return new DataResponse([], Http::STATUS_NOT_FOUND); + } + $circle = reset($circles); + + $result = $this->circleService->circleJoin($circle->getSingleId(), $invitationCode); + + return new DataResponse($this->serializeArray($result)); + } catch (\Exception $e) { + $this->e($e, ['circleId' => $invitationCode]); + throw new OCSException($e->getMessage(), (int)$e->getCode(), $e); + } + } /** * @return void diff --git a/lib/Db/CircleInvitationRequest.php b/lib/Db/CircleInvitationRequest.php new file mode 100644 index 000000000..b95bee682 --- /dev/null +++ b/lib/Db/CircleInvitationRequest.php @@ -0,0 +1,55 @@ +confirmValidId($circleInvitation->getCircleId()); + + $qb = $this->getCircleInvitationInsertSql(); + $qb->setValue('circle_id', $qb->createNamedParameter($circleInvitation->getCircleId())) + ->setValue('invitation_code', $qb->createNamedParameter($circleInvitation->getInvitationCode())) + ->setValue('created_by', $qb->createNamedParameter($circleInvitation->getCreatedBy())); + $qb->executeStatement(); + } + + /** + * @param CircleInvitation $circleInvitation + * + * @throws InvalidIdException + */ + public function replace(CircleInvitation $circleInvitation): void { + $this->delete($circleInvitation->getCircleId()); + $this->save($circleInvitation); + } + + /** + * @param string $circleId + */ + public function delete(string $circleId): void { + $qb = $this->getCircleInvitationDeleteSql(); + $qb->limitToCircleId($circleId); + + $qb->executeStatement(); + } +} diff --git a/lib/Db/CircleRequest.php b/lib/Db/CircleRequest.php index 25496b9d9..f399c47f6 100644 --- a/lib/Db/CircleRequest.php +++ b/lib/Db/CircleRequest.php @@ -167,6 +167,7 @@ public function getCircles(?IFederatedUser $initiator, CircleProbe $probe): arra $qb->limitToInitiator(CoreQueryBuilder::CIRCLE, $initiator); $qb->orderBy($qb->generateAlias(CoreQueryBuilder::CIRCLE, CoreQueryBuilder::INITIATOR) . '.level', 'desc'); $qb->addOrderBy(CoreQueryBuilder::CIRCLE . '.display_name', 'asc'); + $qb->leftJoinCircleInvitation(CoreQueryBuilder::CIRCLE); } if ($probe->hasFilterMember()) { $qb->limitToDirectMembership(CoreQueryBuilder::CIRCLE, $probe->getFilterMember()); @@ -174,6 +175,9 @@ public function getCircles(?IFederatedUser $initiator, CircleProbe $probe): arra if ($probe->hasFilterCircle()) { $qb->filterCircleDetails($probe->getFilterCircle()); } + if ($probe->hasInvitationCode()) { + $qb->filterInvitationCode(CoreQueryBuilder::CIRCLE, $probe->getInvitationCode()); + } if ($probe->hasFilterRemoteInstance()) { $qb->limitToRemoteInstance(CoreQueryBuilder::CIRCLE, $probe->getFilterRemoteInstance(), false); } @@ -369,6 +373,7 @@ public function getCircle( $qb->limitToUniqueId($id); $qb->filterCircles(CoreQueryBuilder::CIRCLE, $probe); $qb->leftJoinOwner(CoreQueryBuilder::CIRCLE); + $qb->leftJoinCircleInvitation(CoreQueryBuilder::CIRCLE); // $qb->setOptions( // [CoreRequestBuilder::CIRCLE, CoreRequestBuilder::INITIATOR], [ // 'mustBeMember' => false, diff --git a/lib/Db/CircleRequestBuilder.php b/lib/Db/CircleRequestBuilder.php index 644ba62c7..3a4584781 100644 --- a/lib/Db/CircleRequestBuilder.php +++ b/lib/Db/CircleRequestBuilder.php @@ -33,6 +33,16 @@ protected function getCircleInsertSql(): CoreQueryBuilder { return $qb; } + /** + * @return CoreQueryBuilder&IQueryBuilder + */ + protected function getCircleInvitationInsertSql(): CoreQueryBuilder { + $qb = $this->getQueryBuilder(); + $qb->insert(self::TABLE_INVITATIONS) + ->setValue('created', $qb->createNamedParameter($this->timezoneService->getUTCDate())); + + return $qb; + } /** * @return CoreQueryBuilder&IQueryBuilder @@ -44,7 +54,6 @@ protected function getCircleUpdateSql(): CoreQueryBuilder { return $qb; } - /** * @param string $alias * @param bool $single @@ -65,7 +74,6 @@ protected function getCircleSelectSql( return $qb; } - /** * Base of the Sql Delete request * @@ -78,6 +86,17 @@ protected function getCircleDeleteSql(): CoreQueryBuilder { return $qb; } + /** + * Base of the Sql Delete request + * + * @return CoreQueryBuilder&IQueryBuilder + */ + protected function getCircleInvitationDeleteSql(): CoreQueryBuilder { + $qb = $this->getQueryBuilder(); + $qb->delete(self::TABLE_INVITATIONS); + + return $qb; + } /** * @param CoreQueryBuilder&IQueryBuilder $qb diff --git a/lib/Db/CoreQueryBuilder.php b/lib/Db/CoreQueryBuilder.php index 0b3ac9314..72263c4f7 100644 --- a/lib/Db/CoreQueryBuilder.php +++ b/lib/Db/CoreQueryBuilder.php @@ -59,7 +59,7 @@ class CoreQueryBuilder extends ExtendedQueryBuilder { public const TOKEN = 'u'; public const OPTIONS = 'v'; public const HELPER = 'w'; - + public const INVITATION = 'x'; public static $SQL_PATH = [ self::SINGLE => [ @@ -69,6 +69,7 @@ class CoreQueryBuilder extends ExtendedQueryBuilder { self::OPTIONS => [ ], self::MEMBER, + self::INVITATION, self::OWNER => [ self::BASED_ON ], @@ -839,6 +840,30 @@ public function leftJoinOwner(string $alias, string $field = 'unique_id'): void $this->leftJoinBasedOn($aliasMember); } + /** + * @param string $alias + * @param string $field + * + * @throws RequestBuilderException + */ + public function leftJoinCircleInvitation(string $alias, string $field = 'unique_id'): void { + if ($this->getType() !== QueryBuilder::SELECT) { + return; + } + + try { + $aliasInvitation = $this->generateAlias($alias, self::INVITATION, $options); + } catch (RequestBuilderException $e) { + return; + } + + $expr = $this->expr(); + $this->generateCircleInvitationSelectAlias($aliasInvitation) + ->leftJoin( + $alias, CoreRequestBuilder::TABLE_INVITATIONS, $aliasInvitation, + $expr->eq($aliasInvitation . '.circle_id', $alias . '.' . $field), + ); + } /** * @param CircleProbe $probe @@ -1308,6 +1333,12 @@ protected function limitInitiatorVisibility(string $alias): ICompositeExpression $aliasMembershipCircle = $this->generateAlias($aliasMembership, self::CONFIG, $options); $levelCheck = [$aliasMembership]; + // no need to check anything, we are filtering by invitation code + $invitationCode = $this->get('filterInvitationCode', $options, ''); + if ($invitationCode) { + return $this->expr()->andX($this->expr()->eq('1', '1')); + } + $directMember = ''; if ($this->getBool('initiatorDirectMember', $options, false)) { $directMember = $this->generateAlias($alias, self::DIRECT_INITIATOR, $options); @@ -1522,6 +1553,27 @@ public function limitToShareOwner( } } + /** + * filter circle by invitation code + * + * @param string $invitationCode + */ + public function filterInvitationCode(string $alias, string $invitationCode): void { + if ($this->getType() !== QueryBuilder::SELECT) { + return; + } + + try { + $aliasInvitation = $this->generateAlias($alias, self::INVITATION, $options); + } catch (RequestBuilderException $e) { + return; + } + + $expr = $this->expr(); + $this->andWhere( + $expr->eq($aliasInvitation . '.invitation_code', $this->createNamedParameter($invitationCode)) + ); + } /** * @param string $aliasMount @@ -1584,6 +1636,23 @@ private function generateMemberSelectAlias(string $alias, array $default = []): return $this; } + /** + * @param string $alias + * @param array $default + * + * @return $this + */ + private function generateCircleInvitationSelectAlias(string $alias, array $default = []): self { + $this->generateSelectAlias( + CoreRequestBuilder::$tables[CoreRequestBuilder::TABLE_INVITATIONS], + $alias, + $alias, + $default + ); + + return $this; + } + /** * @param string $alias diff --git a/lib/Db/CoreRequestBuilder.php b/lib/Db/CoreRequestBuilder.php index 2d49b1ef6..c56f5edff 100644 --- a/lib/Db/CoreRequestBuilder.php +++ b/lib/Db/CoreRequestBuilder.php @@ -32,6 +32,7 @@ class CoreRequestBuilder { public const TABLE_STORAGES = 'storages'; public const TABLE_CIRCLE = 'circles_circle'; + public const TABLE_INVITATIONS = 'circles_invitations'; public const TABLE_MEMBER = 'circles_member'; public const TABLE_MEMBERSHIP = 'circles_membership'; public const TABLE_REMOTE = 'circles_remote'; @@ -64,6 +65,12 @@ class CoreRequestBuilder { 'contact_groupname', 'creation' ], + self::TABLE_INVITATIONS => [ + 'circle_id', + 'invitation_code', + 'created_by', + 'created', + ], self::TABLE_MEMBER => [ 'circle_id', 'member_id', diff --git a/lib/Exceptions/CircleInvitationNotFoundException.php b/lib/Exceptions/CircleInvitationNotFoundException.php new file mode 100644 index 000000000..6cbd9b3c4 --- /dev/null +++ b/lib/Exceptions/CircleInvitationNotFoundException.php @@ -0,0 +1,14 @@ +getCircle(); + + $initiatorHelper = new MemberHelper($circle->getInitiator()); + $initiatorHelper->mustBeAdmin(); + + $new = clone $circle; + + $invitationCode = $this->random->generate( + 16, + 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789', + ); + + $circleInvitation = new CircleInvitation(); + $circleInvitation->setCircleId($circle->getSingleId()); + $circleInvitation->setInvitationCode($invitationCode); + $circleInvitation->setCreatedBy($circle->getInitiator()->getUserId()); + + $new->setCircleInvitation($circleInvitation); + $event->getData()->sObj('circle_invitation', $circleInvitation); + + $event->setOutcome($this->serialize($new)); + } + + /** + * @param FederatedEvent $event + * + * @throws RequestBuilderException + */ + public function manage(FederatedEvent $event): void { + /** @var CircleInvitation $circleInvitation */ + $circleInvitation = $event->getData()->gObj('circle_invitation'); + $this->circleInvitationRequest->replace($circleInvitation); + + // todo: do we need separate event here? + $this->eventService->circleEditing($event); + } + + /** + * @param FederatedEvent $event + * @param array $results + */ + public function result(FederatedEvent $event, array $results): void { + // todo: do we need separate event here? + $this->eventService->circleEdited($event, $results); + } +} diff --git a/lib/FederatedItems/CircleJoin.php b/lib/FederatedItems/CircleJoin.php index f1bb26970..37c3ecd28 100644 --- a/lib/FederatedItems/CircleJoin.php +++ b/lib/FederatedItems/CircleJoin.php @@ -130,7 +130,8 @@ public function verify(FederatedEvent $event): void { $member->setInvitedBy($initiator->getInvitedBy()); } - $this->manageMemberStatus($circle, $member); + $invitationCode = $event->getParams()->g('invitationCode'); + $this->manageMemberStatus($circle, $member, $invitationCode); $this->circleService->confirmCircleNotFull($circle); @@ -251,11 +252,12 @@ public function result(FederatedEvent $event, array $results): void { /** * @param Circle $circle * @param Member $member + * @param string|null $invitationCode * * @throws FederatedItemBadRequestException * @throws RequestBuilderException */ - private function manageMemberStatus(Circle $circle, Member $member) { + private function manageMemberStatus(Circle $circle, Member $member, ?string $invitationCode = null) { try { $knownMember = $this->memberRequest->searchMember($member); if ($knownMember->getLevel() === Member::LEVEL_NONE) { @@ -277,7 +279,8 @@ private function manageMemberStatus(Circle $circle, Member $member) { throw new MemberAlreadyExistsException(StatusCode::$CIRCLE_JOIN[122], 122); } catch (MemberNotFoundException $e) { - if (!$circle->isConfig(Circle::CFG_OPEN)) { + $allowedToJoin = $invitationCode && $circle->getCircleInvitation()?->getInvitationCode() === $invitationCode; + if (!$circle->isConfig(Circle::CFG_OPEN) && !$allowedToJoin) { throw new FederatedItemBadRequestException(StatusCode::$CIRCLE_JOIN[124], 124); } diff --git a/lib/FederatedItems/CircleRevokeInvitation.php b/lib/FederatedItems/CircleRevokeInvitation.php new file mode 100644 index 000000000..8a0c43f95 --- /dev/null +++ b/lib/FederatedItems/CircleRevokeInvitation.php @@ -0,0 +1,74 @@ +getCircle(); + + $initiatorHelper = new MemberHelper($circle->getInitiator()); + $initiatorHelper->mustBeAdmin(); + + $new = clone $circle; + $new->setCircleInvitation(null); + + $event->setOutcome($this->serialize($new)); + } + + /** + * @param FederatedEvent $event + * + * @throws RequestBuilderException + */ + public function manage(FederatedEvent $event): void { + $circle = clone $event->getCircle(); + + $this->circleInvitationRequest->delete($circle->getSingleId()); + // todo: do we need separate event here? + $this->eventService->circleEditing($event); + } + + /** + * @param FederatedEvent $event + * @param array $results + */ + public function result(FederatedEvent $event, array $results): void { + // todo: do we need separate event here? + $this->eventService->circleEdited($event, $results); + } +} diff --git a/lib/Migration/Version8100Date20261129153333.php b/lib/Migration/Version8100Date20261129153333.php new file mode 100644 index 000000000..a1f6e4f8c --- /dev/null +++ b/lib/Migration/Version8100Date20261129153333.php @@ -0,0 +1,74 @@ +hasTable('circles_invitations')) { + $table = $schema->createTable('circles_invitations'); + + $table->addColumn( + 'id', 'integer', [ + 'autoincrement' => true, + 'notnull' => true, + 'length' => 8, + 'unsigned' => true, + ] + ); + $table->addColumn( + 'circle_id', 'string', [ + 'length' => 32, + 'notnull' => true, + ] + ); + $table->addColumn( + 'invitation_code', 'string', [ + 'length' => 16, + 'notnull' => true, + ] + ); + $table->addColumn( + 'created_by', 'string', [ + 'length' => 255, + 'notnull' => true, + ] + ); + $table->addColumn( + 'created', 'datetime', [ + 'notnull' => true, + ] + ); + + $table->setPrimaryKey(['id']); + $table->addUniqueIndex(['circle_id']); + $table->addUniqueIndex(['invitation_code']); + } + + return $schema; + } +} diff --git a/lib/Model/Circle.php b/lib/Model/Circle.php index 7f0d53ce7..7424c56d6 100644 --- a/lib/Model/Circle.php +++ b/lib/Model/Circle.php @@ -204,6 +204,9 @@ class Circle extends ManagedModel implements IEntity, IDeserializable, IQueryRow /** @var int */ private $populationInherited = 0; + /** @var CircleInvitation|null */ + private $circleInvitation = null; + // /** @var bool */ // private $hidden = false; @@ -666,6 +669,23 @@ public function getPopulationInherited(): int { return $this->populationInherited; } + /** + * @param CircleInvitation|null $circleInvitation + * + * @return Circle + */ + public function setCircleInvitation(?CircleInvitation $circleInvitation): self { + $this->circleInvitation = $circleInvitation; + + return $this; + } + + /** + * @return CircleInvitation|null + */ + public function getCircleInvitation(): ?CircleInvitation { + return $this->circleInvitation; + } /** * @param array $settings @@ -808,6 +828,13 @@ public function import(array $data): IDeserializable { } catch (InvalidItemException $e) { } + try { + /** @var CircleInvitation $circleInvitation */ + $circleInvitation = $this->deserialize($this->getArray('invitation', $data), CircleInvitation::class); + $this->setCircleInvitation($circleInvitation); + } catch (InvalidItemException $e) { + } + return $this; } @@ -858,6 +885,7 @@ public function jsonSerialize(): array { try { $initiatorHelper->mustBeAdmin(); $arr['settings'] = $this->getSettings(); + $arr['invitationCode'] = $this->getCircleInvitation()?->getInvitationCode(); } catch (MemberHelperException|MemberLevelException $e) { } } @@ -900,7 +928,6 @@ public function importFromDatabase(array $data, string $prefix = ''): IQueryRow $this->getManager()->manageImportFromDatabase($this, $data, $prefix); - // TODO: deprecated in NC27, remove those (17) lines that was needed to finalise migration to 24 // if password is not hashed (pre-22), hash it and update new settings in DB $curr = $this->get('password_single', $this->getSettings()); diff --git a/lib/Model/CircleInvitation.php b/lib/Model/CircleInvitation.php new file mode 100644 index 000000000..71157ec10 --- /dev/null +++ b/lib/Model/CircleInvitation.php @@ -0,0 +1,178 @@ +circleId = $circleId; + + return $this; + } + + /** + * @return string + */ + public function getCircleId(): string { + return $this->circleId; + } + + /** + * @param string $invitationCode + * + * @return self + */ + public function setInvitationCode(string $invitationCode): self { + $this->invitationCode = $invitationCode; + + return $this; + } + + /** + * @return string + */ + public function getInvitationCode(): string { + return $this->invitationCode; + } + + /** + * @param string $createdBy + * + * @return self + */ + public function setCreatedBy(string $createdBy): self { + $this->createdBy = $createdBy; + + return $this; + } + + /** + * @return string + */ + public function getCreatedBy(): string { + return $this->createdBy; + } + + /** + * @param int $created + * + * @return self + */ + public function setCreated(int $created): self { + $this->created = $created; + + return $this; + } + + /** + * @return int + */ + public function getCreated(): int { + return $this->created; + } + + /** + * @param array $data + * + * @return $this + * @throws InvalidItemException + */ + public function import(array $data): IDeserializable { + if (!$this->get('circleId', $data)) { + throw new InvalidItemException(); + } + + $this->setCircleId($this->get('circleId', $data)) + ->setInvitationCode($this->get('invitationCode', $data)) + ->setCreatedBy($this->get('createdBy', $data)) + ->setCreated($this->getInt('created', $data)); + + return $this; + } + + + /** + * @return array + * @throws FederatedItemException + * @throws RemoteInstanceException + * @throws RemoteNotFoundException + * @throws RemoteResourceNotFoundException + * @throws RequestBuilderException + * @throws UnknownRemoteException + */ + public function jsonSerialize(): array { + $arr = [ + 'circleId' => $this->getCircleId(), + 'invitationCode' => $this->getInvitationCode(), + 'createdBy' => $this->getCreatedBy(), + 'created' => $this->getCreated(), + ]; + + return $arr; + } + + + /** + * @param array $data + * @param string $prefix + * + * @return IQueryRow + * @throws CircleNotFoundException + */ + public function importFromDatabase(array $data, string $prefix = ''): IQueryRow { + if (!$this->get($prefix . 'circle_id', $data)) { + throw new CircleInvitationNotFoundException(); + } + + $this->setCircleId($this->get($prefix . 'circle_id', $data)) + ->setInvitationCode($this->get($prefix . 'invitation_code', $data)) + ->setCreatedBy($this->get($prefix . 'created_by', $data)); + + $created = $this->get($prefix . 'created', $data); + $dateTime = \DateTime::createFromFormat('Y-m-d H:i:s', $created); + $timestamp = $dateTime ? $dateTime->getTimestamp() : (int)strtotime($created); + $this->setCreated($timestamp); + + return $this; + } +} diff --git a/lib/Model/ModelManager.php b/lib/Model/ModelManager.php index 721c6683c..42df40f71 100644 --- a/lib/Model/ModelManager.php +++ b/lib/Model/ModelManager.php @@ -16,6 +16,7 @@ use OCA\Circles\Db\CoreQueryBuilder; use OCA\Circles\Db\MemberRequest; use OCA\Circles\Db\MembershipRequest; +use OCA\Circles\Exceptions\CircleInvitationNotFoundException; use OCA\Circles\Exceptions\CircleNotFoundException; use OCA\Circles\Exceptions\FederatedItemException; use OCA\Circles\Exceptions\FederatedUserNotFoundException; @@ -207,6 +208,12 @@ public function manageImportFromDatabase(ManagedModel $model, array $data, strin } } + if ($model instanceof CircleInvitation) { + if ($base === '') { + $base = CoreQueryBuilder::INVITATION; + } + } + if ($model instanceof Member) { if ($base === '') { $base = CoreQueryBuilder::MEMBER; @@ -288,6 +295,14 @@ private function importIntoCircle(Circle $circle, array $data, string $path, str } catch (MemberNotFoundException $e) { } break; + case CoreQueryBuilder::INVITATION: + try { + $circleInvitation = new CircleInvitation(); + $circleInvitation->importFromDatabase($data, $prefix); + $circle->setCircleInvitation($circleInvitation); + } catch (CircleInvitationNotFoundException $e) { + } + break; } } diff --git a/lib/Model/Probes/CircleProbe.php b/lib/Model/Probes/CircleProbe.php index 1c1766e37..f7165d65f 100644 --- a/lib/Model/Probes/CircleProbe.php +++ b/lib/Model/Probes/CircleProbe.php @@ -23,6 +23,7 @@ class CircleProbe extends MemberProbe { private int $filter = Circle::CFG_SINGLE; private bool $includeNonVisible = false; private bool $visitSingleCircles = false; + private ?string $invitationCode = null; private int $limitConfig = 0; /** @@ -331,6 +332,32 @@ public function isFiltered(int $config): bool { return (($this->filtered() & $config) !== 0); } + /** + * Apply a filter on the invitation code + * + * @param string $invitationCode + * + * @return $this + */ + public function filterByInvitationCode(string $invitationCode): self { + $this->invitationCode = str_replace('-', '', $invitationCode); + + return $this; + } + + /** + * @return bool + */ + public function hasInvitationCode(): bool { + return ($this->invitationCode !== null); + } + + /** + * @return string|null + */ + public function getInvitationCode(): ?string { + return $this->invitationCode; + } /** * Return an array with includes as options @@ -355,6 +382,7 @@ public function getAsOptions(): array { 'filterBackendCircles' => $this->isIncluded(Circle::CFG_BACKEND), 'filterSystemCircles' => $this->isIncluded(Circle::CFG_SYSTEM), 'filterPersonalCircles' => $this->isIncluded(Circle::CFG_PERSONAL), + 'filterInvitationCode' => $this->invitationCode, ], parent::getAsOptions() ); diff --git a/lib/Service/CircleService.php b/lib/Service/CircleService.php index cf45159e3..f1d3cb308 100644 --- a/lib/Service/CircleService.php +++ b/lib/Service/CircleService.php @@ -29,10 +29,12 @@ use OCA\Circles\Exceptions\UnknownRemoteException; use OCA\Circles\FederatedItems\CircleConfig; use OCA\Circles\FederatedItems\CircleCreate; +use OCA\Circles\FederatedItems\CircleCreateInvitation; use OCA\Circles\FederatedItems\CircleDestroy; use OCA\Circles\FederatedItems\CircleEdit; use OCA\Circles\FederatedItems\CircleJoin; use OCA\Circles\FederatedItems\CircleLeave; +use OCA\Circles\FederatedItems\CircleRevokeInvitation; use OCA\Circles\FederatedItems\CircleSetting; use OCA\Circles\IEntity; use OCA\Circles\IFederatedUser; @@ -334,6 +336,60 @@ public function updateSetting(string $circleId, string $setting, ?string $value) } + /** + * @param string $circleId + * + * @return array + * @throws CircleNotFoundException + * @throws FederatedEventException + * @throws FederatedItemException + * @throws InitiatorNotConfirmedException + * @throws InitiatorNotFoundException + * @throws OwnerNotFoundException + * @throws RemoteInstanceException + * @throws RemoteNotFoundException + * @throws RemoteResourceNotFoundException + * @throws RequestBuilderException + * @throws UnknownRemoteException + */ + public function createInvitation(string $circleId): array { + $circle = $this->getCircle($circleId); + + $event = new FederatedEvent(CircleCreateInvitation::class); + $event->setCircle($circle); + + $this->federatedEventService->newEvent($event); + + return $event->getOutcome(); + } + + /** + * @param string $circleId + * + * @return array + * @throws CircleNotFoundException + * @throws FederatedEventException + * @throws FederatedItemException + * @throws InitiatorNotConfirmedException + * @throws InitiatorNotFoundException + * @throws OwnerNotFoundException + * @throws RemoteInstanceException + * @throws RemoteNotFoundException + * @throws RemoteResourceNotFoundException + * @throws RequestBuilderException + * @throws UnknownRemoteException + */ + public function revokeInvitation(string $circleId): array { + $circle = $this->getCircle($circleId); + + $event = new FederatedEvent(CircleRevokeInvitation::class); + $event->setCircle($circle); + + $this->federatedEventService->newEvent($event); + + return $event->getOutcome(); + } + /** * @param string $circleId * @param string $name @@ -420,13 +476,19 @@ public function updateDescription(string $circleId, string $description): array * @throws UnknownRemoteException * @throws RequestBuilderException */ - public function circleJoin(string $circleId): array { + public function circleJoin(string $circleId, ?string $invitationCode = null): array { + $invitationCode = $invitationCode ? str_replace('-', '', $invitationCode) : null; $this->federatedUserService->mustHaveCurrentUser(); $probe = new CircleProbe(); $probe->includeNonVisibleCircles() ->emulateVisitor(); + if ($invitationCode) { + $probe->includeHiddenCircles() + ->filterByInvitationCode($invitationCode); + } + $circle = $this->circleRequest->getCircle( $circleId, $this->federatedUserService->getCurrentUser(), @@ -438,6 +500,7 @@ public function circleJoin(string $circleId): array { } $event = new FederatedEvent(CircleJoin::class); + $event->setParams(new SimpleDataStore(['invitationCode' => $invitationCode])); $event->setCircle($circle); $this->federatedEventService->newEvent($event); @@ -445,7 +508,6 @@ public function circleJoin(string $circleId): array { return $event->getOutcome(); } - /** * @param string $circleId * @param bool $force