Passed
Push — master ( 874402...a63b55 )
by Julius
15:00 queued 12s
created

ReferenceManager::resolveReference()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 20
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 12
nc 4
nop 1
dl 0
loc 20
rs 9.8666
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
/**
5
 * @copyright Copyright (c) 2022 Julius Härtl <[email protected]>
6
 *
7
 * @author Julius Härtl <[email protected]>
8
 *
9
 * @license GNU AGPL version 3 or any later version
10
 *
11
 * This program is free software: you can redistribute it and/or modify
12
 * it under the terms of the GNU Affero General Public License as
13
 * published by the Free Software Foundation, either version 3 of the
14
 * License, or (at your option) any later version.
15
 *
16
 * This program is distributed in the hope that it will be useful,
17
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
18
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19
 * GNU Affero General Public License for more details.
20
 *
21
 * You should have received a copy of the GNU Affero General Public License
22
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
23
 */
24
25
namespace OC\Collaboration\Reference;
26
27
use OC\AppFramework\Bootstrap\Coordinator;
28
use OC\Collaboration\Reference\File\FileReferenceProvider;
29
use OCP\Collaboration\Reference\IDiscoverableReferenceProvider;
30
use OCP\Collaboration\Reference\IReference;
31
use OCP\Collaboration\Reference\IReferenceManager;
32
use OCP\Collaboration\Reference\IReferenceProvider;
33
use OCP\Collaboration\Reference\Reference;
34
use OCP\ICache;
35
use OCP\ICacheFactory;
36
use OCP\IConfig;
37
use OCP\IURLGenerator;
38
use OCP\IUserSession;
39
use Psr\Container\ContainerInterface;
40
use Psr\Log\LoggerInterface;
41
use Throwable;
42
43
class ReferenceManager implements IReferenceManager {
44
	public const CACHE_TTL = 3600;
45
46
	/** @var IReferenceProvider[]|null */
47
	private ?array $providers = null;
48
	private ICache $cache;
49
	private Coordinator $coordinator;
50
	private ContainerInterface $container;
51
	private LinkReferenceProvider $linkReferenceProvider;
52
	private LoggerInterface $logger;
53
	private IConfig $config;
54
	private IUserSession $userSession;
55
56
	public function __construct(LinkReferenceProvider $linkReferenceProvider,
57
								ICacheFactory $cacheFactory,
58
								Coordinator $coordinator,
59
								ContainerInterface $container,
60
								LoggerInterface $logger,
61
								IConfig $config,
62
								IUserSession $userSession) {
63
		$this->linkReferenceProvider = $linkReferenceProvider;
64
		$this->cache = $cacheFactory->createDistributed('reference');
65
		$this->coordinator = $coordinator;
66
		$this->container = $container;
67
		$this->logger = $logger;
68
		$this->config = $config;
69
		$this->userSession = $userSession;
70
	}
71
72
	/**
73
	 * Extract a list of URLs from a text
74
	 *
75
	 * @param string $text
76
	 * @return string[]
77
	 */
78
	public function extractReferences(string $text): array {
79
		preg_match_all(IURLGenerator::URL_REGEX, $text, $matches);
80
		$references = $matches[0] ?? [];
81
		return array_map(function ($reference) {
82
			return trim($reference);
83
		}, $references);
84
	}
85
86
	/**
87
	 * Try to get a cached reference object from a reference string
88
	 *
89
	 * @param string $referenceId
90
	 * @return IReference|null
91
	 */
92
	public function getReferenceFromCache(string $referenceId): ?IReference {
93
		$matchedProvider = $this->getMatchedProvider($referenceId);
94
95
		if ($matchedProvider === null) {
96
			return null;
97
		}
98
99
		$cacheKey = $this->getFullCacheKey($matchedProvider, $referenceId);
100
		return $this->getReferenceByCacheKey($cacheKey);
101
	}
102
103
	/**
104
	 * Try to get a cached reference object from a full cache key
105
	 *
106
	 * @param string $cacheKey
107
	 * @return IReference|null
108
	 */
109
	public function getReferenceByCacheKey(string $cacheKey): ?IReference {
110
		$cached = $this->cache->get($cacheKey);
111
		if ($cached) {
112
			return Reference::fromCache($cached);
113
		}
114
115
		return null;
116
	}
117
118
	/**
119
	 * Get a reference object from a reference string with a matching provider
120
	 * Use a cached reference if possible
121
	 *
122
	 * @param string $referenceId
123
	 * @return IReference|null
124
	 */
125
	public function resolveReference(string $referenceId): ?IReference {
126
		$matchedProvider = $this->getMatchedProvider($referenceId);
127
128
		if ($matchedProvider === null) {
129
			return null;
130
		}
131
132
		$cacheKey = $this->getFullCacheKey($matchedProvider, $referenceId);
133
		$cached = $this->cache->get($cacheKey);
134
		if ($cached) {
135
			return Reference::fromCache($cached);
136
		}
137
138
		$reference = $matchedProvider->resolveReference($referenceId);
139
		if ($reference) {
140
			$this->cache->set($cacheKey, Reference::toCache($reference), self::CACHE_TTL);
141
			return $reference;
142
		}
143
144
		return null;
145
	}
146
147
	/**
148
	 * Try to match a reference string with all the registered providers
149
	 * Fallback to the link reference provider (using OpenGraph)
150
	 *
151
	 * @param string $referenceId
152
	 * @return IReferenceProvider|null the first matching provider
153
	 */
154
	private function getMatchedProvider(string $referenceId): ?IReferenceProvider {
155
		$matchedProvider = null;
156
		foreach ($this->getProviders() as $provider) {
157
			$matchedProvider = $provider->matchReference($referenceId) ? $provider : null;
158
			if ($matchedProvider !== null) {
159
				break;
160
			}
161
		}
162
163
		if ($matchedProvider === null && $this->linkReferenceProvider->matchReference($referenceId)) {
164
			$matchedProvider = $this->linkReferenceProvider;
165
		}
166
167
		return $matchedProvider;
168
	}
169
170
	/**
171
	 * Get a hashed full cache key from a key and prefix given by a provider
172
	 *
173
	 * @param IReferenceProvider $provider
174
	 * @param string $referenceId
175
	 * @return string
176
	 */
177
	private function getFullCacheKey(IReferenceProvider $provider, string $referenceId): string {
178
		$cacheKey = $provider->getCacheKey($referenceId);
179
		return md5($provider->getCachePrefix($referenceId)) . (
180
			$cacheKey !== null ? ('-' . md5($cacheKey)) : ''
181
		);
182
	}
183
184
	/**
185
	 * Remove a specific cache entry from its key+prefix
186
	 *
187
	 * @param string $cachePrefix
188
	 * @param string|null $cacheKey
189
	 * @return void
190
	 */
191
	public function invalidateCache(string $cachePrefix, ?string $cacheKey = null): void {
192
		if ($cacheKey === null) {
193
			$this->cache->clear(md5($cachePrefix));
194
			return;
195
		}
196
197
		$this->cache->remove(md5($cachePrefix) . '-' . md5($cacheKey));
198
	}
199
200
	/**
201
	 * @return IReferenceProvider[]
202
	 */
203
	public function getProviders(): array {
204
		if ($this->providers === null) {
205
			$context = $this->coordinator->getRegistrationContext();
206
			if ($context === null) {
207
				return [];
208
			}
209
210
			$this->providers = array_filter(array_map(function ($registration): ?IReferenceProvider {
211
				try {
212
					/** @var IReferenceProvider $provider */
213
					$provider = $this->container->get($registration->getService());
214
				} catch (Throwable $e) {
215
					$this->logger->error('Could not load reference provider ' . $registration->getService() . ': ' . $e->getMessage(), [
216
						'exception' => $e,
217
					]);
218
					return null;
219
				}
220
221
				return $provider;
222
			}, $context->getReferenceProviders()));
223
224
			$this->providers[] = $this->container->get(FileReferenceProvider::class);
225
		}
226
227
		return $this->providers;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->providers could return the type null which is incompatible with the type-hinted return array. Consider adding an additional type-check to rule them out.
Loading history...
228
	}
229
230
	/**
231
	 * @inheritDoc
232
	 */
233
	public function getDiscoverableProviders(): array {
234
		// preserve 0 based index to avoid returning an object in data responses
235
		return array_values(
236
			array_filter($this->getProviders(), static function (IReferenceProvider $provider) {
237
				return $provider instanceof IDiscoverableReferenceProvider;
238
			})
239
		);
240
	}
241
242
	/**
243
	 * @inheritDoc
244
	 */
245
	public function touchProvider(string $userId, string $providerId, ?int $timestamp = null): bool {
246
		$providers = $this->getDiscoverableProviders();
247
		$matchingProviders = array_filter($providers, static function (IDiscoverableReferenceProvider $provider) use ($providerId) {
248
			return $provider->getId() === $providerId;
249
		});
250
		if (!empty($matchingProviders)) {
251
			if ($timestamp === null) {
252
				$timestamp = time();
253
			}
254
255
			$configKey = 'provider-last-use_' . $providerId;
256
			$this->config->setUserValue($userId, 'references', $configKey, (string) $timestamp);
257
			return true;
258
		}
259
		return false;
260
	}
261
262
	/**
263
	 * @inheritDoc
264
	 */
265
	public function getUserProviderTimestamps(): array {
266
		$user = $this->userSession->getUser();
267
		if ($user === null) {
268
			return [];
269
		}
270
		$userId = $user->getUID();
271
		$keys = $this->config->getUserKeys($userId, 'references');
272
		$prefix = 'provider-last-use_';
273
		$keys = array_filter($keys, static function (string $key) use ($prefix) {
274
			return str_starts_with($key, $prefix);
275
		});
276
		$timestamps = [];
277
		foreach ($keys as $key) {
278
			$providerId = substr($key, strlen($prefix));
279
			$timestamp = (int) $this->config->getUserValue($userId, 'references', $key);
280
			$timestamps[$providerId] = $timestamp;
281
		}
282
		return $timestamps;
283
	}
284
}
285