ReferenceManager.php 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248
  1. <?php
  2. declare(strict_types=1);
  3. /**
  4. * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
  5. * SPDX-License-Identifier: AGPL-3.0-or-later
  6. */
  7. namespace OC\Collaboration\Reference;
  8. use OC\AppFramework\Bootstrap\Coordinator;
  9. use OC\Collaboration\Reference\File\FileReferenceProvider;
  10. use OCP\Collaboration\Reference\IDiscoverableReferenceProvider;
  11. use OCP\Collaboration\Reference\IReference;
  12. use OCP\Collaboration\Reference\IReferenceManager;
  13. use OCP\Collaboration\Reference\IReferenceProvider;
  14. use OCP\Collaboration\Reference\Reference;
  15. use OCP\ICache;
  16. use OCP\ICacheFactory;
  17. use OCP\IConfig;
  18. use OCP\IURLGenerator;
  19. use OCP\IUserSession;
  20. use Psr\Container\ContainerInterface;
  21. use Psr\Log\LoggerInterface;
  22. use Throwable;
  23. class ReferenceManager implements IReferenceManager {
  24. public const CACHE_TTL = 3600;
  25. /** @var IReferenceProvider[]|null */
  26. private ?array $providers = null;
  27. private ICache $cache;
  28. public function __construct(
  29. private LinkReferenceProvider $linkReferenceProvider,
  30. ICacheFactory $cacheFactory,
  31. private Coordinator $coordinator,
  32. private ContainerInterface $container,
  33. private LoggerInterface $logger,
  34. private IConfig $config,
  35. private IUserSession $userSession,
  36. ) {
  37. $this->cache = $cacheFactory->createDistributed('reference');
  38. }
  39. /**
  40. * Extract a list of URLs from a text
  41. *
  42. * @return string[]
  43. */
  44. public function extractReferences(string $text): array {
  45. preg_match_all(IURLGenerator::URL_REGEX, $text, $matches);
  46. $references = $matches[0] ?? [];
  47. return array_map(function ($reference) {
  48. return trim($reference);
  49. }, $references);
  50. }
  51. /**
  52. * Try to get a cached reference object from a reference string
  53. */
  54. public function getReferenceFromCache(string $referenceId): ?IReference {
  55. $matchedProvider = $this->getMatchedProvider($referenceId);
  56. if ($matchedProvider === null) {
  57. return null;
  58. }
  59. $cacheKey = $this->getFullCacheKey($matchedProvider, $referenceId);
  60. return $this->getReferenceByCacheKey($cacheKey);
  61. }
  62. /**
  63. * Try to get a cached reference object from a full cache key
  64. */
  65. public function getReferenceByCacheKey(string $cacheKey): ?IReference {
  66. $cached = $this->cache->get($cacheKey);
  67. if ($cached) {
  68. return Reference::fromCache($cached);
  69. }
  70. return null;
  71. }
  72. /**
  73. * Get a reference object from a reference string with a matching provider
  74. * Use a cached reference if possible
  75. */
  76. public function resolveReference(string $referenceId): ?IReference {
  77. $matchedProvider = $this->getMatchedProvider($referenceId);
  78. if ($matchedProvider === null) {
  79. return null;
  80. }
  81. $cacheKey = $this->getFullCacheKey($matchedProvider, $referenceId);
  82. $cached = $this->cache->get($cacheKey);
  83. if ($cached) {
  84. return Reference::fromCache($cached);
  85. }
  86. $reference = $matchedProvider->resolveReference($referenceId);
  87. if ($reference) {
  88. $cachePrefix = $matchedProvider->getCachePrefix($referenceId);
  89. if ($cachePrefix !== '') {
  90. // If a prefix is used we set an additional key to know when we need to delete by prefix during invalidateCache()
  91. $this->cache->set('hasPrefix-' . md5($cachePrefix), true, self::CACHE_TTL);
  92. }
  93. $this->cache->set($cacheKey, Reference::toCache($reference), self::CACHE_TTL);
  94. return $reference;
  95. }
  96. return null;
  97. }
  98. /**
  99. * Try to match a reference string with all the registered providers
  100. * Fallback to the link reference provider (using OpenGraph)
  101. *
  102. * @return IReferenceProvider|null the first matching provider
  103. */
  104. private function getMatchedProvider(string $referenceId): ?IReferenceProvider {
  105. $matchedProvider = null;
  106. foreach ($this->getProviders() as $provider) {
  107. $matchedProvider = $provider->matchReference($referenceId) ? $provider : null;
  108. if ($matchedProvider !== null) {
  109. break;
  110. }
  111. }
  112. if ($matchedProvider === null && $this->linkReferenceProvider->matchReference($referenceId)) {
  113. $matchedProvider = $this->linkReferenceProvider;
  114. }
  115. return $matchedProvider;
  116. }
  117. /**
  118. * Get a hashed full cache key from a key and prefix given by a provider
  119. */
  120. private function getFullCacheKey(IReferenceProvider $provider, string $referenceId): string {
  121. $cacheKey = $provider->getCacheKey($referenceId);
  122. return md5($provider->getCachePrefix($referenceId)) . (
  123. $cacheKey !== null ? ('-' . md5($cacheKey)) : ''
  124. );
  125. }
  126. /**
  127. * Remove a specific cache entry from its key+prefix
  128. */
  129. public function invalidateCache(string $cachePrefix, ?string $cacheKey = null): void {
  130. if ($cacheKey === null) {
  131. // clear might be a heavy operation, so we only do it if there have actually been keys set
  132. if ($this->cache->remove('hasPrefix-' . md5($cachePrefix))) {
  133. $this->cache->clear(md5($cachePrefix));
  134. }
  135. return;
  136. }
  137. $this->cache->remove(md5($cachePrefix) . '-' . md5($cacheKey));
  138. }
  139. /**
  140. * @return IReferenceProvider[]
  141. */
  142. public function getProviders(): array {
  143. if ($this->providers === null) {
  144. $context = $this->coordinator->getRegistrationContext();
  145. if ($context === null) {
  146. return [];
  147. }
  148. $this->providers = array_filter(array_map(function ($registration): ?IReferenceProvider {
  149. try {
  150. /** @var IReferenceProvider $provider */
  151. $provider = $this->container->get($registration->getService());
  152. } catch (Throwable $e) {
  153. $this->logger->error('Could not load reference provider ' . $registration->getService() . ': ' . $e->getMessage(), [
  154. 'exception' => $e,
  155. ]);
  156. return null;
  157. }
  158. return $provider;
  159. }, $context->getReferenceProviders()));
  160. $this->providers[] = $this->container->get(FileReferenceProvider::class);
  161. }
  162. return $this->providers;
  163. }
  164. /**
  165. * @inheritDoc
  166. */
  167. public function getDiscoverableProviders(): array {
  168. // preserve 0 based index to avoid returning an object in data responses
  169. return array_values(
  170. array_filter($this->getProviders(), static function (IReferenceProvider $provider) {
  171. return $provider instanceof IDiscoverableReferenceProvider;
  172. })
  173. );
  174. }
  175. /**
  176. * @inheritDoc
  177. */
  178. public function touchProvider(string $userId, string $providerId, ?int $timestamp = null): bool {
  179. $providers = $this->getDiscoverableProviders();
  180. $matchingProviders = array_filter($providers, static function (IDiscoverableReferenceProvider $provider) use ($providerId) {
  181. return $provider->getId() === $providerId;
  182. });
  183. if (!empty($matchingProviders)) {
  184. if ($timestamp === null) {
  185. $timestamp = time();
  186. }
  187. $configKey = 'provider-last-use_' . $providerId;
  188. $this->config->setUserValue($userId, 'references', $configKey, (string) $timestamp);
  189. return true;
  190. }
  191. return false;
  192. }
  193. /**
  194. * @inheritDoc
  195. */
  196. public function getUserProviderTimestamps(): array {
  197. $user = $this->userSession->getUser();
  198. if ($user === null) {
  199. return [];
  200. }
  201. $userId = $user->getUID();
  202. $keys = $this->config->getUserKeys($userId, 'references');
  203. $prefix = 'provider-last-use_';
  204. $keys = array_filter($keys, static function (string $key) use ($prefix) {
  205. return str_starts_with($key, $prefix);
  206. });
  207. $timestamps = [];
  208. foreach ($keys as $key) {
  209. $providerId = substr($key, strlen($prefix));
  210. $timestamp = (int) $this->config->getUserValue($userId, 'references', $key);
  211. $timestamps[$providerId] = $timestamp;
  212. }
  213. return $timestamps;
  214. }
  215. }