From 5cbdfac055229d53b7096ad105b8c70e0f56491a Mon Sep 17 00:00:00 2001 From: "Antoon P." Date: Tue, 23 Jun 2026 15:46:12 +0200 Subject: [PATCH] fix(ocm): Move invite-accepted capability and implementation to contacts app. Signed-off-by: Antoon P. --- apps/cloud_federation_api/appinfo/info.xml | 5 + apps/cloud_federation_api/appinfo/routes.php | 6 - .../composer/composer/autoload_classmap.php | 5 +- .../composer/composer/autoload_static.php | 5 +- .../lib/AppInfo/Application.php | 2 - .../cloud_federation_api/lib/Capabilities.php | 34 --- .../Controller/RequestHandlerController.php | 99 ------- .../lib/Db/FederatedInvite.php | 62 ---- .../lib/Db/FederatedInviteMapper.php | 33 --- .../Events/FederatedInviteAcceptedEvent.php | 24 -- .../Migration/DropFederatedInvitesTable.php | 54 ++++ apps/cloud_federation_api/openapi.json | 170 ----------- .../CleanupExpiredOcmTokensJobTest.php | 1 + .../tests/Controller/TokenControllerTest.php | 2 + .../tests/RequestHandlerControllerTest.php | 264 ------------------ core/AppInfo/ConfigLexicon.php | 2 - lib/private/OCM/OCMDiscoveryService.php | 8 +- .../OCM/Events/LocalOCMDiscoveryEvent.php | 11 + openapi.json | 173 ------------ psalm.xml | 1 - 20 files changed, 76 insertions(+), 885 deletions(-) delete mode 100644 apps/cloud_federation_api/lib/Capabilities.php delete mode 100644 apps/cloud_federation_api/lib/Db/FederatedInvite.php delete mode 100644 apps/cloud_federation_api/lib/Db/FederatedInviteMapper.php delete mode 100644 apps/cloud_federation_api/lib/Events/FederatedInviteAcceptedEvent.php create mode 100644 apps/cloud_federation_api/lib/Migration/DropFederatedInvitesTable.php delete mode 100644 apps/cloud_federation_api/tests/RequestHandlerControllerTest.php diff --git a/apps/cloud_federation_api/appinfo/info.xml b/apps/cloud_federation_api/appinfo/info.xml index 7991f4f062d..77b75d698e4 100644 --- a/apps/cloud_federation_api/appinfo/info.xml +++ b/apps/cloud_federation_api/appinfo/info.xml @@ -25,4 +25,9 @@ OCA\CloudFederationAPI\BackgroundJob\CleanupExpiredOcmTokensJob + + + OCA\CloudFederationAPI\Migration\DropFederatedInvitesTable + + diff --git a/apps/cloud_federation_api/appinfo/routes.php b/apps/cloud_federation_api/appinfo/routes.php index cd681ae98e7..9dcffd0aa34 100644 --- a/apps/cloud_federation_api/appinfo/routes.php +++ b/apps/cloud_federation_api/appinfo/routes.php @@ -20,12 +20,6 @@ return [ 'verb' => 'POST', 'root' => '/ocm', ], - [ - 'name' => 'RequestHandler#inviteAccepted', - 'url' => '/invite-accepted', - 'verb' => 'POST', - 'root' => '/ocm', - ], // needs to be kept at the bottom of the list [ diff --git a/apps/cloud_federation_api/composer/composer/autoload_classmap.php b/apps/cloud_federation_api/composer/composer/autoload_classmap.php index 038e4d6873d..085ea9dc176 100644 --- a/apps/cloud_federation_api/composer/composer/autoload_classmap.php +++ b/apps/cloud_federation_api/composer/composer/autoload_classmap.php @@ -9,16 +9,13 @@ return array( 'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php', 'OCA\\CloudFederationAPI\\AppInfo\\Application' => $baseDir . '/../lib/AppInfo/Application.php', 'OCA\\CloudFederationAPI\\BackgroundJob\\CleanupExpiredOcmTokensJob' => $baseDir . '/../lib/BackgroundJob/CleanupExpiredOcmTokensJob.php', - 'OCA\\CloudFederationAPI\\Capabilities' => $baseDir . '/../lib/Capabilities.php', 'OCA\\CloudFederationAPI\\Config' => $baseDir . '/../lib/Config.php', 'OCA\\CloudFederationAPI\\Controller\\OCMRequestController' => $baseDir . '/../lib/Controller/OCMRequestController.php', 'OCA\\CloudFederationAPI\\Controller\\RequestHandlerController' => $baseDir . '/../lib/Controller/RequestHandlerController.php', 'OCA\\CloudFederationAPI\\Controller\\TokenController' => $baseDir . '/../lib/Controller/TokenController.php', - 'OCA\\CloudFederationAPI\\Db\\FederatedInvite' => $baseDir . '/../lib/Db/FederatedInvite.php', - 'OCA\\CloudFederationAPI\\Db\\FederatedInviteMapper' => $baseDir . '/../lib/Db/FederatedInviteMapper.php', 'OCA\\CloudFederationAPI\\Db\\OcmTokenMap' => $baseDir . '/../lib/Db/OcmTokenMap.php', 'OCA\\CloudFederationAPI\\Db\\OcmTokenMapMapper' => $baseDir . '/../lib/Db/OcmTokenMapMapper.php', - 'OCA\\CloudFederationAPI\\Events\\FederatedInviteAcceptedEvent' => $baseDir . '/../lib/Events/FederatedInviteAcceptedEvent.php', + 'OCA\\CloudFederationAPI\\Migration\\DropFederatedInvitesTable' => $baseDir . '/../lib/Migration/DropFederatedInvitesTable.php', 'OCA\\CloudFederationAPI\\Migration\\Version1016Date202502262004' => $baseDir . '/../lib/Migration/Version1016Date202502262004.php', 'OCA\\CloudFederationAPI\\Migration\\Version1017Date20260306120000' => $baseDir . '/../lib/Migration/Version1017Date20260306120000.php', 'OCA\\CloudFederationAPI\\ResponseDefinitions' => $baseDir . '/../lib/ResponseDefinitions.php', diff --git a/apps/cloud_federation_api/composer/composer/autoload_static.php b/apps/cloud_federation_api/composer/composer/autoload_static.php index f68f6714afa..70a4a8fa6a7 100644 --- a/apps/cloud_federation_api/composer/composer/autoload_static.php +++ b/apps/cloud_federation_api/composer/composer/autoload_static.php @@ -24,16 +24,13 @@ class ComposerStaticInitCloudFederationAPI 'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php', 'OCA\\CloudFederationAPI\\AppInfo\\Application' => __DIR__ . '/..' . '/../lib/AppInfo/Application.php', 'OCA\\CloudFederationAPI\\BackgroundJob\\CleanupExpiredOcmTokensJob' => __DIR__ . '/..' . '/../lib/BackgroundJob/CleanupExpiredOcmTokensJob.php', - 'OCA\\CloudFederationAPI\\Capabilities' => __DIR__ . '/..' . '/../lib/Capabilities.php', 'OCA\\CloudFederationAPI\\Config' => __DIR__ . '/..' . '/../lib/Config.php', 'OCA\\CloudFederationAPI\\Controller\\OCMRequestController' => __DIR__ . '/..' . '/../lib/Controller/OCMRequestController.php', 'OCA\\CloudFederationAPI\\Controller\\RequestHandlerController' => __DIR__ . '/..' . '/../lib/Controller/RequestHandlerController.php', 'OCA\\CloudFederationAPI\\Controller\\TokenController' => __DIR__ . '/..' . '/../lib/Controller/TokenController.php', - 'OCA\\CloudFederationAPI\\Db\\FederatedInvite' => __DIR__ . '/..' . '/../lib/Db/FederatedInvite.php', - 'OCA\\CloudFederationAPI\\Db\\FederatedInviteMapper' => __DIR__ . '/..' . '/../lib/Db/FederatedInviteMapper.php', 'OCA\\CloudFederationAPI\\Db\\OcmTokenMap' => __DIR__ . '/..' . '/../lib/Db/OcmTokenMap.php', 'OCA\\CloudFederationAPI\\Db\\OcmTokenMapMapper' => __DIR__ . '/..' . '/../lib/Db/OcmTokenMapMapper.php', - 'OCA\\CloudFederationAPI\\Events\\FederatedInviteAcceptedEvent' => __DIR__ . '/..' . '/../lib/Events/FederatedInviteAcceptedEvent.php', + 'OCA\\CloudFederationAPI\\Migration\\DropFederatedInvitesTable' => __DIR__ . '/..' . '/../lib/Migration/DropFederatedInvitesTable.php', 'OCA\\CloudFederationAPI\\Migration\\Version1016Date202502262004' => __DIR__ . '/..' . '/../lib/Migration/Version1016Date202502262004.php', 'OCA\\CloudFederationAPI\\Migration\\Version1017Date20260306120000' => __DIR__ . '/..' . '/../lib/Migration/Version1017Date20260306120000.php', 'OCA\\CloudFederationAPI\\ResponseDefinitions' => __DIR__ . '/..' . '/../lib/ResponseDefinitions.php', diff --git a/apps/cloud_federation_api/lib/AppInfo/Application.php b/apps/cloud_federation_api/lib/AppInfo/Application.php index 67f55d66637..9170e9d0792 100644 --- a/apps/cloud_federation_api/lib/AppInfo/Application.php +++ b/apps/cloud_federation_api/lib/AppInfo/Application.php @@ -9,7 +9,6 @@ declare(strict_types=1); namespace OCA\CloudFederationAPI\AppInfo; -use OCA\CloudFederationAPI\Capabilities; use OCP\AppFramework\App; use OCP\AppFramework\Bootstrap\IBootContext; use OCP\AppFramework\Bootstrap\IBootstrap; @@ -24,7 +23,6 @@ class Application extends App implements IBootstrap { #[\Override] public function register(IRegistrationContext $context): void { - $context->registerCapability(Capabilities::class); } #[\Override] diff --git a/apps/cloud_federation_api/lib/Capabilities.php b/apps/cloud_federation_api/lib/Capabilities.php deleted file mode 100644 index 99b7ca528cc..00000000000 --- a/apps/cloud_federation_api/lib/Capabilities.php +++ /dev/null @@ -1,34 +0,0 @@ -> - * @throws OCMArgumentException - */ - #[\Override] - public function getCapabilities() { - $provider = $this->ocmDiscoveryService->getLocalOCMProvider(false); - return ['ocm' => $provider->jsonSerialize()]; - } -} diff --git a/apps/cloud_federation_api/lib/Controller/RequestHandlerController.php b/apps/cloud_federation_api/lib/Controller/RequestHandlerController.php index a5af02af2ad..4da9300f1f3 100644 --- a/apps/cloud_federation_api/lib/Controller/RequestHandlerController.php +++ b/apps/cloud_federation_api/lib/Controller/RequestHandlerController.php @@ -10,12 +10,9 @@ namespace OCA\CloudFederationAPI\Controller; use OC\Authentication\Token\PublicKeyTokenProvider; use OC\OCM\OCMSignatoryManager; use OCA\CloudFederationAPI\Config; -use OCA\CloudFederationAPI\Db\FederatedInviteMapper; use OCA\CloudFederationAPI\Db\OcmTokenMapMapper; -use OCA\CloudFederationAPI\Events\FederatedInviteAcceptedEvent; use OCA\CloudFederationAPI\ResponseDefinitions; use OCP\AppFramework\Controller; -use OCP\AppFramework\Db\DoesNotExistException; use OCP\AppFramework\Http; use OCP\AppFramework\Http\Attribute\BruteForceProtection; use OCP\AppFramework\Http\Attribute\NoCSRFRequired; @@ -69,7 +66,6 @@ class RequestHandlerController extends Controller { private ICloudFederationProviderManager $cloudFederationProviderManager, private Config $config, private IEventDispatcher $dispatcher, - private FederatedInviteMapper $federatedInviteMapper, private readonly IAppConfig $appConfig, private ICloudFederationFactory $factory, private ICloudIdManager $cloudIdManager, @@ -272,101 +268,6 @@ class RequestHandlerController extends Controller { return new JSONResponse($responseData, Http::STATUS_CREATED); } - /** - * Inform the sender that an invitation was accepted to start sharing - * - * Inform about an accepted invitation so the user on the sender provider's side - * can initiate the OCM share creation. To protect the identity of the parties, - * for shares created following an OCM invitation, the user id MAY be hashed, - * and recipients implementing the OCM invitation workflow MAY refuse to process - * shares coming from unknown parties. - * @link https://cs3org.github.io/OCM-API/docs.html?branch=v1.1.0&repo=OCM-API&user=cs3org#/paths/~1invite-accepted/post - * - * @param string $recipientProvider The address of the recipent's provider - * @param string $token The token used for the invitation - * @param string $userID The userID of the recipient at the recipient's provider - * @param string $email The email address of the recipient - * @param string $name The display name of the recipient - * - * @return JSONResponse|JSONResponse - * - * Note: Not implementing 404 Invitation token does not exist, instead using 400 - * 200: Invitation accepted - * 400: Invalid token - * 403: Invitation token does not exist - * 409: User is already known by the OCM provider - */ - #[PublicPage] - #[NoCSRFRequired] - #[BruteForceProtection(action: 'inviteAccepted')] - public function inviteAccepted(string $recipientProvider, string $token, string $userID, string $email, string $name): JSONResponse { - $this->logger->debug('Processing share invitation for ' . $userID . ' with token ' . $token . ' and email ' . $email . ' and name ' . $name); - - $updated = $this->timeFactory->getTime(); - - if ($token === '') { - $response = new JSONResponse(['message' => 'Invalid or non existing token', 'error' => true], Http::STATUS_BAD_REQUEST); - $response->throttle(); - return $response; - } - - try { - $invitation = $this->federatedInviteMapper->findByToken($token); - } catch (DoesNotExistException) { - $response = ['message' => 'Invalid or non existing token', 'error' => true]; - $status = Http::STATUS_BAD_REQUEST; - $response = new JSONResponse($response, $status); - $response->throttle(); - return $response; - } - - if ($invitation->isAccepted() === true) { - $response = ['message' => 'Invite already accepted', 'error' => true]; - $status = Http::STATUS_CONFLICT; - return new JSONResponse($response, $status); - } - - if ($invitation->getExpiredAt() !== null && $updated > $invitation->getExpiredAt()) { - $response = ['message' => 'Invitation expired', 'error' => true]; - $status = Http::STATUS_BAD_REQUEST; - return new JSONResponse($response, $status); - } - $localUser = $this->userManager->get($invitation->getUserId()); - if ($localUser === null) { - $response = ['message' => 'Invalid or non existing token', 'error' => true]; - $status = Http::STATUS_BAD_REQUEST; - $response = new JSONResponse($response, $status); - $response->throttle(); - return $response; - } - - $sharedFromEmail = $localUser->getEMailAddress(); - if ($sharedFromEmail === null) { - $response = ['message' => 'Invalid or non existing token', 'error' => true]; - $status = Http::STATUS_BAD_REQUEST; - $response = new JSONResponse($response, $status); - $response->throttle(); - return $response; - } - $sharedFromDisplayName = $localUser->getDisplayName(); - - $response = ['userID' => $localUser->getUID(), 'email' => $sharedFromEmail, 'name' => $sharedFromDisplayName]; - $status = Http::STATUS_OK; - - $invitation->setAccepted(true); - $invitation->setRecipientEmail($email); - $invitation->setRecipientName($name); - $invitation->setRecipientProvider($recipientProvider); - $invitation->setRecipientUserId($userID); - $invitation->setAcceptedAt($updated); - $invitation = $this->federatedInviteMapper->update($invitation); - - $event = new FederatedInviteAcceptedEvent($invitation); - $this->dispatcher->dispatchTyped($event); - - return new JSONResponse($response, $status); - } - /** * Send a notification about an existing share * diff --git a/apps/cloud_federation_api/lib/Db/FederatedInvite.php b/apps/cloud_federation_api/lib/Db/FederatedInvite.php deleted file mode 100644 index b2447ff4e23..00000000000 --- a/apps/cloud_federation_api/lib/Db/FederatedInvite.php +++ /dev/null @@ -1,62 +0,0 @@ -addType('accepted', Types::BOOLEAN); - $this->addType('acceptedAt', Types::BIGINT); - $this->addType('createdAt', Types::BIGINT); - $this->addType('expiredAt', Types::BIGINT); - $this->addType('recipientEmail', Types::STRING); - $this->addType('recipientName', Types::STRING); - $this->addType('recipientProvider', Types::STRING); - $this->addType('recipientUserId', Types::STRING); - $this->addType('token', Types::STRING); - $this->addType('userId', Types::STRING); - } -} diff --git a/apps/cloud_federation_api/lib/Db/FederatedInviteMapper.php b/apps/cloud_federation_api/lib/Db/FederatedInviteMapper.php deleted file mode 100644 index 5feb08b2c7f..00000000000 --- a/apps/cloud_federation_api/lib/Db/FederatedInviteMapper.php +++ /dev/null @@ -1,33 +0,0 @@ - - */ -class FederatedInviteMapper extends QBMapper { - public const TABLE_NAME = 'federated_invites'; - - public function __construct(IDBConnection $db) { - parent::__construct($db, self::TABLE_NAME); - } - - public function findByToken(string $token): FederatedInvite { - $qb = $this->db->getQueryBuilder(); - $qb->select('*') - ->from('federated_invites') - ->where($qb->expr()->eq('token', $qb->createNamedParameter($token))); - return $this->findEntity($qb); - } - -} diff --git a/apps/cloud_federation_api/lib/Events/FederatedInviteAcceptedEvent.php b/apps/cloud_federation_api/lib/Events/FederatedInviteAcceptedEvent.php deleted file mode 100644 index c4d079d083e..00000000000 --- a/apps/cloud_federation_api/lib/Events/FederatedInviteAcceptedEvent.php +++ /dev/null @@ -1,24 +0,0 @@ -invitation; - } -} diff --git a/apps/cloud_federation_api/lib/Migration/DropFederatedInvitesTable.php b/apps/cloud_federation_api/lib/Migration/DropFederatedInvitesTable.php new file mode 100644 index 00000000000..988876ef21f --- /dev/null +++ b/apps/cloud_federation_api/lib/Migration/DropFederatedInvitesTable.php @@ -0,0 +1,54 @@ +db); + if (!$schema->hasTable($table_name)) { + echo("$table_name does not exist"); + return; + } + $qb = $this->db->getQueryBuilder(); + $qb->select('*') + ->from($table_name) + ->setMaxResults(1); + $result = $qb->executeQuery(); + $hasRows = $result->fetchOne(); + if (!$hasRows) { + $schema->dropTable($table_name); + $schema->performDropTableCalls(); + $output->info('Table federated_invites dropped'); + } else { + $output->info('Table federated_invites contains data. Table will be kept.'); + } + } +} diff --git a/apps/cloud_federation_api/openapi.json b/apps/cloud_federation_api/openapi.json index 85272802c56..1f4a4e3a050 100644 --- a/apps/cloud_federation_api/openapi.json +++ b/apps/cloud_federation_api/openapi.json @@ -34,15 +34,6 @@ } } }, - "Capabilities": { - "type": "object", - "additionalProperties": { - "type": "object", - "additionalProperties": { - "type": "object" - } - } - }, "Error": { "type": "object", "required": [ @@ -329,167 +320,6 @@ } } } - }, - "/index.php/ocm/invite-accepted": { - "post": { - "operationId": "request_handler-invite-accepted", - "summary": "Inform the sender that an invitation was accepted to start sharing", - "description": "Inform about an accepted invitation so the user on the sender provider's side can initiate the OCM share creation. To protect the identity of the parties, for shares created following an OCM invitation, the user id MAY be hashed, and recipients implementing the OCM invitation workflow MAY refuse to process shares coming from unknown parties.\nhttps://cs3org.github.io/OCM-API/docs.html?branch=v1.1.0&repo=OCM-API&user=cs3org#/paths/~1invite-accepted/post\nNote: Not implementing 404 Invitation token does not exist, instead using 400", - "tags": [ - "request_handler" - ], - "security": [ - {}, - { - "bearer_auth": [] - }, - { - "basic_auth": [] - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "recipientProvider", - "token", - "userID", - "email", - "name" - ], - "properties": { - "recipientProvider": { - "type": "string", - "description": "The address of the recipent's provider" - }, - "token": { - "type": "string", - "description": "The token used for the invitation" - }, - "userID": { - "type": "string", - "description": "The userID of the recipient at the recipient's provider" - }, - "email": { - "type": "string", - "description": "The email address of the recipient" - }, - "name": { - "type": "string", - "description": "The display name of the recipient" - } - } - } - } - } - }, - "responses": { - "200": { - "description": "Invitation accepted", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "userID", - "email", - "name" - ], - "properties": { - "userID": { - "type": "string" - }, - "email": { - "type": "string" - }, - "name": { - "type": "string" - } - } - } - } - } - }, - "403": { - "description": "Invitation token does not exist", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "message", - "error" - ], - "properties": { - "message": { - "type": "string" - }, - "error": { - "type": "boolean", - "enum": [ - true - ] - } - } - } - } - } - }, - "400": { - "description": "Invalid token", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "message", - "error" - ], - "properties": { - "message": { - "type": "string" - }, - "error": { - "type": "boolean", - "enum": [ - true - ] - } - } - } - } - } - }, - "409": { - "description": "User is already known by the OCM provider", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "message", - "error" - ], - "properties": { - "message": { - "type": "string" - }, - "error": { - "type": "boolean", - "enum": [ - true - ] - } - } - } - } - } - } - } - } } }, "tags": [ diff --git a/apps/cloud_federation_api/tests/BackgroundJob/CleanupExpiredOcmTokensJobTest.php b/apps/cloud_federation_api/tests/BackgroundJob/CleanupExpiredOcmTokensJobTest.php index c81f734579f..bbf6f419dad 100644 --- a/apps/cloud_federation_api/tests/BackgroundJob/CleanupExpiredOcmTokensJobTest.php +++ b/apps/cloud_federation_api/tests/BackgroundJob/CleanupExpiredOcmTokensJobTest.php @@ -20,6 +20,7 @@ class CleanupExpiredOcmTokensJobTest extends TestCase { private OcmTokenMapMapper&MockObject $mapper; private CleanupExpiredOcmTokensJob $job; + #[\Override] protected function setUp(): void { parent::setUp(); diff --git a/apps/cloud_federation_api/tests/Controller/TokenControllerTest.php b/apps/cloud_federation_api/tests/Controller/TokenControllerTest.php index 7ccdca44b7b..2506c673882 100644 --- a/apps/cloud_federation_api/tests/Controller/TokenControllerTest.php +++ b/apps/cloud_federation_api/tests/Controller/TokenControllerTest.php @@ -53,6 +53,7 @@ class TokenControllerTest extends TestCase { /** Public key matching the signatory private key configured by configureHappyPath(). */ private string $publicKeyPem = ''; + #[\Override] protected function setUp(): void { parent::setUp(); @@ -81,6 +82,7 @@ class TokenControllerTest extends TestCase { ); } + #[\Override] protected function tearDown(): void { JWT::$timestamp = null; parent::tearDown(); diff --git a/apps/cloud_federation_api/tests/RequestHandlerControllerTest.php b/apps/cloud_federation_api/tests/RequestHandlerControllerTest.php deleted file mode 100644 index deb7c59d53a..00000000000 --- a/apps/cloud_federation_api/tests/RequestHandlerControllerTest.php +++ /dev/null @@ -1,264 +0,0 @@ -request = $this->createMock(IRequest::class); - $this->logger = $this->createMock(LoggerInterface::class); - $this->userManager = $this->createMock(IUserManager::class); - $this->groupManager = $this->createMock(IGroupManager::class); - $this->urlGenerator = $this->createMock(IURLGenerator::class); - $this->cloudFederationProviderManager = $this->createMock(ICloudFederationProviderManager::class); - $this->config = $this->createMock(Config::class); - $this->eventDispatcher = $this->createMock(IEventDispatcher::class); - $this->federatedInviteMapper = $this->createMock(FederatedInviteMapper::class); - $this->appConfig = $this->createMock(IAppConfig::class); - $this->cloudFederationFactory = $this->createMock(ICloudFederationFactory::class); - $this->cloudIdManager = $this->createMock(ICloudIdManager::class); - $this->discoveryService = $this->createMock(IOCMDiscoveryService::class); - $this->timeFactory = $this->createMock(ITimeFactory::class); - - $this->requestHandlerController = new RequestHandlerController( - 'cloud_federation_api', - $this->request, - $this->logger, - $this->userManager, - $this->groupManager, - $this->urlGenerator, - $this->cloudFederationProviderManager, - $this->config, - $this->eventDispatcher, - $this->federatedInviteMapper, - $this->appConfig, - $this->cloudFederationFactory, - $this->cloudIdManager, - $this->discoveryService, - $this->timeFactory, - ); - } - - public function testInviteAccepted(): void { - $token = 'token'; - $userId = 'userId'; - $invite = new FederatedInvite(); - $invite->setCreatedAt(1); - $invite->setUserId($userId); - $invite->setToken($token); - - $this->federatedInviteMapper->expects(self::once()) - ->method('findByToken') - ->with($token) - ->willReturn($invite); - - $this->federatedInviteMapper->expects(self::once()) - ->method('update') - ->willReturnArgument(0); - - $user = $this->createMock(IUser::class); - $user->method('getUID') - ->willReturn($userId); - $user->method('getEMailAddress') - ->willReturn('email'); - $user->method('getDisplayName') - ->willReturn('displayName'); - - $this->userManager->expects(self::once()) - ->method('get') - ->with($userId) - ->willReturn($user); - - $recipientProvider = 'http://127.0.0.1'; - $recipientId = 'remote'; - $recipientEmail = 'remote@example.org'; - $recipientName = 'Remote Remoteson'; - $response = ['userID' => $userId, 'email' => 'email', 'name' => 'displayName']; - $json = new JSONResponse($response, Http::STATUS_OK); - - $this->assertEquals($json, $this->requestHandlerController->inviteAccepted($recipientProvider, $token, $recipientId, $recipientEmail, $recipientName)); - } - - public function testNotificationReceived(): void { - $notificationType = 'SHARE_ACCEPTED'; - $resourceType = 'file'; - $providerId = '1337'; - $notification = ['sharedSecret' => 'secret']; - $notificationObject = new CloudFederationNotification(); - - $this->appConfig->method('getValueBool')->willReturn(true); - $provider = $this->createMock(ICloudFederationProvider::class); - $provider->method('notificationReceived')->willReturn([]); - $this->cloudFederationFactory->method('getCloudFederationNotification') - ->willReturn($notificationObject); - $this->cloudFederationProviderManager->method('getCloudFederationProvider') - ->willReturn($provider); - - $this->eventDispatcher->expects(self::once()) - ->method('dispatchTyped') - ->with(self::callback( - fn ( - OCMNotificationReceivedEvent $event) - => $event->getNotification() === $notificationObject - ) - ); - $response = $this->requestHandlerController->receiveNotification( - $notificationType, - $resourceType, - $providerId, - $notification - ); - self::assertEquals(Http::STATUS_CREATED, $response->getStatus()); - self::assertEquals([ - 'notificationType' => $notificationType, - 'resourceType' => $resourceType, - 'providerId' => $providerId, - 'notification' => $notification, - ], $notificationObject->getMessage()); - - } - - public function testAddShareRejectsProtocolWithoutSharedSecret(): void { - // Disable signature verification so we reach the protocol validation. - $this->appConfig->method('getValueBool')->willReturn(true); - - $protocol = [ - 'name' => 'multi', - 'webdav' => ['requirements' => ['must-exchange-token']], - ]; - - $result = $this->requestHandlerController->addShare( - 'bob@https://bob.example.com', 'Jupyter', '', '8', - 'alice@alice.example.com', 'alice', 'alice@alice.example.com', 'alice', - $protocol, 'user', 'file', - ); - - $this->assertInstanceOf(JSONResponse::class, $result); - $this->assertEquals(Http::STATUS_BAD_REQUEST, $result->getStatus()); - $this->assertSame('Missing sharedSecret in protocol', $result->getData()['message']); - } - - public function testAddShareAcceptsMultiProtocolSharedSecret(): void { - // Disable signature verification so we reach the protocol validation. - $this->appConfig->method('getValueBool')->willReturn(true); - // No supported share types: the share passes the sharedSecret gate and is - // rejected later with 501, proving the multi envelope validated. - $this->config->method('getSupportedShareTypes')->willReturn([]); - - $protocol = [ - 'name' => 'multi', - 'webdav' => ['sharedSecret' => 'XHRcgrx1X8uZELY8kxApldZtzoreH8Wj', 'requirements' => ['must-exchange-token']], - 'webapp' => ['sharedSecret' => 'XHRcgrx1X8uZELY8kxApldZtzoreH8Wj', 'uri' => 'https://app.example/open'], - ]; - - $result = $this->requestHandlerController->addShare( - 'bob@https://bob.example.com', 'Jupyter', '', '8', - 'alice@alice.example.com', 'alice', 'alice@alice.example.com', 'alice', - $protocol, 'user', 'file', - ); - - $this->assertInstanceOf(JSONResponse::class, $result); - $this->assertEquals(Http::STATUS_NOT_IMPLEMENTED, $result->getStatus()); - $this->assertNotSame('Missing sharedSecret in protocol', $result->getData()['message'] ?? ''); - } - - public function testAddShareRoutesFolderResourceTypeMultiProtocol(): void { - // Disable signature verification so we reach the share handling. - $this->appConfig->method('getValueBool')->willReturn(true); - // The files provider is registered for both 'file' and 'folder'. - $this->config->method('getSupportedShareTypes')->with('folder')->willReturn(['user']); - - $cloudId = $this->createMock(ICloudId::class); - $cloudId->method('getUser')->willReturn('bob'); - $this->cloudIdManager->method('resolveCloudId')->willReturn($cloudId); - - $this->userManager->method('userExists')->with('bob')->willReturn(true); - - $share = $this->createMock(ICloudFederationShare::class); - $this->cloudFederationFactory->method('getCloudFederationShare')->willReturn($share); - - $provider = $this->createMock(ICloudFederationProvider::class); - $provider->expects($this->once()) - ->method('shareReceived') - ->with($share) - ->willReturn('share-id-1'); - $this->cloudFederationProviderManager->expects($this->once()) - ->method('getCloudFederationProvider') - ->with('folder') - ->willReturn($provider); - - $recipient = $this->createMock(IUser::class); - $recipient->method('getDisplayName')->willReturn('Bob'); - $recipient->method('getUID')->willReturn('bob'); - $this->userManager->method('get')->with('bob')->willReturn($recipient); - - $protocol = [ - 'name' => 'multi', - 'webdav' => ['sharedSecret' => 'XHRcgrx1X8uZELY8kxApldZtzoreH8Wj', 'requirements' => ['must-exchange-token']], - 'webapp' => ['sharedSecret' => 'XHRcgrx1X8uZELY8kxApldZtzoreH8Wj', 'uri' => 'https://app.example/open'], - ]; - - $result = $this->requestHandlerController->addShare( - 'bob@https://bob.example.com', 'Jupyter', '', '8', - 'alice@alice.example.com', 'alice', 'alice@alice.example.com', 'alice', - $protocol, 'user', 'folder', - ); - - $this->assertInstanceOf(JSONResponse::class, $result); - $this->assertEquals(Http::STATUS_CREATED, $result->getStatus()); - $this->assertSame('Bob', $result->getData()['recipientDisplayName']); - } -} diff --git a/core/AppInfo/ConfigLexicon.php b/core/AppInfo/ConfigLexicon.php index d733b552be0..3bc2ebeba1b 100644 --- a/core/AppInfo/ConfigLexicon.php +++ b/core/AppInfo/ConfigLexicon.php @@ -28,7 +28,6 @@ class ConfigLexicon implements ILexicon { public const SHARE_LINK_EXPIRE_DATE_ENFORCED = 'shareapi_enforce_expire_date'; public const USER_LANGUAGE = 'lang'; public const OCM_DISCOVERY_ENABLED = 'ocm_discovery_enabled'; - public const OCM_INVITE_ACCEPT_DIALOG = 'ocm_invite_accept_dialog'; public const USER_LOCALE = 'locale'; public const USER_TIMEZONE = 'timezone'; @@ -94,7 +93,6 @@ class ConfigLexicon implements ILexicon { ), new Entry(self::LASTCRON_TIMESTAMP, ValueType::INT, 0, 'timestamp of last cron execution'), new Entry(self::OCM_DISCOVERY_ENABLED, ValueType::BOOL, true, 'enable/disable OCM'), - new Entry(self::OCM_INVITE_ACCEPT_DIALOG, ValueType::STRING, '', 'route to local invite accept dialog', note: 'set as empty string to disable feature'), new Entry(self::UNIFIED_SEARCH_MIN_SEARCH_LENGTH, ValueType::INT, 1, 'Minimum search length to trigger the request', rename: 'unified-search.min-search-length'), new Entry(self::UNIFIED_SEARCH_MAX_RESULTS_PER_REQUEST, ValueType::INT, 25, 'Maximum results returned per search request', rename: 'unified-search.max-results-per-request'), new Entry( diff --git a/lib/private/OCM/OCMDiscoveryService.php b/lib/private/OCM/OCMDiscoveryService.php index d25f0d4cdcf..9975038f333 100644 --- a/lib/private/OCM/OCMDiscoveryService.php +++ b/lib/private/OCM/OCMDiscoveryService.php @@ -206,18 +206,12 @@ final class OCMDiscoveryService implements IOCMDiscoveryService { $provider->setEnabled(true); $provider->setApiVersion(self::API_VERSION); $provider->setEndPoint(substr($url, 0, $pos)); - $provider->setCapabilities(['invite-accepted', 'notifications', 'shares', 'exchange-token']); + $provider->setCapabilities(['notifications', 'shares', 'exchange-token']); $provider->setTokenEndPoint($tokenUrl); if ($signingEnabled) { $provider->setCapabilities(['http-sig']); } - // The inviteAcceptDialog is available from the contacts app, if this config value is set - $inviteAcceptDialog = $this->appConfig->getValueString('core', ConfigLexicon::OCM_INVITE_ACCEPT_DIALOG); - if ($inviteAcceptDialog !== '') { - $provider->setInviteAcceptDialog($this->urlGenerator->linkToRouteAbsolute($inviteAcceptDialog)); - } - $resource = $provider->createNewResourceType(); $resource->setName('file') ->setShareTypes(['user', 'group']) diff --git a/lib/public/OCM/Events/LocalOCMDiscoveryEvent.php b/lib/public/OCM/Events/LocalOCMDiscoveryEvent.php index a6ef943145b..8c4b7cb38d8 100644 --- a/lib/public/OCM/Events/LocalOCMDiscoveryEvent.php +++ b/lib/public/OCM/Events/LocalOCMDiscoveryEvent.php @@ -54,4 +54,15 @@ class LocalOCMDiscoveryEvent extends Event { ->setProtocols($protocols); $this->provider->addResourceType($resourceType); } + + /** + * Returns the ocm provider. + * + * @return IOCMProvider + * + * @since 35.0.0 + */ + public function getProvider(): IOCMProvider { + return $this->provider; + } } diff --git a/openapi.json b/openapi.json index 9371409d2bf..4f56033b7bc 100644 --- a/openapi.json +++ b/openapi.json @@ -1304,15 +1304,6 @@ } } }, - "CloudFederationApiCapabilities": { - "type": "object", - "additionalProperties": { - "type": "object", - "additionalProperties": { - "type": "object" - } - } - }, "CloudFederationApiError": { "type": "object", "required": [ @@ -7314,9 +7305,6 @@ { "$ref": "#/components/schemas/CorePublicCapabilities" }, - { - "$ref": "#/components/schemas/CloudFederationApiCapabilities" - }, { "$ref": "#/components/schemas/CommentsCapabilities" }, @@ -18547,167 +18535,6 @@ } } }, - "/index.php/ocm/invite-accepted": { - "post": { - "operationId": "cloud_federation_api-request_handler-invite-accepted", - "summary": "Inform the sender that an invitation was accepted to start sharing", - "description": "Inform about an accepted invitation so the user on the sender provider's side can initiate the OCM share creation. To protect the identity of the parties, for shares created following an OCM invitation, the user id MAY be hashed, and recipients implementing the OCM invitation workflow MAY refuse to process shares coming from unknown parties.\nhttps://cs3org.github.io/OCM-API/docs.html?branch=v1.1.0&repo=OCM-API&user=cs3org#/paths/~1invite-accepted/post\nNote: Not implementing 404 Invitation token does not exist, instead using 400", - "tags": [ - "cloud_federation_api/request_handler" - ], - "security": [ - {}, - { - "bearer_auth": [] - }, - { - "basic_auth": [] - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "recipientProvider", - "token", - "userID", - "email", - "name" - ], - "properties": { - "recipientProvider": { - "type": "string", - "description": "The address of the recipent's provider" - }, - "token": { - "type": "string", - "description": "The token used for the invitation" - }, - "userID": { - "type": "string", - "description": "The userID of the recipient at the recipient's provider" - }, - "email": { - "type": "string", - "description": "The email address of the recipient" - }, - "name": { - "type": "string", - "description": "The display name of the recipient" - } - } - } - } - } - }, - "responses": { - "200": { - "description": "Invitation accepted", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "userID", - "email", - "name" - ], - "properties": { - "userID": { - "type": "string" - }, - "email": { - "type": "string" - }, - "name": { - "type": "string" - } - } - } - } - } - }, - "403": { - "description": "Invitation token does not exist", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "message", - "error" - ], - "properties": { - "message": { - "type": "string" - }, - "error": { - "type": "boolean", - "enum": [ - true - ] - } - } - } - } - } - }, - "400": { - "description": "Invalid token", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "message", - "error" - ], - "properties": { - "message": { - "type": "string" - }, - "error": { - "type": "boolean", - "enum": [ - true - ] - } - } - } - } - } - }, - "409": { - "description": "User is already known by the OCM provider", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "message", - "error" - ], - "properties": { - "message": { - "type": "string" - }, - "error": { - "type": "boolean", - "enum": [ - true - ] - } - } - } - } - } - } - } - } - }, "/ocs/v2.php/apps/dashboard/api/v1/widget-items": { "get": { "operationId": "dashboard-dashboard_api-get-widget-items", diff --git a/psalm.xml b/psalm.xml index 482abe6d80c..2a460f600e5 100644 --- a/psalm.xml +++ b/psalm.xml @@ -71,7 +71,6 @@ -