From f447eb1ab45c53dd2d62f41ff4896066feca3390 Mon Sep 17 00:00:00 2001 From: Jonas Date: Mon, 8 Jul 2024 11:29:26 +0200 Subject: [PATCH] feat(Reference): Add public API endpoints to get references Calling the public API endpoints will check for matching registered reference providers that implement `IPublicReferenceProvider` and call their respective functions. If no matching provider is found, the default `LinkReferenceProvider` will be used to provide open graph data. The frontend reference widget components will call these endpoints from unauthorized sessions, e.g. in public shares. If present, the sharing token of the origin URL is passed to `resolveReferencePublic()` as additional information for the reference provider to determine the access scope. This allows the respective reference providers to determine whether the origin share has access to the linked resource. `getCacheKeyPublic` also gets the sharing token so it can scope the cached entry to it. Contributes to #45978 Signed-off-by: Jonas --- core/Controller/ReferenceApiController.php | 87 +++++ core/openapi-full.json | 312 ++++++++++++++++++ core/openapi.json | 312 ++++++++++++++++++ lib/composer/composer/autoload_classmap.php | 1 + lib/composer/composer/autoload_static.php | 1 + .../Reference/ReferenceManager.php | 36 +- .../Reference/IPublicReferenceProvider.php | 33 ++ .../Reference/IReferenceManager.php | 6 +- .../Reference/LinkReferenceProvider.php | 18 +- 9 files changed, 792 insertions(+), 14 deletions(-) create mode 100644 lib/public/Collaboration/Reference/IPublicReferenceProvider.php diff --git a/core/Controller/ReferenceApiController.php b/core/Controller/ReferenceApiController.php index 77b4b43fa3ac0..9ce783bb7cd01 100644 --- a/core/Controller/ReferenceApiController.php +++ b/core/Controller/ReferenceApiController.php @@ -10,6 +10,7 @@ use OC\Core\ResponseDefinitions; use OCP\AppFramework\Http; +use OCP\AppFramework\Http\Attribute\AnonRateLimit; use OCP\AppFramework\Http\Attribute\ApiRoute; use OCP\AppFramework\Http\DataResponse; use OCP\Collaboration\Reference\IDiscoverableReferenceProvider; @@ -62,6 +63,39 @@ public function extract(string $text, bool $resolve = false, int $limit = 1): Da ]); } + /** + * @PublicPage + * + * Extract references from a text + * + * @param string $text Text to extract from + * @param string $sharingToken Token of the public share + * @param bool $resolve Resolve the references + * @param int $limit Maximum amount of references to extract + * @return DataResponse}, array{}> + * + * 200: References returned + */ + #[ApiRoute(verb: 'POST', url: '/extractPublic', root: '/references')] + #[AnonRateLimit(limit: 10, period: 120)] + public function extractPublic(string $text, string $sharingToken, bool $resolve = false, int $limit = 1): DataResponse { + $references = $this->referenceManager->extractReferences($text); + + $result = []; + $index = 0; + foreach ($references as $reference) { + if ($index++ >= $limit) { + break; + } + + $result[$reference] = $resolve ? $this->referenceManager->resolveReference($reference, true, $sharingToken)?->jsonSerialize() : null; + } + + return new DataResponse([ + 'references' => $result + ]); + } + /** * @NoAdminRequired * @@ -73,6 +107,7 @@ public function extract(string $text, bool $resolve = false, int $limit = 1): Da * 200: Reference returned */ #[ApiRoute(verb: 'GET', url: '/resolve', root: '/references')] + #[AnonRateLimit(limit: 10, period: 120)] public function resolveOne(string $reference): DataResponse { /** @var ?CoreReference $resolvedReference */ $resolvedReference = $this->referenceManager->resolveReference(trim($reference))?->jsonSerialize(); @@ -82,6 +117,28 @@ public function resolveOne(string $reference): DataResponse { return $response; } + /** + * @PublicPage + * + * Resolve from a public page + * + * @param string $reference Reference to resolve + * @param string $sharingToken Token of the public share + * @return DataResponse}, array{}> + * + * 200: Reference returned + */ + #[ApiRoute(verb: 'GET', url: '/resolvePublic', root: '/references')] + #[AnonRateLimit(limit: 10, period: 120)] + public function resolveOnePublic(string $reference, string $sharingToken): DataResponse { + /** @var ?CoreReference $resolvedReference */ + $resolvedReference = $this->referenceManager->resolveReference(trim($reference), true, trim($sharingToken))?->jsonSerialize(); + + $response = new DataResponse(['references' => [$reference => $resolvedReference]]); + $response->cacheFor(3600, false, true); + return $response; + } + /** * @NoAdminRequired * @@ -110,6 +167,36 @@ public function resolve(array $references, int $limit = 1): DataResponse { ]); } + /** + * @PublicPage + * + * Resolve multiple references from a public page + * + * @param string[] $references References to resolve + * @param string $sharingToken Token of the public share + * @param int $limit Maximum amount of references to resolve + * @return DataResponse}, array{}> + * + * 200: References returned + */ + #[ApiRoute(verb: 'POST', url: '/resolvePublic', root: '/references')] + #[AnonRateLimit(limit: 10, period: 120)] + public function resolvePublic(array $references, string $sharingToken, int $limit = 1): DataResponse { + $result = []; + $index = 0; + foreach ($references as $reference) { + if ($index++ >= $limit) { + break; + } + + $result[$reference] = $this->referenceManager->resolveReference($reference, true, $sharingToken)?->jsonSerialize(); + } + + return new DataResponse([ + 'references' => $result + ]); + } + /** * @NoAdminRequired * diff --git a/core/openapi-full.json b/core/openapi-full.json index b70e82c3ee814..3963a0e89012b 100644 --- a/core/openapi-full.json +++ b/core/openapi-full.json @@ -3059,6 +3059,115 @@ } } }, + "/ocs/v2.php/references/extractPublic": { + "post": { + "operationId": "reference_api-extract-public", + "summary": "Extract references from a text", + "tags": [ + "reference_api" + ], + "security": [ + {}, + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "text", + "sharingToken" + ], + "properties": { + "text": { + "type": "string", + "description": "Text to extract from" + }, + "sharingToken": { + "type": "string", + "description": "Token of the public share" + }, + "resolve": { + "type": "boolean", + "default": false, + "description": "Resolve the references" + }, + "limit": { + "type": "integer", + "format": "int64", + "default": 1, + "description": "Maximum amount of references to extract" + } + } + } + } + } + }, + "parameters": [ + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "References returned", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object", + "required": [ + "references" + ], + "properties": { + "references": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/Reference", + "nullable": true + } + } + } + } + } + } + } + } + } + } + } + } + } + }, "/ocs/v2.php/references/resolve": { "get": { "operationId": "reference_api-resolve-one", @@ -3250,6 +3359,209 @@ } } }, + "/ocs/v2.php/references/resolvePublic": { + "get": { + "operationId": "reference_api-resolve-one-public", + "summary": "Resolve from a public page", + "tags": [ + "reference_api" + ], + "security": [ + {}, + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "reference", + "sharingToken" + ], + "properties": { + "reference": { + "type": "string", + "description": "Reference to resolve" + }, + "sharingToken": { + "type": "string", + "description": "Token of the public share" + } + } + } + } + } + }, + "parameters": [ + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Reference returned", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object", + "required": [ + "references" + ], + "properties": { + "references": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/Reference", + "nullable": true + } + } + } + } + } + } + } + } + } + } + } + } + }, + "post": { + "operationId": "reference_api-resolve-public", + "summary": "Resolve multiple references from a public page", + "tags": [ + "reference_api" + ], + "security": [ + {}, + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "references", + "sharingToken" + ], + "properties": { + "references": { + "type": "array", + "description": "References to resolve", + "items": { + "type": "string" + } + }, + "sharingToken": { + "type": "string", + "description": "Token of the public share" + }, + "limit": { + "type": "integer", + "format": "int64", + "default": 1, + "description": "Maximum amount of references to resolve" + } + } + } + } + } + }, + "parameters": [ + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "References returned", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object", + "required": [ + "references" + ], + "properties": { + "references": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/Reference", + "nullable": true + } + } + } + } + } + } + } + } + } + } + } + } + } + }, "/ocs/v2.php/references/providers": { "get": { "operationId": "reference_api-get-providers-info", diff --git a/core/openapi.json b/core/openapi.json index 3310a03b89d3c..5df9496204fbc 100644 --- a/core/openapi.json +++ b/core/openapi.json @@ -3059,6 +3059,115 @@ } } }, + "/ocs/v2.php/references/extractPublic": { + "post": { + "operationId": "reference_api-extract-public", + "summary": "Extract references from a text", + "tags": [ + "reference_api" + ], + "security": [ + {}, + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "text", + "sharingToken" + ], + "properties": { + "text": { + "type": "string", + "description": "Text to extract from" + }, + "sharingToken": { + "type": "string", + "description": "Token of the public share" + }, + "resolve": { + "type": "boolean", + "default": false, + "description": "Resolve the references" + }, + "limit": { + "type": "integer", + "format": "int64", + "default": 1, + "description": "Maximum amount of references to extract" + } + } + } + } + } + }, + "parameters": [ + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "References returned", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object", + "required": [ + "references" + ], + "properties": { + "references": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/Reference", + "nullable": true + } + } + } + } + } + } + } + } + } + } + } + } + } + }, "/ocs/v2.php/references/resolve": { "get": { "operationId": "reference_api-resolve-one", @@ -3250,6 +3359,209 @@ } } }, + "/ocs/v2.php/references/resolvePublic": { + "get": { + "operationId": "reference_api-resolve-one-public", + "summary": "Resolve from a public page", + "tags": [ + "reference_api" + ], + "security": [ + {}, + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "reference", + "sharingToken" + ], + "properties": { + "reference": { + "type": "string", + "description": "Reference to resolve" + }, + "sharingToken": { + "type": "string", + "description": "Token of the public share" + } + } + } + } + } + }, + "parameters": [ + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Reference returned", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object", + "required": [ + "references" + ], + "properties": { + "references": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/Reference", + "nullable": true + } + } + } + } + } + } + } + } + } + } + } + } + }, + "post": { + "operationId": "reference_api-resolve-public", + "summary": "Resolve multiple references from a public page", + "tags": [ + "reference_api" + ], + "security": [ + {}, + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "references", + "sharingToken" + ], + "properties": { + "references": { + "type": "array", + "description": "References to resolve", + "items": { + "type": "string" + } + }, + "sharingToken": { + "type": "string", + "description": "Token of the public share" + }, + "limit": { + "type": "integer", + "format": "int64", + "default": 1, + "description": "Maximum amount of references to resolve" + } + } + } + } + } + }, + "parameters": [ + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "References returned", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object", + "required": [ + "references" + ], + "properties": { + "references": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/Reference", + "nullable": true + } + } + } + } + } + } + } + } + } + } + } + } + } + }, "/ocs/v2.php/references/providers": { "get": { "operationId": "reference_api-get-providers-info", diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php index f3ab2e6eaaea1..562183714dd67 100644 --- a/lib/composer/composer/autoload_classmap.php +++ b/lib/composer/composer/autoload_classmap.php @@ -189,6 +189,7 @@ 'OCP\\Collaboration\\Collaborators\\SearchResultType' => $baseDir . '/lib/public/Collaboration/Collaborators/SearchResultType.php', 'OCP\\Collaboration\\Reference\\ADiscoverableReferenceProvider' => $baseDir . '/lib/public/Collaboration/Reference/ADiscoverableReferenceProvider.php', 'OCP\\Collaboration\\Reference\\IDiscoverableReferenceProvider' => $baseDir . '/lib/public/Collaboration/Reference/IDiscoverableReferenceProvider.php', + 'OCP\\Collaboration\\Reference\\IPublicReferenceProvider' => $baseDir . '/lib/public/Collaboration/Reference/IPublicReferenceProvider.php', 'OCP\\Collaboration\\Reference\\IReference' => $baseDir . '/lib/public/Collaboration/Reference/IReference.php', 'OCP\\Collaboration\\Reference\\IReferenceManager' => $baseDir . '/lib/public/Collaboration/Reference/IReferenceManager.php', 'OCP\\Collaboration\\Reference\\IReferenceProvider' => $baseDir . '/lib/public/Collaboration/Reference/IReferenceProvider.php', diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php index f3edbf27f3df5..3d7e6a59468ac 100644 --- a/lib/composer/composer/autoload_static.php +++ b/lib/composer/composer/autoload_static.php @@ -222,6 +222,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2 'OCP\\Collaboration\\Collaborators\\SearchResultType' => __DIR__ . '/../../..' . '/lib/public/Collaboration/Collaborators/SearchResultType.php', 'OCP\\Collaboration\\Reference\\ADiscoverableReferenceProvider' => __DIR__ . '/../../..' . '/lib/public/Collaboration/Reference/ADiscoverableReferenceProvider.php', 'OCP\\Collaboration\\Reference\\IDiscoverableReferenceProvider' => __DIR__ . '/../../..' . '/lib/public/Collaboration/Reference/IDiscoverableReferenceProvider.php', + 'OCP\\Collaboration\\Reference\\IPublicReferenceProvider' => __DIR__ . '/../../..' . '/lib/public/Collaboration/Reference/IPublicReferenceProvider.php', 'OCP\\Collaboration\\Reference\\IReference' => __DIR__ . '/../../..' . '/lib/public/Collaboration/Reference/IReference.php', 'OCP\\Collaboration\\Reference\\IReferenceManager' => __DIR__ . '/../../..' . '/lib/public/Collaboration/Reference/IReferenceManager.php', 'OCP\\Collaboration\\Reference\\IReferenceProvider' => __DIR__ . '/../../..' . '/lib/public/Collaboration/Reference/IReferenceProvider.php', diff --git a/lib/private/Collaboration/Reference/ReferenceManager.php b/lib/private/Collaboration/Reference/ReferenceManager.php index 208c50a074b58..5a1b39d9dfff1 100644 --- a/lib/private/Collaboration/Reference/ReferenceManager.php +++ b/lib/private/Collaboration/Reference/ReferenceManager.php @@ -11,6 +11,7 @@ use OC\AppFramework\Bootstrap\Coordinator; use OC\Collaboration\Reference\File\FileReferenceProvider; use OCP\Collaboration\Reference\IDiscoverableReferenceProvider; +use OCP\Collaboration\Reference\IPublicReferenceProvider; use OCP\Collaboration\Reference\IReference; use OCP\Collaboration\Reference\IReferenceManager; use OCP\Collaboration\Reference\IReferenceProvider; @@ -59,14 +60,14 @@ public function extractReferences(string $text): array { /** * Try to get a cached reference object from a reference string */ - public function getReferenceFromCache(string $referenceId): ?IReference { - $matchedProvider = $this->getMatchedProvider($referenceId); + public function getReferenceFromCache(string $referenceId, bool $public = false, string $sharingToken = ''): ?IReference { + $matchedProvider = $this->getMatchedProvider($referenceId, $public); if ($matchedProvider === null) { return null; } - $cacheKey = $this->getFullCacheKey($matchedProvider, $referenceId); + $cacheKey = $this->getFullCacheKey($matchedProvider, $referenceId, $public, $sharingToken); return $this->getReferenceByCacheKey($cacheKey); } @@ -86,20 +87,25 @@ public function getReferenceByCacheKey(string $cacheKey): ?IReference { * Get a reference object from a reference string with a matching provider * Use a cached reference if possible */ - public function resolveReference(string $referenceId): ?IReference { - $matchedProvider = $this->getMatchedProvider($referenceId); + public function resolveReference(string $referenceId, bool $public = false, $sharingToken = ''): ?IReference { + $matchedProvider = $this->getMatchedProvider($referenceId, $public); if ($matchedProvider === null) { return null; } - $cacheKey = $this->getFullCacheKey($matchedProvider, $referenceId); + $cacheKey = $this->getFullCacheKey($matchedProvider, $referenceId, $public, $sharingToken); $cached = $this->cache->get($cacheKey); if ($cached) { return Reference::fromCache($cached); } - $reference = $matchedProvider->resolveReference($referenceId); + $reference = null; + if ($public && $matchedProvider instanceof IPublicReferenceProvider) { + $reference = $matchedProvider->resolveReferencePublic($referenceId, $sharingToken); + } elseif ($matchedProvider instanceof IReferenceProvider) { + $reference = $matchedProvider->resolveReference($referenceId); + } if ($reference) { $cachePrefix = $matchedProvider->getCachePrefix($referenceId); if ($cachePrefix !== '') { @@ -117,11 +123,14 @@ public function resolveReference(string $referenceId): ?IReference { * Try to match a reference string with all the registered providers * Fallback to the link reference provider (using OpenGraph) * - * @return IReferenceProvider|null the first matching provider + * @return IReferenceProvider|IPublicReferenceProvider|null the first matching provider */ - private function getMatchedProvider(string $referenceId): ?IReferenceProvider { + private function getMatchedProvider(string $referenceId, bool $public): null|IReferenceProvider|IPublicReferenceProvider { $matchedProvider = null; foreach ($this->getProviders() as $provider) { + if ($public && !($provider instanceof IPublicReferenceProvider)) { + continue; + } $matchedProvider = $provider->matchReference($referenceId) ? $provider : null; if ($matchedProvider !== null) { break; @@ -138,8 +147,13 @@ private function getMatchedProvider(string $referenceId): ?IReferenceProvider { /** * Get a hashed full cache key from a key and prefix given by a provider */ - private function getFullCacheKey(IReferenceProvider $provider, string $referenceId): string { - $cacheKey = $provider->getCacheKey($referenceId); + private function getFullCacheKey(IReferenceProvider $provider, string $referenceId, bool $public, string $sharingToken): string { + if ($public && !($provider instanceof IPublicReferenceProvider)) { + throw new \RuntimeException('Provider doesn\'t support public lookups'); + } + $cacheKey = $public + ? $provider->getCacheKeyPublic($referenceId, $sharingToken) + : $provider->getCacheKey($referenceId); return md5($provider->getCachePrefix($referenceId)) . ( $cacheKey !== null ? ('-' . md5($cacheKey)) : '' ); diff --git a/lib/public/Collaboration/Reference/IPublicReferenceProvider.php b/lib/public/Collaboration/Reference/IPublicReferenceProvider.php new file mode 100644 index 0000000000000..db6c3d3828b50 --- /dev/null +++ b/lib/public/Collaboration/Reference/IPublicReferenceProvider.php @@ -0,0 +1,33 @@ +resolveReference($referenceText); + } + /** * Populates the reference with OpenGraph data * @@ -201,4 +209,12 @@ public function getCachePrefix(string $referenceId): string { public function getCacheKey(string $referenceId): ?string { return null; } + + /** + * @inheritDoc + * @since 30.0.0 + */ + public function getCacheKeyPublic(string $referenceId, string $sharingToken): ?string { + return null; + } }