diff options
author | Anupam Kumar <kyteinsky@gmail.com> | 2024-02-05 17:14:22 +0530 |
---|---|---|
committer | Anupam Kumar <kyteinsky@gmail.com> | 2024-02-14 18:22:19 +0530 |
commit | 7329b83f925f61836bcb682ad092ddca4afaf726 (patch) | |
tree | d363704485c00150939f823f25721cb33b622c6a | |
parent | 3fb1674251569e4972554f6500a66986eedfebf3 (diff) | |
download | nextcloud-server-7329b83f925f61836bcb682ad092ddca4afaf726.tar.gz nextcloud-server-7329b83f925f61836bcb682ad092ddca4afaf726.zip |
feat: Migrate LinkReferenceProvider to OCP
This would be useful to eleminate the need for using this OC class
when an app dev wants to implement a custom reference provider
for the web client but wants to fall back on opengraph for mobile
clients.
Signed-off-by: Anupam Kumar <kyteinsky@gmail.com>
4 files changed, 221 insertions, 155 deletions
diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php index fa765537953..1ed555d7a72 100644 --- a/lib/composer/composer/autoload_classmap.php +++ b/lib/composer/composer/autoload_classmap.php @@ -184,6 +184,7 @@ return array( 'OCP\\Collaboration\\Reference\\IReferenceManager' => $baseDir . '/lib/public/Collaboration/Reference/IReferenceManager.php', 'OCP\\Collaboration\\Reference\\IReferenceProvider' => $baseDir . '/lib/public/Collaboration/Reference/IReferenceProvider.php', 'OCP\\Collaboration\\Reference\\ISearchableReferenceProvider' => $baseDir . '/lib/public/Collaboration/Reference/ISearchableReferenceProvider.php', + 'OCP\\Collaboration\\Reference\\LinkReferenceProvider' => $baseDir . '/lib/public/Collaboration/Reference/LinkReferenceProvider.php', 'OCP\\Collaboration\\Reference\\Reference' => $baseDir . '/lib/public/Collaboration/Reference/Reference.php', 'OCP\\Collaboration\\Reference\\RenderReferenceEvent' => $baseDir . '/lib/public/Collaboration/Reference/RenderReferenceEvent.php', 'OCP\\Collaboration\\Resources\\CollectionException' => $baseDir . '/lib/public/Collaboration/Resources/CollectionException.php', diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php index 2899125595e..aa5951dc44f 100644 --- a/lib/composer/composer/autoload_static.php +++ b/lib/composer/composer/autoload_static.php @@ -217,6 +217,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2 'OCP\\Collaboration\\Reference\\IReferenceManager' => __DIR__ . '/../../..' . '/lib/public/Collaboration/Reference/IReferenceManager.php', 'OCP\\Collaboration\\Reference\\IReferenceProvider' => __DIR__ . '/../../..' . '/lib/public/Collaboration/Reference/IReferenceProvider.php', 'OCP\\Collaboration\\Reference\\ISearchableReferenceProvider' => __DIR__ . '/../../..' . '/lib/public/Collaboration/Reference/ISearchableReferenceProvider.php', + 'OCP\\Collaboration\\Reference\\LinkReferenceProvider' => __DIR__ . '/../../..' . '/lib/public/Collaboration/Reference/LinkReferenceProvider.php', 'OCP\\Collaboration\\Reference\\Reference' => __DIR__ . '/../../..' . '/lib/public/Collaboration/Reference/Reference.php', 'OCP\\Collaboration\\Reference\\RenderReferenceEvent' => __DIR__ . '/../../..' . '/lib/public/Collaboration/Reference/RenderReferenceEvent.php', 'OCP\\Collaboration\\Resources\\CollectionException' => __DIR__ . '/../../..' . '/lib/public/Collaboration/Resources/CollectionException.php', diff --git a/lib/private/Collaboration/Reference/LinkReferenceProvider.php b/lib/private/Collaboration/Reference/LinkReferenceProvider.php index df6c6cc9da9..08b388b47a4 100644 --- a/lib/private/Collaboration/Reference/LinkReferenceProvider.php +++ b/lib/private/Collaboration/Reference/LinkReferenceProvider.php @@ -5,6 +5,7 @@ declare(strict_types=1); * @copyright Copyright (c) 2022 Julius Härtl <jus@bitgrid.net> * * @author Julius Härtl <jus@bitgrid.net> + * @author Anupam Kumar <kyteinsky@gmail.com> * * @license GNU AGPL version 3 or any later version * @@ -24,160 +25,8 @@ declare(strict_types=1); namespace OC\Collaboration\Reference; -use Fusonic\OpenGraph\Consumer; -use GuzzleHttp\Exception\GuzzleException; -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\Collaboration\Reference\Reference; -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; +use OCP\Collaboration\Reference\LinkReferenceProvider as OCPLinkReferenceProvider; -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' - ]; - - 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, - ) { - } - - 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 { - $headResponse = $client->head($reference->getId(), [ 'timeout' => 10 ]); - } 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 > 5 * 1024 * 1024) { - $this->logger->debug('Skip resolving links pointing to content length > 5 MB'); - return; - } - $linkContentType = $headResponse->getHeader('Content-Type'); - $expectedContentType = 'text/html'; - $suffixedExpectedContentType = $expectedContentType . ';'; - $startsWithSuffixed = str_starts_with($linkContentType, $suffixedExpectedContentType); - // check the header begins with the expected content type - if ($linkContentType !== $expectedContentType && !$startsWithSuffixed) { - $this->logger->debug('Skip resolving links pointing to content type that is not "text/html"'); - return; - } - 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 { - $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()); - } else { - $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 (GuzzleException $e) { - $this->logger->info('Failed to fetch and store the open graph image for ' . $reference->getId(), ['exception' => $e]); - } 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; - } +/** @deprecated 29.0.0 Use OCP\Collaboration\Reference\LinkReferenceProvider instead */ +class LinkReferenceProvider extends OCPLinkReferenceProvider { } diff --git a/lib/public/Collaboration/Reference/LinkReferenceProvider.php b/lib/public/Collaboration/Reference/LinkReferenceProvider.php new file mode 100644 index 00000000000..76f977f2eb0 --- /dev/null +++ b/lib/public/Collaboration/Reference/LinkReferenceProvider.php @@ -0,0 +1,215 @@ +<?php + +declare(strict_types=1); +/** + * @copyright Copyright (c) 2022 Julius Härtl <jus@bitgrid.net> + * + * @author Julius Härtl <jus@bitgrid.net> + * @author Anupam Kumar <kyteinsky@gmail.com> + * + * @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 Fusonic\OpenGraph\Consumer; +use GuzzleHttp\Exception\GuzzleException; +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 { + + /* for image size and webpage header */ + private const MAX_CONTENT_LENGTH = 5 * 1024 * 1024; + + private 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; + } + + /** + * 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' => 10 ]); + } 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('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' => 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 { + $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' => 10]); + $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 (GuzzleException $e) { + $this->logger->info('Failed to fetch and store the open graph image for ' . $reference->getId(), ['exception' => $e]); + } catch (\Throwable $e) { + $this->logger->error('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; + } +} |