Passed
Push — master ( a5a2c7...2fcb6d )
by Roeland
10:48
created

Manager::getResourceRichObject()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 11
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 6
nc 4
nop 1
dl 0
loc 11
rs 10
c 0
b 0
f 0
1
<?php
2
declare(strict_types=1);
3
/**
4
 * @copyright Copyright (c) 2018 Joas Schilling <[email protected]>
5
 *
6
 * @license GNU AGPL version 3 or any later version
7
 *
8
 * This program is free software: you can redistribute it and/or modify
9
 * it under the terms of the GNU Affero General Public License as
10
 * published by the Free Software Foundation, either version 3 of the
11
 * License, or (at your option) any later version.
12
 *
13
 * This program is distributed in the hope that it will be useful,
14
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16
 * GNU Affero General Public License for more details.
17
 *
18
 * You should have received a copy of the GNU Affero General Public License
19
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
20
 *
21
 */
22
23
namespace OC\Collaboration\Resources;
24
25
26
use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
27
use OCP\AppFramework\QueryException;
28
use OCP\Collaboration\Resources\CollectionException;
29
use OCP\Collaboration\Resources\ICollection;
30
use OCP\Collaboration\Resources\IManager;
31
use OCP\Collaboration\Resources\IProvider;
32
use OCP\Collaboration\Resources\IResource;
33
use OCP\Collaboration\Resources\ResourceException;
34
use OCP\DB\QueryBuilder\IQueryBuilder;
35
use OCP\IDBConnection;
36
use OCP\ILogger;
37
use OCP\IUser;
38
39
class Manager implements IManager {
40
41
	public const TABLE_COLLECTIONS = 'collres_collections';
42
	public const TABLE_RESOURCES = 'collres_resources';
43
	public const TABLE_ACCESS_CACHE = 'collres_accesscache';
44
45
	/** @var IDBConnection */
46
	protected $connection;
47
	/** @var ILogger */
48
	protected $logger;
49
50
	/** @var string[] */
51
	protected $providers = [];
52
53
	/** @var IProvider[] */
54
	protected $providerInstances = [];
55
56
	public function __construct(IDBConnection $connection, ILogger $logger) {
57
		$this->connection = $connection;
58
		$this->logger = $logger;
59
	}
60
61
	/**
62
	 * @param int $id
63
	 * @return ICollection
64
	 * @throws CollectionException when the collection could not be found
65
	 * @since 16.0.0
66
	 */
67
	public function getCollection(int $id): ICollection {
68
		$query = $this->connection->getQueryBuilder();
69
		$query->select('*')
70
			->from(self::TABLE_COLLECTIONS)
71
			->where($query->expr()->eq('id', $query->createNamedParameter($id, IQueryBuilder::PARAM_INT)));
72
		$result = $query->execute();
73
		$row = $result->fetch();
74
		$result->closeCursor();
75
76
		if (!$row) {
77
			throw new CollectionException('Collection not found');
78
		}
79
80
		return new Collection($this, $this->connection, (int) $row['id'], (string) $row['name']);
81
	}
82
83
	/**
84
	 * @param int $id
85
	 * @param IUser|null $user
86
	 * @return ICollection
87
	 * @throws CollectionException when the collection could not be found
88
	 * @since 16.0.0
89
	 */
90
	public function getCollectionForUser(int $id, ?IUser $user): ICollection {
91
		$query = $this->connection->getQueryBuilder();
92
		$userId = $user instanceof IUser ? $user->getUID() : '';
93
94
		$query->select('*')
95
			->from(self::TABLE_COLLECTIONS, 'c')
96
			->leftJoin(
97
				'c', self::TABLE_ACCESS_CACHE, 'a',
98
				$query->expr()->andX(
0 ignored issues
show
Bug introduced by
$query->expr()->andX($qu...ryBuilder::PARAM_STR))) of type OCP\DB\QueryBuilder\ICompositeExpression is incompatible with the type string expected by parameter $condition of OCP\DB\QueryBuilder\IQueryBuilder::leftJoin(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

98
				/** @scrutinizer ignore-type */ $query->expr()->andX(
Loading history...
99
					$query->expr()->eq('c.id', 'a.collection_id'),
100
					$query->expr()->eq('a.user_id', $query->createNamedParameter($userId, IQueryBuilder::PARAM_STR))
101
				)
102
			)
103
			->where($query->expr()->eq('c.id', $query->createNamedParameter($id, IQueryBuilder::PARAM_INT)));
104
		$result = $query->execute();
105
		$row = $result->fetch();
106
		$result->closeCursor();
107
108
		if (!$row) {
109
			throw new CollectionException('Collection not found');
110
		}
111
112
		$access = $row['access'] === null ? null : (bool) $row['access'];
113
		if ($user instanceof IUser) {
114
			return new Collection($this, $this->connection, (int) $row['id'], (string) $row['name'], $user, $access);
115
		}
116
117
		return new Collection($this, $this->connection, (int) $row['id'], (string) $row['name'], $user, $access);
118
	}
119
120
	/**
121
	 * @param IUser $user
122
	 * @param string $filter
123
	 * @param int $limit
124
	 * @param int $start
125
	 * @return ICollection[]
126
	 * @since 16.0.0
127
	 */
128
	public function searchCollections(IUser $user, string $filter, int $limit = 50, int $start = 0): array {
129
		$query = $this->connection->getQueryBuilder();
130
		$userId = $user instanceof IUser ? $user->getUID() : '';
0 ignored issues
show
introduced by
$user is always a sub-type of OCP\IUser.
Loading history...
131
132
		$query->select('c.*', 'a.access')
133
			->from(self::TABLE_COLLECTIONS, 'c')
134
			->leftJoin(
135
				'c', self::TABLE_ACCESS_CACHE, 'a',
136
				$query->expr()->andX(
0 ignored issues
show
Bug introduced by
$query->expr()->andX($qu...ryBuilder::PARAM_STR))) of type OCP\DB\QueryBuilder\ICompositeExpression is incompatible with the type string expected by parameter $condition of OCP\DB\QueryBuilder\IQueryBuilder::leftJoin(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

136
				/** @scrutinizer ignore-type */ $query->expr()->andX(
Loading history...
137
					$query->expr()->eq('c.id', 'a.collection_id'),
138
					$query->expr()->eq('a.user_id', $query->createNamedParameter($userId, IQueryBuilder::PARAM_STR))
139
				)
140
			)
141
			->where($query->expr()->iLike('c.name', $query->createNamedParameter($filter, IQueryBuilder::PARAM_STR)))
142
			->andWhere($query->expr()->eq('a.access', $query->createNamedParameter(1, IQueryBuilder::PARAM_INT)))
143
			->orderBy('c.id')
144
			->setMaxResults($limit)
145
			->setFirstResult($start);
146
		$result = $query->execute();
147
		$collections = [];
148
149
		$foundResults = 0;
150
		while ($row = $result->fetch()) {
151
			$foundResults++;
152
			$access = $row['access'] === null ? null : (bool) $row['access'];
153
			$collection = new Collection($this, $this->connection, (int)$row['id'], (string)$row['name'], $user, $access);
154
			if ($collection->canAccess($user)) {
155
				$collections[] = $collection;
156
			}
157
		}
158
		$result->closeCursor();
159
160
		if (empty($collections) && $foundResults === $limit) {
161
			$this->searchCollections($user, $filter, $limit, $start + $limit);
162
		}
163
164
		return $collections;
165
	}
166
167
	/**
168
	 * @param string $name
169
	 * @return ICollection
170
	 * @since 16.0.0
171
	 */
172
	public function newCollection(string $name): ICollection {
173
		$query = $this->connection->getQueryBuilder();
174
		$query->insert(self::TABLE_COLLECTIONS)
175
			->values([
176
				'name' => $query->createNamedParameter($name),
177
			]);
178
		$query->execute();
179
180
		return new Collection($this, $this->connection, $query->getLastInsertId(), $name);
181
	}
182
183
	/**
184
	 * @param string $type
185
	 * @param string $id
186
	 * @return IResource
187
	 * @since 16.0.0
188
	 */
189
	public function createResource(string $type, string $id): IResource {
190
		return new Resource($this, $this->connection, $type, $id);
191
	}
192
193
	/**
194
	 * @param string $type
195
	 * @param string $id
196
	 * @param IUser|null $user
197
	 * @return IResource
198
	 * @throws ResourceException
199
	 * @since 16.0.0
200
	 */
201
	public function getResourceForUser(string $type, string $id, ?IUser $user): IResource {
202
		$query = $this->connection->getQueryBuilder();
203
		$userId = $user instanceof IUser ? $user->getUID() : '';
204
205
		$query->select('r.*', 'a.access')
206
			->from(self::TABLE_RESOURCES, 'r')
207
			->leftJoin(
208
				'r', self::TABLE_ACCESS_CACHE, 'a',
209
				$query->expr()->andX(
0 ignored issues
show
Bug introduced by
$query->expr()->andX($qu...ryBuilder::PARAM_STR))) of type OCP\DB\QueryBuilder\ICompositeExpression is incompatible with the type string expected by parameter $condition of OCP\DB\QueryBuilder\IQueryBuilder::leftJoin(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

209
				/** @scrutinizer ignore-type */ $query->expr()->andX(
Loading history...
210
					$query->expr()->eq('r.resource_id', 'a.resource_id'),
211
					$query->expr()->eq('r.resource_type', 'a.resource_type'),
212
					$query->expr()->eq('a.user_id', $query->createNamedParameter($userId, IQueryBuilder::PARAM_STR))
213
				)
214
			)
215
			->where($query->expr()->eq('r.resource_type', $query->createNamedParameter($type, IQueryBuilder::PARAM_STR)))
216
			->andWhere($query->expr()->eq('r.resource_id', $query->createNamedParameter($id, IQueryBuilder::PARAM_STR)));
217
		$result = $query->execute();
218
		$row = $result->fetch();
219
		$result->closeCursor();
220
221
		if (!$row) {
222
			throw new ResourceException('Resource not found');
223
		}
224
225
		$access = $row['access'] === null ? null : (bool) $row['access'];
226
		if ($user instanceof IUser) {
227
			return new Resource($this, $this->connection, $type, $id, $user, $access);
228
		}
229
230
		return new Resource($this, $this->connection, $type, $id, null, $access);
231
	}
232
233
	/**
234
	 * @param ICollection $collection
235
	 * @param IUser|null $user
236
	 * @return IResource[]
237
	 * @since 16.0.0
238
	 */
239
	public function getResourcesByCollectionForUser(ICollection $collection, ?IUser $user): array {
240
		$query = $this->connection->getQueryBuilder();
241
		$userId = $user instanceof IUser ? $user->getUID() : '';
242
243
		$query->select('r.*', 'a.access')
244
			->from(self::TABLE_RESOURCES, 'r')
245
			->leftJoin(
246
				'r', self::TABLE_ACCESS_CACHE, 'a',
247
				$query->expr()->andX(
0 ignored issues
show
Bug introduced by
$query->expr()->andX($qu...ryBuilder::PARAM_STR))) of type OCP\DB\QueryBuilder\ICompositeExpression is incompatible with the type string expected by parameter $condition of OCP\DB\QueryBuilder\IQueryBuilder::leftJoin(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

247
				/** @scrutinizer ignore-type */ $query->expr()->andX(
Loading history...
248
					$query->expr()->eq('r.resource_id', 'a.resource_id'),
249
					$query->expr()->eq('r.resource_type', 'a.resource_type'),
250
					$query->expr()->eq('a.user_id', $query->createNamedParameter($userId, IQueryBuilder::PARAM_STR))
251
				)
252
			)
253
			->where($query->expr()->eq('r.collection_id', $query->createNamedParameter($collection->getId(), IQueryBuilder::PARAM_INT)));
254
255
		$resources = [];
256
		$result = $query->execute();
257
		while ($row = $result->fetch()) {
258
			$access = $row['access'] === null ? null : (bool) $row['access'];
259
			$resources[] = new Resource($this, $this->connection, $row['resource_type'], $row['resource_id'], $user, $access);
260
		}
261
		$result->closeCursor();
262
263
		return $resources;
264
	}
265
266
	/**
267
	 * @return IProvider[]
268
	 * @since 16.0.0
269
	 */
270
	public function getProviders(): array {
271
		if (!empty($this->providers)) {
272
			foreach ($this->providers as $provider) {
273
				try {
274
					$this->providerInstances[] = \OC::$server->query($provider);
275
				} catch (QueryException $e) {
276
					$this->logger->logException($e, [
277
						'message' => 'Error when instantiating resource provider'
278
					]);
279
				}
280
			}
281
			$this->providers = [];
282
		}
283
284
		return $this->providerInstances;
285
	}
286
287
	/**
288
	 * Get the rich object data of a resource
289
	 *
290
	 * @param IResource $resource
291
	 * @return array
292
	 * @since 16.0.0
293
	 */
294
	public function getResourceRichObject(IResource $resource): array {
295
		foreach ($this->getProviders() as $provider) {
296
			if ($provider->getType() === $resource->getType()) {
297
				try {
298
					return $provider->getResourceRichObject($resource);
299
				} catch (ResourceException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
300
				}
301
			}
302
		}
303
304
		return [];
305
	}
306
307
	/**
308
	 * Can a user/guest access the collection
309
	 *
310
	 * @param IResource $resource
311
	 * @param IUser|null $user
312
	 * @return bool
313
	 * @since 16.0.0
314
	 */
315
	public function canAccessResource(IResource $resource, ?IUser $user): bool {
316
		$access = $this->checkAccessCacheForUserByResource($resource, $user);
317
		if (\is_bool($access)) {
318
			return $access;
319
		}
320
321
		$access = false;
322
		foreach ($this->getProviders() as $provider) {
323
			if ($provider->getType() === $resource->getType()) {
324
				try {
325
					if ($provider->canAccessResource($resource, $user)) {
326
						$access = true;
327
						break;
328
					}
329
				} catch (ResourceException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
330
				}
331
			}
332
		}
333
334
		$this->cacheAccessForResource($resource, $user, $access);
335
		return $access;
336
	}
337
338
	/**
339
	 * Can a user/guest access the collection
340
	 *
341
	 * @param ICollection $collection
342
	 * @param IUser|null $user
343
	 * @return bool
344
	 * @since 16.0.0
345
	 */
346
	public function canAccessCollection(ICollection $collection, ?IUser $user): bool {
347
		$access = $this->checkAccessCacheForUserByCollection($collection, $user);
348
		if (\is_bool($access)) {
349
			return $access;
350
		}
351
352
		$access = false;
353
		foreach ($collection->getResources() as $resource) {
354
			if ($resource->canAccess($user)) {
355
				$access = true;
356
				break;
357
			}
358
		}
359
360
		$this->cacheAccessForCollection($collection, $user, $access);
361
		return $access;
362
	}
363
364
	protected function checkAccessCacheForUserByResource(IResource $resource, ?IUser $user): ?bool {
365
		$query = $this->connection->getQueryBuilder();
366
		$userId = $user instanceof IUser ? $user->getUID() : '';
367
368
		$query->select('access')
369
			->from(self::TABLE_ACCESS_CACHE)
370
			->where($query->expr()->eq('resource_id', $query->createNamedParameter($resource->getId(), IQueryBuilder::PARAM_STR)))
371
			->andWhere($query->expr()->eq('resource_type', $query->createNamedParameter($resource->getType(), IQueryBuilder::PARAM_STR)))
372
			->andWhere($query->expr()->eq('user_id', $query->createNamedParameter($userId, IQueryBuilder::PARAM_STR)))
373
			->setMaxResults(1);
374
375
		$hasAccess = null;
376
		$result = $query->execute();
377
		if ($row = $result->fetch()) {
378
			$hasAccess = (bool) $row['access'];
379
		}
380
		$result->closeCursor();
381
382
		return $hasAccess;
383
	}
384
385
	protected function checkAccessCacheForUserByCollection(ICollection $collection, ?IUser $user): ?bool {
386
		$query = $this->connection->getQueryBuilder();
387
		$userId = $user instanceof IUser ? $user->getUID() : '';
388
389
		$query->select('access')
390
			->from(self::TABLE_ACCESS_CACHE)
391
			->where($query->expr()->eq('collection_id', $query->createNamedParameter($collection->getId(), IQueryBuilder::PARAM_INT)))
392
			->andWhere($query->expr()->eq('user_id', $query->createNamedParameter($userId, IQueryBuilder::PARAM_STR)))
393
			->setMaxResults(1);
394
395
		$hasAccess = null;
396
		$result = $query->execute();
397
		if ($row = $result->fetch()) {
398
			$hasAccess = (bool) $row['access'];
399
		}
400
		$result->closeCursor();
401
402
		return $hasAccess;
403
	}
404
405
	public function cacheAccessForResource(IResource $resource, ?IUser $user, bool $access): void {
406
		$query = $this->connection->getQueryBuilder();
407
		$userId = $user instanceof IUser ? $user->getUID() : '';
408
409
		$query->insert(self::TABLE_ACCESS_CACHE)
410
			->values([
411
				'user_id' => $query->createNamedParameter($userId),
412
				'resource_id' => $query->createNamedParameter($resource->getId()),
413
				'resource_type' => $query->createNamedParameter($resource->getType()),
414
				'access' => $query->createNamedParameter($access),
415
			]);
416
		try {
417
			$query->execute();
418
		} catch (UniqueConstraintViolationException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
419
		}
420
	}
421
422
	public function cacheAccessForCollection(ICollection $collection, ?IUser $user, bool $access): void {
423
		$query = $this->connection->getQueryBuilder();
424
		$userId = $user instanceof IUser ? $user->getUID() : '';
425
426
		$query->insert(self::TABLE_ACCESS_CACHE)
427
			->values([
428
				'user_id' => $query->createNamedParameter($userId),
429
				'collection_id' => $query->createNamedParameter($collection->getId()),
430
				'access' => $query->createNamedParameter($access),
431
			]);
432
		try {
433
			$query->execute();
434
		} catch (UniqueConstraintViolationException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
435
		}
436
	}
437
438
	public function invalidateAccessCacheForUser(?IUser $user): void {
439
		$query = $this->connection->getQueryBuilder();
440
		$userId = $user instanceof IUser ? $user->getUID() : '';
441
442
		$query->delete(self::TABLE_ACCESS_CACHE)
443
			->where($query->expr()->eq('user_id', $query->createNamedParameter($userId)));
444
		$query->execute();
445
	}
446
447
	public function invalidateAccessCacheForResource(IResource $resource): void {
448
		$query = $this->connection->getQueryBuilder();
449
450
		$query->delete(self::TABLE_ACCESS_CACHE)
451
			->where($query->expr()->eq('resource_id', $query->createNamedParameter($resource->getId())))
452
			->andWhere($query->expr()->eq('resource_type', $query->createNamedParameter($resource->getType(), IQueryBuilder::PARAM_STR)));
453
		$query->execute();
454
455
		foreach ($resource->getCollections() as $collection) {
456
			$this->invalidateAccessCacheForCollection($collection);
457
		}
458
	}
459
460
	public function invalidateAccessCacheForCollection(ICollection $collection): void {
461
		$query = $this->connection->getQueryBuilder();
462
463
		$query->delete(self::TABLE_ACCESS_CACHE)
464
			->where($query->expr()->eq('collection_id', $query->createNamedParameter($collection->getId())));
465
		$query->execute();
466
	}
467
468
	public function invalidateAccessCacheForProvider(IProvider $provider): void {
469
		$query = $this->connection->getQueryBuilder();
470
471
		$query->delete(self::TABLE_ACCESS_CACHE)
472
			->where($query->expr()->eq('resource_type', $query->createNamedParameter($provider->getType(), IQueryBuilder::PARAM_STR)));
473
		$query->execute();
474
	}
475
476
	public function invalidateAccessCacheForResourceByUser(IResource $resource, ?IUser $user): void {
477
		$query = $this->connection->getQueryBuilder();
478
		$userId = $user instanceof IUser ? $user->getUID() : '';
479
480
		$query->delete(self::TABLE_ACCESS_CACHE)
481
			->where($query->expr()->eq('resource_id', $query->createNamedParameter($resource->getId())))
482
			->andWhere($query->expr()->eq('user_id', $query->createNamedParameter($userId)));
483
		$query->execute();
484
485
		foreach ($resource->getCollections() as $collection) {
486
			$this->invalidateAccessCacheForCollectionByUser($collection, $user);
487
		}
488
	}
489
490
	protected function invalidateAccessCacheForCollectionByUser(ICollection $collection, ?IUser $user): void {
491
		$query = $this->connection->getQueryBuilder();
492
		$userId = $user instanceof IUser ? $user->getUID() : '';
493
494
		$query->delete(self::TABLE_ACCESS_CACHE)
495
			->where($query->expr()->eq('collection_id', $query->createNamedParameter($collection->getId())))
496
			->andWhere($query->expr()->eq('user_id', $query->createNamedParameter($userId)));
497
		$query->execute();
498
	}
499
500
	public function invalidateAccessCacheForProviderByUser(IProvider $provider, ?IUser $user): void {
501
		$query = $this->connection->getQueryBuilder();
502
		$userId = $user instanceof IUser ? $user->getUID() : '';
503
504
		$query->delete(self::TABLE_ACCESS_CACHE)
505
			->where($query->expr()->eq('resource_type', $query->createNamedParameter($provider->getType(), IQueryBuilder::PARAM_STR)))
506
			->andWhere($query->expr()->eq('user_id', $query->createNamedParameter($userId)));
507
		$query->execute();
508
	}
509
510
	/**
511
	 * @param string $provider
512
	 */
513
	public function registerResourceProvider(string $provider): void {
514
		$this->providers[] = $provider;
515
	}
516
517
	/**
518
	 * Get the resource type of the provider
519
	 *
520
	 * @return string
521
	 * @since 16.0.0
522
	 */
523
	public function getType(): string {
524
		return '';
525
	}
526
}
527