aboutsummaryrefslogtreecommitdiffstats
path: root/lib/private/Collaboration/Reference
diff options
context:
space:
mode:
Diffstat (limited to 'lib/private/Collaboration/Reference')
-rw-r--r--lib/private/Collaboration/Reference/File/FileReferenceEventListener.php57
-rw-r--r--lib/private/Collaboration/Reference/File/FileReferenceProvider.php161
-rw-r--r--lib/private/Collaboration/Reference/LinkReferenceProvider.php15
-rw-r--r--lib/private/Collaboration/Reference/ReferenceManager.php262
-rw-r--r--lib/private/Collaboration/Reference/RenderReferenceEventListener.php48
5 files changed, 543 insertions, 0 deletions
diff --git a/lib/private/Collaboration/Reference/File/FileReferenceEventListener.php b/lib/private/Collaboration/Reference/File/FileReferenceEventListener.php
new file mode 100644
index 00000000000..9c18531c8e7
--- /dev/null
+++ b/lib/private/Collaboration/Reference/File/FileReferenceEventListener.php
@@ -0,0 +1,57 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OC\Collaboration\Reference\File;
+
+use OC\Files\Node\NonExistingFile;
+use OC\Files\Node\NonExistingFolder;
+use OCP\Collaboration\Reference\IReferenceManager;
+use OCP\EventDispatcher\Event;
+use OCP\EventDispatcher\IEventDispatcher;
+use OCP\EventDispatcher\IEventListener;
+use OCP\Files\Events\Node\NodeDeletedEvent;
+use OCP\Files\Events\Node\NodeRenamedEvent;
+use OCP\Share\Events\ShareCreatedEvent;
+use OCP\Share\Events\ShareDeletedEvent;
+
+/** @template-implements IEventListener<Event|NodeDeletedEvent|ShareDeletedEvent|ShareCreatedEvent> */
+class FileReferenceEventListener implements IEventListener {
+ public function __construct(
+ private IReferenceManager $manager,
+ ) {
+ }
+
+ public static function register(IEventDispatcher $eventDispatcher): void {
+ $eventDispatcher->addServiceListener(NodeDeletedEvent::class, FileReferenceEventListener::class);
+ $eventDispatcher->addServiceListener(NodeRenamedEvent::class, FileReferenceEventListener::class);
+ $eventDispatcher->addServiceListener(ShareDeletedEvent::class, FileReferenceEventListener::class);
+ $eventDispatcher->addServiceListener(ShareCreatedEvent::class, FileReferenceEventListener::class);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function handle(Event $event): void {
+ if ($event instanceof NodeDeletedEvent) {
+ if ($event->getNode() instanceof NonExistingFolder || $event->getNode() instanceof NonExistingFile) {
+ return;
+ }
+
+ $this->manager->invalidateCache((string)$event->getNode()->getId());
+ }
+ if ($event instanceof NodeRenamedEvent) {
+ $this->manager->invalidateCache((string)$event->getTarget()->getId());
+ }
+ if ($event instanceof ShareDeletedEvent) {
+ $this->manager->invalidateCache((string)$event->getShare()->getNodeId());
+ }
+ if ($event instanceof ShareCreatedEvent) {
+ $this->manager->invalidateCache((string)$event->getShare()->getNodeId());
+ }
+ }
+}
diff --git a/lib/private/Collaboration/Reference/File/FileReferenceProvider.php b/lib/private/Collaboration/Reference/File/FileReferenceProvider.php
new file mode 100644
index 00000000000..3cb174d9607
--- /dev/null
+++ b/lib/private/Collaboration/Reference/File/FileReferenceProvider.php
@@ -0,0 +1,161 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OC\Collaboration\Reference\File;
+
+use OC\User\NoUserException;
+use OCP\Collaboration\Reference\ADiscoverableReferenceProvider;
+use OCP\Collaboration\Reference\IReference;
+use OCP\Collaboration\Reference\Reference;
+use OCP\Files\IMimeTypeDetector;
+use OCP\Files\InvalidPathException;
+use OCP\Files\IRootFolder;
+use OCP\Files\NotFoundException;
+use OCP\Files\NotPermittedException;
+use OCP\IL10N;
+use OCP\IPreview;
+use OCP\IURLGenerator;
+use OCP\IUserSession;
+use OCP\L10N\IFactory;
+
+class FileReferenceProvider extends ADiscoverableReferenceProvider {
+ private ?string $userId;
+ private IL10N $l10n;
+
+ public function __construct(
+ private IURLGenerator $urlGenerator,
+ private IRootFolder $rootFolder,
+ IUserSession $userSession,
+ private IMimeTypeDetector $mimeTypeDetector,
+ private IPreview $previewManager,
+ IFactory $l10n,
+ ) {
+ $this->userId = $userSession->getUser()?->getUID();
+ $this->l10n = $l10n->get('files');
+ }
+
+ public function matchReference(string $referenceText): bool {
+ return $this->getFilesAppLinkId($referenceText) !== null;
+ }
+
+ private function getFilesAppLinkId(string $referenceText): ?int {
+ $start = $this->urlGenerator->getAbsoluteURL('/apps/files/');
+ $startIndex = $this->urlGenerator->getAbsoluteURL('/index.php/apps/files/');
+
+ $fileId = null;
+
+ if (mb_strpos($referenceText, $start) === 0) {
+ $parts = parse_url($referenceText);
+ parse_str($parts['query'] ?? '', $query);
+ $fileId = isset($query['fileid']) ? (int)$query['fileid'] : $fileId;
+ $fileId = isset($query['openfile']) ? (int)$query['openfile'] : $fileId;
+ }
+
+ if (mb_strpos($referenceText, $startIndex) === 0) {
+ $parts = parse_url($referenceText);
+ parse_str($parts['query'] ?? '', $query);
+ $fileId = isset($query['fileid']) ? (int)$query['fileid'] : $fileId;
+ $fileId = isset($query['openfile']) ? (int)$query['openfile'] : $fileId;
+ }
+
+ if (mb_strpos($referenceText, $this->urlGenerator->getAbsoluteURL('/index.php/f/')) === 0) {
+ $fileId = str_replace($this->urlGenerator->getAbsoluteURL('/index.php/f/'), '', $referenceText);
+ }
+
+ if (mb_strpos($referenceText, $this->urlGenerator->getAbsoluteURL('/f/')) === 0) {
+ $fileId = str_replace($this->urlGenerator->getAbsoluteURL('/f/'), '', $referenceText);
+ }
+
+ return $fileId !== null ? (int)$fileId : null;
+ }
+
+ public function resolveReference(string $referenceText): ?IReference {
+ if ($this->matchReference($referenceText)) {
+ $reference = new Reference($referenceText);
+ try {
+ $this->fetchReference($reference);
+ } catch (NotFoundException $e) {
+ $reference->setRichObject('file', null);
+ $reference->setAccessible(false);
+ }
+ return $reference;
+ }
+
+ return null;
+ }
+
+ /**
+ * @throws NotFoundException
+ */
+ private function fetchReference(Reference $reference): void {
+ if ($this->userId === null) {
+ throw new NotFoundException();
+ }
+
+ $fileId = $this->getFilesAppLinkId($reference->getId());
+ if ($fileId === null) {
+ throw new NotFoundException();
+ }
+
+ try {
+ $userFolder = $this->rootFolder->getUserFolder($this->userId);
+ $file = $userFolder->getFirstNodeById($fileId);
+
+ if (!$file) {
+ throw new NotFoundException();
+ }
+
+ $reference->setTitle($file->getName());
+ $reference->setDescription($file->getMimetype());
+ $reference->setUrl($this->urlGenerator->getAbsoluteURL('/index.php/f/' . $fileId));
+ if ($this->previewManager->isMimeSupported($file->getMimeType())) {
+ $reference->setImageUrl($this->urlGenerator->linkToRouteAbsolute('core.Preview.getPreviewByFileId', ['x' => 1600, 'y' => 630, 'fileId' => $fileId]));
+ } else {
+ $fileTypeIconUrl = $this->mimeTypeDetector->mimeTypeIcon($file->getMimeType());
+ $reference->setImageUrl($fileTypeIconUrl);
+ }
+
+ $reference->setRichObject('file', [
+ 'id' => $file->getId(),
+ 'name' => $file->getName(),
+ 'size' => $file->getSize(),
+ 'path' => $userFolder->getRelativePath($file->getPath()),
+ 'link' => $reference->getUrl(),
+ 'mimetype' => $file->getMimetype(),
+ 'mtime' => $file->getMTime(),
+ 'preview-available' => $this->previewManager->isAvailable($file)
+ ]);
+ } catch (InvalidPathException|NotFoundException|NotPermittedException|NoUserException $e) {
+ throw new NotFoundException();
+ }
+ }
+
+ public function getCachePrefix(string $referenceId): string {
+ return (string)$this->getFilesAppLinkId($referenceId);
+ }
+
+ public function getCacheKey(string $referenceId): ?string {
+ return $this->userId ?? '';
+ }
+
+ public function getId(): string {
+ return 'files';
+ }
+
+ public function getTitle(): string {
+ return $this->l10n->t('Files');
+ }
+
+ public function getOrder(): int {
+ return 0;
+ }
+
+ public function getIconUrl(): string {
+ return $this->urlGenerator->imagePath('files', 'folder.svg');
+ }
+}
diff --git a/lib/private/Collaboration/Reference/LinkReferenceProvider.php b/lib/private/Collaboration/Reference/LinkReferenceProvider.php
new file mode 100644
index 00000000000..5af23bf633d
--- /dev/null
+++ b/lib/private/Collaboration/Reference/LinkReferenceProvider.php
@@ -0,0 +1,15 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OC\Collaboration\Reference;
+
+use OCP\Collaboration\Reference\LinkReferenceProvider as OCPLinkReferenceProvider;
+
+/** @deprecated 29.0.0 Use OCP\Collaboration\Reference\LinkReferenceProvider instead */
+class LinkReferenceProvider extends OCPLinkReferenceProvider {
+}
diff --git a/lib/private/Collaboration/Reference/ReferenceManager.php b/lib/private/Collaboration/Reference/ReferenceManager.php
new file mode 100644
index 00000000000..9287b66b2a2
--- /dev/null
+++ b/lib/private/Collaboration/Reference/ReferenceManager.php
@@ -0,0 +1,262 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+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;
+use OCP\Collaboration\Reference\Reference;
+use OCP\ICache;
+use OCP\ICacheFactory;
+use OCP\IConfig;
+use OCP\IURLGenerator;
+use OCP\IUserSession;
+use Psr\Container\ContainerInterface;
+use Psr\Log\LoggerInterface;
+use Throwable;
+
+class ReferenceManager implements IReferenceManager {
+ public const CACHE_TTL = 3600;
+
+ /** @var IReferenceProvider[]|null */
+ private ?array $providers = null;
+ private ICache $cache;
+
+ public function __construct(
+ private LinkReferenceProvider $linkReferenceProvider,
+ ICacheFactory $cacheFactory,
+ private Coordinator $coordinator,
+ private ContainerInterface $container,
+ private LoggerInterface $logger,
+ private IConfig $config,
+ private IUserSession $userSession,
+ ) {
+ $this->cache = $cacheFactory->createDistributed('reference');
+ }
+
+ /**
+ * Extract a list of URLs from a text
+ *
+ * @return string[]
+ */
+ public function extractReferences(string $text): array {
+ preg_match_all(IURLGenerator::URL_REGEX, $text, $matches);
+ $references = $matches[0] ?? [];
+ return array_map(function ($reference) {
+ return trim($reference);
+ }, $references);
+ }
+
+ /**
+ * Try to get a cached reference object from a reference string
+ */
+ 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, $public, $sharingToken);
+ return $this->getReferenceByCacheKey($cacheKey);
+ }
+
+ /**
+ * Try to get a cached reference object from a full cache key
+ */
+ public function getReferenceByCacheKey(string $cacheKey): ?IReference {
+ $cached = $this->cache->get($cacheKey);
+ if ($cached) {
+ return Reference::fromCache($cached);
+ }
+
+ return null;
+ }
+
+ /**
+ * Get a reference object from a reference string with a matching provider
+ * Use a cached reference if possible
+ */
+ 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, $public, $sharingToken);
+ $cached = $this->cache->get($cacheKey);
+ if ($cached) {
+ return Reference::fromCache($cached);
+ }
+
+ $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 !== '') {
+ // If a prefix is used we set an additional key to know when we need to delete by prefix during invalidateCache()
+ $this->cache->set('hasPrefix-' . md5($cachePrefix), true, self::CACHE_TTL);
+ }
+ $this->cache->set($cacheKey, Reference::toCache($reference), self::CACHE_TTL);
+ return $reference;
+ }
+
+ return null;
+ }
+
+ /**
+ * Try to match a reference string with all the registered providers
+ * Fallback to the link reference provider (using OpenGraph)
+ *
+ * @return IReferenceProvider|IPublicReferenceProvider|null the first matching provider
+ */
+ 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;
+ }
+ }
+
+ if ($matchedProvider === null && $this->linkReferenceProvider->matchReference($referenceId)) {
+ $matchedProvider = $this->linkReferenceProvider;
+ }
+
+ return $matchedProvider;
+ }
+
+ /**
+ * Get a hashed full cache key from a key and prefix given by a provider
+ */
+ 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)) : ''
+ );
+ }
+
+ /**
+ * Remove a specific cache entry from its key+prefix
+ */
+ public function invalidateCache(string $cachePrefix, ?string $cacheKey = null): void {
+ if ($cacheKey === null) {
+ // clear might be a heavy operation, so we only do it if there have actually been keys set
+ if ($this->cache->remove('hasPrefix-' . md5($cachePrefix))) {
+ $this->cache->clear(md5($cachePrefix));
+ }
+
+ return;
+ }
+
+ $this->cache->remove(md5($cachePrefix) . '-' . md5($cacheKey));
+ }
+
+ /**
+ * @return IReferenceProvider[]
+ */
+ public function getProviders(): array {
+ if ($this->providers === null) {
+ $context = $this->coordinator->getRegistrationContext();
+ if ($context === null) {
+ return [];
+ }
+
+ $this->providers = array_filter(array_map(function ($registration): ?IReferenceProvider {
+ try {
+ /** @var IReferenceProvider $provider */
+ $provider = $this->container->get($registration->getService());
+ } catch (Throwable $e) {
+ $this->logger->error('Could not load reference provider ' . $registration->getService() . ': ' . $e->getMessage(), [
+ 'exception' => $e,
+ ]);
+ return null;
+ }
+
+ return $provider;
+ }, $context->getReferenceProviders()));
+
+ $this->providers[] = $this->container->get(FileReferenceProvider::class);
+ }
+
+ return $this->providers;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getDiscoverableProviders(): array {
+ // preserve 0 based index to avoid returning an object in data responses
+ return array_values(
+ array_filter($this->getProviders(), static function (IReferenceProvider $provider) {
+ return $provider instanceof IDiscoverableReferenceProvider;
+ })
+ );
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function touchProvider(string $userId, string $providerId, ?int $timestamp = null): bool {
+ $providers = $this->getDiscoverableProviders();
+ $matchingProviders = array_filter($providers, static function (IDiscoverableReferenceProvider $provider) use ($providerId) {
+ return $provider->getId() === $providerId;
+ });
+ if (!empty($matchingProviders)) {
+ if ($timestamp === null) {
+ $timestamp = time();
+ }
+
+ $configKey = 'provider-last-use_' . $providerId;
+ $this->config->setUserValue($userId, 'references', $configKey, (string)$timestamp);
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getUserProviderTimestamps(): array {
+ $user = $this->userSession->getUser();
+ if ($user === null) {
+ return [];
+ }
+ $userId = $user->getUID();
+ $keys = $this->config->getUserKeys($userId, 'references');
+ $prefix = 'provider-last-use_';
+ $keys = array_filter($keys, static function (string $key) use ($prefix) {
+ return str_starts_with($key, $prefix);
+ });
+ $timestamps = [];
+ foreach ($keys as $key) {
+ $providerId = substr($key, strlen($prefix));
+ $timestamp = (int)$this->config->getUserValue($userId, 'references', $key);
+ $timestamps[$providerId] = $timestamp;
+ }
+ return $timestamps;
+ }
+}
diff --git a/lib/private/Collaboration/Reference/RenderReferenceEventListener.php b/lib/private/Collaboration/Reference/RenderReferenceEventListener.php
new file mode 100644
index 00000000000..9e6192314cb
--- /dev/null
+++ b/lib/private/Collaboration/Reference/RenderReferenceEventListener.php
@@ -0,0 +1,48 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OC\Collaboration\Reference;
+
+use OCP\Collaboration\Reference\IDiscoverableReferenceProvider;
+use OCP\Collaboration\Reference\IReferenceManager;
+use OCP\Collaboration\Reference\RenderReferenceEvent;
+use OCP\EventDispatcher\Event;
+use OCP\EventDispatcher\IEventDispatcher;
+use OCP\EventDispatcher\IEventListener;
+use OCP\IInitialStateService;
+
+/** @template-implements IEventListener<Event|RenderReferenceEvent> */
+class RenderReferenceEventListener implements IEventListener {
+ public function __construct(
+ private IReferenceManager $manager,
+ private IInitialStateService $initialStateService,
+ ) {
+ }
+
+ public static function register(IEventDispatcher $eventDispatcher): void {
+ $eventDispatcher->addServiceListener(RenderReferenceEvent::class, RenderReferenceEventListener::class);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function handle(Event $event): void {
+ if (!($event instanceof RenderReferenceEvent)) {
+ return;
+ }
+
+ $providers = $this->manager->getDiscoverableProviders();
+ $jsonProviders = array_map(static function (IDiscoverableReferenceProvider $provider) {
+ return $provider->jsonSerialize();
+ }, $providers);
+ $this->initialStateService->provideInitialState('core', 'reference-provider-list', $jsonProviders);
+
+ $timestamps = $this->manager->getUserProviderTimestamps();
+ $this->initialStateService->provideInitialState('core', 'reference-provider-timestamps', $timestamps);
+ }
+}