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 <jonas@freesources.org>
pull/46378/head
Jonas 2 years ago
parent b06ce832d8
commit 1671bf3ef2
No known key found for this signature in database
GPG Key ID: 5262E7FF491049FE
  1. 89
      core/Controller/ReferenceApiController.php
  2. 312
      core/openapi-full.json
  3. 312
      core/openapi.json
  4. 1
      lib/composer/composer/autoload_classmap.php
  5. 1
      lib/composer/composer/autoload_static.php
  6. 36
      lib/private/Collaboration/Reference/ReferenceManager.php
  7. 33
      lib/public/Collaboration/Reference/IPublicReferenceProvider.php
  8. 6
      lib/public/Collaboration/Reference/IReferenceManager.php
  9. 18
      lib/public/Collaboration/Reference/LinkReferenceProvider.php

@ -10,6 +10,7 @@ namespace OC\Core\Controller;
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;
@ -22,6 +23,8 @@ use OCP\IRequest;
* @psalm-import-type CoreReferenceProvider from ResponseDefinitions
*/
class ReferenceApiController extends \OCP\AppFramework\OCSController {
private const LIMIT_MAX = 15;
public function __construct(
string $appName,
IRequest $request,
@ -62,6 +65,39 @@ class ReferenceApiController extends \OCP\AppFramework\OCSController {
]);
}
/**
* @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, limited to 15
* @return DataResponse<Http::STATUS_OK, array{references: array<string, CoreReference|null>}, 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++ >= min($limit, self::LIMIT_MAX)) {
break;
}
$result[$reference] = $resolve ? $this->referenceManager->resolveReference($reference, true, $sharingToken)?->jsonSerialize() : null;
}
return new DataResponse([
'references' => $result
]);
}
/**
* @NoAdminRequired
*
@ -73,6 +109,7 @@ class ReferenceApiController extends \OCP\AppFramework\OCSController {
* 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 +119,28 @@ class ReferenceApiController extends \OCP\AppFramework\OCSController {
return $response;
}
/**
* @PublicPage
*
* Resolve from a public page
*
* @param string $reference Reference to resolve
* @param string $sharingToken Token of the public share
* @return DataResponse<Http::STATUS_OK, array{references: array<string, ?CoreReference>}, 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 +169,36 @@ class ReferenceApiController extends \OCP\AppFramework\OCSController {
]);
}
/**
* @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, limited to 15
* @return DataResponse<Http::STATUS_OK, array{references: array<string, CoreReference|null>}, 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++ >= min($limit, self::LIMIT_MAX)) {
break;
}
$result[$reference] = $this->referenceManager->resolveReference($reference, true, $sharingToken)?->jsonSerialize();
}
return new DataResponse([
'references' => $result
]);
}
/**
* @NoAdminRequired
*

@ -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, limited to 15"
}
}
}
}
}
},
"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, limited to 15"
}
}
}
}
}
},
"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",

@ -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, limited to 15"
}
}
}
}
}
},
"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, limited to 15"
}
}
}
}
}
},
"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",

@ -189,6 +189,7 @@ return array(
'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',

@ -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',

@ -11,6 +11,7 @@ namespace OC\Collaboration\Reference;
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 @@ class ReferenceManager implements IReferenceManager {
/**
* 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 @@ class ReferenceManager implements IReferenceManager {
* 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 @@ class ReferenceManager implements IReferenceManager {
* 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 @@ class ReferenceManager implements IReferenceManager {
/**
* 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)) : ''
);

@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCP\Collaboration\Reference;
/**
* @since 30.0.0
*/
interface IPublicReferenceProvider extends IReferenceProvider {
/**
* Return a reference with its metadata for a given reference identifier and sharingToken
*
* @since 30.0.0
*/
public function resolveReferencePublic(string $referenceText, string $sharingToken): ?IReference;
/**
* Return a custom cache key to be used for caching the metadata
* This could be for example the current sharingToken if the reference
* access permissions are different for each share
*
* Should return null, if the cache is only related to the
* reference id and has no further dependency
*
* @since 30.0.0
*/
public function getCacheKeyPublic(string $referenceId, string $sharingToken): ?string;
}

@ -27,8 +27,9 @@ interface IReferenceManager {
* but may still return null in case this is disabled or the fetching fails
*
* @since 25.0.0
* @since 30.0.0 optional arguments `$public` and `$sharingToken`
*/
public function resolveReference(string $referenceId): ?IReference;
public function resolveReference(string $referenceId, bool $public = false, string $sharingToken = ''): ?IReference;
/**
* Get a reference by its cache key
@ -42,8 +43,9 @@ interface IReferenceManager {
* the cache can then be filled with a separate request from the frontend
*
* @since 25.0.0
* @since 30.0.0 optional arguments `$public` and `$sharingToken`
*/
public function getReferenceFromCache(string $referenceId): ?IReference;
public function getReferenceFromCache(string $referenceId, bool $public = false, string $sharingToken = ''): ?IReference;
/**
* Invalidate all cache entries with a prefix or just one if the cache key is provided

@ -26,7 +26,7 @@ use Psr\Log\LoggerInterface;
/**
* @since 29.0.0
*/
class LinkReferenceProvider implements IReferenceProvider {
class LinkReferenceProvider implements IReferenceProvider, IPublicReferenceProvider {
/**
* for image size and webpage header
@ -87,6 +87,14 @@ class LinkReferenceProvider implements IReferenceProvider {
return null;
}
/**
* @inheritDoc
* @since 30.0.0
*/
public function resolveReferencePublic(string $referenceText, string $sharingToken): ?IReference {
return $this->resolveReference($referenceText);
}
/**
* Populates the reference with OpenGraph data
*
@ -201,4 +209,12 @@ class LinkReferenceProvider implements IReferenceProvider {
public function getCacheKey(string $referenceId): ?string {
return null;
}
/**
* @inheritDoc
* @since 30.0.0
*/
public function getCacheKeyPublic(string $referenceId, string $sharingToken): ?string {
return null;
}
}

Loading…
Cancel
Save