From 623f2f0240016bbc142387741a44864ecb0458e2 Mon Sep 17 00:00:00 2001 From: Micke Nordin Date: Fri, 14 Mar 2025 09:53:16 +0100 Subject: [PATCH] feat(OCM-invites): Implementation of invitation flow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This patchset: * implements the /invite-accepted endpoint * adds capabilities and inviteAceptDialog to the discovery * adds a FederatedInviteAcceptedEvent https://cs3org.github.io/OCM-API/docs.html?branch=v1.1.0&repo=OCM-API&user=cs3org#/paths/~1invite-accepted/post Co-authored-by: Anna Co-authored-by: Côme Chilliet Co-authored-by: Joas Schilling <213943+nickvergessen@users.noreply.github.com> Co-authored-by: Navid Shokri Signed-off-by: Micke Nordin --- apps/cloud_federation_api/appinfo/info.xml | 2 +- apps/cloud_federation_api/appinfo/routes.php | 12 +- .../composer/composer/autoload_classmap.php | 4 + .../composer/composer/autoload_static.php | 4 + .../cloud_federation_api/lib/Capabilities.php | 27 +- .../Controller/RequestHandlerController.php | 111 +++++++- .../lib/Db/FederatedInvite.php | 62 +++++ .../lib/Db/FederatedInviteMapper.php | 33 +++ .../Events/FederatedInviteAcceptedEvent.php | 24 ++ .../Migration/Version1016Date202502262004.php | 89 +++++++ apps/cloud_federation_api/openapi.json | 238 ++++++++++++------ .../tests/RequestHandlerControllerTest.php | 136 ++++++++++ lib/composer/autoload.php | 5 +- lib/composer/composer/autoload_classmap.php | 1 + lib/composer/composer/autoload_static.php | 1 + lib/private/OCM/Model/OCMProvider.php | 94 +++++-- lib/private/OCM/OCMDiscoveryService.php | 8 +- lib/private/Server.php | 5 +- .../OCM/ICapabilityAwareOCMProvider.php | 60 +++++ openapi.json | 238 ++++++++++++------ 20 files changed, 949 insertions(+), 205 deletions(-) create mode 100644 apps/cloud_federation_api/lib/Db/FederatedInvite.php create mode 100644 apps/cloud_federation_api/lib/Db/FederatedInviteMapper.php create mode 100644 apps/cloud_federation_api/lib/Events/FederatedInviteAcceptedEvent.php create mode 100644 apps/cloud_federation_api/lib/Migration/Version1016Date202502262004.php create mode 100644 apps/cloud_federation_api/tests/RequestHandlerControllerTest.php create mode 100644 lib/public/OCM/ICapabilityAwareOCMProvider.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/appinfo/routes.php b/apps/cloud_federation_api/appinfo/routes.php index 6b0774627a4..6467005e21b 100644 --- a/apps/cloud_federation_api/appinfo/routes.php +++ b/apps/cloud_federation_api/appinfo/routes.php @@ -20,11 +20,11 @@ return [ 'verb' => 'POST', 'root' => '/ocm', ], - // [ - // 'name' => 'RequestHandler#inviteAccepted', - // 'url' => '/invite-accepted', - // 'verb' => 'POST', - // 'root' => '/ocm', - // ] + [ + 'name' => 'RequestHandler#inviteAccepted', + 'url' => '/invite-accepted', + 'verb' => 'POST', + 'root' => '/ocm', + ] ], ]; diff --git a/apps/cloud_federation_api/composer/composer/autoload_classmap.php b/apps/cloud_federation_api/composer/composer/autoload_classmap.php index dd096ebf563..3cadc540c88 100644 --- a/apps/cloud_federation_api/composer/composer/autoload_classmap.php +++ b/apps/cloud_federation_api/composer/composer/autoload_classmap.php @@ -11,5 +11,9 @@ 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\\Db\\FederatedInvite' => $baseDir . '/../lib/Db/FederatedInvite.php', + 'OCA\\CloudFederationAPI\\Db\\FederatedInviteMapper' => $baseDir . '/../lib/Db/FederatedInviteMapper.php', + 'OCA\\CloudFederationAPI\\Events\\FederatedInviteAcceptedEvent' => $baseDir . '/../lib/Events/FederatedInviteAcceptedEvent.php', + 'OCA\\CloudFederationAPI\\Migration\\Version1016Date202502262004' => $baseDir . '/../lib/Migration/Version1016Date202502262004.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 75557a20126..849b755cd2f 100644 --- a/apps/cloud_federation_api/composer/composer/autoload_static.php +++ b/apps/cloud_federation_api/composer/composer/autoload_static.php @@ -26,6 +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\\Db\\FederatedInvite' => __DIR__ . '/..' . '/../lib/Db/FederatedInvite.php', + 'OCA\\CloudFederationAPI\\Db\\FederatedInviteMapper' => __DIR__ . '/..' . '/../lib/Db/FederatedInviteMapper.php', + 'OCA\\CloudFederationAPI\\Events\\FederatedInviteAcceptedEvent' => __DIR__ . '/..' . '/../lib/Events/FederatedInviteAcceptedEvent.php', + 'OCA\\CloudFederationAPI\\Migration\\Version1016Date202502262004' => __DIR__ . '/..' . '/../lib/Migration/Version1016Date202502262004.php', 'OCA\\CloudFederationAPI\\ResponseDefinitions' => __DIR__ . '/..' . '/../lib/ResponseDefinitions.php', ); diff --git a/apps/cloud_federation_api/lib/Capabilities.php b/apps/cloud_federation_api/lib/Capabilities.php index 0348f6e7c11..599733123b3 100644 --- a/apps/cloud_federation_api/lib/Capabilities.php +++ b/apps/cloud_federation_api/lib/Capabilities.php @@ -6,6 +6,7 @@ declare(strict_types=1); * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later */ + namespace OCA\CloudFederationAPI; use NCU\Security\Signature\Exceptions\IdentityNotFoundException; @@ -16,16 +17,16 @@ use OCP\Capabilities\IInitialStateExcludedCapability; use OCP\IAppConfig; use OCP\IURLGenerator; use OCP\OCM\Exceptions\OCMArgumentException; -use OCP\OCM\IOCMProvider; +use OCP\OCM\ICapabilityAwareOCMProvider; use Psr\Log\LoggerInterface; class Capabilities implements ICapability, IInitialStateExcludedCapability { - public const API_VERSION = '1.1'; // informative, real version. + public const API_VERSION = '1.1.0'; public function __construct( private IURLGenerator $urlGenerator, private IAppConfig $appConfig, - private IOCMProvider $provider, + private ICapabilityAwareOCMProvider $provider, private readonly OCMSignatoryManager $ocmSignatoryManager, private readonly LoggerInterface $logger, ) { @@ -34,23 +35,7 @@ class Capabilities implements ICapability, IInitialStateExcludedCapability { /** * Function an app uses to return the capabilities * - * @return array{ - * ocm: array{ - * apiVersion: '1.0-proposal1', - * enabled: bool, - * endPoint: string, - * publicKey?: array{ - * keyId: string, - * publicKeyPem: string, - * }, - * resourceTypes: list, - * protocols: array - * }>, - * version: string - * } - * } + * @return array> * @throws OCMArgumentException */ public function getCapabilities() { @@ -62,6 +47,8 @@ class Capabilities implements ICapability, IInitialStateExcludedCapability { $this->provider->setEnabled(true); $this->provider->setApiVersion(self::API_VERSION); + $this->provider->setCapabilities(['/invite-accepted', '/notifications', '/shares']); + $this->provider->setEndPoint(substr($url, 0, $pos)); $resource = $this->provider->createNewResourceType(); diff --git a/apps/cloud_federation_api/lib/Controller/RequestHandlerController.php b/apps/cloud_federation_api/lib/Controller/RequestHandlerController.php index cbd66f52382..e8a38ff9da7 100644 --- a/apps/cloud_federation_api/lib/Controller/RequestHandlerController.php +++ b/apps/cloud_federation_api/lib/Controller/RequestHandlerController.php @@ -1,8 +1,10 @@ |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->getPrimaryEMailAddress(); + 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 * @@ -233,7 +339,8 @@ class RequestHandlerController extends Controller { #[BruteForceProtection(action: 'receiveFederatedShareNotification')] 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) diff --git a/apps/cloud_federation_api/lib/Db/FederatedInvite.php b/apps/cloud_federation_api/lib/Db/FederatedInvite.php new file mode 100644 index 00000000000..b2447ff4e23 --- /dev/null +++ b/apps/cloud_federation_api/lib/Db/FederatedInvite.php @@ -0,0 +1,62 @@ +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 new file mode 100644 index 00000000000..5feb08b2c7f --- /dev/null +++ b/apps/cloud_federation_api/lib/Db/FederatedInviteMapper.php @@ -0,0 +1,33 @@ + + */ +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 new file mode 100644 index 00000000000..c4d079d083e --- /dev/null +++ b/apps/cloud_federation_api/lib/Events/FederatedInviteAcceptedEvent.php @@ -0,0 +1,24 @@ +invitation; + } +} diff --git a/apps/cloud_federation_api/lib/Migration/Version1016Date202502262004.php b/apps/cloud_federation_api/lib/Migration/Version1016Date202502262004.php new file mode 100644 index 00000000000..a3523d45e38 --- /dev/null +++ b/apps/cloud_federation_api/lib/Migration/Version1016Date202502262004.php @@ -0,0 +1,89 @@ +hasTable($table_name)) { + $table = $schema->createTable($table_name); + $table->addColumn('id', Types::BIGINT, [ + 'autoincrement' => true, + 'notnull' => true, + 'length' => 11, + 'unsigned' => true, + ]); + $table->addColumn('user_id', Types::STRING, [ + 'notnull' => true, + 'length' => 64, + + ]); + // https://saturncloud.io/blog/what-is-the-maximum-length-of-a-url-in-different-browsers/#maximum-url-length-in-different-browsers + // We use the least common denominator, the minimum length supported by browsers + $table->addColumn('recipient_provider', Types::STRING, [ + 'notnull' => false, + 'length' => 2083, + ]); + $table->addColumn('recipient_user_id', Types::STRING, [ + 'notnull' => false, + 'length' => 1024, + ]); + $table->addColumn('recipient_name', Types::STRING, [ + 'notnull' => false, + 'length' => 1024, + ]); + // https://www.directedignorance.com/blog/maximum-length-of-email-address + $table->addColumn('recipient_email', Types::STRING, [ + 'notnull' => false, + 'length' => 320, + ]); + $table->addColumn('token', Types::STRING, [ + 'notnull' => true, + 'length' => 60, + ]); + $table->addColumn('accepted', Types::BOOLEAN, [ + 'notnull' => false, + 'default' => false + ]); + $table->addColumn('created_at', Types::BIGINT, [ + 'notnull' => true, + ]); + + $table->addColumn('expired_at', Types::BIGINT, [ + 'notnull' => false, + ]); + + $table->addColumn('accepted_at', Types::BIGINT, [ + 'notnull' => false, + ]); + + $table->addUniqueConstraint(['token']); + $table->setPrimaryKey(['id']); + return $schema; + } + + return null; + } +} diff --git a/apps/cloud_federation_api/openapi.json b/apps/cloud_federation_api/openapi.json index 730af73628f..9c92a152bf8 100644 --- a/apps/cloud_federation_api/openapi.json +++ b/apps/cloud_federation_api/openapi.json @@ -36,79 +36,10 @@ }, "Capabilities": { "type": "object", - "required": [ - "ocm" - ], - "properties": { - "ocm": { - "type": "object", - "required": [ - "apiVersion", - "enabled", - "endPoint", - "resourceTypes", - "version" - ], - "properties": { - "apiVersion": { - "type": "string", - "enum": [ - "1.0-proposal1" - ] - }, - "enabled": { - "type": "boolean" - }, - "endPoint": { - "type": "string" - }, - "publicKey": { - "type": "object", - "required": [ - "keyId", - "publicKeyPem" - ], - "properties": { - "keyId": { - "type": "string" - }, - "publicKeyPem": { - "type": "string" - } - } - }, - "resourceTypes": { - "type": "array", - "items": { - "type": "object", - "required": [ - "name", - "shareTypes", - "protocols" - ], - "properties": { - "name": { - "type": "string" - }, - "shareTypes": { - "type": "array", - "items": { - "type": "string" - } - }, - "protocols": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - } - } - }, - "version": { - "type": "string" - } - } + "additionalProperties": { + "type": "object", + "additionalProperties": { + "type": "object" } } }, @@ -396,6 +327,167 @@ } } } + }, + "/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/RequestHandlerControllerTest.php b/apps/cloud_federation_api/tests/RequestHandlerControllerTest.php new file mode 100644 index 00000000000..eeb24cf9372 --- /dev/null +++ b/apps/cloud_federation_api/tests/RequestHandlerControllerTest.php @@ -0,0 +1,136 @@ +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->addressHandler = $this->createMock(AddressHandler::class); + $this->appConfig = $this->createMock(IAppConfig::class); + $this->cloudFederationFactory = $this->createMock(ICloudFederationFactory::class); + $this->cloudIdManager = $this->createMock(ICloudIdManager::class); + $this->signatureManager = $this->createMock(ISignatureManager::class); + $this->signatoryManager = $this->createMock(OCMSignatoryManager::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->addressHandler, + $this->appConfig, + $this->cloudFederationFactory, + $this->cloudIdManager, + $this->signatureManager, + $this->signatoryManager, + $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('getPrimaryEMailAddress') + ->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)); + } +} diff --git a/lib/composer/autoload.php b/lib/composer/autoload.php index b3b39129e7a..7b1481e876c 100644 --- a/lib/composer/autoload.php +++ b/lib/composer/autoload.php @@ -14,10 +14,7 @@ if (PHP_VERSION_ID < 50600) { echo $err; } } - trigger_error( - $err, - E_USER_ERROR - ); + throw new RuntimeException($err); } require_once __DIR__ . '/composer/autoload_real.php'; diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php index 36f64d970c3..e0d86d084dd 100644 --- a/lib/composer/composer/autoload_classmap.php +++ b/lib/composer/composer/autoload_classmap.php @@ -679,6 +679,7 @@ return array( 'OCP\\OCM\\Events\\ResourceTypeRegisterEvent' => $baseDir . '/lib/public/OCM/Events/ResourceTypeRegisterEvent.php', 'OCP\\OCM\\Exceptions\\OCMArgumentException' => $baseDir . '/lib/public/OCM/Exceptions/OCMArgumentException.php', 'OCP\\OCM\\Exceptions\\OCMProviderException' => $baseDir . '/lib/public/OCM/Exceptions/OCMProviderException.php', + 'OCP\\OCM\\ICapabilityAwareOCMProvider' => $baseDir . '/lib/public/OCM/ICapabilityAwareOCMProvider.php', 'OCP\\OCM\\IOCMDiscoveryService' => $baseDir . '/lib/public/OCM/IOCMDiscoveryService.php', 'OCP\\OCM\\IOCMProvider' => $baseDir . '/lib/public/OCM/IOCMProvider.php', 'OCP\\OCM\\IOCMResource' => $baseDir . '/lib/public/OCM/IOCMResource.php', diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php index 327366ca889..0bf675a77be 100644 --- a/lib/composer/composer/autoload_static.php +++ b/lib/composer/composer/autoload_static.php @@ -720,6 +720,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2 'OCP\\OCM\\Events\\ResourceTypeRegisterEvent' => __DIR__ . '/../../..' . '/lib/public/OCM/Events/ResourceTypeRegisterEvent.php', 'OCP\\OCM\\Exceptions\\OCMArgumentException' => __DIR__ . '/../../..' . '/lib/public/OCM/Exceptions/OCMArgumentException.php', 'OCP\\OCM\\Exceptions\\OCMProviderException' => __DIR__ . '/../../..' . '/lib/public/OCM/Exceptions/OCMProviderException.php', + 'OCP\\OCM\\ICapabilityAwareOCMProvider' => __DIR__ . '/../../..' . '/lib/public/OCM/ICapabilityAwareOCMProvider.php', 'OCP\\OCM\\IOCMDiscoveryService' => __DIR__ . '/../../..' . '/lib/public/OCM/IOCMDiscoveryService.php', 'OCP\\OCM\\IOCMProvider' => __DIR__ . '/../../..' . '/lib/public/OCM/IOCMProvider.php', 'OCP\\OCM\\IOCMResource' => __DIR__ . '/../../..' . '/lib/public/OCM/IOCMResource.php', diff --git a/lib/private/OCM/Model/OCMProvider.php b/lib/private/OCM/Model/OCMProvider.php index f4b0ac584de..be13d65a40f 100644 --- a/lib/private/OCM/Model/OCMProvider.php +++ b/lib/private/OCM/Model/OCMProvider.php @@ -11,18 +11,22 @@ namespace OC\OCM\Model; use NCU\Security\Signature\Model\Signatory; use OCP\EventDispatcher\IEventDispatcher; +use OCP\IConfig; use OCP\OCM\Events\ResourceTypeRegisterEvent; use OCP\OCM\Exceptions\OCMArgumentException; use OCP\OCM\Exceptions\OCMProviderException; -use OCP\OCM\IOCMProvider; +use OCP\OCM\ICapabilityAwareOCMProvider; use OCP\OCM\IOCMResource; /** * @since 28.0.0 */ -class OCMProvider implements IOCMProvider { +class OCMProvider implements ICapabilityAwareOCMProvider { + private string $provider; private bool $enabled = false; private string $apiVersion = ''; + private string $inviteAcceptDialog = ''; + private array $capabilities = []; private string $endPoint = ''; /** @var IOCMResource[] */ private array $resourceTypes = []; @@ -31,7 +35,9 @@ class OCMProvider implements IOCMProvider { public function __construct( protected IEventDispatcher $dispatcher, + protected IConfig $config, ) { + $this->provider = 'Nextcloud ' . $config->getSystemValue('version'); } /** @@ -70,6 +76,30 @@ class OCMProvider implements IOCMProvider { return $this->apiVersion; } + /** + * returns the invite accept dialog + * + * @return string + * @since 32.0.0 + */ + public function getInviteAcceptDialog(): string { + return $this->inviteAcceptDialog; + } + + /** + * set the invite accept dialog + * + * @param string $inviteAcceptDialog + * + * @return $this + * @since 32.0.0 + */ + public function setInviteAcceptDialog(string $inviteAcceptDialog): static { + $this->inviteAcceptDialog = $inviteAcceptDialog; + + return $this; + } + /** * @param string $endPoint * @@ -88,6 +118,34 @@ class OCMProvider implements IOCMProvider { return $this->endPoint; } + /** + * @return string + */ + public function getProvider(): string { + return $this->provider; + } + + /** + * @param array $capabilities + * + * @return $this + */ + public function setCapabilities(array $capabilities): static { + foreach ($capabilities as $value) { + if (!in_array($value, $this->capabilities)) { + array_push($this->capabilities, $value); + } + } + + return $this; + } + + /** + * @return array + */ + public function getCapabilities(): array { + return $this->capabilities; + } /** * create a new resource to later add it with {@see IOCMProvider::addResourceType()} * @return IOCMResource @@ -166,9 +224,8 @@ class OCMProvider implements IOCMProvider { * * @param array $data * - * @return $this + * @return OCMProvider&static * @throws OCMProviderException in case a descent provider cannot be generated from data - * @see self::jsonSerialize() */ public function import(array $data): static { $this->setEnabled(is_bool($data['enabled'] ?? '') ? $data['enabled'] : false) @@ -209,21 +266,7 @@ class OCMProvider implements IOCMProvider { } /** - * @return array{ - * enabled: bool, - * apiVersion: '1.0-proposal1', - * endPoint: string, - * publicKey?: array{ - * keyId: string, - * publicKeyPem: string - * }, - * resourceTypes: list, - * protocols: array - * }>, - * version: string - * } + * @since 28.0.0 */ public function jsonSerialize(): array { $resourceTypes = []; @@ -231,7 +274,7 @@ class OCMProvider implements IOCMProvider { $resourceTypes[] = $res->jsonSerialize(); } - return [ + $response = [ 'enabled' => $this->isEnabled(), 'apiVersion' => '1.0-proposal1', // deprecated, but keep it to stay compatible with old version 'version' => $this->getApiVersion(), // informative but real version @@ -239,5 +282,16 @@ class OCMProvider implements IOCMProvider { 'publicKey' => $this->getSignatory()?->jsonSerialize(), 'resourceTypes' => $resourceTypes ]; + + $capabilities = $this->getCapabilities(); + $inviteAcceptDialog = $this->getInviteAcceptDialog(); + if ($capabilities) { + $response['capabilities'] = $capabilities; + } + if ($inviteAcceptDialog) { + $response['inviteAcceptDialog'] = $inviteAcceptDialog; + } + return $response; + } } diff --git a/lib/private/OCM/OCMDiscoveryService.php b/lib/private/OCM/OCMDiscoveryService.php index af612416372..a151bbc753c 100644 --- a/lib/private/OCM/OCMDiscoveryService.php +++ b/lib/private/OCM/OCMDiscoveryService.php @@ -17,8 +17,8 @@ use OCP\ICache; use OCP\ICacheFactory; use OCP\IConfig; use OCP\OCM\Exceptions\OCMProviderException; +use OCP\OCM\ICapabilityAwareOCMProvider; use OCP\OCM\IOCMDiscoveryService; -use OCP\OCM\IOCMProvider; use Psr\Log\LoggerInterface; /** @@ -31,7 +31,7 @@ class OCMDiscoveryService implements IOCMDiscoveryService { ICacheFactory $cacheFactory, private IClientService $clientService, private IConfig $config, - private IOCMProvider $provider, + private ICapabilityAwareOCMProvider $provider, private LoggerInterface $logger, ) { $this->cache = $cacheFactory->createDistributed('ocm-discovery'); @@ -42,10 +42,10 @@ class OCMDiscoveryService implements IOCMDiscoveryService { * @param string $remote * @param bool $skipCache * - * @return IOCMProvider + * @return ICapabilityAwareOCMProvider * @throws OCMProviderException */ - public function discover(string $remote, bool $skipCache = false): IOCMProvider { + public function discover(string $remote, bool $skipCache = false): ICapabilityAwareOCMProvider { $remote = rtrim($remote, '/'); if (!str_starts_with($remote, 'http://') && !str_starts_with($remote, 'https://')) { // if scheme not specified, we test both; diff --git a/lib/private/Server.php b/lib/private/Server.php index 83eb95cd671..5ca97b261f4 100644 --- a/lib/private/Server.php +++ b/lib/private/Server.php @@ -1,4 +1,5 @@ registerAlias(IPhoneNumberUtil::class, PhoneNumberUtil::class); - $this->registerAlias(IOCMProvider::class, OCMProvider::class); + $this->registerAlias(ICapabilityAwareOCMProvider::class, OCMProvider::class); $this->registerAlias(ISetupCheckManager::class, SetupCheckManager::class); diff --git a/lib/public/OCM/ICapabilityAwareOCMProvider.php b/lib/public/OCM/ICapabilityAwareOCMProvider.php new file mode 100644 index 00000000000..d3ad2e29932 --- /dev/null +++ b/lib/public/OCM/ICapabilityAwareOCMProvider.php @@ -0,0 +1,60 @@ +