123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262 |
- <?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;
- }
- }
|