diff options
Diffstat (limited to 'lib/public/Collaboration')
26 files changed, 1616 insertions, 0 deletions
diff --git a/lib/public/Collaboration/AutoComplete/AutoCompleteEvent.php b/lib/public/Collaboration/AutoComplete/AutoCompleteEvent.php new file mode 100644 index 00000000000..1a225178310 --- /dev/null +++ b/lib/public/Collaboration/AutoComplete/AutoCompleteEvent.php @@ -0,0 +1,83 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCP\Collaboration\AutoComplete; + +use OCP\EventDispatcher\GenericEvent; + +/** + * @since 16.0.0 + * @deprecated 28.0.0 Use {@see AutoCompleteFilterEvent} instead + */ +class AutoCompleteEvent extends GenericEvent { + /** + * @param array $arguments + * @since 16.0.0 + */ + public function __construct(array $arguments) { + parent::__construct(null, $arguments); + } + + /** + * @since 16.0.0 + */ + public function getResults(): array { + return $this->getArgument('results'); + } + + /** + * @param array $results + * @since 16.0.0 + */ + public function setResults(array $results): void { + $this->setArgument('results', $results); + } + + /** + * @since 16.0.0 + */ + public function getSearchTerm(): string { + return $this->getArgument('search'); + } + + /** + * @return int[] + * @since 16.0.0 + */ + public function getShareTypes(): array { + return $this->getArgument('shareTypes'); + } + + /** + * @since 16.0.0 + */ + public function getItemType(): string { + return $this->getArgument('itemType'); + } + + /** + * @since 16.0.0 + */ + public function getItemId(): string { + return $this->getArgument('itemId'); + } + + /** + * @since 16.0.0 + */ + public function getSorter(): string { + return $this->getArgument('sorter'); + } + + /** + * @since 16.0.0 + */ + public function getLimit(): int { + return $this->getArgument('limit'); + } +} diff --git a/lib/public/Collaboration/AutoComplete/AutoCompleteFilterEvent.php b/lib/public/Collaboration/AutoComplete/AutoCompleteFilterEvent.php new file mode 100644 index 00000000000..0ae49a9a1f3 --- /dev/null +++ b/lib/public/Collaboration/AutoComplete/AutoCompleteFilterEvent.php @@ -0,0 +1,90 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCP\Collaboration\AutoComplete; + +use OCP\EventDispatcher\Event; + +/** + * @since 28.0.0 + */ +class AutoCompleteFilterEvent extends Event { + /** + * @since 28.0.0 + */ + public function __construct( + protected array $results, + protected string $search, + protected ?string $itemType, + protected ?string $itemId, + protected ?string $sorter, + protected array $shareTypes, + protected int $limit, + ) { + parent::__construct(); + } + + /** + * @since 28.0.0 + */ + public function getResults(): array { + return $this->results; + } + + /** + * @param array $results + * @since 28.0.0 + */ + public function setResults(array $results): void { + $this->results = $results; + } + + /** + * @since 28.0.0 + */ + public function getSearchTerm(): string { + return $this->search; + } + + /** + * @return int[] List of `\OCP\Share\IShare::TYPE_*` constants + * @since 28.0.0 + */ + public function getShareTypes(): array { + return $this->shareTypes; + } + + /** + * @since 28.0.0 + */ + public function getItemType(): ?string { + return $this->itemType; + } + + /** + * @since 28.0.0 + */ + public function getItemId(): ?string { + return $this->itemId; + } + + /** + * @return ?string List of desired sort identifiers, top priority first. When multiple are given they are joined with a pipe: `commenters|share-recipients` + * @since 28.0.0 + */ + public function getSorter(): ?string { + return $this->sorter; + } + + /** + * @since 28.0.0 + */ + public function getLimit(): int { + return $this->limit; + } +} diff --git a/lib/public/Collaboration/AutoComplete/IManager.php b/lib/public/Collaboration/AutoComplete/IManager.php new file mode 100644 index 00000000000..2d5443b921d --- /dev/null +++ b/lib/public/Collaboration/AutoComplete/IManager.php @@ -0,0 +1,28 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCP\Collaboration\AutoComplete; + +/** + * Interface IManager + * + * @since 13.0.0 + */ +interface IManager { + /** + * @param string $className – class name of the ISorter implementation + * @since 13.0.0 + */ + public function registerSorter($className); + + /** + * @param array $sorters list of sorter IDs, separated by "|" + * @param array $sortArray array representation of OCP\Collaboration\Collaborators\ISearchResult + * @param array{itemType: string, itemId: string, search?: string} $context context info of the search + * @since 13.0.0 + */ + public function runSorters(array $sorters, array &$sortArray, array $context); +} diff --git a/lib/public/Collaboration/AutoComplete/ISorter.php b/lib/public/Collaboration/AutoComplete/ISorter.php new file mode 100644 index 00000000000..1009092af6a --- /dev/null +++ b/lib/public/Collaboration/AutoComplete/ISorter.php @@ -0,0 +1,31 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCP\Collaboration\AutoComplete; + +/** + * Interface ISorter + * + * Sorts the list of .e.g users for auto completion + * + * @since 13.0.0 + */ +interface ISorter { + /** + * @return string The ID of the sorter, e.g. commenters + * @since 13.0.0 + */ + public function getId(); + + /** + * executes the sort action + * + * @param array $sortArray the array to be sorted, provided as reference + * @param array{itemType: string, itemId: string, search?: string} $context carries key 'itemType' and 'itemId' of the source object (e.g. a file) + * @since 13.0.0 + */ + public function sort(array &$sortArray, array $context); +} diff --git a/lib/public/Collaboration/Collaborators/ISearch.php b/lib/public/Collaboration/Collaborators/ISearch.php new file mode 100644 index 00000000000..d2c5c85c07f --- /dev/null +++ b/lib/public/Collaboration/Collaborators/ISearch.php @@ -0,0 +1,32 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCP\Collaboration\Collaborators; + +/** + * Interface ISearch + * + * @since 13.0.0 + */ +interface ISearch { + /** + * @param string $search + * @param array $shareTypes + * @param bool $lookup + * @param int $limit + * @param int $offset + * @return array with two elements, 1st ISearchResult as array, 2nd a bool indicating whether more result are available + * @since 13.0.0 + */ + public function search($search, array $shareTypes, $lookup, $limit, $offset); + + /** + * @param array $pluginInfo with keys 'shareType' containing the name of a corresponding constant in \OCP\Share and + * 'class' with the class name of the plugin + * @since 13.0.0 + */ + public function registerPlugin(array $pluginInfo); +} diff --git a/lib/public/Collaboration/Collaborators/ISearchPlugin.php b/lib/public/Collaboration/Collaborators/ISearchPlugin.php new file mode 100644 index 00000000000..e55a095e2b6 --- /dev/null +++ b/lib/public/Collaboration/Collaborators/ISearchPlugin.php @@ -0,0 +1,24 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCP\Collaboration\Collaborators; + +/** + * Interface ISearchPlugin + * + * @since 13.0.0 + */ +interface ISearchPlugin { + /** + * @param string $search + * @param int $limit + * @param int $offset + * @param ISearchResult $searchResult + * @return bool whether the plugin has more results + * @since 13.0.0 + */ + public function search($search, $limit, $offset, ISearchResult $searchResult); +} diff --git a/lib/public/Collaboration/Collaborators/ISearchResult.php b/lib/public/Collaboration/Collaborators/ISearchResult.php new file mode 100644 index 00000000000..bcc5a91ce99 --- /dev/null +++ b/lib/public/Collaboration/Collaborators/ISearchResult.php @@ -0,0 +1,63 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCP\Collaboration\Collaborators; + +/** + * Interface ISearchResult + * + * @since 13.0.0 + */ +interface ISearchResult { + /** + * @param SearchResultType $type + * @param array $matches + * @param array|null $exactMatches + * @since 13.0.0 + */ + public function addResultSet(SearchResultType $type, array $matches, ?array $exactMatches = null); + + /** + * @param SearchResultType $type + * @param string $collaboratorId + * @return bool + * @since 13.0.0 + */ + public function hasResult(SearchResultType $type, $collaboratorId); + + /** + * Removes all result where $collaborationId exactly matches shareWith of + * any of wide and exact result matches of the given type. + * + * @since 22.0.0 + */ + public function removeCollaboratorResult(SearchResultType $type, string $collaboratorId): bool; + + /** + * @param SearchResultType $type + * @since 13.0.0 + */ + public function unsetResult(SearchResultType $type); + + /** + * @param SearchResultType $type + * @since 13.0.0 + */ + public function markExactIdMatch(SearchResultType $type); + + /** + * @param SearchResultType $type + * @return bool + * @since 13.0.0 + */ + public function hasExactIdMatch(SearchResultType $type); + + /** + * @return array + * @since 13.0.0 + */ + public function asArray(); +} diff --git a/lib/public/Collaboration/Collaborators/SearchResultType.php b/lib/public/Collaboration/Collaborators/SearchResultType.php new file mode 100644 index 00000000000..3fdbefdcf1f --- /dev/null +++ b/lib/public/Collaboration/Collaborators/SearchResultType.php @@ -0,0 +1,55 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCP\Collaboration\Collaborators; + +/** + * Class SearchResultType + * + * @since 13.0.0 + */ +class SearchResultType { + /** @var string */ + protected $label; + + /** + * SearchResultType constructor. + * + * @param string $label + * @since 13.0.0 + */ + public function __construct($label) { + $this->label = $this->getValidatedType($label); + } + + /** + * @return string + * @since 13.0.0 + */ + public function getLabel() { + return $this->label; + } + + /** + * @param $type + * @return string + * @throws \InvalidArgumentException + * @since 13.0.0 + */ + protected function getValidatedType($type) { + $type = trim((string)$type); + + if ($type === '') { + throw new \InvalidArgumentException('Type must not be empty'); + } + + if ($type === 'exact') { + throw new \InvalidArgumentException('Provided type is a reserved word'); + } + + return $type; + } +} diff --git a/lib/public/Collaboration/Reference/ADiscoverableReferenceProvider.php b/lib/public/Collaboration/Reference/ADiscoverableReferenceProvider.php new file mode 100644 index 00000000000..582f51beea3 --- /dev/null +++ b/lib/public/Collaboration/Reference/ADiscoverableReferenceProvider.php @@ -0,0 +1,32 @@ +<?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; + +use JsonSerializable; + +/** + * @since 26.0.0 + */ +abstract class ADiscoverableReferenceProvider implements IDiscoverableReferenceProvider, JsonSerializable { + /** + * @since 26.0.0 + */ + public function jsonSerialize(): array { + $json = [ + 'id' => $this->getId(), + 'title' => $this->getTitle(), + 'icon_url' => $this->getIconUrl(), + 'order' => $this->getOrder(), + ]; + if ($this instanceof ISearchableReferenceProvider) { + $json['search_providers_ids'] = $this->getSupportedSearchProviderIds(); + } + return $json; + } +} diff --git a/lib/public/Collaboration/Reference/IDiscoverableReferenceProvider.php b/lib/public/Collaboration/Reference/IDiscoverableReferenceProvider.php new file mode 100644 index 00000000000..af4c6b3b1f6 --- /dev/null +++ b/lib/public/Collaboration/Reference/IDiscoverableReferenceProvider.php @@ -0,0 +1,44 @@ +<?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 26.0.0 + */ +interface IDiscoverableReferenceProvider extends IReferenceProvider { + /** + * @return string Unique id that identifies the reference provider + * @since 26.0.0 + */ + public function getId(): string; + + /** + * @return string User facing title of the widget + * @since 26.0.0 + */ + public function getTitle(): string; + + /** + * @return int Initial order for reference provider sorting + * @since 26.0.0 + */ + public function getOrder(): int; + + /** + * @return string url to an icon that can be displayed next to the reference provider title + * @since 26.0.0 + */ + public function getIconUrl(): string; + + /** + * @return array representation of the provider + * @since 26.0.0 + */ + public function jsonSerialize(): array; +} diff --git a/lib/public/Collaboration/Reference/IPublicReferenceProvider.php b/lib/public/Collaboration/Reference/IPublicReferenceProvider.php new file mode 100644 index 00000000000..db6c3d3828b --- /dev/null +++ b/lib/public/Collaboration/Reference/IPublicReferenceProvider.php @@ -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; +} diff --git a/lib/public/Collaboration/Reference/IReference.php b/lib/public/Collaboration/Reference/IReference.php new file mode 100644 index 00000000000..eaa82c323b5 --- /dev/null +++ b/lib/public/Collaboration/Reference/IReference.php @@ -0,0 +1,120 @@ +<?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; + +use JsonSerializable; + +/** + * @since 25.0.0 + */ +interface IReference extends JsonSerializable { + /** + * @since 25.0.0 + */ + public function getId(): string; + + /** + * Accessible flag indicates if the user has access to the provided reference + * + * @since 25.0.0 + */ + public function setAccessible(bool $accessible): void; + + /** + * Accessible flag indicates if the user has access to the provided reference + * + * @since 25.0.0 + */ + public function getAccessible(): bool; + + /** + * @since 25.0.0 + */ + public function setTitle(string $title): void; + + /** + * @since 25.0.0 + */ + public function getTitle(): string; + + /** + * @since 25.0.0 + */ + public function setDescription(?string $description): void; + + /** + * @since 25.0.0 + */ + public function getDescription(): ?string; + + /** + * @since 25.0.0 + */ + public function setImageUrl(?string $imageUrl): void; + + /** + * @since 25.0.0 + */ + public function getImageUrl(): ?string; + + /** + * @since 25.0.0 + */ + public function setImageContentType(?string $contentType): void; + + /** + * @since 25.0.0 + */ + public function getImageContentType(): ?string; + + /** + * @since 25.0.0 + */ + public function setUrl(?string $url): void; + + /** + * @since 25.0.0 + */ + public function getUrl(): string; + + /** + * Set the reference specific rich object representation + * + * @since 25.0.0 + */ + public function setRichObject(string $type, ?array $richObject): void; + + /** + * Returns the type of the reference specific rich object + * + * @since 25.0.0 + */ + public function getRichObjectType(): string; + + /** + * Returns the reference specific rich object representation + * + * @since 25.0.0 + */ + public function getRichObject(): array; + + /** + * Returns the opengraph rich object representation + * + * @since 25.0.0 + */ + public function getOpenGraphObject(): array; + + /** + * @return array{richObjectType: string, richObject: array<string, mixed>, openGraphObject: array{id: string, name: string, description: ?string, thumb: ?string, link: string}, accessible: bool} + * + * @since 25.0.0 + */ + public function jsonSerialize(): array; +} diff --git a/lib/public/Collaboration/Reference/IReferenceManager.php b/lib/public/Collaboration/Reference/IReferenceManager.php new file mode 100644 index 00000000000..c3cf7ca8e7b --- /dev/null +++ b/lib/public/Collaboration/Reference/IReferenceManager.php @@ -0,0 +1,84 @@ +<?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 25.0.0 + */ +interface IReferenceManager { + /** + * Return all reference identifiers within a string as an array + * + * @return string[] Array of found references (urls) + * @since 25.0.0 + */ + public function extractReferences(string $text): array; + + /** + * Resolve a given reference id to its metadata with all available providers + * + * This method has a fallback to always provide the open graph metadata, + * 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, bool $public = false, string $sharingToken = ''): ?IReference; + + /** + * Get a reference by its cache key + * + * @since 25.0.0 + */ + public function getReferenceByCacheKey(string $cacheKey): ?IReference; + + /** + * Explicitly get a reference from the cache to avoid heavy fetches for cases + * 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, bool $public = false, string $sharingToken = ''): ?IReference; + + /** + * Invalidate all cache entries with a prefix or just one if the cache key is provided + * + * @since 25.0.0 + */ + public function invalidateCache(string $cachePrefix, ?string $cacheKey = null): void; + + /** + * Get information on discoverable reference providers (id, title, icon and order) + * If the provider is searchable, also get the list of supported unified search providers + * + * @return IDiscoverableReferenceProvider[] + * @since 26.0.0 + */ + public function getDiscoverableProviders(): array; + + /** + * Update or set the last used timestamp for a provider + * + * @param string $userId + * @param string $providerId + * @param int|null $timestamp use current timestamp if null + * @return bool + * @since 26.0.0 + */ + public function touchProvider(string $userId, string $providerId, ?int $timestamp = null): bool; + + /** + * Get all known last used timestamps for reference providers + * + * @return int[] + * @since 26.0.0 + */ + public function getUserProviderTimestamps(): array; +} diff --git a/lib/public/Collaboration/Reference/IReferenceProvider.php b/lib/public/Collaboration/Reference/IReferenceProvider.php new file mode 100644 index 00000000000..9ff9a728063 --- /dev/null +++ b/lib/public/Collaboration/Reference/IReferenceProvider.php @@ -0,0 +1,47 @@ +<?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 25.0.0 + */ +interface IReferenceProvider { + /** + * Validate that a given reference identifier matches the current provider + * + * @since 25.0.0 + */ + public function matchReference(string $referenceText): bool; + + /** + * Return a reference with its metadata for a given reference identifier + * + * @since 25.0.0 + */ + public function resolveReference(string $referenceText): ?IReference; + + /** + * Return true if the reference metadata can be globally cached + * + * @since 25.0.0 + */ + public function getCachePrefix(string $referenceId): string; + + /** + * Return a custom cache key to be used for caching the metadata + * This could be for example the current user id if the reference + * access permissions are different for each user + * + * Should return null, if the cache is only related to the + * reference id and has no further dependency + * + * @since 25.0.0 + */ + public function getCacheKey(string $referenceId): ?string; +} diff --git a/lib/public/Collaboration/Reference/ISearchableReferenceProvider.php b/lib/public/Collaboration/Reference/ISearchableReferenceProvider.php new file mode 100644 index 00000000000..d66a9de9453 --- /dev/null +++ b/lib/public/Collaboration/Reference/ISearchableReferenceProvider.php @@ -0,0 +1,20 @@ +<?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 26.0.0 + */ +interface ISearchableReferenceProvider extends IDiscoverableReferenceProvider { + /** + * @return string[] list of search provider IDs that can be used by the vue-richtext picker + * @since 26.0.0 + */ + public function getSupportedSearchProviderIds(): array; +} diff --git a/lib/public/Collaboration/Reference/LinkReferenceProvider.php b/lib/public/Collaboration/Reference/LinkReferenceProvider.php new file mode 100644 index 00000000000..65bdcecb577 --- /dev/null +++ b/lib/public/Collaboration/Reference/LinkReferenceProvider.php @@ -0,0 +1,227 @@ +<?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; + +use Fusonic\OpenGraph\Consumer; +use GuzzleHttp\Psr7\LimitStream; +use GuzzleHttp\Psr7\Utils; +use OC\Security\RateLimiting\Exception\RateLimitExceededException; +use OC\Security\RateLimiting\Limiter; +use OC\SystemConfig; +use OCP\Files\AppData\IAppDataFactory; +use OCP\Files\NotFoundException; +use OCP\Http\Client\IClientService; +use OCP\IRequest; +use OCP\IURLGenerator; +use OCP\IUserSession; +use Psr\Log\LoggerInterface; + +/** + * @since 29.0.0 + */ +class LinkReferenceProvider implements IReferenceProvider, IPublicReferenceProvider { + + /** + * for image size and webpage header + * @since 29.0.0 + */ + public const MAX_CONTENT_LENGTH = 5 * 1024 * 1024; + + /** + * @since 29.0.0 + */ + public const ALLOWED_CONTENT_TYPES = [ + 'image/png', + 'image/jpg', + 'image/jpeg', + 'image/gif', + 'image/svg+xml', + 'image/webp' + ]; + + /** + * @since 29.0.0 + */ + public function __construct( + private IClientService $clientService, + private LoggerInterface $logger, + private SystemConfig $systemConfig, + private IAppDataFactory $appDataFactory, + private IURLGenerator $urlGenerator, + private Limiter $limiter, + private IUserSession $userSession, + private IRequest $request, + ) { + } + + /** + * @inheritDoc + * @since 29.0.0 + */ + public function matchReference(string $referenceText): bool { + if ($this->systemConfig->getValue('reference_opengraph', true) !== true) { + return false; + } + + return (bool)preg_match(IURLGenerator::URL_REGEX, $referenceText); + } + + /** + * @inheritDoc + * @since 29.0.0 + */ + public function resolveReference(string $referenceText): ?IReference { + if ($this->matchReference($referenceText)) { + $reference = new Reference($referenceText); + $this->fetchReference($reference); + return $reference; + } + + 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 + * + * @param Reference $reference + * @since 29.0.0 + */ + private function fetchReference(Reference $reference): void { + try { + $user = $this->userSession->getUser(); + if ($user) { + $this->limiter->registerUserRequest('opengraph', 10, 120, $user); + } else { + $this->limiter->registerAnonRequest('opengraph', 10, 120, $this->request->getRemoteAddress()); + } + } catch (RateLimitExceededException $e) { + return; + } + + $client = $this->clientService->newClient(); + try { + $headResponse = $client->head($reference->getId(), [ 'timeout' => 3 ]); + } catch (\Exception $e) { + $this->logger->debug('Failed to perform HEAD request to get target metadata', ['exception' => $e]); + return; + } + + $linkContentLength = $headResponse->getHeader('Content-Length'); + if (is_numeric($linkContentLength) && (int)$linkContentLength > self::MAX_CONTENT_LENGTH) { + $this->logger->debug('[Head] Skip resolving links pointing to content length > 5 MiB'); + return; + } + + $linkContentType = $headResponse->getHeader('Content-Type'); + $expectedContentTypeRegex = '/^text\/html;?/i'; + + // check the header begins with the expected content type + if (!preg_match($expectedContentTypeRegex, $linkContentType)) { + $this->logger->debug('Skip resolving links pointing to content type that is not "text/html"'); + return; + } + + try { + $response = $client->get($reference->getId(), [ 'timeout' => 3, 'stream' => true ]); + } catch (\Exception $e) { + $this->logger->debug('Failed to fetch link for obtaining open graph data', ['exception' => $e]); + return; + } + + $body = $response->getBody(); + if (is_resource($body)) { + $responseContent = fread($body, self::MAX_CONTENT_LENGTH); + if (!feof($body)) { + $this->logger->debug('[Get] Skip resolving links pointing to content length > 5 MiB'); + return; + } + } else { + $this->logger->error('[Get] Impossible to check content length'); + return; + } + + // OpenGraph handling + $consumer = new Consumer(); + $consumer->useFallbackMode = true; + $object = $consumer->loadHtml($responseContent); + + $reference->setUrl($reference->getId()); + + if ($object->title) { + $reference->setTitle($object->title); + } + + if ($object->description) { + $reference->setDescription($object->description); + } + + if ($object->images) { + try { + $host = parse_url($object->images[0]->url, PHP_URL_HOST); + if ($host === false || $host === null) { + $this->logger->warning('Could not detect host of open graph image URI for ' . $reference->getId()); + return; + } + + $appData = $this->appDataFactory->get('core'); + try { + $folder = $appData->getFolder('opengraph'); + } catch (NotFoundException $e) { + $folder = $appData->newFolder('opengraph'); + } + + $response = $client->get($object->images[0]->url, ['timeout' => 3]); + $contentType = $response->getHeader('Content-Type'); + $contentLength = $response->getHeader('Content-Length'); + + if (in_array($contentType, self::ALLOWED_CONTENT_TYPES, true) && $contentLength < self::MAX_CONTENT_LENGTH) { + $stream = Utils::streamFor($response->getBody()); + $bodyStream = new LimitStream($stream, self::MAX_CONTENT_LENGTH, 0); + $reference->setImageContentType($contentType); + $folder->newFile(md5($reference->getId()), $bodyStream->getContents()); + $reference->setImageUrl($this->urlGenerator->linkToRouteAbsolute('core.Reference.preview', ['referenceId' => md5($reference->getId())])); + } + } catch (\Exception $e) { + $this->logger->debug('Failed to fetch and store the open graph image for ' . $reference->getId(), ['exception' => $e]); + } + } + } + + /** + * @inheritDoc + * @since 29.0.0 + */ + public function getCachePrefix(string $referenceId): string { + return $referenceId; + } + + /** + * @inheritDoc + * @since 29.0.0 + */ + public function getCacheKey(string $referenceId): ?string { + return null; + } + + /** + * @inheritDoc + * @since 30.0.0 + */ + public function getCacheKeyPublic(string $referenceId, string $sharingToken): ?string { + return null; + } +} diff --git a/lib/public/Collaboration/Reference/Reference.php b/lib/public/Collaboration/Reference/Reference.php new file mode 100644 index 00000000000..3698ae419b7 --- /dev/null +++ b/lib/public/Collaboration/Reference/Reference.php @@ -0,0 +1,237 @@ +<?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 25.0.0 + * @psalm-type OpenGraphObject = array{id: string, name: string, description: ?string, thumb: ?string, link: string} + */ +class Reference implements IReference { + protected string $reference; + + protected bool $accessible = true; + + protected ?string $title = null; + protected ?string $description = null; + protected ?string $imageUrl = null; + protected ?string $contentType = null; + protected ?string $url = null; + + protected ?string $richObjectType = null; + protected ?array $richObject = null; + + /** + * @since 25.0.0 + */ + public function __construct(string $reference) { + $this->reference = $reference; + } + + /** + * @inheritdoc + * @since 25.0.0 + */ + public function getId(): string { + return $this->reference; + } + + /** + * @inheritdoc + * @since 25.0.0 + */ + public function setAccessible(bool $accessible): void { + $this->accessible = $accessible; + } + + /** + * @inheritdoc + * @since 25.0.0 + */ + public function getAccessible(): bool { + return $this->accessible; + } + + /** + * @inheritdoc + * @since 25.0.0 + */ + public function setTitle(string $title): void { + $this->title = $title; + } + + /** + * @inheritdoc + * @since 25.0.0 + */ + public function getTitle(): string { + return $this->title ?? $this->reference; + } + + /** + * @inheritdoc + * @since 25.0.0 + */ + public function setDescription(?string $description): void { + $this->description = $description; + } + + /** + * @inheritdoc + * @since 25.0.0 + */ + public function getDescription(): ?string { + return $this->description; + } + + /** + * @inheritdoc + * @since 25.0.0 + */ + public function setImageUrl(?string $imageUrl): void { + $this->imageUrl = $imageUrl; + } + + /** + * @inheritdoc + * @since 25.0.0 + */ + public function getImageUrl(): ?string { + return $this->imageUrl; + } + + /** + * @inheritdoc + * @since 25.0.0 + */ + public function setImageContentType(?string $contentType): void { + $this->contentType = $contentType; + } + + /** + * @inheritdoc + * @since 25.0.0 + */ + public function getImageContentType(): ?string { + return $this->contentType; + } + + /** + * @inheritdoc + * @since 25.0.0 + */ + public function setUrl(?string $url): void { + $this->url = $url; + } + + /** + * @inheritdoc + * @since 25.0.0 + */ + public function getUrl(): string { + return $this->url ?? $this->reference; + } + + /** + * @inheritdoc + * @since 25.0.0 + */ + public function setRichObject(string $type, ?array $richObject): void { + $this->richObjectType = $type; + $this->richObject = $richObject; + } + + /** + * @inheritdoc + * @since 25.0.0 + */ + public function getRichObjectType(): string { + if ($this->richObjectType === null) { + return 'open-graph'; + } + return $this->richObjectType; + } + + /** + * @inheritdoc + * @since 25.0.0 + * @return array<string, mixed> + */ + public function getRichObject(): array { + if ($this->richObject === null) { + return $this->getOpenGraphObject(); + } + return $this->richObject; + } + + /** + * @inheritdoc + * @since 25.0.0 + * @return OpenGraphObject + */ + public function getOpenGraphObject(): array { + return [ + 'id' => $this->getId(), + 'name' => $this->getTitle(), + 'description' => $this->getDescription(), + 'thumb' => $this->getImageUrl(), + 'link' => $this->getUrl() + ]; + } + + /** + * @param IReference $reference + * @return array + * @since 25.0.0 + */ + public static function toCache(IReference $reference): array { + return [ + 'id' => $reference->getId(), + 'title' => $reference->getTitle(), + 'imageUrl' => $reference->getImageUrl(), + 'imageContentType' => $reference->getImageContentType(), + 'description' => $reference->getDescription(), + 'link' => $reference->getUrl(), + 'accessible' => $reference->getAccessible(), + 'richObjectType' => $reference->getRichObjectType(), + 'richObject' => $reference->getRichObject(), + ]; + } + + /** + * @param array $cache + * @return IReference + * @since 25.0.0 + */ + public static function fromCache(array $cache): IReference { + $reference = new Reference($cache['id']); + $reference->setTitle($cache['title']); + $reference->setDescription($cache['description']); + $reference->setImageUrl($cache['imageUrl']); + $reference->setImageContentType($cache['imageContentType']); + $reference->setUrl($cache['link']); + $reference->setRichObject($cache['richObjectType'], $cache['richObject']); + $reference->setAccessible($cache['accessible']); + return $reference; + } + + /** + * @inheritdoc + * @since 25.0.0 + * @return array{richObjectType: string, richObject: array<string, mixed>, openGraphObject: OpenGraphObject, accessible: bool} + */ + public function jsonSerialize(): array { + return [ + 'richObjectType' => $this->getRichObjectType(), + 'richObject' => $this->getRichObject(), + 'openGraphObject' => $this->getOpenGraphObject(), + 'accessible' => $this->accessible + ]; + } +} diff --git a/lib/public/Collaboration/Reference/RenderReferenceEvent.php b/lib/public/Collaboration/Reference/RenderReferenceEvent.php new file mode 100644 index 00000000000..692098dbf60 --- /dev/null +++ b/lib/public/Collaboration/Reference/RenderReferenceEvent.php @@ -0,0 +1,22 @@ +<?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; + +use OCP\EventDispatcher\Event; + +/** + * Event emitted when apps might render references like link previews or smart picker widgets. + * + * This can be used to inject scripts for extending that. + * Further details can be found in the :ref:`Reference providers` deep dive. + * + * @since 25.0.0 + */ +class RenderReferenceEvent extends Event { +} diff --git a/lib/public/Collaboration/Resources/CollectionException.php b/lib/public/Collaboration/Resources/CollectionException.php new file mode 100644 index 00000000000..da1c66352b0 --- /dev/null +++ b/lib/public/Collaboration/Resources/CollectionException.php @@ -0,0 +1,15 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCP\Collaboration\Resources; + +/** + * @since 16.0.0 + */ +class CollectionException extends \RuntimeException { +} diff --git a/lib/public/Collaboration/Resources/ICollection.php b/lib/public/Collaboration/Resources/ICollection.php new file mode 100644 index 00000000000..6f9e14c52e2 --- /dev/null +++ b/lib/public/Collaboration/Resources/ICollection.php @@ -0,0 +1,66 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCP\Collaboration\Resources; + +use OCP\IUser; + +/** + * @since 16.0.0 + */ +interface ICollection { + /** + * @return int + * @since 16.0.0 + */ + public function getId(): int; + + /** + * @return string + * @since 16.0.0 + */ + public function getName(): string; + + /** + * @param string $name + * @since 16.0.0 + */ + public function setName(string $name): void; + + /** + * @return IResource[] + * @since 16.0.0 + */ + public function getResources(): array; + + /** + * Adds a resource to a collection + * + * @param IResource $resource + * @throws ResourceException when the resource is already part of the collection + * @since 16.0.0 + */ + public function addResource(IResource $resource): void; + + /** + * Removes a resource from a collection + * + * @param IResource $resource + * @since 16.0.0 + */ + public function removeResource(IResource $resource): void; + + /** + * Can a user/guest access the collection + * + * @param IUser|null $user + * @return bool + * @since 16.0.0 + */ + public function canAccess(?IUser $user): bool; +} diff --git a/lib/public/Collaboration/Resources/IManager.php b/lib/public/Collaboration/Resources/IManager.php new file mode 100644 index 00000000000..b3f47676784 --- /dev/null +++ b/lib/public/Collaboration/Resources/IManager.php @@ -0,0 +1,107 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCP\Collaboration\Resources; + +use OCP\IUser; + +/** + * @since 16.0.0 + */ +interface IManager extends IProvider { + /** + * @param int $id + * @return ICollection + * @throws CollectionException when the collection could not be found + * @since 16.0.0 + */ + public function getCollection(int $id): ICollection; + + /** + * @param int $id + * @param IUser|null $user + * @return ICollection + * @throws CollectionException when the collection could not be found + * @since 16.0.0 + */ + public function getCollectionForUser(int $id, ?IUser $user): ICollection; + + /** + * @param string $name + * @return ICollection + * @since 16.0.0 + */ + public function newCollection(string $name): ICollection; + + /** + * Can a user/guest access the collection + * + * @param ICollection $collection + * @param IUser|null $user + * @return bool + * @since 16.0.0 + */ + public function canAccessCollection(ICollection $collection, ?IUser $user): bool; + + /** + * @param IUser|null $user + * @since 16.0.0 + */ + public function invalidateAccessCacheForUser(?IUser $user): void; + + /** + * @param IResource $resource + * @since 16.0.0 + */ + public function invalidateAccessCacheForResource(IResource $resource): void; + + /** + * @param IResource $resource + * @param IUser|null $user + * @since 16.0.0 + */ + public function invalidateAccessCacheForResourceByUser(IResource $resource, ?IUser $user): void; + + /** + * @param IProvider $provider + * @since 16.0.0 + */ + public function invalidateAccessCacheForProvider(IProvider $provider): void; + + /** + * @param IProvider $provider + * @param IUser|null $user + * @since 16.0.0 + */ + public function invalidateAccessCacheForProviderByUser(IProvider $provider, ?IUser $user): void; + + /** + * @param string $type + * @param string $id + * @return IResource + * @since 16.0.0 + */ + public function createResource(string $type, string $id): IResource; + + /** + * @param string $type + * @param string $id + * @param IUser|null $user + * @return IResource + * @throws ResourceException + * @since 16.0.0 + */ + public function getResourceForUser(string $type, string $id, ?IUser $user): IResource; + + /** + * @param string $provider + * @since 16.0.0 + * @deprecated 18.0.0 Use IProviderManager::registerResourceProvider instead + */ + public function registerResourceProvider(string $provider): void; +} diff --git a/lib/public/Collaboration/Resources/IProvider.php b/lib/public/Collaboration/Resources/IProvider.php new file mode 100644 index 00000000000..f0e54b1ef6f --- /dev/null +++ b/lib/public/Collaboration/Resources/IProvider.php @@ -0,0 +1,43 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCP\Collaboration\Resources; + +use OCP\IUser; + +/** + * @since 16.0.0 + */ +interface IProvider { + /** + * Get the resource type of the provider + * + * @return string + * @since 16.0.0 + */ + public function getType(): string; + + /** + * Get the rich object data of a resource + * + * @param IResource $resource + * @return array + * @since 16.0.0 + */ + public function getResourceRichObject(IResource $resource): array; + + /** + * Can a user/guest access the collection + * + * @param IResource $resource + * @param IUser|null $user + * @return bool + * @since 16.0.0 + */ + public function canAccessResource(IResource $resource, ?IUser $user): bool; +} diff --git a/lib/public/Collaboration/Resources/IProviderManager.php b/lib/public/Collaboration/Resources/IProviderManager.php new file mode 100644 index 00000000000..2f5a652dcef --- /dev/null +++ b/lib/public/Collaboration/Resources/IProviderManager.php @@ -0,0 +1,26 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCP\Collaboration\Resources; + +/** + * @since 18.0.0 + */ +interface IProviderManager { + /** + * @return IProvider[] list of resource providers + * @since 18.0.0 + */ + public function getResourceProviders(): array; + + /** + * @param string $provider provider's class name + * @since 18.0.0 + */ + public function registerResourceProvider(string $provider): void; +} diff --git a/lib/public/Collaboration/Resources/IResource.php b/lib/public/Collaboration/Resources/IResource.php new file mode 100644 index 00000000000..ccea7acb4a7 --- /dev/null +++ b/lib/public/Collaboration/Resources/IResource.php @@ -0,0 +1,49 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCP\Collaboration\Resources; + +use OCP\IUser; + +/** + * @since 16.0.0 + */ +interface IResource { + /** + * @return string + * @since 16.0.0 + */ + public function getType(): string; + + /** + * @return string + * @since 16.0.0 + */ + public function getId(): string; + + /** + * @return array + * @since 16.0.0 + */ + public function getRichObject(): array; + + /** + * Can a user/guest access the resource + * + * @param IUser|null $user + * @return bool + * @since 16.0.0 + */ + public function canAccess(?IUser $user): bool; + + /** + * @return ICollection[] + * @since 16.0.0 + */ + public function getCollections(): array; +} diff --git a/lib/public/Collaboration/Resources/LoadAdditionalScriptsEvent.php b/lib/public/Collaboration/Resources/LoadAdditionalScriptsEvent.php new file mode 100644 index 00000000000..e83c7d4289f --- /dev/null +++ b/lib/public/Collaboration/Resources/LoadAdditionalScriptsEvent.php @@ -0,0 +1,23 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCP\Collaboration\Resources; + +use OCP\EventDispatcher\Event; + +/** + * This event is used by apps to register their own frontend scripts for integrating + * projects in their app. Apps also need to dispatch the event in order to load + * scripts during page load + * + * @see https://docs.nextcloud.com/server/latest/developer_manual/digging_deeper/projects.html + * @since 25.0.0 + */ +class LoadAdditionalScriptsEvent extends Event { +} diff --git a/lib/public/Collaboration/Resources/ResourceException.php b/lib/public/Collaboration/Resources/ResourceException.php new file mode 100644 index 00000000000..60d86f95f7a --- /dev/null +++ b/lib/public/Collaboration/Resources/ResourceException.php @@ -0,0 +1,15 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCP\Collaboration\Resources; + +/** + * @since 16.0.0 + */ +class ResourceException extends \RuntimeException { +} |