Passed
Push — master ( 182517...f88e95 )
by Roeland
11:13 queued 11s
created

Manager::invalidateAccessCacheForAllCollections()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 4
c 0
b 0
f 0
nc 1
nop 0
dl 0
loc 6
rs 10
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()->eq('a.access', $query->createNamedParameter(1, IQueryBuilder::PARAM_INT)))
142
			->orderBy('c.id')
143
			->setMaxResults($limit)
144
			->setFirstResult($start);
145
146
		if ($filter !== '') {
147
			$query->where($query->expr()->iLike('c.name', $query->createNamedParameter('%' . $this->connection->escapeLikeParameter($filter) . '%')));
148
		}
149
150
		$result = $query->execute();
151
		$collections = [];
152
153
		$foundResults = 0;
154
		while ($row = $result->fetch()) {
155
			$foundResults++;
156
			$access = $row['access'] === null ? null : (bool) $row['access'];
157
			$collection = new Collection($this, $this->connection, (int)$row['id'], (string)$row['name'], $user, $access);
158
			if ($collection->canAccess($user)) {
159
				$collections[] = $collection;
160
			}
161
		}
162
		$result->closeCursor();
163
164
		if (empty($collections) && $foundResults === $limit) {
165
			return $this->searchCollections($user, $filter, $limit, $start + $limit);
166
		}
167
168
		return $collections;
169
	}
170
171
	/**
172
	 * @param string $name
173
	 * @return ICollection
174
	 * @since 16.0.0
175
	 */
176
	public function newCollection(string $name): ICollection {
177
		$query = $this->connection->getQueryBuilder();
178
		$query->insert(self::TABLE_COLLECTIONS)
179
			->values([
180
				'name' => $query->createNamedParameter($name),
181
			]);
182
		$query->execute();
183
184
		return new Collection($this, $this->connection, $query->getLastInsertId(), $name);
185
	}
186
187
	/**
188
	 * @param string $type
189
	 * @param string $id
190
	 * @return IResource
191
	 * @since 16.0.0
192
	 */
193
	public function createResource(string $type, string $id): IResource {
194
		return new Resource($this, $this->connection, $type, $id);
195
	}
196
197
	/**
198
	 * @param string $type
199
	 * @param string $id
200
	 * @param IUser|null $user
201
	 * @return IResource
202
	 * @throws ResourceException
203
	 * @since 16.0.0
204
	 */
205
	public function getResourceForUser(string $type, string $id, ?IUser $user): IResource {
206
		$query = $this->connection->getQueryBuilder();
207
		$userId = $user instanceof IUser ? $user->getUID() : '';
208
209
		$query->select('r.*', 'a.access')
210
			->from(self::TABLE_RESOURCES, 'r')
211
			->leftJoin(
212
				'r', self::TABLE_ACCESS_CACHE, 'a',
213
				$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

213
				/** @scrutinizer ignore-type */ $query->expr()->andX(
Loading history...
214
					$query->expr()->eq('r.resource_id', 'a.resource_id'),
215
					$query->expr()->eq('r.resource_type', 'a.resource_type'),
216
					$query->expr()->eq('a.user_id', $query->createNamedParameter($userId, IQueryBuilder::PARAM_STR))
217
				)
218
			)
219
			->where($query->expr()->eq('r.resource_type', $query->createNamedParameter($type, IQueryBuilder::PARAM_STR)))
220
			->andWhere($query->expr()->eq('r.resource_id', $query->createNamedParameter($id, IQueryBuilder::PARAM_STR)));
221
		$result = $query->execute();
222
		$row = $result->fetch();
223
		$result->closeCursor();
224
225
		if (!$row) {
226
			throw new ResourceException('Resource not found');
227
		}
228
229
		$access = $row['access'] === null ? null : (bool) $row['access'];
230
		if ($user instanceof IUser) {
231
			return new Resource($this, $this->connection, $type, $id, $user, $access);
232
		}
233
234
		return new Resource($this, $this->connection, $type, $id, null, $access);
235
	}
236
237
	/**
238
	 * @param ICollection $collection
239
	 * @param IUser|null $user
240
	 * @return IResource[]
241
	 * @since 16.0.0
242
	 */
243
	public function getResourcesByCollectionForUser(ICollection $collection, ?IUser $user): array {
244
		$query = $this->connection->getQueryBuilder();
245
		$userId = $user instanceof IUser ? $user->getUID() : '';
246
247
		$query->select('r.*', 'a.access')
248
			->from(self::TABLE_RESOURCES, 'r')
249
			->leftJoin(
250
				'r', self::TABLE_ACCESS_CACHE, 'a',
251
				$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

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