Manager   F
last analyzed

Complexity

Total Complexity 82

Size/Duplication

Total Lines 714
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 344
dl 0
loc 714
rs 2
c 0
b 0
f 0
wmc 82

28 Methods

Rating   Name   Duplication   Size   Complexity  
A getAllConfiguredEvents() 0 33 3
A getRuleMatcher() 0 7 1
A __construct() 0 21 1
B getChecks() 0 35 6
A formatOperation() 0 16 2
A updateOperation() 0 40 4
B getAllOperations() 0 41 7
A getOperation() 0 14 2
C validateOperation() 0 55 13
A getAllConfiguredScopesForOperation() 0 36 5
A addCheck() 0 25 2
A addScope() 0 10 1
A getEntitiesList() 0 5 1
A getCheckList() 0 5 1
A deleteOperation() 0 29 5
A getOperatorList() 0 5 1
A registerOperation() 0 2 1
B validateEvents() 0 28 7
A registerCheck() 0 2 1
A insertOperation() 0 23 1
A getBuildInChecks() 0 16 2
A isUserScopeEnabled() 0 2 1
A canModify() 0 26 4
A getOperations() 0 5 2
A getBuildInEntities() 0 8 2
A registerEntity() 0 2 1
A getBuildInOperators() 0 8 2
A addOperation() 0 29 3

How to fix   Complexity   

Complex Class

Complex classes like Manager often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Manager, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * @copyright Copyright (c) 2016 Morris Jobke <[email protected]>
4
 *
5
 * @author Arthur Schiwon <[email protected]>
6
 * @author blizzz <[email protected]>
7
 * @author Christoph Wurst <[email protected]>
8
 * @author Daniel Kesselberg <[email protected]>
9
 * @author Joas Schilling <[email protected]>
10
 * @author Julius Härtl <[email protected]>
11
 * @author Morris Jobke <[email protected]>
12
 * @author Roeland Jago Douma <[email protected]>
13
 *
14
 * @license GNU AGPL version 3 or any later version
15
 *
16
 * This program is free software: you can redistribute it and/or modify
17
 * it under the terms of the GNU Affero General Public License as
18
 * published by the Free Software Foundation, either version 3 of the
19
 * License, or (at your option) any later version.
20
 *
21
 * This program is distributed in the hope that it will be useful,
22
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
23
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
24
 * GNU Affero General Public License for more details.
25
 *
26
 * You should have received a copy of the GNU Affero General Public License
27
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
28
 *
29
 */
30
namespace OCA\WorkflowEngine;
31
32
use Doctrine\DBAL\Exception;
33
use OCP\Cache\CappedMemoryCache;
34
use OCA\WorkflowEngine\AppInfo\Application;
35
use OCA\WorkflowEngine\Check\FileMimeType;
36
use OCA\WorkflowEngine\Check\FileName;
37
use OCA\WorkflowEngine\Check\FileSize;
38
use OCA\WorkflowEngine\Check\FileSystemTags;
39
use OCA\WorkflowEngine\Check\RequestRemoteAddress;
40
use OCA\WorkflowEngine\Check\RequestTime;
41
use OCA\WorkflowEngine\Check\RequestURL;
42
use OCA\WorkflowEngine\Check\RequestUserAgent;
43
use OCA\WorkflowEngine\Check\UserGroupMembership;
44
use OCA\WorkflowEngine\Entity\File;
45
use OCA\WorkflowEngine\Helper\ScopeContext;
46
use OCA\WorkflowEngine\Service\Logger;
47
use OCA\WorkflowEngine\Service\RuleMatcher;
48
use OCP\AppFramework\QueryException;
49
use OCP\DB\QueryBuilder\IQueryBuilder;
50
use OCP\EventDispatcher\IEventDispatcher;
51
use OCP\Files\Storage\IStorage;
52
use OCP\ICacheFactory;
53
use OCP\IConfig;
54
use OCP\IDBConnection;
55
use OCP\IL10N;
56
use OCP\ILogger;
57
use OCP\IServerContainer;
58
use OCP\IUserSession;
59
use OCP\WorkflowEngine\Events\RegisterChecksEvent;
60
use OCP\WorkflowEngine\Events\RegisterEntitiesEvent;
61
use OCP\WorkflowEngine\Events\RegisterOperationsEvent;
62
use OCP\WorkflowEngine\ICheck;
63
use OCP\WorkflowEngine\IComplexOperation;
64
use OCP\WorkflowEngine\IEntity;
65
use OCP\WorkflowEngine\IEntityEvent;
66
use OCP\WorkflowEngine\IManager;
67
use OCP\WorkflowEngine\IOperation;
68
use OCP\WorkflowEngine\IRuleMatcher;
69
use Symfony\Component\EventDispatcher\EventDispatcherInterface as LegacyDispatcher;
70
use Symfony\Component\EventDispatcher\GenericEvent;
71
72
class Manager implements IManager {
73
	/** @var IStorage */
74
	protected $storage;
75
76
	/** @var string */
77
	protected $path;
78
79
	/** @var object */
80
	protected $entity;
81
82
	/** @var array[] */
83
	protected $operations = [];
84
85
	/** @var array[] */
86
	protected $checks = [];
87
88
	/** @var IDBConnection */
89
	protected $connection;
90
91
	/** @var IServerContainer|\OC\Server */
92
	protected $container;
93
94
	/** @var IL10N */
95
	protected $l;
96
97
	/** @var LegacyDispatcher */
98
	protected $legacyEventDispatcher;
99
100
	/** @var IEntity[] */
101
	protected $registeredEntities = [];
102
103
	/** @var IOperation[] */
104
	protected $registeredOperators = [];
105
106
	/** @var ICheck[] */
107
	protected $registeredChecks = [];
108
109
	/** @var ILogger */
110
	protected $logger;
111
112
	/** @var CappedMemoryCache<int[]> */
113
	protected CappedMemoryCache $operationsByScope;
114
115
	/** @var IUserSession */
116
	protected $session;
117
118
	/** @var IEventDispatcher */
119
	private $dispatcher;
120
121
	/** @var IConfig */
122
	private $config;
123
	private ICacheFactory $cacheFactory;
124
125
	public function __construct(
126
		IDBConnection $connection,
127
		IServerContainer $container,
128
		IL10N $l,
129
		LegacyDispatcher $eventDispatcher,
130
		ILogger $logger,
131
		IUserSession $session,
132
		IEventDispatcher $dispatcher,
133
		IConfig $config,
134
		ICacheFactory $cacheFactory,
135
	) {
136
		$this->connection = $connection;
137
		$this->container = $container;
138
		$this->l = $l;
139
		$this->legacyEventDispatcher = $eventDispatcher;
140
		$this->logger = $logger;
141
		$this->operationsByScope = new CappedMemoryCache(64);
142
		$this->session = $session;
143
		$this->dispatcher = $dispatcher;
144
		$this->config = $config;
145
		$this->cacheFactory = $cacheFactory;
146
	}
147
148
	public function getRuleMatcher(): IRuleMatcher {
149
		return new RuleMatcher(
150
			$this->session,
151
			$this->container,
152
			$this->l,
153
			$this,
154
			$this->container->query(Logger::class)
155
		);
156
	}
157
158
	public function getAllConfiguredEvents() {
159
		$cache = $this->cacheFactory->createDistributed('flow');
160
		$cached = $cache->get('events');
161
		if ($cached !== null) {
162
			return $cached;
163
		}
164
165
		$query = $this->connection->getQueryBuilder();
166
167
		$query->select('class', 'entity')
168
			->selectAlias($query->expr()->castColumn('events', IQueryBuilder::PARAM_STR), 'events')
169
			->from('flow_operations')
170
			->where($query->expr()->neq('events', $query->createNamedParameter('[]'), IQueryBuilder::PARAM_STR))
171
			->groupBy('class', 'entity', $query->expr()->castColumn('events', IQueryBuilder::PARAM_STR));
172
173
		$result = $query->execute();
174
		$operations = [];
175
		while ($row = $result->fetch()) {
176
			$eventNames = \json_decode($row['events']);
177
178
			$operation = $row['class'];
179
			$entity = $row['entity'];
180
181
			$operations[$operation] = $operations[$row['class']] ?? [];
182
			$operations[$operation][$entity] = $operations[$operation][$entity] ?? [];
183
184
			$operations[$operation][$entity] = array_unique(array_merge($operations[$operation][$entity], $eventNames ?? []));
185
		}
186
		$result->closeCursor();
187
188
		$cache->set('events', $operations, 3600);
189
190
		return $operations;
191
	}
192
193
	/**
194
	 * @param string $operationClass
195
	 * @return ScopeContext[]
196
	 */
197
	public function getAllConfiguredScopesForOperation(string $operationClass): array {
198
		static $scopesByOperation = [];
199
		if (isset($scopesByOperation[$operationClass])) {
200
			return $scopesByOperation[$operationClass];
201
		}
202
203
		try {
204
			/** @var IOperation $operation */
205
			$operation = $this->container->query($operationClass);
206
		} catch (QueryException $e) {
207
			return [];
208
		}
209
210
		$query = $this->connection->getQueryBuilder();
211
212
		$query->selectDistinct('s.type')
213
			->addSelect('s.value')
214
			->from('flow_operations', 'o')
215
			->leftJoin('o', 'flow_operations_scope', 's', $query->expr()->eq('o.id', 's.operation_id'))
216
			->where($query->expr()->eq('o.class', $query->createParameter('operationClass')));
217
218
		$query->setParameters(['operationClass' => $operationClass]);
219
		$result = $query->execute();
220
221
		$scopesByOperation[$operationClass] = [];
222
		while ($row = $result->fetch()) {
223
			$scope = new ScopeContext($row['type'], $row['value']);
224
225
			if (!$operation->isAvailableForScope((int) $row['type'])) {
226
				continue;
227
			}
228
229
			$scopesByOperation[$operationClass][$scope->getHash()] = $scope;
230
		}
231
232
		return $scopesByOperation[$operationClass];
233
	}
234
235
	public function getAllOperations(ScopeContext $scopeContext): array {
236
		if (isset($this->operations[$scopeContext->getHash()])) {
237
			return $this->operations[$scopeContext->getHash()];
238
		}
239
240
		$query = $this->connection->getQueryBuilder();
241
242
		$query->select('o.*')
243
			->selectAlias('s.type', 'scope_type')
244
			->selectAlias('s.value', 'scope_actor_id')
245
			->from('flow_operations', 'o')
246
			->leftJoin('o', 'flow_operations_scope', 's', $query->expr()->eq('o.id', 's.operation_id'))
247
			->where($query->expr()->eq('s.type', $query->createParameter('scope')));
248
249
		if ($scopeContext->getScope() === IManager::SCOPE_USER) {
250
			$query->andWhere($query->expr()->eq('s.value', $query->createParameter('scopeId')));
251
		}
252
253
		$query->setParameters(['scope' => $scopeContext->getScope(), 'scopeId' => $scopeContext->getScopeId()]);
254
		$result = $query->execute();
255
256
		$this->operations[$scopeContext->getHash()] = [];
257
		while ($row = $result->fetch()) {
258
			try {
259
				/** @var IOperation $operation */
260
				$operation = $this->container->query($row['class']);
261
			} catch (QueryException $e) {
262
				continue;
263
			}
264
265
			if (!$operation->isAvailableForScope((int) $row['scope_type'])) {
266
				continue;
267
			}
268
269
			if (!isset($this->operations[$scopeContext->getHash()][$row['class']])) {
270
				$this->operations[$scopeContext->getHash()][$row['class']] = [];
271
			}
272
			$this->operations[$scopeContext->getHash()][$row['class']][] = $row;
273
		}
274
275
		return $this->operations[$scopeContext->getHash()];
276
	}
277
278
	public function getOperations(string $class, ScopeContext $scopeContext): array {
279
		if (!isset($this->operations[$scopeContext->getHash()])) {
280
			$this->getAllOperations($scopeContext);
281
		}
282
		return $this->operations[$scopeContext->getHash()][$class] ?? [];
283
	}
284
285
	/**
286
	 * @param int $id
287
	 * @return array
288
	 * @throws \UnexpectedValueException
289
	 */
290
	protected function getOperation($id) {
291
		$query = $this->connection->getQueryBuilder();
292
		$query->select('*')
293
			->from('flow_operations')
294
			->where($query->expr()->eq('id', $query->createNamedParameter($id)));
295
		$result = $query->execute();
296
		$row = $result->fetch();
297
		$result->closeCursor();
298
299
		if ($row) {
300
			return $row;
301
		}
302
303
		throw new \UnexpectedValueException($this->l->t('Operation #%s does not exist', [$id]));
304
	}
305
306
	protected function insertOperation(
307
		string $class,
308
		string $name,
309
		array $checkIds,
310
		string $operation,
311
		string $entity,
312
		array $events
313
	): int {
314
		$query = $this->connection->getQueryBuilder();
315
		$query->insert('flow_operations')
316
			->values([
317
				'class' => $query->createNamedParameter($class),
318
				'name' => $query->createNamedParameter($name),
319
				'checks' => $query->createNamedParameter(json_encode(array_unique($checkIds))),
320
				'operation' => $query->createNamedParameter($operation),
321
				'entity' => $query->createNamedParameter($entity),
322
				'events' => $query->createNamedParameter(json_encode($events))
323
			]);
324
		$query->execute();
325
326
		$this->cacheFactory->createDistributed('flow')->remove('events');
327
328
		return $query->getLastInsertId();
329
	}
330
331
	/**
332
	 * @param string $class
333
	 * @param string $name
334
	 * @param array[] $checks
335
	 * @param string $operation
336
	 * @return array The added operation
337
	 * @throws \UnexpectedValueException
338
	 * @throw Exception
339
	 */
340
	public function addOperation(
341
		string $class,
342
		string $name,
343
		array $checks,
344
		string $operation,
345
		ScopeContext $scope,
346
		string $entity,
347
		array $events
348
	) {
349
		$this->validateOperation($class, $name, $checks, $operation, $scope, $entity, $events);
350
351
		$this->connection->beginTransaction();
352
353
		try {
354
			$checkIds = [];
355
			foreach ($checks as $check) {
356
				$checkIds[] = $this->addCheck($check['class'], $check['operator'], $check['value']);
357
			}
358
359
			$id = $this->insertOperation($class, $name, $checkIds, $operation, $entity, $events);
360
			$this->addScope($id, $scope);
361
362
			$this->connection->commit();
363
		} catch (Exception $e) {
364
			$this->connection->rollBack();
365
			throw $e;
366
		}
367
368
		return $this->getOperation($id);
369
	}
370
371
	protected function canModify(int $id, ScopeContext $scopeContext):bool {
372
		if (isset($this->operationsByScope[$scopeContext->getHash()])) {
373
			return in_array($id, $this->operationsByScope[$scopeContext->getHash()], true);
0 ignored issues
show
Bug introduced by
$this->operationsByScope...copeContext->getHash()] of type OCP\Cache\T is incompatible with the type array expected by parameter $haystack of in_array(). ( Ignorable by Annotation )

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

373
			return in_array($id, /** @scrutinizer ignore-type */ $this->operationsByScope[$scopeContext->getHash()], true);
Loading history...
374
		}
375
376
		$qb = $this->connection->getQueryBuilder();
377
		$qb = $qb->select('o.id')
378
			->from('flow_operations', 'o')
379
			->leftJoin('o', 'flow_operations_scope', 's', $qb->expr()->eq('o.id', 's.operation_id'))
380
			->where($qb->expr()->eq('s.type', $qb->createParameter('scope')));
381
382
		if ($scopeContext->getScope() !== IManager::SCOPE_ADMIN) {
383
			$qb->where($qb->expr()->eq('s.value', $qb->createParameter('scopeId')));
384
		}
385
386
		$qb->setParameters(['scope' => $scopeContext->getScope(), 'scopeId' => $scopeContext->getScopeId()]);
387
		$result = $qb->execute();
388
389
		$operations = [];
390
		while (($opId = $result->fetchOne()) !== false) {
391
			$operations[] = (int)$opId;
392
		}
393
		$this->operationsByScope[$scopeContext->getHash()] = $operations;
394
		$result->closeCursor();
395
396
		return in_array($id, $this->operationsByScope[$scopeContext->getHash()], true);
397
	}
398
399
	/**
400
	 * @param int $id
401
	 * @param string $name
402
	 * @param array[] $checks
403
	 * @param string $operation
404
	 * @return array The updated operation
405
	 * @throws \UnexpectedValueException
406
	 * @throws \DomainException
407
	 * @throws Exception
408
	 */
409
	public function updateOperation(
410
		int $id,
411
		string $name,
412
		array $checks,
413
		string $operation,
414
		ScopeContext $scopeContext,
415
		string $entity,
416
		array $events
417
	): array {
418
		if (!$this->canModify($id, $scopeContext)) {
419
			throw new \DomainException('Target operation not within scope');
420
		};
421
		$row = $this->getOperation($id);
422
		$this->validateOperation($row['class'], $name, $checks, $operation, $scopeContext, $entity, $events);
423
424
		$checkIds = [];
425
		try {
426
			$this->connection->beginTransaction();
427
			foreach ($checks as $check) {
428
				$checkIds[] = $this->addCheck($check['class'], $check['operator'], $check['value']);
429
			}
430
431
			$query = $this->connection->getQueryBuilder();
432
			$query->update('flow_operations')
433
				->set('name', $query->createNamedParameter($name))
434
				->set('checks', $query->createNamedParameter(json_encode(array_unique($checkIds))))
435
				->set('operation', $query->createNamedParameter($operation))
436
				->set('entity', $query->createNamedParameter($entity))
437
				->set('events', $query->createNamedParameter(json_encode($events)))
438
				->where($query->expr()->eq('id', $query->createNamedParameter($id)));
439
			$query->execute();
440
			$this->connection->commit();
441
		} catch (Exception $e) {
442
			$this->connection->rollBack();
443
			throw $e;
444
		}
445
		unset($this->operations[$scopeContext->getHash()]);
446
		$this->cacheFactory->createDistributed('flow')->remove('events');
447
448
		return $this->getOperation($id);
449
	}
450
451
	/**
452
	 * @param int $id
453
	 * @return bool
454
	 * @throws \UnexpectedValueException
455
	 * @throws Exception
456
	 * @throws \DomainException
457
	 */
458
	public function deleteOperation($id, ScopeContext $scopeContext) {
459
		if (!$this->canModify($id, $scopeContext)) {
460
			throw new \DomainException('Target operation not within scope');
461
		};
462
		$query = $this->connection->getQueryBuilder();
463
		try {
464
			$this->connection->beginTransaction();
465
			$result = (bool)$query->delete('flow_operations')
466
				->where($query->expr()->eq('id', $query->createNamedParameter($id)))
467
				->execute();
468
			if ($result) {
469
				$qb = $this->connection->getQueryBuilder();
470
				$result &= (bool)$qb->delete('flow_operations_scope')
471
					->where($qb->expr()->eq('operation_id', $qb->createNamedParameter($id)))
472
					->execute();
473
			}
474
			$this->connection->commit();
475
		} catch (Exception $e) {
476
			$this->connection->rollBack();
477
			throw $e;
478
		}
479
480
		if (isset($this->operations[$scopeContext->getHash()])) {
481
			unset($this->operations[$scopeContext->getHash()]);
482
		}
483
484
		$this->cacheFactory->createDistributed('flow')->remove('events');
485
486
		return $result;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $result also could return the type integer which is incompatible with the documented return type boolean.
Loading history...
487
	}
488
489
	protected function validateEvents(string $entity, array $events, IOperation $operation) {
490
		try {
491
			/** @var IEntity $instance */
492
			$instance = $this->container->query($entity);
493
		} catch (QueryException $e) {
494
			throw new \UnexpectedValueException($this->l->t('Entity %s does not exist', [$entity]));
495
		}
496
497
		if (!$instance instanceof IEntity) {
0 ignored issues
show
introduced by
$instance is always a sub-type of OCP\WorkflowEngine\IEntity.
Loading history...
498
			throw new \UnexpectedValueException($this->l->t('Entity %s is invalid', [$entity]));
499
		}
500
501
		if (empty($events)) {
502
			if (!$operation instanceof IComplexOperation) {
503
				throw new \UnexpectedValueException($this->l->t('No events are chosen.'));
504
			}
505
			return;
506
		}
507
508
		$availableEvents = [];
509
		foreach ($instance->getEvents() as $event) {
510
			/** @var IEntityEvent $event */
511
			$availableEvents[] = $event->getEventName();
512
		}
513
514
		$diff = array_diff($events, $availableEvents);
515
		if (!empty($diff)) {
516
			throw new \UnexpectedValueException($this->l->t('Entity %s has no event %s', [$entity, array_shift($diff)]));
517
		}
518
	}
519
520
	/**
521
	 * @param string $class
522
	 * @param string $name
523
	 * @param array[] $checks
524
	 * @param string $operation
525
	 * @param ScopeContext $scope
526
	 * @param string $entity
527
	 * @param array $events
528
	 * @throws \UnexpectedValueException
529
	 */
530
	public function validateOperation($class, $name, array $checks, $operation, ScopeContext $scope, string $entity, array $events) {
531
		try {
532
			/** @var IOperation $instance */
533
			$instance = $this->container->query($class);
534
		} catch (QueryException $e) {
535
			throw new \UnexpectedValueException($this->l->t('Operation %s does not exist', [$class]));
536
		}
537
538
		if (!($instance instanceof IOperation)) {
0 ignored issues
show
introduced by
$instance is always a sub-type of OCP\WorkflowEngine\IOperation.
Loading history...
539
			throw new \UnexpectedValueException($this->l->t('Operation %s is invalid', [$class]));
540
		}
541
542
		if (!$instance->isAvailableForScope($scope->getScope())) {
543
			throw new \UnexpectedValueException($this->l->t('Operation %s is invalid', [$class]));
544
		}
545
546
		$this->validateEvents($entity, $events, $instance);
547
548
		if (count($checks) === 0) {
549
			throw new \UnexpectedValueException($this->l->t('At least one check needs to be provided'));
550
		}
551
552
		if (strlen((string)$operation) > IManager::MAX_OPERATION_VALUE_BYTES) {
553
			throw new \UnexpectedValueException($this->l->t('The provided operation data is too long'));
554
		}
555
556
		$instance->validateOperation($name, $checks, $operation);
557
558
		foreach ($checks as $check) {
559
			if (!is_string($check['class'])) {
560
				throw new \UnexpectedValueException($this->l->t('Invalid check provided'));
561
			}
562
563
			try {
564
				/** @var ICheck $instance */
565
				$instance = $this->container->query($check['class']);
566
			} catch (QueryException $e) {
567
				throw new \UnexpectedValueException($this->l->t('Check %s does not exist', [$class]));
568
			}
569
570
			if (!($instance instanceof ICheck)) {
571
				throw new \UnexpectedValueException($this->l->t('Check %s is invalid', [$class]));
572
			}
573
574
			if (!empty($instance->supportedEntities())
575
				&& !in_array($entity, $instance->supportedEntities())
576
			) {
577
				throw new \UnexpectedValueException($this->l->t('Check %s is not allowed with this entity', [$class]));
578
			}
579
580
			if (strlen((string)$check['value']) > IManager::MAX_CHECK_VALUE_BYTES) {
581
				throw new \UnexpectedValueException($this->l->t('The provided check value is too long'));
582
			}
583
584
			$instance->validateCheck($check['operator'], $check['value']);
585
		}
586
	}
587
588
	/**
589
	 * @param int[] $checkIds
590
	 * @return array[]
591
	 */
592
	public function getChecks(array $checkIds) {
593
		$checkIds = array_map('intval', $checkIds);
594
595
		$checks = [];
596
		foreach ($checkIds as $i => $checkId) {
597
			if (isset($this->checks[$checkId])) {
598
				$checks[$checkId] = $this->checks[$checkId];
599
				unset($checkIds[$i]);
600
			}
601
		}
602
603
		if (empty($checkIds)) {
604
			return $checks;
605
		}
606
607
		$query = $this->connection->getQueryBuilder();
608
		$query->select('*')
609
			->from('flow_checks')
610
			->where($query->expr()->in('id', $query->createNamedParameter($checkIds, IQueryBuilder::PARAM_INT_ARRAY)));
611
		$result = $query->execute();
612
613
		while ($row = $result->fetch()) {
614
			$this->checks[(int) $row['id']] = $row;
615
			$checks[(int) $row['id']] = $row;
616
		}
617
		$result->closeCursor();
618
619
		$checkIds = array_diff($checkIds, array_keys($checks));
620
621
		if (!empty($checkIds)) {
622
			$missingCheck = array_pop($checkIds);
623
			throw new \UnexpectedValueException($this->l->t('Check #%s does not exist', $missingCheck));
624
		}
625
626
		return $checks;
627
	}
628
629
	/**
630
	 * @param string $class
631
	 * @param string $operator
632
	 * @param string $value
633
	 * @return int Check unique ID
634
	 */
635
	protected function addCheck($class, $operator, $value) {
636
		$hash = md5($class . '::' . $operator . '::' . $value);
637
638
		$query = $this->connection->getQueryBuilder();
639
		$query->select('id')
640
			->from('flow_checks')
641
			->where($query->expr()->eq('hash', $query->createNamedParameter($hash)));
642
		$result = $query->execute();
643
644
		if ($row = $result->fetch()) {
645
			$result->closeCursor();
646
			return (int) $row['id'];
647
		}
648
649
		$query = $this->connection->getQueryBuilder();
650
		$query->insert('flow_checks')
651
			->values([
652
				'class' => $query->createNamedParameter($class),
653
				'operator' => $query->createNamedParameter($operator),
654
				'value' => $query->createNamedParameter($value),
655
				'hash' => $query->createNamedParameter($hash),
656
			]);
657
		$query->execute();
658
659
		return $query->getLastInsertId();
660
	}
661
662
	protected function addScope(int $operationId, ScopeContext $scope): void {
663
		$query = $this->connection->getQueryBuilder();
664
665
		$insertQuery = $query->insert('flow_operations_scope');
666
		$insertQuery->values([
667
			'operation_id' => $query->createNamedParameter($operationId),
668
			'type' => $query->createNamedParameter($scope->getScope()),
669
			'value' => $query->createNamedParameter($scope->getScopeId()),
670
		]);
671
		$insertQuery->execute();
672
	}
673
674
	public function formatOperation(array $operation): array {
675
		$checkIds = json_decode($operation['checks'], true);
676
		$checks = $this->getChecks($checkIds);
677
678
		$operation['checks'] = [];
679
		foreach ($checks as $check) {
680
			// Remove internal values
681
			unset($check['id']);
682
			unset($check['hash']);
683
684
			$operation['checks'][] = $check;
685
		}
686
		$operation['events'] = json_decode($operation['events'], true) ?? [];
687
688
689
		return $operation;
690
	}
691
692
	/**
693
	 * @return IEntity[]
694
	 */
695
	public function getEntitiesList(): array {
696
		$this->dispatcher->dispatchTyped(new RegisterEntitiesEvent($this));
697
		$this->legacyEventDispatcher->dispatch(IManager::EVENT_NAME_REG_ENTITY, new GenericEvent($this));
0 ignored issues
show
Unused Code introduced by
The call to Symfony\Contracts\EventD...erInterface::dispatch() has too many arguments starting with new Symfony\Component\Ev...her\GenericEvent($this). ( Ignorable by Annotation )

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

697
		$this->legacyEventDispatcher->/** @scrutinizer ignore-call */ 
698
                                dispatch(IManager::EVENT_NAME_REG_ENTITY, new GenericEvent($this));

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
Deprecated Code introduced by
The constant OCP\WorkflowEngine\IManager::EVENT_NAME_REG_ENTITY has been deprecated: 17.0.0 ( Ignorable by Annotation )

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

697
		$this->legacyEventDispatcher->dispatch(/** @scrutinizer ignore-deprecated */ IManager::EVENT_NAME_REG_ENTITY, new GenericEvent($this));

This class constant has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the constant will be removed from the class and what other constant to use instead.

Loading history...
Bug introduced by
OCP\WorkflowEngine\IManager::EVENT_NAME_REG_ENTITY of type string is incompatible with the type object expected by parameter $event of Symfony\Contracts\EventD...erInterface::dispatch(). ( Ignorable by Annotation )

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

697
		$this->legacyEventDispatcher->dispatch(/** @scrutinizer ignore-type */ IManager::EVENT_NAME_REG_ENTITY, new GenericEvent($this));
Loading history...
698
699
		return array_values(array_merge($this->getBuildInEntities(), $this->registeredEntities));
700
	}
701
702
	/**
703
	 * @return IOperation[]
704
	 */
705
	public function getOperatorList(): array {
706
		$this->dispatcher->dispatchTyped(new RegisterOperationsEvent($this));
707
		$this->legacyEventDispatcher->dispatch(IManager::EVENT_NAME_REG_OPERATION, new GenericEvent($this));
0 ignored issues
show
Unused Code introduced by
The call to Symfony\Contracts\EventD...erInterface::dispatch() has too many arguments starting with new Symfony\Component\Ev...her\GenericEvent($this). ( Ignorable by Annotation )

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

707
		$this->legacyEventDispatcher->/** @scrutinizer ignore-call */ 
708
                                dispatch(IManager::EVENT_NAME_REG_OPERATION, new GenericEvent($this));

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
Bug introduced by
OCP\WorkflowEngine\IMana...VENT_NAME_REG_OPERATION of type string is incompatible with the type object expected by parameter $event of Symfony\Contracts\EventD...erInterface::dispatch(). ( Ignorable by Annotation )

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

707
		$this->legacyEventDispatcher->dispatch(/** @scrutinizer ignore-type */ IManager::EVENT_NAME_REG_OPERATION, new GenericEvent($this));
Loading history...
Deprecated Code introduced by
The constant OCP\WorkflowEngine\IMana...VENT_NAME_REG_OPERATION has been deprecated: 17.0.0 Will be removed in NC19. Use the dedicated events in OCP\WorkflowEngine\Events ( Ignorable by Annotation )

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

707
		$this->legacyEventDispatcher->dispatch(/** @scrutinizer ignore-deprecated */ IManager::EVENT_NAME_REG_OPERATION, new GenericEvent($this));

This class constant has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the constant will be removed from the class and what other constant to use instead.

Loading history...
708
709
		return array_merge($this->getBuildInOperators(), $this->registeredOperators);
710
	}
711
712
	/**
713
	 * @return ICheck[]
714
	 */
715
	public function getCheckList(): array {
716
		$this->dispatcher->dispatchTyped(new RegisterChecksEvent($this));
717
		$this->legacyEventDispatcher->dispatch(IManager::EVENT_NAME_REG_CHECK, new GenericEvent($this));
0 ignored issues
show
Bug introduced by
OCP\WorkflowEngine\IManager::EVENT_NAME_REG_CHECK of type string is incompatible with the type object expected by parameter $event of Symfony\Contracts\EventD...erInterface::dispatch(). ( Ignorable by Annotation )

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

717
		$this->legacyEventDispatcher->dispatch(/** @scrutinizer ignore-type */ IManager::EVENT_NAME_REG_CHECK, new GenericEvent($this));
Loading history...
Deprecated Code introduced by
The constant OCP\WorkflowEngine\IManager::EVENT_NAME_REG_CHECK has been deprecated: 17.0.0 ( Ignorable by Annotation )

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

717
		$this->legacyEventDispatcher->dispatch(/** @scrutinizer ignore-deprecated */ IManager::EVENT_NAME_REG_CHECK, new GenericEvent($this));

This class constant has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the constant will be removed from the class and what other constant to use instead.

Loading history...
Unused Code introduced by
The call to Symfony\Contracts\EventD...erInterface::dispatch() has too many arguments starting with new Symfony\Component\Ev...her\GenericEvent($this). ( Ignorable by Annotation )

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

717
		$this->legacyEventDispatcher->/** @scrutinizer ignore-call */ 
718
                                dispatch(IManager::EVENT_NAME_REG_CHECK, new GenericEvent($this));

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
718
719
		return array_merge($this->getBuildInChecks(), $this->registeredChecks);
720
	}
721
722
	public function registerEntity(IEntity $entity): void {
723
		$this->registeredEntities[get_class($entity)] = $entity;
724
	}
725
726
	public function registerOperation(IOperation $operator): void {
727
		$this->registeredOperators[get_class($operator)] = $operator;
728
	}
729
730
	public function registerCheck(ICheck $check): void {
731
		$this->registeredChecks[get_class($check)] = $check;
732
	}
733
734
	/**
735
	 * @return IEntity[]
736
	 */
737
	protected function getBuildInEntities(): array {
738
		try {
739
			return [
740
				File::class => $this->container->query(File::class),
741
			];
742
		} catch (QueryException $e) {
743
			$this->logger->logException($e);
744
			return [];
745
		}
746
	}
747
748
	/**
749
	 * @return IOperation[]
750
	 */
751
	protected function getBuildInOperators(): array {
752
		try {
753
			return [
754
				// None yet
755
			];
756
		} catch (QueryException $e) {
0 ignored issues
show
Unused Code introduced by
catch (\OCP\AppFramework\QueryException $e) is not reachable.

This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.

Unreachable code is most often the result of return, die or exit statements that have been added for debug purposes.

function fx() {
    try {
        doSomething();
        return true;
    }
    catch (\Exception $e) {
        return false;
    }

    return false;
}

In the above example, the last return false will never be executed, because a return statement has already been met in every possible execution path.

Loading history...
757
			$this->logger->logException($e);
758
			return [];
759
		}
760
	}
761
762
	/**
763
	 * @return ICheck[]
764
	 */
765
	protected function getBuildInChecks(): array {
766
		try {
767
			return [
768
				$this->container->query(FileMimeType::class),
769
				$this->container->query(FileName::class),
770
				$this->container->query(FileSize::class),
771
				$this->container->query(FileSystemTags::class),
772
				$this->container->query(RequestRemoteAddress::class),
773
				$this->container->query(RequestTime::class),
774
				$this->container->query(RequestURL::class),
775
				$this->container->query(RequestUserAgent::class),
776
				$this->container->query(UserGroupMembership::class),
777
			];
778
		} catch (QueryException $e) {
779
			$this->logger->logException($e);
780
			return [];
781
		}
782
	}
783
784
	public function isUserScopeEnabled(): bool {
785
		return $this->config->getAppValue(Application::APP_ID, 'user_scope_disabled', 'no') === 'no';
786
	}
787
}
788