diff --git a/appinfo/info.xml b/appinfo/info.xml
index ecbdf8608..1aa78a635 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.
]]>
- 35.0.0-dev.0
+ 35.0.0-dev.1
agpl
Maxence Lange
diff --git a/appinfo/routes.php b/appinfo/routes.php
index fce3f8d5b..25b0da7f7 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#circleAvatar', 'url' => '/circles/{circleId}/avatar', 'verb' => 'GET'],
['name' => 'Local#uploadAvatar', 'url' => '/circles/{circleId}/avatar', 'verb' => 'POST'],
diff --git a/lib/Controller/LocalController.php b/lib/Controller/LocalController.php
index 709302c81..06bafac08 100644
--- a/lib/Controller/LocalController.php
+++ b/lib/Controller/LocalController.php
@@ -593,6 +593,97 @@ public function link(string $circleId, string $singleId): DataResponse {
}
}
+ #[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);
+ }
+ }
+
+ #[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);
+ }
+ }
+
+ #[NoAdminRequired]
+ #[BruteForceProtection(action: 'getInvitation')]
+ 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);
+ }
+ }
+
+ #[NoAdminRequired]
+ #[BruteForceProtection(action: 'joinInvitation')]
+ 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);
+ }
+ }
+
/**
* @throws FederatedUserException
* @throws FederatedUserNotFoundException
diff --git a/lib/Db/CircleInvitationRequest.php b/lib/Db/CircleInvitationRequest.php
new file mode 100644
index 000000000..492214e67
--- /dev/null
+++ b/lib/Db/CircleInvitationRequest.php
@@ -0,0 +1,54 @@
+confirmValidId($circleInvitation->getCircleId());
+
+ $qb = $this->getQueryBuilder();
+ $qb->insert(self::TABLE_INVITATIONS)
+ ->setValue('created', $qb->createNamedParameter($this->timezoneService->getUTCDate()));
+ $qb->setValue('circle_id', $qb->createNamedParameter($circleInvitation->getCircleId()))
+ ->setValue('invitation_code', $qb->createNamedParameter($circleInvitation->getInvitationCode()))
+ ->setValue('created_by', $qb->createNamedParameter($circleInvitation->getCreatedBy()));
+ $qb->executeStatement();
+ }
+
+ /**
+ * @throws InvalidIdException
+ */
+ public function replace(CircleInvitation $circleInvitation): void {
+ $qb = $this->getQueryBuilder();
+ $qb->delete(self::TABLE_INVITATIONS);
+ $qb->limitToCircleId($circleInvitation->getCircleId());
+ $qb->executeStatement();
+
+ $this->save($circleInvitation);
+ }
+
+ public function delete(string $circleId): void {
+ $qb = $this->getQueryBuilder();
+ $qb->delete(self::TABLE_INVITATIONS);
+ $qb->limitToCircleId($circleId);
+ $qb->executeStatement();
+ }
+}
diff --git a/lib/Db/CircleRequest.php b/lib/Db/CircleRequest.php
index 0388c6b4c..f26be7f7c 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);
}
@@ -375,6 +379,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 1f5a2bad6..62ac44db0 100644
--- a/lib/Db/CircleRequestBuilder.php
+++ b/lib/Db/CircleRequestBuilder.php
@@ -33,7 +33,6 @@ protected function getCircleInsertSql(): CoreQueryBuilder {
return $qb;
}
-
/**
* @return CoreQueryBuilder&IQueryBuilder
*/
@@ -44,7 +43,6 @@ protected function getCircleUpdateSql(): CoreQueryBuilder {
return $qb;
}
-
/**
* @param string $alias
* @param bool $single
@@ -65,7 +63,6 @@ protected function getCircleSelectSql(
return $qb;
}
-
/**
* Base of the Sql Delete request
*
@@ -78,6 +75,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 93c4da6f8..3b596bd87 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
],
@@ -865,6 +866,27 @@ public function leftJoinOwner(string $alias, string $field = 'unique_id'): void
$this->leftJoinBasedOn($aliasMember);
}
+ /**
+ * @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
@@ -1332,6 +1354,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);
@@ -1547,6 +1575,22 @@ public function limitToShareOwner(
}
}
+ 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
@@ -1635,6 +1679,17 @@ private function generateMemberSelectAlias(string $alias, 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 e02a19dca..fbcdac819 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, ISecureRandom::CHAR_HUMAN_READABLE);
+
+ $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));
+ }
+
+ 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);
+ }
+
+ 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 2e89056f3..27dabb2c3 100644
--- a/lib/FederatedItems/CircleJoin.php
+++ b/lib/FederatedItems/CircleJoin.php
@@ -23,7 +23,6 @@
use OCA\Circles\Exceptions\RequestBuilderException;
use OCA\Circles\Exceptions\UnknownRemoteException;
use OCA\Circles\IFederatedItem;
-use OCA\Circles\IFederatedItemAsyncProcess;
use OCA\Circles\IFederatedItemHighSeverity;
use OCA\Circles\IFederatedItemInitiatorMembershipNotRequired;
use OCA\Circles\IFederatedItemMemberCheckNotRequired;
@@ -47,7 +46,6 @@
class CircleJoin implements
IFederatedItem,
IFederatedItemInitiatorMembershipNotRequired,
- IFederatedItemAsyncProcess,
IFederatedItemHighSeverity,
IFederatedItemMemberCheckNotRequired,
IFederatedItemMemberOptional {
@@ -98,7 +96,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);
@@ -219,11 +218,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) {
@@ -245,7 +245,8 @@ private function manageMemberStatus(Circle $circle, Member $member) {
throw new MemberAlreadyExistsException(StatusCode::$CIRCLE_JOIN[122], 122);
} catch (MemberNotFoundException) {
- 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..9285ff19a
--- /dev/null
+++ b/lib/FederatedItems/CircleRevokeInvitation.php
@@ -0,0 +1,52 @@
+getCircle();
+
+ $initiatorHelper = new MemberHelper($circle->getInitiator());
+ $initiatorHelper->mustBeAdmin();
+
+ $new = clone $circle;
+ $new->setCircleInvitation(null);
+
+ $event->setOutcome($this->serialize($new));
+ }
+
+ 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);
+ }
+
+ 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 0e1b76688..f88133f08 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;
@@ -665,6 +668,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
@@ -807,6 +827,13 @@ public function import(array $data): IDeserializable {
} catch (InvalidItemException) {
}
+ 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) {
}
}
diff --git a/lib/Model/CircleInvitation.php b/lib/Model/CircleInvitation.php
new file mode 100644
index 000000000..24248c5e0
--- /dev/null
+++ b/lib/Model/CircleInvitation.php
@@ -0,0 +1,110 @@
+circleId = $circleId;
+
+ return $this;
+ }
+
+ public function getCircleId(): string {
+ return $this->circleId;
+ }
+
+ public function setInvitationCode(string $invitationCode): self {
+ $this->invitationCode = $invitationCode;
+
+ return $this;
+ }
+
+ public function getInvitationCode(): string {
+ return $this->invitationCode;
+ }
+
+ public function setCreatedBy(string $createdBy): self {
+ $this->createdBy = $createdBy;
+
+ return $this;
+ }
+
+ public function getCreatedBy(): string {
+ return $this->createdBy;
+ }
+
+ public function setCreated(int $created): self {
+ $this->created = $created;
+
+ return $this;
+ }
+
+ public function getCreated(): int {
+ return $this->created;
+ }
+
+ 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;
+ }
+
+
+ public function jsonSerialize(): array {
+ $arr = [
+ 'circleId' => $this->getCircleId(),
+ 'invitationCode' => $this->getInvitationCode(),
+ 'createdBy' => $this->getCreatedBy(),
+ 'created' => $this->getCreated(),
+ ];
+
+ return $arr;
+ }
+
+
+ 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 c96e82b5c..4913dcbe0 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;
@@ -184,6 +185,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;
@@ -265,6 +272,14 @@ private function importIntoCircle(Circle $circle, array $data, string $path, str
} catch (MemberNotFoundException) {
}
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 2c8f34ef9..e21f061bc 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;
@@ -296,6 +298,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
@@ -382,7 +438,8 @@ 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();
/** @var FederatedUser $currentUser */
$currentUser = $this->federatedUserService->getCurrentUser();
@@ -391,6 +448,12 @@ public function circleJoin(string $circleId): array {
$probe->includeNonVisibleCircles()
->emulateVisitor();
+ if ($invitationCode) {
+ $probe->includeSystemCircles()
+ ->includeHiddenCircles()
+ ->filterByInvitationCode($invitationCode);
+ }
+
$circle = $this->circleRequest->getCircle(
$circleId,
$currentUser,
@@ -402,6 +465,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);
@@ -411,7 +475,6 @@ public function circleJoin(string $circleId): array {
return $event->getOutcome();
}
-
/**
* @param string $circleId
* @param bool $force