diff options
author | Julius Härtl <jus@bitgrid.net> | 2022-08-31 21:12:34 +0300 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-08-31 21:12:34 +0300 |
commit | b3eb0bfe05f6626cb02fe09d6b4f144f4b7dbc8e (patch) | |
tree | 97edf7d4275cfb3bc030df366a4adc2955bab1e7 /lib | |
parent | 4209dc7d66d192f49ab8dee305015491d7a32089 (diff) | |
parent | 1ab66988bc6e5dca0b0b18ad9366880124fb28e1 (diff) |
Merge pull request #33494 from nextcloud/enh/references
Backend for reference metadata fetching
Diffstat (limited to 'lib')
-rw-r--r-- | lib/base.php | 5 | ||||
-rw-r--r-- | lib/composer/composer/autoload_classmap.php | 10 | ||||
-rw-r--r-- | lib/composer/composer/autoload_static.php | 10 | ||||
-rw-r--r-- | lib/private/AppFramework/Bootstrap/RegistrationContext.php | 22 | ||||
-rw-r--r-- | lib/private/Collaboration/Reference/File/FileReferenceEventListener.php | 61 | ||||
-rw-r--r-- | lib/private/Collaboration/Reference/File/FileReferenceProvider.php | 153 | ||||
-rw-r--r-- | lib/private/Collaboration/Reference/LinkReferenceProvider.php | 162 | ||||
-rw-r--r-- | lib/private/Collaboration/Reference/Reference.php | 163 | ||||
-rw-r--r-- | lib/private/Collaboration/Reference/ReferenceManager.php | 169 | ||||
-rw-r--r-- | lib/private/Server.php | 4 | ||||
-rw-r--r-- | lib/public/AppFramework/Bootstrap/IRegistrationContext.php | 10 | ||||
-rw-r--r-- | lib/public/Collaboration/Reference/IReference.php | 130 | ||||
-rw-r--r-- | lib/public/Collaboration/Reference/IReferenceManager.php | 70 | ||||
-rw-r--r-- | lib/public/Collaboration/Reference/IReferenceProvider.php | 63 | ||||
-rw-r--r-- | lib/public/IURLGenerator.php | 10 |
15 files changed, 1042 insertions, 0 deletions
diff --git a/lib/base.php b/lib/base.php index 1fca124f072..055cc6786f0 100644 --- a/lib/base.php +++ b/lib/base.php @@ -750,6 +750,7 @@ class OC { self::registerEncryptionWrapperAndHooks(); self::registerAccountHooks(); self::registerResourceCollectionHooks(); + self::registerFileReferenceEventListener(); self::registerAppRestrictionsHooks(); // Make sure that the application class is not loaded before the database is setup @@ -912,6 +913,10 @@ class OC { \OC\Collaboration\Resources\Listener::register(Server::get(SymfonyAdapter::class), Server::get(IEventDispatcher::class)); } + private static function registerFileReferenceEventListener() { + \OC\Collaboration\Reference\File\FileReferenceEventListener::register(Server::get(IEventDispatcher::class)); + } + /** * register hooks for the filesystem */ diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php index 5b8e057636c..8c485fe53d9 100644 --- a/lib/composer/composer/autoload_classmap.php +++ b/lib/composer/composer/autoload_classmap.php @@ -142,6 +142,9 @@ return array( 'OCP\\Collaboration\\Collaborators\\ISearchPlugin' => $baseDir . '/lib/public/Collaboration/Collaborators/ISearchPlugin.php', 'OCP\\Collaboration\\Collaborators\\ISearchResult' => $baseDir . '/lib/public/Collaboration/Collaborators/ISearchResult.php', 'OCP\\Collaboration\\Collaborators\\SearchResultType' => $baseDir . '/lib/public/Collaboration/Collaborators/SearchResultType.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', 'OCP\\Collaboration\\Resources\\CollectionException' => $baseDir . '/lib/public/Collaboration/Resources/CollectionException.php', 'OCP\\Collaboration\\Resources\\ICollection' => $baseDir . '/lib/public/Collaboration/Resources/ICollection.php', 'OCP\\Collaboration\\Resources\\IManager' => $baseDir . '/lib/public/Collaboration/Resources/IManager.php', @@ -823,6 +826,11 @@ return array( 'OC\\Collaboration\\Collaborators\\Search' => $baseDir . '/lib/private/Collaboration/Collaborators/Search.php', 'OC\\Collaboration\\Collaborators\\SearchResult' => $baseDir . '/lib/private/Collaboration/Collaborators/SearchResult.php', 'OC\\Collaboration\\Collaborators\\UserPlugin' => $baseDir . '/lib/private/Collaboration/Collaborators/UserPlugin.php', + 'OC\\Collaboration\\Reference\\File\\FileReferenceEventListener' => $baseDir . '/lib/private/Collaboration/Reference/File/FileReferenceEventListener.php', + 'OC\\Collaboration\\Reference\\File\\FileReferenceProvider' => $baseDir . '/lib/private/Collaboration/Reference/File/FileReferenceProvider.php', + 'OC\\Collaboration\\Reference\\LinkReferenceProvider' => $baseDir . '/lib/private/Collaboration/Reference/LinkReferenceProvider.php', + 'OC\\Collaboration\\Reference\\Reference' => $baseDir . '/lib/private/Collaboration/Reference/Reference.php', + 'OC\\Collaboration\\Reference\\ReferenceManager' => $baseDir . '/lib/private/Collaboration/Reference/ReferenceManager.php', 'OC\\Collaboration\\Resources\\Collection' => $baseDir . '/lib/private/Collaboration/Resources/Collection.php', 'OC\\Collaboration\\Resources\\Listener' => $baseDir . '/lib/private/Collaboration/Resources/Listener.php', 'OC\\Collaboration\\Resources\\Manager' => $baseDir . '/lib/private/Collaboration/Resources/Manager.php', @@ -975,6 +983,8 @@ return array( 'OC\\Core\\Controller\\ProfileApiController' => $baseDir . '/core/Controller/ProfileApiController.php', 'OC\\Core\\Controller\\ProfilePageController' => $baseDir . '/core/Controller/ProfilePageController.php', 'OC\\Core\\Controller\\RecommendedAppsController' => $baseDir . '/core/Controller/RecommendedAppsController.php', + 'OC\\Core\\Controller\\ReferenceApiController' => $baseDir . '/core/Controller/ReferenceApiController.php', + 'OC\\Core\\Controller\\ReferenceController' => $baseDir . '/core/Controller/ReferenceController.php', 'OC\\Core\\Controller\\SearchController' => $baseDir . '/core/Controller/SearchController.php', 'OC\\Core\\Controller\\SetupController' => $baseDir . '/core/Controller/SetupController.php', 'OC\\Core\\Controller\\TwoFactorChallengeController' => $baseDir . '/core/Controller/TwoFactorChallengeController.php', diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php index dd3031d3a9b..0dd18e5ddbf 100644 --- a/lib/composer/composer/autoload_static.php +++ b/lib/composer/composer/autoload_static.php @@ -175,6 +175,9 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2 'OCP\\Collaboration\\Collaborators\\ISearchPlugin' => __DIR__ . '/../../..' . '/lib/public/Collaboration/Collaborators/ISearchPlugin.php', 'OCP\\Collaboration\\Collaborators\\ISearchResult' => __DIR__ . '/../../..' . '/lib/public/Collaboration/Collaborators/ISearchResult.php', 'OCP\\Collaboration\\Collaborators\\SearchResultType' => __DIR__ . '/../../..' . '/lib/public/Collaboration/Collaborators/SearchResultType.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', 'OCP\\Collaboration\\Resources\\CollectionException' => __DIR__ . '/../../..' . '/lib/public/Collaboration/Resources/CollectionException.php', 'OCP\\Collaboration\\Resources\\ICollection' => __DIR__ . '/../../..' . '/lib/public/Collaboration/Resources/ICollection.php', 'OCP\\Collaboration\\Resources\\IManager' => __DIR__ . '/../../..' . '/lib/public/Collaboration/Resources/IManager.php', @@ -856,6 +859,11 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2 'OC\\Collaboration\\Collaborators\\Search' => __DIR__ . '/../../..' . '/lib/private/Collaboration/Collaborators/Search.php', 'OC\\Collaboration\\Collaborators\\SearchResult' => __DIR__ . '/../../..' . '/lib/private/Collaboration/Collaborators/SearchResult.php', 'OC\\Collaboration\\Collaborators\\UserPlugin' => __DIR__ . '/../../..' . '/lib/private/Collaboration/Collaborators/UserPlugin.php', + 'OC\\Collaboration\\Reference\\File\\FileReferenceEventListener' => __DIR__ . '/../../..' . '/lib/private/Collaboration/Reference/File/FileReferenceEventListener.php', + 'OC\\Collaboration\\Reference\\File\\FileReferenceProvider' => __DIR__ . '/../../..' . '/lib/private/Collaboration/Reference/File/FileReferenceProvider.php', + 'OC\\Collaboration\\Reference\\LinkReferenceProvider' => __DIR__ . '/../../..' . '/lib/private/Collaboration/Reference/LinkReferenceProvider.php', + 'OC\\Collaboration\\Reference\\Reference' => __DIR__ . '/../../..' . '/lib/private/Collaboration/Reference/Reference.php', + 'OC\\Collaboration\\Reference\\ReferenceManager' => __DIR__ . '/../../..' . '/lib/private/Collaboration/Reference/ReferenceManager.php', 'OC\\Collaboration\\Resources\\Collection' => __DIR__ . '/../../..' . '/lib/private/Collaboration/Resources/Collection.php', 'OC\\Collaboration\\Resources\\Listener' => __DIR__ . '/../../..' . '/lib/private/Collaboration/Resources/Listener.php', 'OC\\Collaboration\\Resources\\Manager' => __DIR__ . '/../../..' . '/lib/private/Collaboration/Resources/Manager.php', @@ -1008,6 +1016,8 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2 'OC\\Core\\Controller\\ProfileApiController' => __DIR__ . '/../../..' . '/core/Controller/ProfileApiController.php', 'OC\\Core\\Controller\\ProfilePageController' => __DIR__ . '/../../..' . '/core/Controller/ProfilePageController.php', 'OC\\Core\\Controller\\RecommendedAppsController' => __DIR__ . '/../../..' . '/core/Controller/RecommendedAppsController.php', + 'OC\\Core\\Controller\\ReferenceApiController' => __DIR__ . '/../../..' . '/core/Controller/ReferenceApiController.php', + 'OC\\Core\\Controller\\ReferenceController' => __DIR__ . '/../../..' . '/core/Controller/ReferenceController.php', 'OC\\Core\\Controller\\SearchController' => __DIR__ . '/../../..' . '/core/Controller/SearchController.php', 'OC\\Core\\Controller\\SetupController' => __DIR__ . '/../../..' . '/core/Controller/SetupController.php', 'OC\\Core\\Controller\\TwoFactorChallengeController' => __DIR__ . '/../../..' . '/core/Controller/TwoFactorChallengeController.php', diff --git a/lib/private/AppFramework/Bootstrap/RegistrationContext.php b/lib/private/AppFramework/Bootstrap/RegistrationContext.php index c98f968c999..3ade98e334f 100644 --- a/lib/private/AppFramework/Bootstrap/RegistrationContext.php +++ b/lib/private/AppFramework/Bootstrap/RegistrationContext.php @@ -32,6 +32,7 @@ namespace OC\AppFramework\Bootstrap; use Closure; use OCP\Calendar\Resource\IBackend as IResourceBackend; use OCP\Calendar\Room\IBackend as IRoomBackend; +use OCP\Collaboration\Reference\IReferenceProvider; use OCP\Talk\ITalkBackend; use RuntimeException; use function array_shift; @@ -121,6 +122,9 @@ class RegistrationContext { /** @var ServiceRegistration<ICalendarProvider>[] */ private $calendarProviders = []; + /** @var ServiceRegistration<IReferenceProvider>[] */ + private array $referenceProviders = []; + /** @var ParameterRegistration[] */ private $sensitiveMethods = []; @@ -273,6 +277,13 @@ class RegistrationContext { ); } + public function registerReferenceProvider(string $class): void { + $this->context->registerReferenceProvider( + $this->appId, + $class + ); + } + public function registerProfileLinkAction(string $actionClass): void { $this->context->registerProfileLinkAction( $this->appId, @@ -398,6 +409,10 @@ class RegistrationContext { $this->calendarProviders[] = new ServiceRegistration($appId, $class); } + public function registerReferenceProvider(string $appId, string $class): void { + $this->referenceProviders[] = new ServiceRegistration($appId, $class); + } + /** * @psalm-param class-string<ILinkAction> $actionClass */ @@ -692,6 +707,13 @@ class RegistrationContext { } /** + * @return ServiceRegistration<IReferenceProvider>[] + */ + public function getReferenceProviders(): array { + return $this->referenceProviders; + } + + /** * @return ServiceRegistration<ILinkAction>[] */ public function getProfileLinkActions(): array { diff --git a/lib/private/Collaboration/Reference/File/FileReferenceEventListener.php b/lib/private/Collaboration/Reference/File/FileReferenceEventListener.php new file mode 100644 index 00000000000..6ccae9903dc --- /dev/null +++ b/lib/private/Collaboration/Reference/File/FileReferenceEventListener.php @@ -0,0 +1,61 @@ +<?php + +declare(strict_types=1); +/** + * @copyright Copyright (c) 2022 Julius Härtl <jus@bitgrid.net> + * + * @author Julius Härtl <jus@bitgrid.net> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +namespace OC\Collaboration\Reference\File; + +use OCP\Collaboration\Reference\IReferenceManager; +use OCP\EventDispatcher\Event; +use OCP\EventDispatcher\IEventDispatcher; +use OCP\Files\Events\Node\NodeDeletedEvent; +use OCP\Share\Events\ShareCreatedEvent; +use OCP\Share\Events\ShareDeletedEvent; + +class FileReferenceEventListener implements \OCP\EventDispatcher\IEventListener { + private IReferenceManager $manager; + + public function __construct(IReferenceManager $manager) { + $this->manager = $manager; + } + + public static function register(IEventDispatcher $eventDispatcher): void { + $eventDispatcher->addServiceListener(NodeDeletedEvent::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) { + $this->manager->invalidateCache((string)$event->getNode()->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..39cdb62b09a --- /dev/null +++ b/lib/private/Collaboration/Reference/File/FileReferenceProvider.php @@ -0,0 +1,153 @@ +<?php + +declare(strict_types=1); +/** + * @copyright Copyright (c) 2022 Julius Härtl <jus@bitgrid.net> + * + * @author Julius Härtl <jus@bitgrid.net> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +namespace OC\Collaboration\Reference\File; + +use OC\Collaboration\Reference\Reference; +use OC\User\NoUserException; +use OCP\Collaboration\Reference\IReference; +use OCP\Collaboration\Reference\IReferenceProvider; +use OCP\Files\InvalidPathException; +use OCP\Files\IRootFolder; +use OCP\Files\Node; +use OCP\Files\NotFoundException; +use OCP\Files\NotPermittedException; +use OCP\IPreview; +use OCP\IURLGenerator; +use OCP\IUserSession; + +class FileReferenceProvider implements IReferenceProvider { + private IURLGenerator $urlGenerator; + private IRootFolder $rootFolder; + private ?string $userId; + private IPreview $previewManager; + + public function __construct(IURLGenerator $urlGenerator, IRootFolder $rootFolder, IUserSession $userSession, IPreview $previewManager) { + $this->urlGenerator = $urlGenerator; + $this->rootFolder = $rootFolder; + $this->userId = $userSession->getUser() ? $userSession->getUser()->getUID() : null; + $this->previewManager = $previewManager; + } + + 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); + $files = $userFolder->getById($fileId); + + if (empty($files)) { + throw new NotFoundException(); + } + + /** @var Node $file */ + $file = array_shift($files); + + $reference->setTitle($file->getName()); + $reference->setDescription($file->getMimetype()); + $reference->setUrl($this->urlGenerator->getAbsoluteURL('/index.php/f/' . $fileId)); + $reference->setImageUrl($this->urlGenerator->linkToRouteAbsolute('core.Preview.getPreviewByFileId', ['x' => 1600, 'y' => 630, 'fileId' => $fileId])); + + $reference->setRichObject('file', [ + 'id' => $file->getId(), + 'name' => $file->getName(), + 'size' => $file->getSize(), + 'path' => $file->getPath(), + 'link' => $reference->getUrl(), + 'mimetype' => $file->getMimetype(), + '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 ?? ''; + } +} diff --git a/lib/private/Collaboration/Reference/LinkReferenceProvider.php b/lib/private/Collaboration/Reference/LinkReferenceProvider.php new file mode 100644 index 00000000000..36fbdd0b168 --- /dev/null +++ b/lib/private/Collaboration/Reference/LinkReferenceProvider.php @@ -0,0 +1,162 @@ +<?php + +declare(strict_types=1); +/** + * @copyright Copyright (c) 2022 Julius Härtl <jus@bitgrid.net> + * + * @author Julius Härtl <jus@bitgrid.net> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +namespace OC\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\Collaboration\Reference\IReference; +use OCP\Collaboration\Reference\IReferenceProvider; +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; + +class LinkReferenceProvider implements IReferenceProvider { + public const MAX_PREVIEW_SIZE = 1024 * 1024; + + public const ALLOWED_CONTENT_TYPES = [ + 'image/png', + 'image/jpg', + 'image/jpeg', + 'image/gif', + 'image/svg+xml', + 'image/webp' + ]; + + private IClientService $clientService; + private LoggerInterface $logger; + private SystemConfig $systemConfig; + private IAppDataFactory $appDataFactory; + private IURLGenerator $urlGenerator; + private Limiter $limiter; + private IUserSession $userSession; + private IRequest $request; + + public function __construct(IClientService $clientService, LoggerInterface $logger, SystemConfig $systemConfig, IAppDataFactory $appDataFactory, IURLGenerator $urlGenerator, Limiter $limiter, IUserSession $userSession, IRequest $request) { + $this->clientService = $clientService; + $this->logger = $logger; + $this->systemConfig = $systemConfig; + $this->appDataFactory = $appDataFactory; + $this->urlGenerator = $urlGenerator; + $this->limiter = $limiter; + $this->userSession = $userSession; + $this->request = $request; + } + + public function matchReference(string $referenceText): bool { + if ($this->systemConfig->getValue('reference_opengraph', true) !== true) { + return false; + } + + return (bool)preg_match(IURLGenerator::URL_REGEX, $referenceText); + } + + public function resolveReference(string $referenceText): ?IReference { + if ($this->matchReference($referenceText)) { + $reference = new Reference($referenceText); + $this->fetchReference($reference); + return $reference; + } + + return null; + } + + 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 { + $response = $client->get($reference->getId(), [ 'timeout' => 10 ]); + } catch (\Exception $e) { + $this->logger->debug('Failed to fetch link for obtaining open graph data', ['exception' => $e]); + return; + } + + $responseBody = (string)$response->getBody(); + + // OpenGraph handling + $consumer = new Consumer(); + $consumer->useFallbackMode = true; + $object = $consumer->loadHtml($responseBody); + + $reference->setUrl($reference->getId()); + + if ($object->title) { + $reference->setTitle($object->title); + } + + if ($object->description) { + $reference->setDescription($object->description); + } + + if ($object->images) { + try { + $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' => 10 ]); + $contentType = $response->getHeader('Content-Type'); + $contentLength = $response->getHeader('Content-Length'); + + if (in_array($contentType, self::ALLOWED_CONTENT_TYPES, true) && $contentLength < self::MAX_PREVIEW_SIZE) { + $stream = Utils::streamFor($response->getBody()); + $bodyStream = new LimitStream($stream, self::MAX_PREVIEW_SIZE, 0); + $reference->setImageContentType($contentType); + $folder->newFile(md5($reference->getId()), $bodyStream->getContents()); + $reference->setImageUrl($this->urlGenerator->linkToRouteAbsolute('core.Reference.preview', ['referenceId' => md5($reference->getId())])); + } + } catch (\Throwable $e) { + $this->logger->error('Failed to fetch and store the open graph image for ' . $reference->getId(), ['exception' => $e]); + } + } + } + + public function getCachePrefix(string $referenceId): string { + return $referenceId; + } + + public function getCacheKey(string $referenceId): ?string { + return null; + } +} diff --git a/lib/private/Collaboration/Reference/Reference.php b/lib/private/Collaboration/Reference/Reference.php new file mode 100644 index 00000000000..22dc57782d8 --- /dev/null +++ b/lib/private/Collaboration/Reference/Reference.php @@ -0,0 +1,163 @@ +<?php + +declare(strict_types=1); + +/** + * @copyright Copyright (c) 2022 Julius Härtl <jus@bitgrid.net> + * + * @author Julius Härtl <jus@bitgrid.net> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +namespace OC\Collaboration\Reference; + +use OCP\Collaboration\Reference\IReference; + +class Reference implements IReference { + private string $reference; + + private bool $accessible = true; + + private ?string $title = null; + private ?string $description = null; + private ?string $imageUrl = null; + private ?string $contentType = null; + private ?string $url = null; + + private ?string $richObjectType = null; + private ?array $richObject = null; + + public function __construct(string $reference) { + $this->reference = $reference; + } + + public function getId(): string { + return $this->reference; + } + + public function setAccessible(bool $accessible): void { + $this->accessible = $accessible; + } + + public function getAccessible(): bool { + return $this->accessible; + } + + public function setTitle(string $title): void { + $this->title = $title; + } + + public function getTitle(): string { + return $this->title ?? $this->reference; + } + + public function setDescription(?string $description): void { + $this->description = $description; + } + + public function getDescription(): ?string { + return $this->description; + } + + public function setImageUrl(?string $imageUrl): void { + $this->imageUrl = $imageUrl; + } + + public function getImageUrl(): ?string { + return $this->imageUrl; + } + + public function setImageContentType(?string $contentType): void { + $this->contentType = $contentType; + } + + public function getImageContentType(): ?string { + return $this->contentType; + } + + public function setUrl(?string $url): void { + $this->url = $url; + } + + public function getUrl(): ?string { + return $this->url; + } + + public function setRichObject(string $type, ?array $richObject): void { + $this->richObjectType = $type; + $this->richObject = $richObject; + } + + public function getRichObjectType(): string { + if ($this->richObjectType === null) { + return 'open-graph'; + } + return $this->richObjectType; + } + + public function getRichObject(): array { + if ($this->richObject === null) { + return $this->getOpenGraphObject(); + } + return $this->richObject; + } + + public function getOpenGraphObject(): array { + return [ + 'id' => $this->getId(), + 'name' => $this->getTitle(), + 'description' => $this->getDescription(), + 'thumb' => $this->getImageUrl(), + 'link' => $this->getUrl() + ]; + } + + 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(), + ]; + } + + 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; + } + + public function jsonSerialize() { + return [ + 'richObjectType' => $this->getRichObjectType(), + 'richObject' => $this->getRichObject(), + 'openGraphObject' => $this->getOpenGraphObject(), + 'accessible' => $this->accessible + ]; + } +} diff --git a/lib/private/Collaboration/Reference/ReferenceManager.php b/lib/private/Collaboration/Reference/ReferenceManager.php new file mode 100644 index 00000000000..304d693804f --- /dev/null +++ b/lib/private/Collaboration/Reference/ReferenceManager.php @@ -0,0 +1,169 @@ +<?php + +declare(strict_types=1); +/** + * @copyright Copyright (c) 2022 Julius Härtl <jus@bitgrid.net> + * + * @author Julius Härtl <jus@bitgrid.net> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +namespace OC\Collaboration\Reference; + +use OC\AppFramework\Bootstrap\Coordinator; +use OC\Collaboration\Reference\File\FileReferenceProvider; +use OCP\Collaboration\Reference\IReference; +use OCP\Collaboration\Reference\IReferenceManager; +use OCP\Collaboration\Reference\IReferenceProvider; +use OCP\ICache; +use OCP\ICacheFactory; +use OCP\IURLGenerator; +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; + private Coordinator $coordinator; + private ContainerInterface $container; + private LinkReferenceProvider $linkReferenceProvider; + private LoggerInterface $logger; + + public function __construct(LinkReferenceProvider $linkReferenceProvider, ICacheFactory $cacheFactory, Coordinator $coordinator, ContainerInterface $container, LoggerInterface $logger) { + $this->linkReferenceProvider = $linkReferenceProvider; + $this->cache = $cacheFactory->createDistributed('reference'); + $this->coordinator = $coordinator; + $this->container = $container; + $this->logger = $logger; + } + + 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); + } + + public function getReferenceFromCache(string $referenceId): ?IReference { + $matchedProvider = $this->getMatchedProvider($referenceId); + + if ($matchedProvider === null) { + return null; + } + + $cacheKey = $this->getFullCacheKey($matchedProvider, $referenceId); + return $this->getReferenceByCacheKey($cacheKey); + } + + public function getReferenceByCacheKey(string $cacheKey): ?IReference { + $cached = $this->cache->get($cacheKey); + if ($cached) { + return Reference::fromCache($cached); + } + + return null; + } + + public function resolveReference(string $referenceId): ?IReference { + $matchedProvider = $this->getMatchedProvider($referenceId); + + if ($matchedProvider === null) { + return null; + } + + $cacheKey = $this->getFullCacheKey($matchedProvider, $referenceId); + $cached = $this->cache->get($cacheKey); + if ($cached) { + return Reference::fromCache($cached); + } + + $reference = $matchedProvider->resolveReference($referenceId); + if ($reference) { + $this->cache->set($cacheKey, Reference::toCache($reference), self::CACHE_TTL); + return $reference; + } + + return null; + } + + private function getMatchedProvider(string $referenceId): ?IReferenceProvider { + $matchedProvider = null; + foreach ($this->getProviders() as $provider) { + $matchedProvider = $provider->matchReference($referenceId) ? $provider : null; + if ($matchedProvider !== null) { + break; + } + } + + if ($matchedProvider === null && $this->linkReferenceProvider->matchReference($referenceId)) { + $matchedProvider = $this->linkReferenceProvider; + } + + return $matchedProvider; + } + + private function getFullCacheKey(IReferenceProvider $provider, string $referenceId): string { + $cacheKey = $provider->getCacheKey($referenceId); + return md5($provider->getCachePrefix($referenceId)) . ( + $cacheKey !== null ? ('-' . md5($cacheKey)) : '' + ); + } + + public function invalidateCache(string $cachePrefix, ?string $cacheKey = null): void { + if ($cacheKey === null) { + $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; + } +} diff --git a/lib/private/Server.php b/lib/private/Server.php index 09074e39046..f18ac7b6534 100644 --- a/lib/private/Server.php +++ b/lib/private/Server.php @@ -73,6 +73,7 @@ use OC\Collaboration\Collaborators\MailPlugin; use OC\Collaboration\Collaborators\RemoteGroupPlugin; use OC\Collaboration\Collaborators\RemotePlugin; use OC\Collaboration\Collaborators\UserPlugin; +use OC\Collaboration\Reference\ReferenceManager; use OC\Command\CronBus; use OC\Comments\ManagerFactory as CommentsManagerFactory; use OC\Contacts\ContactsMenu\ActionFactory; @@ -162,6 +163,7 @@ use OCP\App\IAppManager; use OCP\Authentication\LoginCredentials\IStore; use OCP\BackgroundJob\IJobList; use OCP\Collaboration\AutoComplete\IManager; +use OCP\Collaboration\Reference\IReferenceManager; use OCP\Command\IBus; use OCP\Comments\ICommentsManager; use OCP\Contacts\ContactsMenu\IActionFactory; @@ -1338,6 +1340,8 @@ class Server extends ServerContainer implements IServerContainer { $this->registerAlias(\OCP\Collaboration\Resources\IProviderManager::class, \OC\Collaboration\Resources\ProviderManager::class); $this->registerAlias(\OCP\Collaboration\Resources\IManager::class, \OC\Collaboration\Resources\Manager::class); + $this->registerAlias(IReferenceManager::class, ReferenceManager::class); + $this->registerDeprecatedAlias('SettingsManager', \OC\Settings\Manager::class); $this->registerAlias(\OCP\Settings\IManager::class, \OC\Settings\Manager::class); $this->registerService(\OC\Files\AppData\Factory::class, function (ContainerInterface $c) { diff --git a/lib/public/AppFramework/Bootstrap/IRegistrationContext.php b/lib/public/AppFramework/Bootstrap/IRegistrationContext.php index 6b10d7bfc0f..0f398c13979 100644 --- a/lib/public/AppFramework/Bootstrap/IRegistrationContext.php +++ b/lib/public/AppFramework/Bootstrap/IRegistrationContext.php @@ -33,6 +33,7 @@ use OCP\AppFramework\IAppContainer; use OCP\Authentication\TwoFactorAuth\IProvider; use OCP\Calendar\ICalendarProvider; use OCP\Capabilities\ICapability; +use OCP\Collaboration\Reference\IReferenceProvider; use OCP\EventDispatcher\IEventDispatcher; use OCP\Files\Template\ICustomTemplateProvider; use OCP\IContainer; @@ -255,6 +256,15 @@ interface IRegistrationContext { public function registerCalendarProvider(string $class): void; /** + * Register a reference provider + * + * @param string $class + * @psalm-param class-string<IReferenceProvider> $class + * @since 25.0.0 + */ + public function registerReferenceProvider(string $class): void; + + /** * Register an implementation of \OCP\Profile\ILinkAction that * will handle the implementation of a profile link action * diff --git a/lib/public/Collaboration/Reference/IReference.php b/lib/public/Collaboration/Reference/IReference.php new file mode 100644 index 00000000000..0155ae86dd8 --- /dev/null +++ b/lib/public/Collaboration/Reference/IReference.php @@ -0,0 +1,130 @@ +<?php + +declare(strict_types=1); +/** + * @copyright Copyright (c) 2022 Julius Härtl <jus@bitgrid.net> + * + * @author Julius Härtl <jus@bitgrid.net> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +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; +} diff --git a/lib/public/Collaboration/Reference/IReferenceManager.php b/lib/public/Collaboration/Reference/IReferenceManager.php new file mode 100644 index 00000000000..487e243c7ed --- /dev/null +++ b/lib/public/Collaboration/Reference/IReferenceManager.php @@ -0,0 +1,70 @@ +<?php + +declare(strict_types=1); +/** + * @copyright Copyright (c) 2022 Julius Härtl <jus@bitgrid.net> + * + * @author Julius Härtl <jus@bitgrid.net> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +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 + */ + public function resolveReference(string $referenceId): ?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 + */ + public function getReferenceFromCache(string $referenceId): ?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; +} diff --git a/lib/public/Collaboration/Reference/IReferenceProvider.php b/lib/public/Collaboration/Reference/IReferenceProvider.php new file mode 100644 index 00000000000..100374b78b3 --- /dev/null +++ b/lib/public/Collaboration/Reference/IReferenceProvider.php @@ -0,0 +1,63 @@ +<?php + +declare(strict_types=1); +/** + * @copyright Copyright (c) 2022 Julius Härtl <jus@bitgrid.net> + * + * @author Julius Härtl <jus@bitgrid.net> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +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/IURLGenerator.php b/lib/public/IURLGenerator.php index 580536b8b5f..808ba66c862 100644 --- a/lib/public/IURLGenerator.php +++ b/lib/public/IURLGenerator.php @@ -35,6 +35,16 @@ namespace OCP; * @since 6.0.0 */ interface IURLGenerator { + + /** + * Regex for matching http(s) urls + * + * This is a copy of the frontend regex in core/src/OCP/comments.js, make sure to adjust both when changing + * + * @since 25.0.0 + */ + public const URL_REGEX = '/(\s|\n|^)(https?:\/\/)?((?:[-A-Z0-9+_]+\.)+[-A-Z]+(?:\/[-A-Z0-9+&@#%?=~_|!:,.;()]*)*)(\s|\n|$)/mi'; + /** * Returns the URL for a route * @param string $routeName the name of the route |