From 5999d7789391dcd7fdc6f5c202b03d92b86e0702 Mon Sep 17 00:00:00 2001 From: Micke Nordin Date: Fri, 28 Feb 2025 16:37:18 +0100 Subject: [PATCH] feat(OCM-invites): Add invitation class and emit event I realize that we need to be able to react to accepted invites elsewhere, e.g. contacts app, so adding invite class and event for that purpose. Also rename the migration and bump the version so it will take affect correctly. Co-authored-by: Navid Shokri Signed-off-by: Micke Nordin --- apps/cloud_federation_api/appinfo/info.xml | 2 +- .../composer/composer/InstalledVersions.php | 27 +------ .../composer/composer/autoload_classmap.php | 3 + .../composer/composer/autoload_static.php | 3 + .../Controller/RequestHandlerController.php | 74 ++++++++++++++----- .../lib/Events/OCMInvitationAcceptedEvent.php | 26 +++++++ ...04.php => Version1016Date202502262004.php} | 2 +- .../lib/OCMInvitation.php | 18 +++++ lib/composer/composer/autoload_psr4.php | 1 - lib/composer/composer/autoload_static.php | 8 -- 10 files changed, 111 insertions(+), 53 deletions(-) create mode 100644 apps/cloud_federation_api/lib/Events/OCMInvitationAcceptedEvent.php rename apps/cloud_federation_api/lib/Migration/{Version1015Date202502262004.php => Version1016Date202502262004.php} (97%) create mode 100644 apps/cloud_federation_api/lib/OCMInvitation.php diff --git a/apps/cloud_federation_api/appinfo/info.xml b/apps/cloud_federation_api/appinfo/info.xml index 57a8d9b50a0..81343cb49bf 100644 --- a/apps/cloud_federation_api/appinfo/info.xml +++ b/apps/cloud_federation_api/appinfo/info.xml @@ -9,7 +9,7 @@ Cloud Federation API Enable clouds to communicate with each other and exchange data The Cloud Federation API enables various Nextcloud instances to communicate with each other and to exchange data. - 1.15.0 + 1.16.0 agpl Bjoern Schiessle CloudFederationAPI diff --git a/apps/cloud_federation_api/composer/composer/InstalledVersions.php b/apps/cloud_federation_api/composer/composer/InstalledVersions.php index 6d29bff66aa..51e734a774b 100644 --- a/apps/cloud_federation_api/composer/composer/InstalledVersions.php +++ b/apps/cloud_federation_api/composer/composer/InstalledVersions.php @@ -32,11 +32,6 @@ class InstalledVersions */ private static $installed; - /** - * @var bool - */ - private static $installedIsLocalDir; - /** * @var bool|null */ @@ -314,12 +309,6 @@ class InstalledVersions { self::$installed = $data; self::$installedByVendor = array(); - - // when using reload, we disable the duplicate protection to ensure that self::$installed data is - // always returned, but we cannot know whether it comes from the installed.php in __DIR__ or not, - // so we have to assume it does not, and that may result in duplicate data being returned when listing - // all installed packages for example - self::$installedIsLocalDir = false; } /** @@ -333,27 +322,19 @@ class InstalledVersions } $installed = array(); - $copiedLocalDir = false; if (self::$canGetVendors) { - $selfDir = strtr(__DIR__, '\\', '/'); foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) { - $vendorDir = strtr($vendorDir, '\\', '/'); if (isset(self::$installedByVendor[$vendorDir])) { $installed[] = self::$installedByVendor[$vendorDir]; } elseif (is_file($vendorDir.'/composer/installed.php')) { /** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array} $required */ $required = require $vendorDir.'/composer/installed.php'; - self::$installedByVendor[$vendorDir] = $required; - $installed[] = $required; - if (self::$installed === null && $vendorDir.'/composer' === $selfDir) { - self::$installed = $required; - self::$installedIsLocalDir = true; + $installed[] = self::$installedByVendor[$vendorDir] = $required; + if (null === self::$installed && strtr($vendorDir.'/composer', '\\', '/') === strtr(__DIR__, '\\', '/')) { + self::$installed = $installed[count($installed) - 1]; } } - if (self::$installedIsLocalDir && $vendorDir.'/composer' === $selfDir) { - $copiedLocalDir = true; - } } } @@ -369,7 +350,7 @@ class InstalledVersions } } - if (self::$installed !== array() && !$copiedLocalDir) { + if (self::$installed !== array()) { $installed[] = self::$installed; } diff --git a/apps/cloud_federation_api/composer/composer/autoload_classmap.php b/apps/cloud_federation_api/composer/composer/autoload_classmap.php index dd096ebf563..aa0c7c5ad79 100644 --- a/apps/cloud_federation_api/composer/composer/autoload_classmap.php +++ b/apps/cloud_federation_api/composer/composer/autoload_classmap.php @@ -11,5 +11,8 @@ return array( 'OCA\\CloudFederationAPI\\Capabilities' => $baseDir . '/../lib/Capabilities.php', 'OCA\\CloudFederationAPI\\Config' => $baseDir . '/../lib/Config.php', 'OCA\\CloudFederationAPI\\Controller\\RequestHandlerController' => $baseDir . '/../lib/Controller/RequestHandlerController.php', + 'OCA\\CloudFederationAPI\\Migration\\Version1016Date202502262004' => $baseDir . '/../lib/Migration/Version1016Date202502262004.php', 'OCA\\CloudFederationAPI\\ResponseDefinitions' => $baseDir . '/../lib/ResponseDefinitions.php', + 'OCA\\CloudFederationApi\\Events\\OCMInvitationAcceptedEvent' => $baseDir . '/../lib/Events/OCMInvitationAcceptedEvent.php', + 'OCA\\CloudFederationApi\\OCMInvitation' => $baseDir . '/../lib/OCMInvitation.php', ); diff --git a/apps/cloud_federation_api/composer/composer/autoload_static.php b/apps/cloud_federation_api/composer/composer/autoload_static.php index 75557a20126..133580e463e 100644 --- a/apps/cloud_federation_api/composer/composer/autoload_static.php +++ b/apps/cloud_federation_api/composer/composer/autoload_static.php @@ -26,7 +26,10 @@ class ComposerStaticInitCloudFederationAPI 'OCA\\CloudFederationAPI\\Capabilities' => __DIR__ . '/..' . '/../lib/Capabilities.php', 'OCA\\CloudFederationAPI\\Config' => __DIR__ . '/..' . '/../lib/Config.php', 'OCA\\CloudFederationAPI\\Controller\\RequestHandlerController' => __DIR__ . '/..' . '/../lib/Controller/RequestHandlerController.php', + 'OCA\\CloudFederationAPI\\Migration\\Version1016Date202502262004' => __DIR__ . '/..' . '/../lib/Migration/Version1016Date202502262004.php', 'OCA\\CloudFederationAPI\\ResponseDefinitions' => __DIR__ . '/..' . '/../lib/ResponseDefinitions.php', + 'OCA\\CloudFederationApi\\Events\\OCMInvitationAcceptedEvent' => __DIR__ . '/..' . '/../lib/Events/OCMInvitationAcceptedEvent.php', + 'OCA\\CloudFederationApi\\OCMInvitation' => __DIR__ . '/..' . '/../lib/OCMInvitation.php', ); public static function getInitializer(ClassLoader $loader) diff --git a/apps/cloud_federation_api/lib/Controller/RequestHandlerController.php b/apps/cloud_federation_api/lib/Controller/RequestHandlerController.php index 6a1c66661e9..636f78d0a9d 100644 --- a/apps/cloud_federation_api/lib/Controller/RequestHandlerController.php +++ b/apps/cloud_federation_api/lib/Controller/RequestHandlerController.php @@ -1,8 +1,10 @@ factory->getCloudFederationShare($shareWith, $name, $description, $providerId, $owner, $ownerDisplayName, $sharedBy, $sharedByDisplayName, '', $shareType, $resourceType); $share->setProtocol($protocol); $provider->shareReceived($share); - } catch (ProviderDoesNotExistsException|ProviderCouldNotAddShareException $e) { + } catch (ProviderDoesNotExistsException | ProviderCouldNotAddShareException $e) { return new JSONResponse( ['message' => $e->getMessage()], Http::STATUS_NOT_IMPLEMENTED @@ -243,7 +252,8 @@ class RequestHandlerController extends Controller { #[PublicPage] #[NoCSRFRequired] #[BruteForceProtection(action: 'inviteAccepted')] - public function inviteAccepted(string $recipientProvider, string $token, string $userId, string $email, string $name): JSONResponse { + public function inviteAccepted(string $recipientProvider, string $token, string $userId, string $email, string $name): JSONResponse + { $this->logger->debug('Invite accepted for ' . $userId . ' with token ' . $token . ' and email ' . $email . ' and name ' . $name); /** @var IQueryBuilder $qb */ @@ -255,6 +265,7 @@ class RequestHandlerController extends Controller { $data = $result->fetch(); $result->closeCursor(); $found_for_this_user = false; + $updated = new DateTime("now"); if ($data) { $found_for_this_user = $data['recipient_user_id'] === $userId && isset($data['user_id']); } @@ -265,17 +276,22 @@ class RequestHandlerController extends Controller { $response->throttle(); return $response; } - if(!$this->trustedServers->isTrustedServer($recipientProvider)) { + if (!$this->trustedServers->isTrustedServer($recipientProvider)) { $response = ['message' => 'Remote server not trusted', 'error' => true]; $status = Http::STATUS_FORBIDDEN; - return new JSONResponse($response,$status); + return new JSONResponse($response, $status); } // Note: Not implementing 404 Invitation token does not exist, instead using 400 - if ($data['accepted'] === true ) { + if ($data['accepted'] === true) { $response = ['message' => 'Invite already accepted', 'error' => true]; $status = Http::STATUS_CONFLICT; - return new JSONResponse($response,$status); + return new JSONResponse($response, $status); + } + if ($data['expiresAt'] < $updated) { + $response = ['message' => 'Invitation expired', 'error' => true]; + $status = Http::STATUS_BAD_REQUEST; + return new JSONResponse($response, $status); } $localUser = $this->userManager->get($data['user_id']); @@ -284,7 +300,6 @@ class RequestHandlerController extends Controller { $response = ['userID' => $data['user_id'], 'email' => $sharedFromEmail, 'name' => $sharedFromDisplayName]; $status = Http::STATUS_OK; - $updated = new DateTime("now"); $qb->update('federated_invites') ->set('accepted', $qb->createNamedParameter(true)) ->set('acceptedAt', $qb->createNamedParameter($updated)) @@ -295,7 +310,21 @@ class RequestHandlerController extends Controller { ->where($qb->expr()->eq('token', $qb->createNamedParameter($token))); $qb->executeStatement(); - return new JSONResponse($response,$status); + $invitation = new OCMInvitation( + accepted: true, + recipient_email: $email, + recipient_name: $name, + recipient_user_id: $userId, + recipient_provider: $recipientProvider, + token: $token, + user_id: $data['user_id'], + acceptedAt: $updated, + createdAt: $data['createdAt'], + expiresAt: $data['expiresAt'] + ); + $this->dispatcher->dispatchTyped(new OCMInvitationAcceptedEvent($invitation)); + + return new JSONResponse($response, $status); } /** @@ -316,9 +345,11 @@ class RequestHandlerController extends Controller { #[NoCSRFRequired] #[PublicPage] #[BruteForceProtection(action: 'receiveFederatedShareNotification')] - public function receiveNotification($notificationType, $resourceType, $providerId, ?array $notification) { + public function receiveNotification($notificationType, $resourceType, $providerId, ?array $notification) + { // check if all required parameters are set - if ($notificationType === null || + if ( + $notificationType === null || $resourceType === null || $providerId === null || !is_array($notification) @@ -394,7 +425,8 @@ class RequestHandlerController extends Controller { * @param string $uid * @return string mixed */ - private function mapUid($uid) { + private function mapUid($uid) + { // FIXME this should be a method in the user management instead $this->logger->debug('shareWith before, ' . $uid, ['app' => $this->appName]); Util::emitHook( @@ -417,12 +449,13 @@ class RequestHandlerController extends Controller { * @return IIncomingSignedRequest|null null if remote does not (and never did) support signed request * @throws IncomingRequestException */ - private function getSignedRequest(): ?IIncomingSignedRequest { + private function getSignedRequest(): ?IIncomingSignedRequest + { try { $signedRequest = $this->signatureManager->getIncomingSignedRequest($this->signatoryManager); $this->logger->debug('signed request available', ['signedRequest' => $signedRequest]); return $signedRequest; - } catch (SignatureNotFoundException|SignatoryNotFoundException $e) { + } catch (SignatureNotFoundException | SignatoryNotFoundException $e) { $this->logger->debug('remote does not support signed request', ['exception' => $e]); // remote does not support signed request. // currently we still accept unsigned request until lazy appconfig @@ -452,7 +485,8 @@ class RequestHandlerController extends Controller { * * @throws IncomingRequestException */ - private function confirmSignedOrigin(?IIncomingSignedRequest $signedRequest, string $key, string $value): void { + private function confirmSignedOrigin(?IIncomingSignedRequest $signedRequest, string $key, string $value): void + { if ($signedRequest === null) { $instance = $this->getHostFromFederationId($value); try { @@ -516,7 +550,8 @@ class RequestHandlerController extends Controller { * @return void * @throws IncomingRequestException */ - private function confirmNotificationEntry(?IIncomingSignedRequest $signedRequest, string $entry): void { + private function confirmNotificationEntry(?IIncomingSignedRequest $signedRequest, string $entry): void + { $instance = $this->getHostFromFederationId($entry); if ($signedRequest === null) { try { @@ -535,7 +570,8 @@ class RequestHandlerController extends Controller { * @return string * @throws IncomingRequestException */ - private function getHostFromFederationId(string $entry): string { + private function getHostFromFederationId(string $entry): string + { if (!str_contains($entry, '@')) { throw new IncomingRequestException('entry ' . $entry . ' does not contains @'); } diff --git a/apps/cloud_federation_api/lib/Events/OCMInvitationAcceptedEvent.php b/apps/cloud_federation_api/lib/Events/OCMInvitationAcceptedEvent.php new file mode 100644 index 00000000000..d1ebcdaf7d8 --- /dev/null +++ b/apps/cloud_federation_api/lib/Events/OCMInvitationAcceptedEvent.php @@ -0,0 +1,26 @@ + + * SPDX-License-Identifier: AGPL-3.0-only + */ + +namespace OCA\CloudFederationApi\Events; + +use OCP\EventDispatcher\Event; +use OCA\CloudFederationApi\OCMInvitation; + +class OCMInvitationAcceptedEvent extends Event +{ + public function __construct( + private OCMInvitation $invitation + ) { + parent::__construct(); + } + + public function getInvitation(): OCMInvitation + { + return $this->invitation; + } +} diff --git a/apps/cloud_federation_api/lib/Migration/Version1015Date202502262004.php b/apps/cloud_federation_api/lib/Migration/Version1016Date202502262004.php similarity index 97% rename from apps/cloud_federation_api/lib/Migration/Version1015Date202502262004.php rename to apps/cloud_federation_api/lib/Migration/Version1016Date202502262004.php index 436bdcb7f5d..ab4bf627c14 100644 --- a/apps/cloud_federation_api/lib/Migration/Version1015Date202502262004.php +++ b/apps/cloud_federation_api/lib/Migration/Version1016Date202502262004.php @@ -15,7 +15,7 @@ use OCP\DB\Types; use OCP\Migration\IOutput; use OCP\Migration\SimpleMigrationStep; -class Version1015Date202502262004 extends SimpleMigrationStep +class Version1016Date202502262004 extends SimpleMigrationStep { /** diff --git a/apps/cloud_federation_api/lib/OCMInvitation.php b/apps/cloud_federation_api/lib/OCMInvitation.php new file mode 100644 index 00000000000..ac3dfe3ca53 --- /dev/null +++ b/apps/cloud_federation_api/lib/OCMInvitation.php @@ -0,0 +1,18 @@ + array($baseDir . '/lib/private'), 'OCP\\' => array($baseDir . '/lib/public'), 'NCU\\' => array($baseDir . '/lib/unstable'), - 'Bamarni\\Composer\\Bin\\' => array($vendorDir . '/bamarni/composer-bin-plugin/src'), '' => array($baseDir . '/lib/private/legacy'), ); diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php index 956fa820ac5..7a38da17c82 100644 --- a/lib/composer/composer/autoload_static.php +++ b/lib/composer/composer/autoload_static.php @@ -21,10 +21,6 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2 array ( 'NCU\\' => 4, ), - 'B' => - array ( - 'Bamarni\\Composer\\Bin\\' => 21, - ), ); public static $prefixDirsPsr4 = array ( @@ -44,10 +40,6 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2 array ( 0 => __DIR__ . '/../../..' . '/lib/unstable', ), - 'Bamarni\\Composer\\Bin\\' => - array ( - 0 => __DIR__ . '/..' . '/bamarni/composer-bin-plugin/src', - ), ); public static $fallbackDirsPsr4 = array (