Passed
Push — master ( 346770...2398d1 )
by Roeland
10:40 queued 35s
created

Manager::searchCollections()   B

Complexity

Conditions 7
Paths 20

Size

Total Lines 37
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
eloc 27
nc 20
nop 4
dl 0
loc 37
rs 8.5546
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 display name of a resource
289
	 *
290
	 * @param IResource $resource
291
	 * @return string
292
	 * @since 16.0.0
293
	 */
294
	public function getName(IResource $resource): string {
295
		foreach ($this->getProviders() as $provider) {
296
			if ($provider->getType() === $resource->getType()) {
297
				try {
298
					return $provider->getName($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
	 *
309
	 * @param IResource $resource
310
	 * @return string
311
	 */
312
	public function getIconClass(IResource $resource): string {
313
		foreach ($this->getProviders() as $provider) {
314
			if ($provider->getType() === $resource->getType()) {
315
				try {
316
					return $provider->getIconClass($resource);
317
				} catch (ResourceException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
318
				}
319
			}
320
		}
321
322
		return '';
323
	}
324
325
	/**
326
	 * Can a user/guest access the collection
327
	 *
328
	 * @param IResource $resource
329
	 * @param IUser|null $user
330
	 * @return bool
331
	 * @since 16.0.0
332
	 */
333
	public function canAccessResource(IResource $resource, ?IUser $user): bool {
334
		$access = $this->checkAccessCacheForUserByResource($resource, $user);
335
		if (\is_bool($access)) {
336
			return $access;
337
		}
338
339
		$access = false;
340
		foreach ($this->getProviders() as $provider) {
341
			if ($provider->getType() === $resource->getType()) {
342
				try {
343
					if ($provider->canAccessResource($resource, $user)) {
344
						$access = true;
345
						break;
346
					}
347
				} catch (ResourceException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
348
				}
349
			}
350
		}
351
352
		$this->cacheAccessForResource($resource, $user, $access);
353
		return $access;
354
	}
355
356
	/**
357
	 * Can a user/guest access the collection
358
	 *
359
	 * @param ICollection $collection
360
	 * @param IUser|null $user
361
	 * @return bool
362
	 * @since 16.0.0
363
	 */
364
	public function canAccessCollection(ICollection $collection, ?IUser $user): bool {
365
		$access = $this->checkAccessCacheForUserByCollection($collection, $user);
366
		if (\is_bool($access)) {
367
			return $access;
368
		}
369
370
		$access = false;
371
		foreach ($collection->getResources() as $resource) {
372
			if ($resource->canAccess($user)) {
373
				$access = true;
374
				break;
375
			}
376
		}
377
378
		$this->cacheAccessForCollection($collection, $user, $access);
379
		return $access;
380
	}
381
382
	protected function checkAccessCacheForUserByResource(IResource $resource, ?IUser $user): ?bool {
383
		$query = $this->connection->getQueryBuilder();
384
		$userId = $user instanceof IUser ? $user->getUID() : '';
385
386
		$query->select('access')
387
			->from(self::TABLE_ACCESS_CACHE)
388
			->where($query->expr()->eq('resource_id', $query->createNamedParameter($resource->getId(), IQueryBuilder::PARAM_STR)))
389
			->andWhere($query->expr()->eq('resource_type', $query->createNamedParameter($resource->getType(), IQueryBuilder::PARAM_STR)))
390
			->andWhere($query->expr()->eq('user_id', $query->createNamedParameter($userId, IQueryBuilder::PARAM_STR)))
391
			->setMaxResults(1);
392
393
		$hasAccess = null;
394
		$result = $query->execute();
395
		if ($row = $result->fetch()) {
396
			$hasAccess = (bool) $row['access'];
397
		}
398
		$result->closeCursor();
399
400
		return $hasAccess;
401
	}
402
403
	protected function checkAccessCacheForUserByCollection(ICollection $collection, ?IUser $user): ?bool {
404
		$query = $this->connection->getQueryBuilder();
405
		$userId = $user instanceof IUser ? $user->getUID() : '';
406
407
		$query->select('access')
408
			->from(self::TABLE_ACCESS_CACHE)
409
			->where($query->expr()->eq('collection_id', $query->createNamedParameter($collection->getId(), IQueryBuilder::PARAM_INT)))
410
			->andWhere($query->expr()->eq('user_id', $query->createNamedParameter($userId, IQueryBuilder::PARAM_STR)))
411
			->setMaxResults(1);
412
413
		$hasAccess = null;
414
		$result = $query->execute();
415
		if ($row = $result->fetch()) {
416
			$hasAccess = (bool) $row['access'];
417
		}
418
		$result->closeCursor();
419
420
		return $hasAccess;
421
	}
422
423
	public function cacheAccessForResource(IResource $resource, ?IUser $user, bool $access): void {
424
		$query = $this->connection->getQueryBuilder();
425
		$userId = $user instanceof IUser ? $user->getUID() : '';
426
427
		$query->insert(self::TABLE_ACCESS_CACHE)
428
			->values([
429
				'user_id' => $query->createNamedParameter($userId),
430
				'resource_id' => $query->createNamedParameter($resource->getId()),
431
				'resource_type' => $query->createNamedParameter($resource->getType()),
432
				'access' => $query->createNamedParameter($access),
433
			]);
434
		try {
435
			$query->execute();
436
		} catch (UniqueConstraintViolationException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
437
		}
438
	}
439
440
	public function cacheAccessForCollection(ICollection $collection, ?IUser $user, bool $access): void {
441
		$query = $this->connection->getQueryBuilder();
442
		$userId = $user instanceof IUser ? $user->getUID() : '';
443
444
		$query->insert(self::TABLE_ACCESS_CACHE)
445
			->values([
446
				'user_id' => $query->createNamedParameter($userId),
447
				'collection_id' => $query->createNamedParameter($collection->getId()),
448
				'access' => $query->createNamedParameter($access),
449
			]);
450
		try {
451
			$query->execute();
452
		} catch (UniqueConstraintViolationException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
453
		}
454
	}
455
456
	public function invalidateAccessCacheForUser(?IUser $user): void {
457
		$query = $this->connection->getQueryBuilder();
458
		$userId = $user instanceof IUser ? $user->getUID() : '';
459
460
		$query->delete(self::TABLE_ACCESS_CACHE)
461
			->where($query->expr()->eq('user_id', $query->createNamedParameter($userId)));
462
		$query->execute();
463
	}
464
465
	public function invalidateAccessCacheForResource(IResource $resource): void {
466
		$query = $this->connection->getQueryBuilder();
467
468
		$query->delete(self::TABLE_ACCESS_CACHE)
469
			->where($query->expr()->eq('resource_id', $query->createNamedParameter($resource->getId())))
470
			->andWhere($query->expr()->eq('resource_type', $query->createNamedParameter($resource->getType(), IQueryBuilder::PARAM_STR)));
471
		$query->execute();
472
473
		foreach ($resource->getCollections() as $collection) {
474
			$this->invalidateAccessCacheForCollection($collection);
475
		}
476
	}
477
478
	public function invalidateAccessCacheForCollection(ICollection $collection): void {
479
		$query = $this->connection->getQueryBuilder();
480
481
		$query->delete(self::TABLE_ACCESS_CACHE)
482
			->where($query->expr()->eq('collection_id', $query->createNamedParameter($collection->getId())));
483
		$query->execute();
484
	}
485
486
	public function invalidateAccessCacheForProvider(IProvider $provider): void {
487
		$query = $this->connection->getQueryBuilder();
488
489
		$query->delete(self::TABLE_ACCESS_CACHE)
490
			->where($query->expr()->eq('resource_type', $query->createNamedParameter($provider->getType(), IQueryBuilder::PARAM_STR)));
491
		$query->execute();
492
	}
493
494
	public function invalidateAccessCacheForResourceByUser(IResource $resource, ?IUser $user): void {
495
		$query = $this->connection->getQueryBuilder();
496
		$userId = $user instanceof IUser ? $user->getUID() : '';
497
498
		$query->delete(self::TABLE_ACCESS_CACHE)
499
			->where($query->expr()->eq('resource_id', $query->createNamedParameter($resource->getId())))
500
			->andWhere($query->expr()->eq('user_id', $query->createNamedParameter($userId)));
501
		$query->execute();
502
503
		foreach ($resource->getCollections() as $collection) {
504
			$this->invalidateAccessCacheForCollectionByUser($collection, $user);
505
		}
506
	}
507
508
	protected function invalidateAccessCacheForCollectionByUser(ICollection $collection, ?IUser $user): void {
509
		$query = $this->connection->getQueryBuilder();
510
		$userId = $user instanceof IUser ? $user->getUID() : '';
511
512
		$query->delete(self::TABLE_ACCESS_CACHE)
513
			->where($query->expr()->eq('collection_id', $query->createNamedParameter($collection->getId())))
514
			->andWhere($query->expr()->eq('user_id', $query->createNamedParameter($userId)));
515
		$query->execute();
516
	}
517
518
	public function invalidateAccessCacheForProviderByUser(IProvider $provider, ?IUser $user): void {
519
		$query = $this->connection->getQueryBuilder();
520
		$userId = $user instanceof IUser ? $user->getUID() : '';
521
522
		$query->delete(self::TABLE_ACCESS_CACHE)
523
			->where($query->expr()->eq('resource_type', $query->createNamedParameter($provider->getType(), IQueryBuilder::PARAM_STR)))
524
			->andWhere($query->expr()->eq('user_id', $query->createNamedParameter($userId)));
525
		$query->execute();
526
	}
527
528
	/**
529
	 * @param string $provider
530
	 */
531
	public function registerResourceProvider(string $provider): void {
532
		$this->providers[] = $provider;
533
	}
534
535
	/**
536
	 * Get the resource type of the provider
537
	 *
538
	 * @return string
539
	 * @since 16.0.0
540
	 */
541
	public function getType(): string {
542
		return '';
543
	}
544
545
	/**
546
	 * Get the link to a resource
547
	 *
548
	 * @param IResource $resource
549
	 * @return string
550
	 * @since 16.0.0
551
	 */
552
	public function getLink(IResource $resource): string {
553
		foreach ($this->getProviders() as $provider) {
554
			if ($provider->getType() === $resource->getType()) {
555
				try {
556
					return $provider->getLink($resource);
557
				} catch (ResourceException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
558
				}
559
			}
560
		}
561
562
		return '';
563
	}
564
}
565