ReferenceManager.php 8.0 KB

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