Completed
Push — master ( 8e7bda...5b1d92 )
by
unknown
30:22
created
apps/workflowengine/lib/Manager.php 2 patches
Indentation   +668 added lines, -668 removed lines patch added patch discarded remove patch
@@ -46,672 +46,672 @@
 block discarded – undo
46 46
  * @psalm-type Check = array{id: int, class: class-string<ICheck>, operator: string, value: string, hash: string}
47 47
  */
48 48
 class Manager implements IManager {
49
-	/** @var array[] */
50
-	protected array $operations = [];
51
-
52
-	/** @var array<int, Check> */
53
-	protected array $checks = [];
54
-
55
-	/** @var IEntity[] */
56
-	protected array $registeredEntities = [];
57
-
58
-	/** @var IOperation[] */
59
-	protected array $registeredOperators = [];
60
-
61
-	/** @var ICheck[] */
62
-	protected array $registeredChecks = [];
63
-
64
-	/** @var CappedMemoryCache<int[]> */
65
-	protected CappedMemoryCache $operationsByScope;
66
-
67
-	public function __construct(
68
-		protected readonly IDBConnection $connection,
69
-		protected readonly ContainerInterface $container,
70
-		protected readonly IL10N $l,
71
-		protected readonly LoggerInterface $logger,
72
-		protected readonly IUserSession $session,
73
-		private readonly IEventDispatcher $dispatcher,
74
-		private readonly IAppConfig $appConfig,
75
-		private readonly ICacheFactory $cacheFactory,
76
-	) {
77
-		$this->operationsByScope = new CappedMemoryCache(64);
78
-	}
79
-
80
-	public function getRuleMatcher(): IRuleMatcher {
81
-		return new RuleMatcher(
82
-			$this->session,
83
-			$this->container,
84
-			$this->l,
85
-			$this,
86
-			$this->container->get(Logger::class)
87
-		);
88
-	}
89
-
90
-	public function getAllConfiguredEvents() {
91
-		$cache = $this->cacheFactory->createDistributed('flow');
92
-		$cached = $cache->get('events');
93
-		if ($cached !== null) {
94
-			return $cached;
95
-		}
96
-
97
-		$query = $this->connection->getQueryBuilder();
98
-
99
-		$query->select('class', 'entity')
100
-			->selectAlias($query->expr()->castColumn('events', IQueryBuilder::PARAM_STR), 'events')
101
-			->from('flow_operations')
102
-			->where($query->expr()->neq('events', $query->createNamedParameter('[]'), IQueryBuilder::PARAM_STR))
103
-			->groupBy('class', 'entity', $query->expr()->castColumn('events', IQueryBuilder::PARAM_STR));
104
-
105
-		$result = $query->executeQuery();
106
-		$operations = [];
107
-		while ($row = $result->fetchAssociative()) {
108
-			$eventNames = \json_decode($row['events']);
109
-
110
-			$operation = $row['class'];
111
-			$entity = $row['entity'];
112
-
113
-			$operations[$operation] = $operations[$row['class']] ?? [];
114
-			$operations[$operation][$entity] = $operations[$operation][$entity] ?? [];
115
-
116
-			$operations[$operation][$entity] = array_unique(array_merge($operations[$operation][$entity], $eventNames ?? []));
117
-		}
118
-		$result->closeCursor();
119
-
120
-		$cache->set('events', $operations, 3600);
121
-
122
-		return $operations;
123
-	}
124
-
125
-	/**
126
-	 * @param class-string<IOperation> $operationClass
127
-	 * @return ScopeContext[]
128
-	 */
129
-	public function getAllConfiguredScopesForOperation(string $operationClass): array {
130
-		/** @var array<class-string<IOperation>, ScopeContext[]> $scopesByOperation */
131
-		static $scopesByOperation = [];
132
-		if (isset($scopesByOperation[$operationClass])) {
133
-			return $scopesByOperation[$operationClass];
134
-		}
135
-
136
-		try {
137
-			/** @var IOperation $operation */
138
-			$operation = $this->container->get($operationClass);
139
-		} catch (ContainerExceptionInterface $e) {
140
-			return [];
141
-		}
142
-
143
-		$query = $this->connection->getQueryBuilder();
144
-
145
-		$query->selectDistinct('s.type')
146
-			->addSelect('s.value')
147
-			->from('flow_operations', 'o')
148
-			->leftJoin('o', 'flow_operations_scope', 's', $query->expr()->eq('o.id', 's.operation_id'))
149
-			->where($query->expr()->eq('o.class', $query->createParameter('operationClass')));
150
-
151
-		$query->setParameters(['operationClass' => $operationClass]);
152
-		$result = $query->executeQuery();
153
-
154
-		$scopesByOperation[$operationClass] = [];
155
-		while ($row = $result->fetchAssociative()) {
156
-			$scope = new ScopeContext($row['type'], $row['value']);
157
-
158
-			if (!$operation->isAvailableForScope((int)$row['type'])) {
159
-				continue;
160
-			}
161
-
162
-			$scopesByOperation[$operationClass][$scope->getHash()] = $scope;
163
-		}
164
-
165
-		return $scopesByOperation[$operationClass];
166
-	}
167
-
168
-	public function getAllOperations(ScopeContext $scopeContext): array {
169
-		if (isset($this->operations[$scopeContext->getHash()])) {
170
-			return $this->operations[$scopeContext->getHash()];
171
-		}
172
-
173
-		$query = $this->connection->getQueryBuilder();
174
-
175
-		$query->select('o.*')
176
-			->selectAlias('s.type', 'scope_type')
177
-			->selectAlias('s.value', 'scope_actor_id')
178
-			->from('flow_operations', 'o')
179
-			->leftJoin('o', 'flow_operations_scope', 's', $query->expr()->eq('o.id', 's.operation_id'))
180
-			->where($query->expr()->eq('s.type', $query->createParameter('scope')));
181
-
182
-		if ($scopeContext->getScope() === IManager::SCOPE_USER) {
183
-			$query->andWhere($query->expr()->eq('s.value', $query->createParameter('scopeId')));
184
-		}
185
-
186
-		$query->setParameters(['scope' => $scopeContext->getScope(), 'scopeId' => $scopeContext->getScopeId()]);
187
-		$result = $query->executeQuery();
188
-
189
-		$this->operations[$scopeContext->getHash()] = [];
190
-		while ($row = $result->fetchAssociative()) {
191
-			try {
192
-				/** @var IOperation $operation */
193
-				$operation = $this->container->get($row['class']);
194
-			} catch (ContainerExceptionInterface $e) {
195
-				continue;
196
-			}
197
-
198
-			if (!$operation->isAvailableForScope((int)$row['scope_type'])) {
199
-				continue;
200
-			}
201
-
202
-			if (!isset($this->operations[$scopeContext->getHash()][$row['class']])) {
203
-				$this->operations[$scopeContext->getHash()][$row['class']] = [];
204
-			}
205
-			$this->operations[$scopeContext->getHash()][$row['class']][] = $row;
206
-		}
207
-
208
-		return $this->operations[$scopeContext->getHash()];
209
-	}
210
-
211
-	public function getOperations(string $class, ScopeContext $scopeContext): array {
212
-		if (!isset($this->operations[$scopeContext->getHash()])) {
213
-			$this->getAllOperations($scopeContext);
214
-		}
215
-		return $this->operations[$scopeContext->getHash()][$class] ?? [];
216
-	}
217
-
218
-	/**
219
-	 * @param int $id
220
-	 * @return array
221
-	 * @throws \UnexpectedValueException
222
-	 */
223
-	protected function getOperation($id) {
224
-		$query = $this->connection->getQueryBuilder();
225
-		$query->select('*')
226
-			->from('flow_operations')
227
-			->where($query->expr()->eq('id', $query->createNamedParameter($id)));
228
-		$result = $query->executeQuery();
229
-		$row = $result->fetchAssociative();
230
-		$result->closeCursor();
231
-
232
-		if ($row) {
233
-			return $row;
234
-		}
235
-
236
-		throw new \UnexpectedValueException($this->l->t('Operation #%s does not exist', [$id]));
237
-	}
238
-
239
-	protected function insertOperation(
240
-		string $class,
241
-		string $name,
242
-		array $checkIds,
243
-		string $operation,
244
-		string $entity,
245
-		array $events,
246
-	): int {
247
-		$query = $this->connection->getQueryBuilder();
248
-		$query->insert('flow_operations')
249
-			->values([
250
-				'class' => $query->createNamedParameter($class),
251
-				'name' => $query->createNamedParameter($name),
252
-				'checks' => $query->createNamedParameter(json_encode(array_unique($checkIds))),
253
-				'operation' => $query->createNamedParameter($operation),
254
-				'entity' => $query->createNamedParameter($entity),
255
-				'events' => $query->createNamedParameter(json_encode($events))
256
-			]);
257
-		$query->executeStatement();
258
-
259
-		$this->cacheFactory->createDistributed('flow')->remove('events');
260
-
261
-		return $query->getLastInsertId();
262
-	}
263
-
264
-	/**
265
-	 * @param string $class
266
-	 * @param string $name
267
-	 * @param array<int, Check> $checks
268
-	 * @param string $operation
269
-	 * @return array The added operation
270
-	 * @throws \UnexpectedValueException
271
-	 * @throws Exception
272
-	 */
273
-	public function addOperation(
274
-		string $class,
275
-		string $name,
276
-		array $checks,
277
-		string $operation,
278
-		ScopeContext $scope,
279
-		string $entity,
280
-		array $events,
281
-	) {
282
-		$this->validateOperation($class, $name, $checks, $operation, $scope, $entity, $events);
283
-
284
-		$this->connection->beginTransaction();
285
-
286
-		try {
287
-			$checkIds = [];
288
-			foreach ($checks as $check) {
289
-				$checkIds[] = $this->addCheck($check['class'], $check['operator'], $check['value']);
290
-			}
291
-
292
-			$id = $this->insertOperation($class, $name, $checkIds, $operation, $entity, $events);
293
-			$this->addScope($id, $scope);
294
-
295
-			$this->connection->commit();
296
-		} catch (Exception $e) {
297
-			$this->connection->rollBack();
298
-			throw $e;
299
-		}
300
-
301
-		return $this->getOperation($id);
302
-	}
303
-
304
-	protected function canModify(int $id, ScopeContext $scopeContext):bool {
305
-		if (isset($this->operationsByScope[$scopeContext->getHash()])) {
306
-			return in_array($id, $this->operationsByScope[$scopeContext->getHash()], true);
307
-		}
308
-
309
-		$qb = $this->connection->getQueryBuilder();
310
-		$qb = $qb->select('o.id')
311
-			->from('flow_operations', 'o')
312
-			->leftJoin('o', 'flow_operations_scope', 's', $qb->expr()->eq('o.id', 's.operation_id'))
313
-			->where($qb->expr()->eq('s.type', $qb->createParameter('scope')));
314
-
315
-		if ($scopeContext->getScope() !== IManager::SCOPE_ADMIN) {
316
-			$qb->andWhere($qb->expr()->eq('s.value', $qb->createParameter('scopeId')));
317
-		}
318
-
319
-		$qb->setParameters(['scope' => $scopeContext->getScope(), 'scopeId' => $scopeContext->getScopeId()]);
320
-		$result = $qb->executeQuery();
321
-
322
-		$operations = [];
323
-		while (($opId = $result->fetchOne()) !== false) {
324
-			$operations[] = (int)$opId;
325
-		}
326
-		$this->operationsByScope[$scopeContext->getHash()] = $operations;
327
-		$result->closeCursor();
328
-
329
-		return in_array($id, $this->operationsByScope[$scopeContext->getHash()], true);
330
-	}
331
-
332
-	/**
333
-	 * @param int $id
334
-	 * @param string $name
335
-	 * @param array[] $checks
336
-	 * @param string $operation
337
-	 * @return array The updated operation
338
-	 * @throws \UnexpectedValueException
339
-	 * @throws \DomainException
340
-	 * @throws Exception
341
-	 */
342
-	public function updateOperation(
343
-		int $id,
344
-		string $name,
345
-		array $checks,
346
-		string $operation,
347
-		ScopeContext $scopeContext,
348
-		string $entity,
349
-		array $events,
350
-	): array {
351
-		if (!$this->canModify($id, $scopeContext)) {
352
-			throw new \DomainException('Target operation not within scope');
353
-		};
354
-		$row = $this->getOperation($id);
355
-		$this->validateOperation($row['class'], $name, $checks, $operation, $scopeContext, $entity, $events);
356
-
357
-		$checkIds = [];
358
-		try {
359
-			$this->connection->beginTransaction();
360
-			foreach ($checks as $check) {
361
-				$checkIds[] = $this->addCheck($check['class'], $check['operator'], $check['value']);
362
-			}
363
-
364
-			$query = $this->connection->getQueryBuilder();
365
-			$query->update('flow_operations')
366
-				->set('name', $query->createNamedParameter($name))
367
-				->set('checks', $query->createNamedParameter(json_encode(array_unique($checkIds))))
368
-				->set('operation', $query->createNamedParameter($operation))
369
-				->set('entity', $query->createNamedParameter($entity))
370
-				->set('events', $query->createNamedParameter(json_encode($events)))
371
-				->where($query->expr()->eq('id', $query->createNamedParameter($id)));
372
-			$query->executeStatement();
373
-			$this->connection->commit();
374
-		} catch (Exception $e) {
375
-			$this->connection->rollBack();
376
-			throw $e;
377
-		}
378
-		unset($this->operations[$scopeContext->getHash()]);
379
-		$this->cacheFactory->createDistributed('flow')->remove('events');
380
-
381
-		return $this->getOperation($id);
382
-	}
383
-
384
-	/**
385
-	 * @throws \UnexpectedValueException
386
-	 * @throws Exception
387
-	 * @throws \DomainException
388
-	 */
389
-	public function deleteOperation(int $id, ScopeContext $scopeContext): bool {
390
-		if (!$this->canModify($id, $scopeContext)) {
391
-			throw new \DomainException('Target operation not within scope');
392
-		};
393
-		$query = $this->connection->getQueryBuilder();
394
-		try {
395
-			$this->connection->beginTransaction();
396
-			$result = (bool)$query->delete('flow_operations')
397
-				->where($query->expr()->eq('id', $query->createNamedParameter($id)))
398
-				->executeStatement();
399
-			if ($result) {
400
-				$qb = $this->connection->getQueryBuilder();
401
-				$result = (bool)$qb->delete('flow_operations_scope')
402
-					->where($qb->expr()->eq('operation_id', $qb->createNamedParameter($id)))
403
-					->executeStatement();
404
-			}
405
-			$this->connection->commit();
406
-		} catch (Exception $e) {
407
-			$this->connection->rollBack();
408
-			throw $e;
409
-		}
410
-
411
-		if (isset($this->operations[$scopeContext->getHash()])) {
412
-			unset($this->operations[$scopeContext->getHash()]);
413
-		}
414
-
415
-		$this->cacheFactory->createDistributed('flow')->remove('events');
416
-
417
-		return $result;
418
-	}
419
-
420
-	/**
421
-	 * @param class-string<IEntity> $entity
422
-	 * @param array $events
423
-	 */
424
-	protected function validateEvents(string $entity, array $events, IOperation $operation): void {
425
-		/** @psalm-suppress TaintedCallable newInstance is not called */
426
-		$reflection = new \ReflectionClass($entity);
427
-		if ($entity !== IEntity::class && !in_array(IEntity::class, $reflection->getInterfaceNames())) {
428
-			throw new \UnexpectedValueException($this->l->t('Entity %s is invalid', [$entity]));
429
-		}
430
-
431
-		try {
432
-			$instance = $this->container->get($entity);
433
-		} catch (ContainerExceptionInterface $e) {
434
-			throw new \UnexpectedValueException($this->l->t('Entity %s does not exist', [$entity]));
435
-		}
436
-
437
-		if (empty($events)) {
438
-			if (!$operation instanceof IComplexOperation) {
439
-				throw new \UnexpectedValueException($this->l->t('No events are chosen.'));
440
-			}
441
-			return;
442
-		}
443
-
444
-		$availableEvents = [];
445
-		foreach ($instance->getEvents() as $event) {
446
-			/** @var IEntityEvent $event */
447
-			$availableEvents[] = $event->getEventName();
448
-		}
449
-
450
-		$diff = array_diff($events, $availableEvents);
451
-		if (!empty($diff)) {
452
-			throw new \UnexpectedValueException($this->l->t('Entity %s has no event %s', [$entity, array_shift($diff)]));
453
-		}
454
-	}
455
-
456
-	/**
457
-	 * @param class-string<IOperation> $class
458
-	 * @param array<int, Check> $checks
459
-	 * @param array $events
460
-	 * @throws \UnexpectedValueException
461
-	 */
462
-	public function validateOperation(string $class, string $name, array $checks, string $operation, ScopeContext $scope, string $entity, array $events): void {
463
-		if (strlen($operation) > IManager::MAX_OPERATION_VALUE_BYTES) {
464
-			throw new \UnexpectedValueException($this->l->t('The provided operation data is too long'));
465
-		}
466
-
467
-		/** @psalm-suppress TaintedCallable newInstance is not called */
468
-		$reflection = new \ReflectionClass($class);
469
-		if ($class !== IOperation::class && !in_array(IOperation::class, $reflection->getInterfaceNames())) {
470
-			throw new \UnexpectedValueException($this->l->t('Operation %s is invalid', [$class]) . join(', ', $reflection->getInterfaceNames()));
471
-		}
472
-
473
-		try {
474
-			/** @var IOperation $instance */
475
-			$instance = $this->container->get($class);
476
-		} catch (ContainerExceptionInterface $e) {
477
-			throw new \UnexpectedValueException($this->l->t('Operation %s does not exist', [$class]));
478
-		}
479
-
480
-		if (!$instance->isAvailableForScope($scope->getScope())) {
481
-			throw new \UnexpectedValueException($this->l->t('Operation %s is invalid', [$class]));
482
-		}
483
-
484
-		$this->validateEvents($entity, $events, $instance);
485
-
486
-		if (count($checks) === 0) {
487
-			throw new \UnexpectedValueException($this->l->t('At least one check needs to be provided'));
488
-		}
489
-
490
-		$instance->validateOperation($name, $checks, $operation);
491
-
492
-		foreach ($checks as $check) {
493
-			if (!is_string($check['class'])) {
494
-				throw new \UnexpectedValueException($this->l->t('Invalid check provided'));
495
-			}
496
-
497
-			if (strlen((string)$check['value']) > IManager::MAX_CHECK_VALUE_BYTES) {
498
-				throw new \UnexpectedValueException($this->l->t('The provided check value is too long'));
499
-			}
500
-
501
-			$reflection = new \ReflectionClass($check['class']);
502
-			if ($check['class'] !== ICheck::class && !in_array(ICheck::class, $reflection->getInterfaceNames())) {
503
-				throw new \UnexpectedValueException($this->l->t('Check %s is invalid', [$class]));
504
-			}
505
-
506
-			try {
507
-				/** @var ICheck $instance */
508
-				$instance = $this->container->get($check['class']);
509
-			} catch (ContainerExceptionInterface) {
510
-				throw new \UnexpectedValueException($this->l->t('Check %s does not exist', [$class]));
511
-			}
512
-
513
-			if (!empty($instance->supportedEntities())
514
-				&& !in_array($entity, $instance->supportedEntities())
515
-			) {
516
-				throw new \UnexpectedValueException($this->l->t('Check %s is not allowed with this entity', [$class]));
517
-			}
518
-
519
-			$instance->validateCheck($check['operator'], $check['value']);
520
-		}
521
-	}
522
-
523
-	/**
524
-	 * @param int[] $checkIds
525
-	 * @return array<int, Check>
526
-	 */
527
-	public function getChecks(array $checkIds): array {
528
-		$checkIds = array_map('intval', $checkIds);
529
-
530
-		$checks = [];
531
-		foreach ($checkIds as $i => $checkId) {
532
-			if (isset($this->checks[$checkId])) {
533
-				$checks[$checkId] = $this->checks[$checkId];
534
-				unset($checkIds[$i]);
535
-			}
536
-		}
537
-
538
-		if (empty($checkIds)) {
539
-			return $checks;
540
-		}
541
-
542
-		$query = $this->connection->getQueryBuilder();
543
-		$query->select('*')
544
-			->from('flow_checks')
545
-			->where($query->expr()->in('id', $query->createNamedParameter($checkIds, IQueryBuilder::PARAM_INT_ARRAY)));
546
-		$result = $query->executeQuery();
547
-
548
-		while ($row = $result->fetchAssociative()) {
549
-			/** @var Check $row */
550
-			$this->checks[(int)$row['id']] = $row;
551
-			$checks[(int)$row['id']] = $row;
552
-		}
553
-		$result->closeCursor();
554
-
555
-		$checkIds = array_diff($checkIds, array_keys($checks));
556
-
557
-		if (!empty($checkIds)) {
558
-			$missingCheck = array_pop($checkIds);
559
-			throw new \UnexpectedValueException($this->l->t('Check #%s does not exist', (string)$missingCheck));
560
-		}
561
-
562
-		return $checks;
563
-	}
564
-
565
-	/**
566
-	 * @return int Check unique ID
567
-	 */
568
-	protected function addCheck(string $class, string $operator, string $value): int {
569
-		$hash = md5($class . '::' . $operator . '::' . $value);
570
-
571
-		$query = $this->connection->getQueryBuilder();
572
-		$query->select('id')
573
-			->from('flow_checks')
574
-			->where($query->expr()->eq('hash', $query->createNamedParameter($hash)));
575
-		$result = $query->executeQuery();
576
-
577
-		if ($row = $result->fetchAssociative()) {
578
-			$result->closeCursor();
579
-			return (int)$row['id'];
580
-		}
581
-
582
-		$query = $this->connection->getQueryBuilder();
583
-		$query->insert('flow_checks')
584
-			->values([
585
-				'class' => $query->createNamedParameter($class),
586
-				'operator' => $query->createNamedParameter($operator),
587
-				'value' => $query->createNamedParameter($value),
588
-				'hash' => $query->createNamedParameter($hash),
589
-			]);
590
-		$query->executeStatement();
591
-
592
-		return $query->getLastInsertId();
593
-	}
594
-
595
-	protected function addScope(int $operationId, ScopeContext $scope): void {
596
-		$query = $this->connection->getQueryBuilder();
597
-
598
-		$insertQuery = $query->insert('flow_operations_scope');
599
-		$insertQuery->values([
600
-			'operation_id' => $query->createNamedParameter($operationId),
601
-			'type' => $query->createNamedParameter($scope->getScope()),
602
-			'value' => $query->createNamedParameter($scope->getScopeId()),
603
-		]);
604
-		$insertQuery->executeStatement();
605
-	}
606
-
607
-	public function formatOperation(array $operation): array {
608
-		$checkIds = json_decode($operation['checks'], true);
609
-		$checks = $this->getChecks($checkIds);
610
-
611
-		$operation['checks'] = [];
612
-		foreach ($checks as $check) {
613
-			// Remove internal values
614
-			unset($check['id']);
615
-			unset($check['hash']);
616
-
617
-			$operation['checks'][] = $check;
618
-		}
619
-		$operation['events'] = json_decode($operation['events'], true) ?? [];
620
-
621
-
622
-		return $operation;
623
-	}
624
-
625
-	/**
626
-	 * @return IEntity[]
627
-	 */
628
-	public function getEntitiesList(): array {
629
-		$this->dispatcher->dispatchTyped(new RegisterEntitiesEvent($this));
630
-
631
-		return array_values(array_merge($this->getBuildInEntities(), $this->registeredEntities));
632
-	}
633
-
634
-	/**
635
-	 * @return IOperation[]
636
-	 */
637
-	public function getOperatorList(): array {
638
-		$this->dispatcher->dispatchTyped(new RegisterOperationsEvent($this));
639
-
640
-		return array_merge($this->getBuildInOperators(), $this->registeredOperators);
641
-	}
642
-
643
-	/**
644
-	 * @return ICheck[]
645
-	 */
646
-	public function getCheckList(): array {
647
-		$this->dispatcher->dispatchTyped(new RegisterChecksEvent($this));
648
-
649
-		return array_merge($this->getBuildInChecks(), $this->registeredChecks);
650
-	}
651
-
652
-	public function registerEntity(IEntity $entity): void {
653
-		$this->registeredEntities[get_class($entity)] = $entity;
654
-	}
655
-
656
-	public function registerOperation(IOperation $operator): void {
657
-		$this->registeredOperators[get_class($operator)] = $operator;
658
-	}
659
-
660
-	public function registerCheck(ICheck $check): void {
661
-		$this->registeredChecks[get_class($check)] = $check;
662
-	}
663
-
664
-	/**
665
-	 * @return IEntity[]
666
-	 */
667
-	protected function getBuildInEntities(): array {
668
-		try {
669
-			return [
670
-				File::class => $this->container->get(File::class),
671
-			];
672
-		} catch (ContainerExceptionInterface $e) {
673
-			$this->logger->error($e->getMessage(), ['exception' => $e]);
674
-			return [];
675
-		}
676
-	}
677
-
678
-	/**
679
-	 * @return IOperation[]
680
-	 */
681
-	protected function getBuildInOperators(): array {
682
-		try {
683
-			return [
684
-				// None yet
685
-			];
686
-		} catch (ContainerExceptionInterface $e) {
687
-			$this->logger->error($e->getMessage(), ['exception' => $e]);
688
-			return [];
689
-		}
690
-	}
691
-
692
-	/**
693
-	 * @return ICheck[]
694
-	 */
695
-	protected function getBuildInChecks(): array {
696
-		try {
697
-			return [
698
-				$this->container->get(FileMimeType::class),
699
-				$this->container->get(FileName::class),
700
-				$this->container->get(FileSize::class),
701
-				$this->container->get(FileSystemTags::class),
702
-				$this->container->get(RequestRemoteAddress::class),
703
-				$this->container->get(RequestTime::class),
704
-				$this->container->get(RequestURL::class),
705
-				$this->container->get(RequestUserAgent::class),
706
-				$this->container->get(UserGroupMembership::class),
707
-			];
708
-		} catch (ContainerExceptionInterface $e) {
709
-			$this->logger->error($e->getMessage(), ['exception' => $e]);
710
-			return [];
711
-		}
712
-	}
713
-
714
-	public function isUserScopeEnabled(): bool {
715
-		return !$this->appConfig->getAppValueBool('user_scope_disabled');
716
-	}
49
+    /** @var array[] */
50
+    protected array $operations = [];
51
+
52
+    /** @var array<int, Check> */
53
+    protected array $checks = [];
54
+
55
+    /** @var IEntity[] */
56
+    protected array $registeredEntities = [];
57
+
58
+    /** @var IOperation[] */
59
+    protected array $registeredOperators = [];
60
+
61
+    /** @var ICheck[] */
62
+    protected array $registeredChecks = [];
63
+
64
+    /** @var CappedMemoryCache<int[]> */
65
+    protected CappedMemoryCache $operationsByScope;
66
+
67
+    public function __construct(
68
+        protected readonly IDBConnection $connection,
69
+        protected readonly ContainerInterface $container,
70
+        protected readonly IL10N $l,
71
+        protected readonly LoggerInterface $logger,
72
+        protected readonly IUserSession $session,
73
+        private readonly IEventDispatcher $dispatcher,
74
+        private readonly IAppConfig $appConfig,
75
+        private readonly ICacheFactory $cacheFactory,
76
+    ) {
77
+        $this->operationsByScope = new CappedMemoryCache(64);
78
+    }
79
+
80
+    public function getRuleMatcher(): IRuleMatcher {
81
+        return new RuleMatcher(
82
+            $this->session,
83
+            $this->container,
84
+            $this->l,
85
+            $this,
86
+            $this->container->get(Logger::class)
87
+        );
88
+    }
89
+
90
+    public function getAllConfiguredEvents() {
91
+        $cache = $this->cacheFactory->createDistributed('flow');
92
+        $cached = $cache->get('events');
93
+        if ($cached !== null) {
94
+            return $cached;
95
+        }
96
+
97
+        $query = $this->connection->getQueryBuilder();
98
+
99
+        $query->select('class', 'entity')
100
+            ->selectAlias($query->expr()->castColumn('events', IQueryBuilder::PARAM_STR), 'events')
101
+            ->from('flow_operations')
102
+            ->where($query->expr()->neq('events', $query->createNamedParameter('[]'), IQueryBuilder::PARAM_STR))
103
+            ->groupBy('class', 'entity', $query->expr()->castColumn('events', IQueryBuilder::PARAM_STR));
104
+
105
+        $result = $query->executeQuery();
106
+        $operations = [];
107
+        while ($row = $result->fetchAssociative()) {
108
+            $eventNames = \json_decode($row['events']);
109
+
110
+            $operation = $row['class'];
111
+            $entity = $row['entity'];
112
+
113
+            $operations[$operation] = $operations[$row['class']] ?? [];
114
+            $operations[$operation][$entity] = $operations[$operation][$entity] ?? [];
115
+
116
+            $operations[$operation][$entity] = array_unique(array_merge($operations[$operation][$entity], $eventNames ?? []));
117
+        }
118
+        $result->closeCursor();
119
+
120
+        $cache->set('events', $operations, 3600);
121
+
122
+        return $operations;
123
+    }
124
+
125
+    /**
126
+     * @param class-string<IOperation> $operationClass
127
+     * @return ScopeContext[]
128
+     */
129
+    public function getAllConfiguredScopesForOperation(string $operationClass): array {
130
+        /** @var array<class-string<IOperation>, ScopeContext[]> $scopesByOperation */
131
+        static $scopesByOperation = [];
132
+        if (isset($scopesByOperation[$operationClass])) {
133
+            return $scopesByOperation[$operationClass];
134
+        }
135
+
136
+        try {
137
+            /** @var IOperation $operation */
138
+            $operation = $this->container->get($operationClass);
139
+        } catch (ContainerExceptionInterface $e) {
140
+            return [];
141
+        }
142
+
143
+        $query = $this->connection->getQueryBuilder();
144
+
145
+        $query->selectDistinct('s.type')
146
+            ->addSelect('s.value')
147
+            ->from('flow_operations', 'o')
148
+            ->leftJoin('o', 'flow_operations_scope', 's', $query->expr()->eq('o.id', 's.operation_id'))
149
+            ->where($query->expr()->eq('o.class', $query->createParameter('operationClass')));
150
+
151
+        $query->setParameters(['operationClass' => $operationClass]);
152
+        $result = $query->executeQuery();
153
+
154
+        $scopesByOperation[$operationClass] = [];
155
+        while ($row = $result->fetchAssociative()) {
156
+            $scope = new ScopeContext($row['type'], $row['value']);
157
+
158
+            if (!$operation->isAvailableForScope((int)$row['type'])) {
159
+                continue;
160
+            }
161
+
162
+            $scopesByOperation[$operationClass][$scope->getHash()] = $scope;
163
+        }
164
+
165
+        return $scopesByOperation[$operationClass];
166
+    }
167
+
168
+    public function getAllOperations(ScopeContext $scopeContext): array {
169
+        if (isset($this->operations[$scopeContext->getHash()])) {
170
+            return $this->operations[$scopeContext->getHash()];
171
+        }
172
+
173
+        $query = $this->connection->getQueryBuilder();
174
+
175
+        $query->select('o.*')
176
+            ->selectAlias('s.type', 'scope_type')
177
+            ->selectAlias('s.value', 'scope_actor_id')
178
+            ->from('flow_operations', 'o')
179
+            ->leftJoin('o', 'flow_operations_scope', 's', $query->expr()->eq('o.id', 's.operation_id'))
180
+            ->where($query->expr()->eq('s.type', $query->createParameter('scope')));
181
+
182
+        if ($scopeContext->getScope() === IManager::SCOPE_USER) {
183
+            $query->andWhere($query->expr()->eq('s.value', $query->createParameter('scopeId')));
184
+        }
185
+
186
+        $query->setParameters(['scope' => $scopeContext->getScope(), 'scopeId' => $scopeContext->getScopeId()]);
187
+        $result = $query->executeQuery();
188
+
189
+        $this->operations[$scopeContext->getHash()] = [];
190
+        while ($row = $result->fetchAssociative()) {
191
+            try {
192
+                /** @var IOperation $operation */
193
+                $operation = $this->container->get($row['class']);
194
+            } catch (ContainerExceptionInterface $e) {
195
+                continue;
196
+            }
197
+
198
+            if (!$operation->isAvailableForScope((int)$row['scope_type'])) {
199
+                continue;
200
+            }
201
+
202
+            if (!isset($this->operations[$scopeContext->getHash()][$row['class']])) {
203
+                $this->operations[$scopeContext->getHash()][$row['class']] = [];
204
+            }
205
+            $this->operations[$scopeContext->getHash()][$row['class']][] = $row;
206
+        }
207
+
208
+        return $this->operations[$scopeContext->getHash()];
209
+    }
210
+
211
+    public function getOperations(string $class, ScopeContext $scopeContext): array {
212
+        if (!isset($this->operations[$scopeContext->getHash()])) {
213
+            $this->getAllOperations($scopeContext);
214
+        }
215
+        return $this->operations[$scopeContext->getHash()][$class] ?? [];
216
+    }
217
+
218
+    /**
219
+     * @param int $id
220
+     * @return array
221
+     * @throws \UnexpectedValueException
222
+     */
223
+    protected function getOperation($id) {
224
+        $query = $this->connection->getQueryBuilder();
225
+        $query->select('*')
226
+            ->from('flow_operations')
227
+            ->where($query->expr()->eq('id', $query->createNamedParameter($id)));
228
+        $result = $query->executeQuery();
229
+        $row = $result->fetchAssociative();
230
+        $result->closeCursor();
231
+
232
+        if ($row) {
233
+            return $row;
234
+        }
235
+
236
+        throw new \UnexpectedValueException($this->l->t('Operation #%s does not exist', [$id]));
237
+    }
238
+
239
+    protected function insertOperation(
240
+        string $class,
241
+        string $name,
242
+        array $checkIds,
243
+        string $operation,
244
+        string $entity,
245
+        array $events,
246
+    ): int {
247
+        $query = $this->connection->getQueryBuilder();
248
+        $query->insert('flow_operations')
249
+            ->values([
250
+                'class' => $query->createNamedParameter($class),
251
+                'name' => $query->createNamedParameter($name),
252
+                'checks' => $query->createNamedParameter(json_encode(array_unique($checkIds))),
253
+                'operation' => $query->createNamedParameter($operation),
254
+                'entity' => $query->createNamedParameter($entity),
255
+                'events' => $query->createNamedParameter(json_encode($events))
256
+            ]);
257
+        $query->executeStatement();
258
+
259
+        $this->cacheFactory->createDistributed('flow')->remove('events');
260
+
261
+        return $query->getLastInsertId();
262
+    }
263
+
264
+    /**
265
+     * @param string $class
266
+     * @param string $name
267
+     * @param array<int, Check> $checks
268
+     * @param string $operation
269
+     * @return array The added operation
270
+     * @throws \UnexpectedValueException
271
+     * @throws Exception
272
+     */
273
+    public function addOperation(
274
+        string $class,
275
+        string $name,
276
+        array $checks,
277
+        string $operation,
278
+        ScopeContext $scope,
279
+        string $entity,
280
+        array $events,
281
+    ) {
282
+        $this->validateOperation($class, $name, $checks, $operation, $scope, $entity, $events);
283
+
284
+        $this->connection->beginTransaction();
285
+
286
+        try {
287
+            $checkIds = [];
288
+            foreach ($checks as $check) {
289
+                $checkIds[] = $this->addCheck($check['class'], $check['operator'], $check['value']);
290
+            }
291
+
292
+            $id = $this->insertOperation($class, $name, $checkIds, $operation, $entity, $events);
293
+            $this->addScope($id, $scope);
294
+
295
+            $this->connection->commit();
296
+        } catch (Exception $e) {
297
+            $this->connection->rollBack();
298
+            throw $e;
299
+        }
300
+
301
+        return $this->getOperation($id);
302
+    }
303
+
304
+    protected function canModify(int $id, ScopeContext $scopeContext):bool {
305
+        if (isset($this->operationsByScope[$scopeContext->getHash()])) {
306
+            return in_array($id, $this->operationsByScope[$scopeContext->getHash()], true);
307
+        }
308
+
309
+        $qb = $this->connection->getQueryBuilder();
310
+        $qb = $qb->select('o.id')
311
+            ->from('flow_operations', 'o')
312
+            ->leftJoin('o', 'flow_operations_scope', 's', $qb->expr()->eq('o.id', 's.operation_id'))
313
+            ->where($qb->expr()->eq('s.type', $qb->createParameter('scope')));
314
+
315
+        if ($scopeContext->getScope() !== IManager::SCOPE_ADMIN) {
316
+            $qb->andWhere($qb->expr()->eq('s.value', $qb->createParameter('scopeId')));
317
+        }
318
+
319
+        $qb->setParameters(['scope' => $scopeContext->getScope(), 'scopeId' => $scopeContext->getScopeId()]);
320
+        $result = $qb->executeQuery();
321
+
322
+        $operations = [];
323
+        while (($opId = $result->fetchOne()) !== false) {
324
+            $operations[] = (int)$opId;
325
+        }
326
+        $this->operationsByScope[$scopeContext->getHash()] = $operations;
327
+        $result->closeCursor();
328
+
329
+        return in_array($id, $this->operationsByScope[$scopeContext->getHash()], true);
330
+    }
331
+
332
+    /**
333
+     * @param int $id
334
+     * @param string $name
335
+     * @param array[] $checks
336
+     * @param string $operation
337
+     * @return array The updated operation
338
+     * @throws \UnexpectedValueException
339
+     * @throws \DomainException
340
+     * @throws Exception
341
+     */
342
+    public function updateOperation(
343
+        int $id,
344
+        string $name,
345
+        array $checks,
346
+        string $operation,
347
+        ScopeContext $scopeContext,
348
+        string $entity,
349
+        array $events,
350
+    ): array {
351
+        if (!$this->canModify($id, $scopeContext)) {
352
+            throw new \DomainException('Target operation not within scope');
353
+        };
354
+        $row = $this->getOperation($id);
355
+        $this->validateOperation($row['class'], $name, $checks, $operation, $scopeContext, $entity, $events);
356
+
357
+        $checkIds = [];
358
+        try {
359
+            $this->connection->beginTransaction();
360
+            foreach ($checks as $check) {
361
+                $checkIds[] = $this->addCheck($check['class'], $check['operator'], $check['value']);
362
+            }
363
+
364
+            $query = $this->connection->getQueryBuilder();
365
+            $query->update('flow_operations')
366
+                ->set('name', $query->createNamedParameter($name))
367
+                ->set('checks', $query->createNamedParameter(json_encode(array_unique($checkIds))))
368
+                ->set('operation', $query->createNamedParameter($operation))
369
+                ->set('entity', $query->createNamedParameter($entity))
370
+                ->set('events', $query->createNamedParameter(json_encode($events)))
371
+                ->where($query->expr()->eq('id', $query->createNamedParameter($id)));
372
+            $query->executeStatement();
373
+            $this->connection->commit();
374
+        } catch (Exception $e) {
375
+            $this->connection->rollBack();
376
+            throw $e;
377
+        }
378
+        unset($this->operations[$scopeContext->getHash()]);
379
+        $this->cacheFactory->createDistributed('flow')->remove('events');
380
+
381
+        return $this->getOperation($id);
382
+    }
383
+
384
+    /**
385
+     * @throws \UnexpectedValueException
386
+     * @throws Exception
387
+     * @throws \DomainException
388
+     */
389
+    public function deleteOperation(int $id, ScopeContext $scopeContext): bool {
390
+        if (!$this->canModify($id, $scopeContext)) {
391
+            throw new \DomainException('Target operation not within scope');
392
+        };
393
+        $query = $this->connection->getQueryBuilder();
394
+        try {
395
+            $this->connection->beginTransaction();
396
+            $result = (bool)$query->delete('flow_operations')
397
+                ->where($query->expr()->eq('id', $query->createNamedParameter($id)))
398
+                ->executeStatement();
399
+            if ($result) {
400
+                $qb = $this->connection->getQueryBuilder();
401
+                $result = (bool)$qb->delete('flow_operations_scope')
402
+                    ->where($qb->expr()->eq('operation_id', $qb->createNamedParameter($id)))
403
+                    ->executeStatement();
404
+            }
405
+            $this->connection->commit();
406
+        } catch (Exception $e) {
407
+            $this->connection->rollBack();
408
+            throw $e;
409
+        }
410
+
411
+        if (isset($this->operations[$scopeContext->getHash()])) {
412
+            unset($this->operations[$scopeContext->getHash()]);
413
+        }
414
+
415
+        $this->cacheFactory->createDistributed('flow')->remove('events');
416
+
417
+        return $result;
418
+    }
419
+
420
+    /**
421
+     * @param class-string<IEntity> $entity
422
+     * @param array $events
423
+     */
424
+    protected function validateEvents(string $entity, array $events, IOperation $operation): void {
425
+        /** @psalm-suppress TaintedCallable newInstance is not called */
426
+        $reflection = new \ReflectionClass($entity);
427
+        if ($entity !== IEntity::class && !in_array(IEntity::class, $reflection->getInterfaceNames())) {
428
+            throw new \UnexpectedValueException($this->l->t('Entity %s is invalid', [$entity]));
429
+        }
430
+
431
+        try {
432
+            $instance = $this->container->get($entity);
433
+        } catch (ContainerExceptionInterface $e) {
434
+            throw new \UnexpectedValueException($this->l->t('Entity %s does not exist', [$entity]));
435
+        }
436
+
437
+        if (empty($events)) {
438
+            if (!$operation instanceof IComplexOperation) {
439
+                throw new \UnexpectedValueException($this->l->t('No events are chosen.'));
440
+            }
441
+            return;
442
+        }
443
+
444
+        $availableEvents = [];
445
+        foreach ($instance->getEvents() as $event) {
446
+            /** @var IEntityEvent $event */
447
+            $availableEvents[] = $event->getEventName();
448
+        }
449
+
450
+        $diff = array_diff($events, $availableEvents);
451
+        if (!empty($diff)) {
452
+            throw new \UnexpectedValueException($this->l->t('Entity %s has no event %s', [$entity, array_shift($diff)]));
453
+        }
454
+    }
455
+
456
+    /**
457
+     * @param class-string<IOperation> $class
458
+     * @param array<int, Check> $checks
459
+     * @param array $events
460
+     * @throws \UnexpectedValueException
461
+     */
462
+    public function validateOperation(string $class, string $name, array $checks, string $operation, ScopeContext $scope, string $entity, array $events): void {
463
+        if (strlen($operation) > IManager::MAX_OPERATION_VALUE_BYTES) {
464
+            throw new \UnexpectedValueException($this->l->t('The provided operation data is too long'));
465
+        }
466
+
467
+        /** @psalm-suppress TaintedCallable newInstance is not called */
468
+        $reflection = new \ReflectionClass($class);
469
+        if ($class !== IOperation::class && !in_array(IOperation::class, $reflection->getInterfaceNames())) {
470
+            throw new \UnexpectedValueException($this->l->t('Operation %s is invalid', [$class]) . join(', ', $reflection->getInterfaceNames()));
471
+        }
472
+
473
+        try {
474
+            /** @var IOperation $instance */
475
+            $instance = $this->container->get($class);
476
+        } catch (ContainerExceptionInterface $e) {
477
+            throw new \UnexpectedValueException($this->l->t('Operation %s does not exist', [$class]));
478
+        }
479
+
480
+        if (!$instance->isAvailableForScope($scope->getScope())) {
481
+            throw new \UnexpectedValueException($this->l->t('Operation %s is invalid', [$class]));
482
+        }
483
+
484
+        $this->validateEvents($entity, $events, $instance);
485
+
486
+        if (count($checks) === 0) {
487
+            throw new \UnexpectedValueException($this->l->t('At least one check needs to be provided'));
488
+        }
489
+
490
+        $instance->validateOperation($name, $checks, $operation);
491
+
492
+        foreach ($checks as $check) {
493
+            if (!is_string($check['class'])) {
494
+                throw new \UnexpectedValueException($this->l->t('Invalid check provided'));
495
+            }
496
+
497
+            if (strlen((string)$check['value']) > IManager::MAX_CHECK_VALUE_BYTES) {
498
+                throw new \UnexpectedValueException($this->l->t('The provided check value is too long'));
499
+            }
500
+
501
+            $reflection = new \ReflectionClass($check['class']);
502
+            if ($check['class'] !== ICheck::class && !in_array(ICheck::class, $reflection->getInterfaceNames())) {
503
+                throw new \UnexpectedValueException($this->l->t('Check %s is invalid', [$class]));
504
+            }
505
+
506
+            try {
507
+                /** @var ICheck $instance */
508
+                $instance = $this->container->get($check['class']);
509
+            } catch (ContainerExceptionInterface) {
510
+                throw new \UnexpectedValueException($this->l->t('Check %s does not exist', [$class]));
511
+            }
512
+
513
+            if (!empty($instance->supportedEntities())
514
+                && !in_array($entity, $instance->supportedEntities())
515
+            ) {
516
+                throw new \UnexpectedValueException($this->l->t('Check %s is not allowed with this entity', [$class]));
517
+            }
518
+
519
+            $instance->validateCheck($check['operator'], $check['value']);
520
+        }
521
+    }
522
+
523
+    /**
524
+     * @param int[] $checkIds
525
+     * @return array<int, Check>
526
+     */
527
+    public function getChecks(array $checkIds): array {
528
+        $checkIds = array_map('intval', $checkIds);
529
+
530
+        $checks = [];
531
+        foreach ($checkIds as $i => $checkId) {
532
+            if (isset($this->checks[$checkId])) {
533
+                $checks[$checkId] = $this->checks[$checkId];
534
+                unset($checkIds[$i]);
535
+            }
536
+        }
537
+
538
+        if (empty($checkIds)) {
539
+            return $checks;
540
+        }
541
+
542
+        $query = $this->connection->getQueryBuilder();
543
+        $query->select('*')
544
+            ->from('flow_checks')
545
+            ->where($query->expr()->in('id', $query->createNamedParameter($checkIds, IQueryBuilder::PARAM_INT_ARRAY)));
546
+        $result = $query->executeQuery();
547
+
548
+        while ($row = $result->fetchAssociative()) {
549
+            /** @var Check $row */
550
+            $this->checks[(int)$row['id']] = $row;
551
+            $checks[(int)$row['id']] = $row;
552
+        }
553
+        $result->closeCursor();
554
+
555
+        $checkIds = array_diff($checkIds, array_keys($checks));
556
+
557
+        if (!empty($checkIds)) {
558
+            $missingCheck = array_pop($checkIds);
559
+            throw new \UnexpectedValueException($this->l->t('Check #%s does not exist', (string)$missingCheck));
560
+        }
561
+
562
+        return $checks;
563
+    }
564
+
565
+    /**
566
+     * @return int Check unique ID
567
+     */
568
+    protected function addCheck(string $class, string $operator, string $value): int {
569
+        $hash = md5($class . '::' . $operator . '::' . $value);
570
+
571
+        $query = $this->connection->getQueryBuilder();
572
+        $query->select('id')
573
+            ->from('flow_checks')
574
+            ->where($query->expr()->eq('hash', $query->createNamedParameter($hash)));
575
+        $result = $query->executeQuery();
576
+
577
+        if ($row = $result->fetchAssociative()) {
578
+            $result->closeCursor();
579
+            return (int)$row['id'];
580
+        }
581
+
582
+        $query = $this->connection->getQueryBuilder();
583
+        $query->insert('flow_checks')
584
+            ->values([
585
+                'class' => $query->createNamedParameter($class),
586
+                'operator' => $query->createNamedParameter($operator),
587
+                'value' => $query->createNamedParameter($value),
588
+                'hash' => $query->createNamedParameter($hash),
589
+            ]);
590
+        $query->executeStatement();
591
+
592
+        return $query->getLastInsertId();
593
+    }
594
+
595
+    protected function addScope(int $operationId, ScopeContext $scope): void {
596
+        $query = $this->connection->getQueryBuilder();
597
+
598
+        $insertQuery = $query->insert('flow_operations_scope');
599
+        $insertQuery->values([
600
+            'operation_id' => $query->createNamedParameter($operationId),
601
+            'type' => $query->createNamedParameter($scope->getScope()),
602
+            'value' => $query->createNamedParameter($scope->getScopeId()),
603
+        ]);
604
+        $insertQuery->executeStatement();
605
+    }
606
+
607
+    public function formatOperation(array $operation): array {
608
+        $checkIds = json_decode($operation['checks'], true);
609
+        $checks = $this->getChecks($checkIds);
610
+
611
+        $operation['checks'] = [];
612
+        foreach ($checks as $check) {
613
+            // Remove internal values
614
+            unset($check['id']);
615
+            unset($check['hash']);
616
+
617
+            $operation['checks'][] = $check;
618
+        }
619
+        $operation['events'] = json_decode($operation['events'], true) ?? [];
620
+
621
+
622
+        return $operation;
623
+    }
624
+
625
+    /**
626
+     * @return IEntity[]
627
+     */
628
+    public function getEntitiesList(): array {
629
+        $this->dispatcher->dispatchTyped(new RegisterEntitiesEvent($this));
630
+
631
+        return array_values(array_merge($this->getBuildInEntities(), $this->registeredEntities));
632
+    }
633
+
634
+    /**
635
+     * @return IOperation[]
636
+     */
637
+    public function getOperatorList(): array {
638
+        $this->dispatcher->dispatchTyped(new RegisterOperationsEvent($this));
639
+
640
+        return array_merge($this->getBuildInOperators(), $this->registeredOperators);
641
+    }
642
+
643
+    /**
644
+     * @return ICheck[]
645
+     */
646
+    public function getCheckList(): array {
647
+        $this->dispatcher->dispatchTyped(new RegisterChecksEvent($this));
648
+
649
+        return array_merge($this->getBuildInChecks(), $this->registeredChecks);
650
+    }
651
+
652
+    public function registerEntity(IEntity $entity): void {
653
+        $this->registeredEntities[get_class($entity)] = $entity;
654
+    }
655
+
656
+    public function registerOperation(IOperation $operator): void {
657
+        $this->registeredOperators[get_class($operator)] = $operator;
658
+    }
659
+
660
+    public function registerCheck(ICheck $check): void {
661
+        $this->registeredChecks[get_class($check)] = $check;
662
+    }
663
+
664
+    /**
665
+     * @return IEntity[]
666
+     */
667
+    protected function getBuildInEntities(): array {
668
+        try {
669
+            return [
670
+                File::class => $this->container->get(File::class),
671
+            ];
672
+        } catch (ContainerExceptionInterface $e) {
673
+            $this->logger->error($e->getMessage(), ['exception' => $e]);
674
+            return [];
675
+        }
676
+    }
677
+
678
+    /**
679
+     * @return IOperation[]
680
+     */
681
+    protected function getBuildInOperators(): array {
682
+        try {
683
+            return [
684
+                // None yet
685
+            ];
686
+        } catch (ContainerExceptionInterface $e) {
687
+            $this->logger->error($e->getMessage(), ['exception' => $e]);
688
+            return [];
689
+        }
690
+    }
691
+
692
+    /**
693
+     * @return ICheck[]
694
+     */
695
+    protected function getBuildInChecks(): array {
696
+        try {
697
+            return [
698
+                $this->container->get(FileMimeType::class),
699
+                $this->container->get(FileName::class),
700
+                $this->container->get(FileSize::class),
701
+                $this->container->get(FileSystemTags::class),
702
+                $this->container->get(RequestRemoteAddress::class),
703
+                $this->container->get(RequestTime::class),
704
+                $this->container->get(RequestURL::class),
705
+                $this->container->get(RequestUserAgent::class),
706
+                $this->container->get(UserGroupMembership::class),
707
+            ];
708
+        } catch (ContainerExceptionInterface $e) {
709
+            $this->logger->error($e->getMessage(), ['exception' => $e]);
710
+            return [];
711
+        }
712
+    }
713
+
714
+    public function isUserScopeEnabled(): bool {
715
+        return !$this->appConfig->getAppValueBool('user_scope_disabled');
716
+    }
717 717
 }
Please login to merge, or discard this patch.
Spacing   +12 added lines, -12 removed lines patch added patch discarded remove patch
@@ -155,7 +155,7 @@  discard block
 block discarded – undo
155 155
 		while ($row = $result->fetchAssociative()) {
156 156
 			$scope = new ScopeContext($row['type'], $row['value']);
157 157
 
158
-			if (!$operation->isAvailableForScope((int)$row['type'])) {
158
+			if (!$operation->isAvailableForScope((int) $row['type'])) {
159 159
 				continue;
160 160
 			}
161 161
 
@@ -195,7 +195,7 @@  discard block
 block discarded – undo
195 195
 				continue;
196 196
 			}
197 197
 
198
-			if (!$operation->isAvailableForScope((int)$row['scope_type'])) {
198
+			if (!$operation->isAvailableForScope((int) $row['scope_type'])) {
199 199
 				continue;
200 200
 			}
201 201
 
@@ -321,7 +321,7 @@  discard block
 block discarded – undo
321 321
 
322 322
 		$operations = [];
323 323
 		while (($opId = $result->fetchOne()) !== false) {
324
-			$operations[] = (int)$opId;
324
+			$operations[] = (int) $opId;
325 325
 		}
326 326
 		$this->operationsByScope[$scopeContext->getHash()] = $operations;
327 327
 		$result->closeCursor();
@@ -393,12 +393,12 @@  discard block
 block discarded – undo
393 393
 		$query = $this->connection->getQueryBuilder();
394 394
 		try {
395 395
 			$this->connection->beginTransaction();
396
-			$result = (bool)$query->delete('flow_operations')
396
+			$result = (bool) $query->delete('flow_operations')
397 397
 				->where($query->expr()->eq('id', $query->createNamedParameter($id)))
398 398
 				->executeStatement();
399 399
 			if ($result) {
400 400
 				$qb = $this->connection->getQueryBuilder();
401
-				$result = (bool)$qb->delete('flow_operations_scope')
401
+				$result = (bool) $qb->delete('flow_operations_scope')
402 402
 					->where($qb->expr()->eq('operation_id', $qb->createNamedParameter($id)))
403 403
 					->executeStatement();
404 404
 			}
@@ -467,7 +467,7 @@  discard block
 block discarded – undo
467 467
 		/** @psalm-suppress TaintedCallable newInstance is not called */
468 468
 		$reflection = new \ReflectionClass($class);
469 469
 		if ($class !== IOperation::class && !in_array(IOperation::class, $reflection->getInterfaceNames())) {
470
-			throw new \UnexpectedValueException($this->l->t('Operation %s is invalid', [$class]) . join(', ', $reflection->getInterfaceNames()));
470
+			throw new \UnexpectedValueException($this->l->t('Operation %s is invalid', [$class]).join(', ', $reflection->getInterfaceNames()));
471 471
 		}
472 472
 
473 473
 		try {
@@ -494,7 +494,7 @@  discard block
 block discarded – undo
494 494
 				throw new \UnexpectedValueException($this->l->t('Invalid check provided'));
495 495
 			}
496 496
 
497
-			if (strlen((string)$check['value']) > IManager::MAX_CHECK_VALUE_BYTES) {
497
+			if (strlen((string) $check['value']) > IManager::MAX_CHECK_VALUE_BYTES) {
498 498
 				throw new \UnexpectedValueException($this->l->t('The provided check value is too long'));
499 499
 			}
500 500
 
@@ -547,8 +547,8 @@  discard block
 block discarded – undo
547 547
 
548 548
 		while ($row = $result->fetchAssociative()) {
549 549
 			/** @var Check $row */
550
-			$this->checks[(int)$row['id']] = $row;
551
-			$checks[(int)$row['id']] = $row;
550
+			$this->checks[(int) $row['id']] = $row;
551
+			$checks[(int) $row['id']] = $row;
552 552
 		}
553 553
 		$result->closeCursor();
554 554
 
@@ -556,7 +556,7 @@  discard block
 block discarded – undo
556 556
 
557 557
 		if (!empty($checkIds)) {
558 558
 			$missingCheck = array_pop($checkIds);
559
-			throw new \UnexpectedValueException($this->l->t('Check #%s does not exist', (string)$missingCheck));
559
+			throw new \UnexpectedValueException($this->l->t('Check #%s does not exist', (string) $missingCheck));
560 560
 		}
561 561
 
562 562
 		return $checks;
@@ -566,7 +566,7 @@  discard block
 block discarded – undo
566 566
 	 * @return int Check unique ID
567 567
 	 */
568 568
 	protected function addCheck(string $class, string $operator, string $value): int {
569
-		$hash = md5($class . '::' . $operator . '::' . $value);
569
+		$hash = md5($class.'::'.$operator.'::'.$value);
570 570
 
571 571
 		$query = $this->connection->getQueryBuilder();
572 572
 		$query->select('id')
@@ -576,7 +576,7 @@  discard block
 block discarded – undo
576 576
 
577 577
 		if ($row = $result->fetchAssociative()) {
578 578
 			$result->closeCursor();
579
-			return (int)$row['id'];
579
+			return (int) $row['id'];
580 580
 		}
581 581
 
582 582
 		$query = $this->connection->getQueryBuilder();
Please login to merge, or discard this patch.
apps/workflowengine/tests/ManagerTest.php 1 patch
Indentation   +747 added lines, -747 removed lines patch added patch discarded remove patch
@@ -39,33 +39,33 @@  discard block
 block discarded – undo
39 39
 use Test\TestCase;
40 40
 
41 41
 class TestAdminOp implements IOperation {
42
-	public function getDisplayName(): string {
43
-		return 'Admin';
44
-	}
42
+    public function getDisplayName(): string {
43
+        return 'Admin';
44
+    }
45 45
 
46
-	public function getDescription(): string {
47
-		return '';
48
-	}
46
+    public function getDescription(): string {
47
+        return '';
48
+    }
49 49
 
50
-	public function getIcon(): string {
51
-		return '';
52
-	}
50
+    public function getIcon(): string {
51
+        return '';
52
+    }
53 53
 
54
-	public function isAvailableForScope(int $scope): bool {
55
-		return true;
56
-	}
54
+    public function isAvailableForScope(int $scope): bool {
55
+        return true;
56
+    }
57 57
 
58
-	public function validateOperation(string $name, array $checks, string $operation): void {
59
-	}
58
+    public function validateOperation(string $name, array $checks, string $operation): void {
59
+    }
60 60
 
61
-	public function onEvent(string $eventName, Event $event, IRuleMatcher $ruleMatcher): void {
62
-	}
61
+    public function onEvent(string $eventName, Event $event, IRuleMatcher $ruleMatcher): void {
62
+    }
63 63
 }
64 64
 
65 65
 class TestUserOp extends TestAdminOp {
66
-	public function getDisplayName(): string {
67
-		return 'User';
68
-	}
66
+    public function getDisplayName(): string {
67
+        return 'User';
68
+    }
69 69
 }
70 70
 
71 71
 /**
@@ -75,732 +75,732 @@  discard block
 block discarded – undo
75 75
  */
76 76
 #[\PHPUnit\Framework\Attributes\Group('DB')]
77 77
 class ManagerTest extends TestCase {
78
-	protected Manager $manager;
79
-	protected IDBConnection $db;
80
-	protected LoggerInterface&MockObject $logger;
81
-	protected ContainerInterface&MockObject $container;
82
-	protected IUserSession&MockObject $session;
83
-	protected IL10N&MockObject $l;
84
-	protected IEventDispatcher&MockObject $dispatcher;
85
-	protected IAppConfig&MockObject $config;
86
-	protected ICacheFactory&MockObject $cacheFactory;
87
-
88
-	protected function setUp(): void {
89
-		parent::setUp();
90
-
91
-		$this->db = Server::get(IDBConnection::class);
92
-		$this->container = $this->createMock(ContainerInterface::class);
93
-		$this->l = $this->createMock(IL10N::class);
94
-		$this->l->method('t')
95
-			->willReturnCallback(function ($text, $parameters = []) {
96
-				return vsprintf($text, $parameters);
97
-			});
98
-
99
-		$this->logger = $this->createMock(LoggerInterface::class);
100
-		$this->session = $this->createMock(IUserSession::class);
101
-		$this->dispatcher = $this->createMock(IEventDispatcher::class);
102
-		$this->config = $this->createMock(IAppConfig::class);
103
-		$this->cacheFactory = $this->createMock(ICacheFactory::class);
104
-
105
-		$this->manager = new Manager(
106
-			$this->db,
107
-			$this->container,
108
-			$this->l,
109
-			$this->logger,
110
-			$this->session,
111
-			$this->dispatcher,
112
-			$this->config,
113
-			$this->cacheFactory
114
-		);
115
-		$this->clearTables();
116
-	}
117
-
118
-	protected function tearDown(): void {
119
-		$this->clearTables();
120
-		parent::tearDown();
121
-	}
122
-
123
-	protected function buildScope(?string $scopeId = null): MockObject&ScopeContext {
124
-		$scopeContext = $this->createMock(ScopeContext::class);
125
-		$scopeContext->expects($this->any())
126
-			->method('getScope')
127
-			->willReturn($scopeId ? IManager::SCOPE_USER : IManager::SCOPE_ADMIN);
128
-		$scopeContext->expects($this->any())
129
-			->method('getScopeId')
130
-			->willReturn($scopeId ?? '');
131
-		$scopeContext->expects($this->any())
132
-			->method('getHash')
133
-			->willReturn(md5($scopeId ?? ''));
134
-
135
-		return $scopeContext;
136
-	}
137
-
138
-	public function clearTables() {
139
-		$query = $this->db->getQueryBuilder();
140
-		foreach (['flow_checks', 'flow_operations', 'flow_operations_scope'] as $table) {
141
-			$query->delete($table)
142
-				->executeStatement();
143
-		}
144
-	}
145
-
146
-	public function testChecks(): void {
147
-		$check1 = $this->invokePrivate($this->manager, 'addCheck', ['Test', 'equal', 1]);
148
-		$check2 = $this->invokePrivate($this->manager, 'addCheck', ['Test', '!equal', 2]);
149
-
150
-		$data = $this->manager->getChecks([$check1]);
151
-		$this->assertArrayHasKey($check1, $data);
152
-		$this->assertArrayNotHasKey($check2, $data);
153
-
154
-		$data = $this->manager->getChecks([$check1, $check2]);
155
-		$this->assertArrayHasKey($check1, $data);
156
-		$this->assertArrayHasKey($check2, $data);
157
-
158
-		$data = $this->manager->getChecks([$check2, $check1]);
159
-		$this->assertArrayHasKey($check1, $data);
160
-		$this->assertArrayHasKey($check2, $data);
161
-
162
-		$data = $this->manager->getChecks([$check2]);
163
-		$this->assertArrayNotHasKey($check1, $data);
164
-		$this->assertArrayHasKey($check2, $data);
165
-	}
166
-
167
-	public function testScope(): void {
168
-		$adminScope = $this->buildScope();
169
-		$userScope = $this->buildScope('jackie');
170
-		$entity = File::class;
171
-
172
-		$opId1 = $this->invokePrivate(
173
-			$this->manager,
174
-			'insertOperation',
175
-			['OCA\WFE\TestOp', 'Test01', [11, 22], 'foo', $entity, []]
176
-		);
177
-		$this->invokePrivate($this->manager, 'addScope', [$opId1, $adminScope]);
178
-
179
-		$opId2 = $this->invokePrivate(
180
-			$this->manager,
181
-			'insertOperation',
182
-			['OCA\WFE\TestOp', 'Test02', [33, 22], 'bar', $entity, []]
183
-		);
184
-		$this->invokePrivate($this->manager, 'addScope', [$opId2, $userScope]);
185
-		$opId3 = $this->invokePrivate(
186
-			$this->manager,
187
-			'insertOperation',
188
-			['OCA\WFE\TestOp', 'Test03', [11, 44], 'foobar', $entity, []]
189
-		);
190
-		$this->invokePrivate($this->manager, 'addScope', [$opId3, $userScope]);
191
-
192
-		$this->assertTrue($this->invokePrivate($this->manager, 'canModify', [$opId1, $adminScope]));
193
-		$this->assertFalse($this->invokePrivate($this->manager, 'canModify', [$opId2, $adminScope]));
194
-		$this->assertFalse($this->invokePrivate($this->manager, 'canModify', [$opId3, $adminScope]));
195
-
196
-		$this->assertFalse($this->invokePrivate($this->manager, 'canModify', [$opId1, $userScope]));
197
-		$this->assertTrue($this->invokePrivate($this->manager, 'canModify', [$opId2, $userScope]));
198
-		$this->assertTrue($this->invokePrivate($this->manager, 'canModify', [$opId3, $userScope]));
199
-	}
200
-
201
-	public function testGetAllOperations(): void {
202
-		$adminScope = $this->buildScope();
203
-		$userScope = $this->buildScope('jackie');
204
-		$entity = File::class;
205
-
206
-		$adminOperation = $this->createMock(IOperation::class);
207
-		$adminOperation->expects($this->any())
208
-			->method('isAvailableForScope')
209
-			->willReturnMap([
210
-				[IManager::SCOPE_ADMIN, true],
211
-				[IManager::SCOPE_USER, false],
212
-			]);
213
-		$userOperation = $this->createMock(IOperation::class);
214
-		$userOperation->expects($this->any())
215
-			->method('isAvailableForScope')
216
-			->willReturnMap([
217
-				[IManager::SCOPE_ADMIN, false],
218
-				[IManager::SCOPE_USER, true],
219
-			]);
220
-
221
-		$this->container->expects($this->any())
222
-			->method('get')
223
-			->willReturnCallback(function ($className) use ($adminOperation, $userOperation) {
224
-				switch ($className) {
225
-					case 'OCA\WFE\TestAdminOp':
226
-						return $adminOperation;
227
-					case 'OCA\WFE\TestUserOp':
228
-						return $userOperation;
229
-				}
230
-			});
231
-
232
-		$opId1 = $this->invokePrivate(
233
-			$this->manager,
234
-			'insertOperation',
235
-			['OCA\WFE\TestAdminOp', 'Test01', [11, 22], 'foo', $entity, []]
236
-		);
237
-		$this->invokePrivate($this->manager, 'addScope', [$opId1, $adminScope]);
238
-
239
-		$opId2 = $this->invokePrivate(
240
-			$this->manager,
241
-			'insertOperation',
242
-			['OCA\WFE\TestUserOp', 'Test02', [33, 22], 'bar', $entity, []]
243
-		);
244
-		$this->invokePrivate($this->manager, 'addScope', [$opId2, $userScope]);
245
-		$opId3 = $this->invokePrivate(
246
-			$this->manager,
247
-			'insertOperation',
248
-			['OCA\WFE\TestUserOp', 'Test03', [11, 44], 'foobar', $entity, []]
249
-		);
250
-		$this->invokePrivate($this->manager, 'addScope', [$opId3, $userScope]);
251
-
252
-		$opId4 = $this->invokePrivate(
253
-			$this->manager,
254
-			'insertOperation',
255
-			['OCA\WFE\TestAdminOp', 'Test04', [41, 10, 4], 'NoBar', $entity, []]
256
-		);
257
-		$this->invokePrivate($this->manager, 'addScope', [$opId4, $userScope]);
258
-
259
-		$adminOps = $this->manager->getAllOperations($adminScope);
260
-		$userOps = $this->manager->getAllOperations($userScope);
261
-
262
-		$this->assertSame(1, count($adminOps));
263
-		$this->assertTrue(array_key_exists('OCA\WFE\TestAdminOp', $adminOps));
264
-		$this->assertFalse(array_key_exists('OCA\WFE\TestUserOp', $adminOps));
265
-
266
-		$this->assertSame(1, count($userOps));
267
-		$this->assertFalse(array_key_exists('OCA\WFE\TestAdminOp', $userOps));
268
-		$this->assertTrue(array_key_exists('OCA\WFE\TestUserOp', $userOps));
269
-		$this->assertSame(2, count($userOps['OCA\WFE\TestUserOp']));
270
-	}
271
-
272
-	public function testGetOperations(): void {
273
-		$adminScope = $this->buildScope();
274
-		$userScope = $this->buildScope('jackie');
275
-		$entity = File::class;
276
-
277
-		$opId1 = $this->invokePrivate(
278
-			$this->manager,
279
-			'insertOperation',
280
-			['OCA\WFE\TestOp', 'Test01', [11, 22], 'foo', $entity, []]
281
-		);
282
-		$this->invokePrivate($this->manager, 'addScope', [$opId1, $adminScope]);
283
-		$opId4 = $this->invokePrivate(
284
-			$this->manager,
285
-			'insertOperation',
286
-			['OCA\WFE\OtherTestOp', 'Test04', [5], 'foo', $entity, []]
287
-		);
288
-		$this->invokePrivate($this->manager, 'addScope', [$opId4, $adminScope]);
289
-
290
-		$opId2 = $this->invokePrivate(
291
-			$this->manager,
292
-			'insertOperation',
293
-			['OCA\WFE\TestOp', 'Test02', [33, 22], 'bar', $entity, []]
294
-		);
295
-		$this->invokePrivate($this->manager, 'addScope', [$opId2, $userScope]);
296
-		$opId3 = $this->invokePrivate(
297
-			$this->manager,
298
-			'insertOperation',
299
-			['OCA\WFE\TestOp', 'Test03', [11, 44], 'foobar', $entity, []]
300
-		);
301
-		$this->invokePrivate($this->manager, 'addScope', [$opId3, $userScope]);
302
-		$opId5 = $this->invokePrivate(
303
-			$this->manager,
304
-			'insertOperation',
305
-			['OCA\WFE\OtherTestOp', 'Test05', [5], 'foobar', $entity, []]
306
-		);
307
-		$this->invokePrivate($this->manager, 'addScope', [$opId5, $userScope]);
308
-
309
-		$operation = $this->createMock(IOperation::class);
310
-		$operation->expects($this->any())
311
-			->method('isAvailableForScope')
312
-			->willReturnMap([
313
-				[IManager::SCOPE_ADMIN, true],
314
-				[IManager::SCOPE_USER, true],
315
-			]);
316
-
317
-		$this->container->expects($this->any())
318
-			->method('get')
319
-			->willReturnCallback(function ($className) use ($operation) {
320
-				switch ($className) {
321
-					case 'OCA\WFE\TestOp':
322
-						return $operation;
323
-					case 'OCA\WFE\OtherTestOp':
324
-						throw new QueryException();
325
-				}
326
-			});
327
-
328
-		$adminOps = $this->manager->getOperations('OCA\WFE\TestOp', $adminScope);
329
-		$userOps = $this->manager->getOperations('OCA\WFE\TestOp', $userScope);
330
-
331
-		$this->assertSame(1, count($adminOps));
332
-		array_walk($adminOps, function ($op): void {
333
-			$this->assertTrue($op['class'] === 'OCA\WFE\TestOp');
334
-		});
335
-
336
-		$this->assertSame(2, count($userOps));
337
-		array_walk($userOps, function ($op): void {
338
-			$this->assertTrue($op['class'] === 'OCA\WFE\TestOp');
339
-		});
340
-	}
341
-
342
-	public function testGetAllConfiguredEvents(): void {
343
-		$adminScope = $this->buildScope();
344
-		$userScope = $this->buildScope('jackie');
345
-		$entity = File::class;
346
-
347
-		$opId5 = $this->invokePrivate(
348
-			$this->manager,
349
-			'insertOperation',
350
-			['OCA\WFE\OtherTestOp', 'Test04', [], 'foo', $entity, [NodeCreatedEvent::class]]
351
-		);
352
-		$this->invokePrivate($this->manager, 'addScope', [$opId5, $userScope]);
353
-
354
-		$allOperations = null;
355
-
356
-		$cache = $this->createMock(ICache::class);
357
-		$cache
358
-			->method('get')
359
-			->willReturnCallback(function () use (&$allOperations) {
360
-				if ($allOperations) {
361
-					return $allOperations;
362
-				}
363
-
364
-				return null;
365
-			});
366
-
367
-		$this->cacheFactory->method('createDistributed')->willReturn($cache);
368
-		$allOperations = $this->manager->getAllConfiguredEvents();
369
-		$this->assertCount(1, $allOperations);
370
-
371
-		$allOperationsCached = $this->manager->getAllConfiguredEvents();
372
-		$this->assertCount(1, $allOperationsCached);
373
-		$this->assertEquals($allOperationsCached, $allOperations);
374
-	}
375
-
376
-	public function testUpdateOperation(): void {
377
-		$adminScope = $this->buildScope();
378
-		$userScope = $this->buildScope('jackie');
379
-		$entity = File::class;
380
-
381
-		$cache = $this->createMock(ICache::class);
382
-		$cache->expects($this->exactly(4))
383
-			->method('remove')
384
-			->with('events');
385
-		$this->cacheFactory->method('createDistributed')
386
-			->willReturn($cache);
387
-
388
-		$expectedCalls = [
389
-			[IManager::SCOPE_ADMIN],
390
-			[IManager::SCOPE_USER],
391
-		];
392
-		$i = 0;
393
-		$operationMock = $this->createMock(IOperation::class);
394
-		$operationMock->expects($this->any())
395
-			->method('isAvailableForScope')
396
-			->willReturnCallback(function () use (&$expectedCalls, &$i): bool {
397
-				$this->assertEquals($expectedCalls[$i], func_get_args());
398
-				$i++;
399
-				return true;
400
-			});
401
-
402
-		$this->container->expects($this->any())
403
-			->method('get')
404
-			->willReturnCallback(function ($class) use ($operationMock) {
405
-				if (substr($class, -2) === 'Op') {
406
-					return $operationMock;
407
-				} elseif ($class === File::class) {
408
-					return $this->getMockBuilder(File::class)
409
-						->setConstructorArgs([
410
-							$this->l,
411
-							$this->createMock(IURLGenerator::class),
412
-							$this->createMock(IRootFolder::class),
413
-							$this->createMock(IUserSession::class),
414
-							$this->createMock(ISystemTagManager::class),
415
-							$this->createMock(IUserManager::class),
416
-							$this->createMock(UserMountCache::class),
417
-							$this->createMock(IMountManager::class),
418
-						])
419
-						->onlyMethods($this->filterClassMethods(File::class, ['getEvents']))
420
-						->getMock();
421
-				}
422
-				return $this->createMock(ICheck::class);
423
-			});
424
-
425
-		$opId1 = $this->invokePrivate(
426
-			$this->manager,
427
-			'insertOperation',
428
-			[TestAdminOp::class, 'Test01', [11, 22], 'foo', $entity, []]
429
-		);
430
-		$this->invokePrivate($this->manager, 'addScope', [$opId1, $adminScope]);
431
-
432
-		$opId2 = $this->invokePrivate(
433
-			$this->manager,
434
-			'insertOperation',
435
-			[TestUserOp::class, 'Test02', [33, 22], 'bar', $entity, []]
436
-		);
437
-		$this->invokePrivate($this->manager, 'addScope', [$opId2, $userScope]);
438
-
439
-		$check1 = ['class' => ICheck::class, 'operator' => 'eq', 'value' => 'asdf'];
440
-		$check2 = ['class' => ICheck::class, 'operator' => 'eq', 'value' => 23456];
441
-
442
-		/** @noinspection PhpUnhandledExceptionInspection */
443
-		$op = $this->manager->updateOperation($opId1, 'Test01a', [$check1, $check2], 'foohur', $adminScope, $entity, ['\OCP\Files::postDelete']);
444
-		$this->assertSame('Test01a', $op['name']);
445
-		$this->assertSame('foohur', $op['operation']);
446
-
447
-		/** @noinspection PhpUnhandledExceptionInspection */
448
-		$op = $this->manager->updateOperation($opId2, 'Test02a', [$check1], 'barfoo', $userScope, $entity, ['\OCP\Files::postDelete']);
449
-		$this->assertSame('Test02a', $op['name']);
450
-		$this->assertSame('barfoo', $op['operation']);
451
-
452
-		foreach ([[$adminScope, $opId2], [$userScope, $opId1]] as $run) {
453
-			try {
454
-				/** @noinspection PhpUnhandledExceptionInspection */
455
-				$this->manager->updateOperation($run[1], 'Evil', [$check2], 'hackx0r', $run[0], $entity, []);
456
-				$this->assertTrue(false, 'DomainException not thrown');
457
-			} catch (\DomainException $e) {
458
-				$this->assertTrue(true);
459
-			}
460
-		}
461
-	}
462
-
463
-	public function testDeleteOperation(): void {
464
-		$adminScope = $this->buildScope();
465
-		$userScope = $this->buildScope('jackie');
466
-		$entity = File::class;
467
-
468
-		$cache = $this->createMock(ICache::class);
469
-		$cache->expects($this->exactly(4))
470
-			->method('remove')
471
-			->with('events');
472
-		$this->cacheFactory->method('createDistributed')->willReturn($cache);
473
-
474
-		$opId1 = $this->invokePrivate(
475
-			$this->manager,
476
-			'insertOperation',
477
-			['OCA\WFE\TestAdminOp', 'Test01', [11, 22], 'foo', $entity, []]
478
-		);
479
-		$this->invokePrivate($this->manager, 'addScope', [$opId1, $adminScope]);
480
-
481
-		$opId2 = $this->invokePrivate(
482
-			$this->manager,
483
-			'insertOperation',
484
-			['OCA\WFE\TestUserOp', 'Test02', [33, 22], 'bar', $entity, []]
485
-		);
486
-		$this->invokePrivate($this->manager, 'addScope', [$opId2, $userScope]);
487
-
488
-		foreach ([[$adminScope, $opId2], [$userScope, $opId1]] as $run) {
489
-			try {
490
-				/** @noinspection PhpUnhandledExceptionInspection */
491
-				$this->manager->deleteOperation($run[1], $run[0]);
492
-				$this->assertTrue(false, 'DomainException not thrown');
493
-			} catch (\Exception $e) {
494
-				$this->assertInstanceOf(\DomainException::class, $e);
495
-			}
496
-		}
497
-
498
-		/** @noinspection PhpUnhandledExceptionInspection */
499
-		$this->manager->deleteOperation($opId1, $adminScope);
500
-		/** @noinspection PhpUnhandledExceptionInspection */
501
-		$this->manager->deleteOperation($opId2, $userScope);
502
-
503
-		foreach ([$opId1, $opId2] as $opId) {
504
-			try {
505
-				$this->invokePrivate($this->manager, 'getOperation', [$opId]);
506
-				$this->assertTrue(false, 'UnexpectedValueException not thrown');
507
-			} catch (\Exception $e) {
508
-				$this->assertInstanceOf(\UnexpectedValueException::class, $e);
509
-			}
510
-		}
511
-	}
512
-
513
-	public function testGetEntitiesListBuildInOnly(): void {
514
-		$fileEntityMock = $this->createMock(File::class);
515
-
516
-		$this->container->expects($this->once())
517
-			->method('get')
518
-			->with(File::class)
519
-			->willReturn($fileEntityMock);
520
-
521
-		$entities = $this->manager->getEntitiesList();
522
-
523
-		$this->assertCount(1, $entities);
524
-		$this->assertInstanceOf(IEntity::class, $entities[0]);
525
-	}
526
-
527
-	public function testGetEntitiesList(): void {
528
-		$fileEntityMock = $this->createMock(File::class);
529
-
530
-		$this->container->expects($this->once())
531
-			->method('get')
532
-			->with(File::class)
533
-			->willReturn($fileEntityMock);
534
-
535
-		$extraEntity = $this->createMock(IEntity::class);
536
-
537
-		$this->dispatcher->expects($this->once())
538
-			->method('dispatchTyped')
539
-			->willReturnCallback(function (RegisterEntitiesEvent $e) use ($extraEntity): void {
540
-				$this->manager->registerEntity($extraEntity);
541
-			});
542
-
543
-		$entities = $this->manager->getEntitiesList();
544
-
545
-		$this->assertCount(2, $entities);
546
-
547
-		$entityTypeCounts = array_reduce($entities, function (array $carry, IEntity $entity) {
548
-			if ($entity instanceof File) {
549
-				$carry[0]++;
550
-			} elseif ($entity instanceof IEntity) {
551
-				$carry[1]++;
552
-			}
553
-			return $carry;
554
-		}, [0, 0]);
555
-
556
-		$this->assertSame(1, $entityTypeCounts[0]);
557
-		$this->assertSame(1, $entityTypeCounts[1]);
558
-	}
559
-
560
-	public function testValidateOperationOK(): void {
561
-		$check = [
562
-			'class' => ICheck::class,
563
-			'operator' => 'is',
564
-			'value' => 'barfoo',
565
-		];
566
-
567
-		$operationMock = $this->createMock(IOperation::class);
568
-		$entityMock = $this->createMock(IEntity::class);
569
-		$eventEntityMock = $this->createMock(IEntityEvent::class);
570
-		$checkMock = $this->createMock(ICheck::class);
571
-		$scopeMock = $this->createMock(ScopeContext::class);
572
-
573
-		$scopeMock->expects($this->any())
574
-			->method('getScope')
575
-			->willReturn(IManager::SCOPE_ADMIN);
576
-
577
-		$operationMock->expects($this->once())
578
-			->method('isAvailableForScope')
579
-			->with(IManager::SCOPE_ADMIN)
580
-			->willReturn(true);
581
-
582
-		$operationMock->expects($this->once())
583
-			->method('validateOperation')
584
-			->with('test', [$check], 'operationData');
585
-
586
-		$entityMock->expects($this->any())
587
-			->method('getEvents')
588
-			->willReturn([$eventEntityMock]);
589
-
590
-		$eventEntityMock->expects($this->any())
591
-			->method('getEventName')
592
-			->willReturn('MyEvent');
593
-
594
-		$checkMock->expects($this->any())
595
-			->method('supportedEntities')
596
-			->willReturn([IEntity::class]);
597
-		$checkMock->expects($this->atLeastOnce())
598
-			->method('validateCheck');
599
-
600
-		$this->container->expects($this->any())
601
-			->method('get')
602
-			->willReturnCallback(function ($className) use ($operationMock, $entityMock, $eventEntityMock, $checkMock) {
603
-				switch ($className) {
604
-					case IOperation::class:
605
-						return $operationMock;
606
-					case IEntity::class:
607
-						return $entityMock;
608
-					case IEntityEvent::class:
609
-						return $eventEntityMock;
610
-					case ICheck::class:
611
-						return $checkMock;
612
-					default:
613
-						return $this->createMock($className);
614
-				}
615
-			});
616
-
617
-		$this->manager->validateOperation(IOperation::class, 'test', [$check], 'operationData', $scopeMock, IEntity::class, ['MyEvent']);
618
-	}
619
-
620
-	public function testValidateOperationCheckInputLengthError(): void {
621
-		$check = [
622
-			'class' => ICheck::class,
623
-			'operator' => 'is',
624
-			'value' => str_pad('', IManager::MAX_CHECK_VALUE_BYTES + 1, 'FooBar'),
625
-		];
626
-
627
-		$operationMock = $this->createMock(IOperation::class);
628
-		$entityMock = $this->createMock(IEntity::class);
629
-		$eventEntityMock = $this->createMock(IEntityEvent::class);
630
-		$checkMock = $this->createMock(ICheck::class);
631
-		$scopeMock = $this->createMock(ScopeContext::class);
632
-
633
-		$scopeMock->expects($this->any())
634
-			->method('getScope')
635
-			->willReturn(IManager::SCOPE_ADMIN);
636
-
637
-		$operationMock->expects($this->once())
638
-			->method('isAvailableForScope')
639
-			->with(IManager::SCOPE_ADMIN)
640
-			->willReturn(true);
641
-
642
-		$operationMock->expects($this->once())
643
-			->method('validateOperation')
644
-			->with('test', [$check], 'operationData');
645
-
646
-		$entityMock->expects($this->any())
647
-			->method('getEvents')
648
-			->willReturn([$eventEntityMock]);
649
-
650
-		$eventEntityMock->expects($this->any())
651
-			->method('getEventName')
652
-			->willReturn('MyEvent');
653
-
654
-		$checkMock->expects($this->any())
655
-			->method('supportedEntities')
656
-			->willReturn([IEntity::class]);
657
-		$checkMock->expects($this->never())
658
-			->method('validateCheck');
659
-
660
-		$this->container->expects($this->any())
661
-			->method('get')
662
-			->willReturnCallback(function ($className) use ($operationMock, $entityMock, $eventEntityMock, $checkMock) {
663
-				switch ($className) {
664
-					case IOperation::class:
665
-						return $operationMock;
666
-					case IEntity::class:
667
-						return $entityMock;
668
-					case IEntityEvent::class:
669
-						return $eventEntityMock;
670
-					case ICheck::class:
671
-						return $checkMock;
672
-					default:
673
-						return $this->createMock($className);
674
-				}
675
-			});
676
-
677
-		try {
678
-			$this->manager->validateOperation(IOperation::class, 'test', [$check], 'operationData', $scopeMock, IEntity::class, ['MyEvent']);
679
-		} catch (\UnexpectedValueException $e) {
680
-			$this->assertSame('The provided check value is too long', $e->getMessage());
681
-		}
682
-	}
683
-
684
-	public function testValidateOperationDataLengthError(): void {
685
-		$check = [
686
-			'class' => ICheck::class,
687
-			'operator' => 'is',
688
-			'value' => 'barfoo',
689
-		];
690
-		$operationData = str_pad('', IManager::MAX_OPERATION_VALUE_BYTES + 1, 'FooBar');
691
-
692
-		$operationMock = $this->createMock(IOperation::class);
693
-		$entityMock = $this->createMock(IEntity::class);
694
-		$eventEntityMock = $this->createMock(IEntityEvent::class);
695
-		$checkMock = $this->createMock(ICheck::class);
696
-		$scopeMock = $this->createMock(ScopeContext::class);
697
-
698
-		$scopeMock->expects($this->any())
699
-			->method('getScope')
700
-			->willReturn(IManager::SCOPE_ADMIN);
701
-
702
-		$operationMock->expects($this->never())
703
-			->method('validateOperation');
704
-
705
-		$entityMock->expects($this->any())
706
-			->method('getEvents')
707
-			->willReturn([$eventEntityMock]);
708
-
709
-		$eventEntityMock->expects($this->any())
710
-			->method('getEventName')
711
-			->willReturn('MyEvent');
712
-
713
-		$checkMock->expects($this->any())
714
-			->method('supportedEntities')
715
-			->willReturn([IEntity::class]);
716
-		$checkMock->expects($this->never())
717
-			->method('validateCheck');
718
-
719
-		$this->container->expects($this->any())
720
-			->method('get')
721
-			->willReturnCallback(function ($className) use ($operationMock, $entityMock, $eventEntityMock, $checkMock) {
722
-				switch ($className) {
723
-					case IOperation::class:
724
-						return $operationMock;
725
-					case IEntity::class:
726
-						return $entityMock;
727
-					case IEntityEvent::class:
728
-						return $eventEntityMock;
729
-					case ICheck::class:
730
-						return $checkMock;
731
-					default:
732
-						return $this->createMock($className);
733
-				}
734
-			});
735
-
736
-		try {
737
-			$this->manager->validateOperation(IOperation::class, 'test', [$check], $operationData, $scopeMock, IEntity::class, ['MyEvent']);
738
-		} catch (\UnexpectedValueException $e) {
739
-			$this->assertSame('The provided operation data is too long', $e->getMessage());
740
-		}
741
-	}
742
-
743
-	public function testValidateOperationScopeNotAvailable(): void {
744
-		$check = [
745
-			'class' => ICheck::class,
746
-			'operator' => 'is',
747
-			'value' => 'barfoo',
748
-		];
749
-		$operationData = str_pad('', IManager::MAX_OPERATION_VALUE_BYTES - 1, 'FooBar');
750
-
751
-		$operationMock = $this->createMock(IOperation::class);
752
-		$entityMock = $this->createMock(IEntity::class);
753
-		$eventEntityMock = $this->createMock(IEntityEvent::class);
754
-		$checkMock = $this->createMock(ICheck::class);
755
-		$scopeMock = $this->createMock(ScopeContext::class);
756
-
757
-		$scopeMock->expects($this->any())
758
-			->method('getScope')
759
-			->willReturn(IManager::SCOPE_ADMIN);
760
-
761
-		$operationMock->expects($this->once())
762
-			->method('isAvailableForScope')
763
-			->with(IManager::SCOPE_ADMIN)
764
-			->willReturn(false);
765
-
766
-		$operationMock->expects($this->never())
767
-			->method('validateOperation');
768
-
769
-		$entityMock->expects($this->any())
770
-			->method('getEvents')
771
-			->willReturn([$eventEntityMock]);
772
-
773
-		$eventEntityMock->expects($this->any())
774
-			->method('getEventName')
775
-			->willReturn('MyEvent');
776
-
777
-		$checkMock->expects($this->any())
778
-			->method('supportedEntities')
779
-			->willReturn([IEntity::class]);
780
-		$checkMock->expects($this->never())
781
-			->method('validateCheck');
782
-
783
-		$this->container->expects($this->any())
784
-			->method('get')
785
-			->willReturnCallback(function ($className) use ($operationMock, $entityMock, $eventEntityMock, $checkMock) {
786
-				switch ($className) {
787
-					case IOperation::class:
788
-						return $operationMock;
789
-					case IEntity::class:
790
-						return $entityMock;
791
-					case IEntityEvent::class:
792
-						return $eventEntityMock;
793
-					case ICheck::class:
794
-						return $checkMock;
795
-					default:
796
-						return $this->createMock($className);
797
-				}
798
-			});
799
-
800
-		try {
801
-			$this->manager->validateOperation(IOperation::class, 'test', [$check], $operationData, $scopeMock, IEntity::class, ['MyEvent']);
802
-		} catch (\UnexpectedValueException $e) {
803
-			$this->assertSame('Operation OCP\WorkflowEngine\IOperation is invalid', $e->getMessage());
804
-		}
805
-	}
78
+    protected Manager $manager;
79
+    protected IDBConnection $db;
80
+    protected LoggerInterface&MockObject $logger;
81
+    protected ContainerInterface&MockObject $container;
82
+    protected IUserSession&MockObject $session;
83
+    protected IL10N&MockObject $l;
84
+    protected IEventDispatcher&MockObject $dispatcher;
85
+    protected IAppConfig&MockObject $config;
86
+    protected ICacheFactory&MockObject $cacheFactory;
87
+
88
+    protected function setUp(): void {
89
+        parent::setUp();
90
+
91
+        $this->db = Server::get(IDBConnection::class);
92
+        $this->container = $this->createMock(ContainerInterface::class);
93
+        $this->l = $this->createMock(IL10N::class);
94
+        $this->l->method('t')
95
+            ->willReturnCallback(function ($text, $parameters = []) {
96
+                return vsprintf($text, $parameters);
97
+            });
98
+
99
+        $this->logger = $this->createMock(LoggerInterface::class);
100
+        $this->session = $this->createMock(IUserSession::class);
101
+        $this->dispatcher = $this->createMock(IEventDispatcher::class);
102
+        $this->config = $this->createMock(IAppConfig::class);
103
+        $this->cacheFactory = $this->createMock(ICacheFactory::class);
104
+
105
+        $this->manager = new Manager(
106
+            $this->db,
107
+            $this->container,
108
+            $this->l,
109
+            $this->logger,
110
+            $this->session,
111
+            $this->dispatcher,
112
+            $this->config,
113
+            $this->cacheFactory
114
+        );
115
+        $this->clearTables();
116
+    }
117
+
118
+    protected function tearDown(): void {
119
+        $this->clearTables();
120
+        parent::tearDown();
121
+    }
122
+
123
+    protected function buildScope(?string $scopeId = null): MockObject&ScopeContext {
124
+        $scopeContext = $this->createMock(ScopeContext::class);
125
+        $scopeContext->expects($this->any())
126
+            ->method('getScope')
127
+            ->willReturn($scopeId ? IManager::SCOPE_USER : IManager::SCOPE_ADMIN);
128
+        $scopeContext->expects($this->any())
129
+            ->method('getScopeId')
130
+            ->willReturn($scopeId ?? '');
131
+        $scopeContext->expects($this->any())
132
+            ->method('getHash')
133
+            ->willReturn(md5($scopeId ?? ''));
134
+
135
+        return $scopeContext;
136
+    }
137
+
138
+    public function clearTables() {
139
+        $query = $this->db->getQueryBuilder();
140
+        foreach (['flow_checks', 'flow_operations', 'flow_operations_scope'] as $table) {
141
+            $query->delete($table)
142
+                ->executeStatement();
143
+        }
144
+    }
145
+
146
+    public function testChecks(): void {
147
+        $check1 = $this->invokePrivate($this->manager, 'addCheck', ['Test', 'equal', 1]);
148
+        $check2 = $this->invokePrivate($this->manager, 'addCheck', ['Test', '!equal', 2]);
149
+
150
+        $data = $this->manager->getChecks([$check1]);
151
+        $this->assertArrayHasKey($check1, $data);
152
+        $this->assertArrayNotHasKey($check2, $data);
153
+
154
+        $data = $this->manager->getChecks([$check1, $check2]);
155
+        $this->assertArrayHasKey($check1, $data);
156
+        $this->assertArrayHasKey($check2, $data);
157
+
158
+        $data = $this->manager->getChecks([$check2, $check1]);
159
+        $this->assertArrayHasKey($check1, $data);
160
+        $this->assertArrayHasKey($check2, $data);
161
+
162
+        $data = $this->manager->getChecks([$check2]);
163
+        $this->assertArrayNotHasKey($check1, $data);
164
+        $this->assertArrayHasKey($check2, $data);
165
+    }
166
+
167
+    public function testScope(): void {
168
+        $adminScope = $this->buildScope();
169
+        $userScope = $this->buildScope('jackie');
170
+        $entity = File::class;
171
+
172
+        $opId1 = $this->invokePrivate(
173
+            $this->manager,
174
+            'insertOperation',
175
+            ['OCA\WFE\TestOp', 'Test01', [11, 22], 'foo', $entity, []]
176
+        );
177
+        $this->invokePrivate($this->manager, 'addScope', [$opId1, $adminScope]);
178
+
179
+        $opId2 = $this->invokePrivate(
180
+            $this->manager,
181
+            'insertOperation',
182
+            ['OCA\WFE\TestOp', 'Test02', [33, 22], 'bar', $entity, []]
183
+        );
184
+        $this->invokePrivate($this->manager, 'addScope', [$opId2, $userScope]);
185
+        $opId3 = $this->invokePrivate(
186
+            $this->manager,
187
+            'insertOperation',
188
+            ['OCA\WFE\TestOp', 'Test03', [11, 44], 'foobar', $entity, []]
189
+        );
190
+        $this->invokePrivate($this->manager, 'addScope', [$opId3, $userScope]);
191
+
192
+        $this->assertTrue($this->invokePrivate($this->manager, 'canModify', [$opId1, $adminScope]));
193
+        $this->assertFalse($this->invokePrivate($this->manager, 'canModify', [$opId2, $adminScope]));
194
+        $this->assertFalse($this->invokePrivate($this->manager, 'canModify', [$opId3, $adminScope]));
195
+
196
+        $this->assertFalse($this->invokePrivate($this->manager, 'canModify', [$opId1, $userScope]));
197
+        $this->assertTrue($this->invokePrivate($this->manager, 'canModify', [$opId2, $userScope]));
198
+        $this->assertTrue($this->invokePrivate($this->manager, 'canModify', [$opId3, $userScope]));
199
+    }
200
+
201
+    public function testGetAllOperations(): void {
202
+        $adminScope = $this->buildScope();
203
+        $userScope = $this->buildScope('jackie');
204
+        $entity = File::class;
205
+
206
+        $adminOperation = $this->createMock(IOperation::class);
207
+        $adminOperation->expects($this->any())
208
+            ->method('isAvailableForScope')
209
+            ->willReturnMap([
210
+                [IManager::SCOPE_ADMIN, true],
211
+                [IManager::SCOPE_USER, false],
212
+            ]);
213
+        $userOperation = $this->createMock(IOperation::class);
214
+        $userOperation->expects($this->any())
215
+            ->method('isAvailableForScope')
216
+            ->willReturnMap([
217
+                [IManager::SCOPE_ADMIN, false],
218
+                [IManager::SCOPE_USER, true],
219
+            ]);
220
+
221
+        $this->container->expects($this->any())
222
+            ->method('get')
223
+            ->willReturnCallback(function ($className) use ($adminOperation, $userOperation) {
224
+                switch ($className) {
225
+                    case 'OCA\WFE\TestAdminOp':
226
+                        return $adminOperation;
227
+                    case 'OCA\WFE\TestUserOp':
228
+                        return $userOperation;
229
+                }
230
+            });
231
+
232
+        $opId1 = $this->invokePrivate(
233
+            $this->manager,
234
+            'insertOperation',
235
+            ['OCA\WFE\TestAdminOp', 'Test01', [11, 22], 'foo', $entity, []]
236
+        );
237
+        $this->invokePrivate($this->manager, 'addScope', [$opId1, $adminScope]);
238
+
239
+        $opId2 = $this->invokePrivate(
240
+            $this->manager,
241
+            'insertOperation',
242
+            ['OCA\WFE\TestUserOp', 'Test02', [33, 22], 'bar', $entity, []]
243
+        );
244
+        $this->invokePrivate($this->manager, 'addScope', [$opId2, $userScope]);
245
+        $opId3 = $this->invokePrivate(
246
+            $this->manager,
247
+            'insertOperation',
248
+            ['OCA\WFE\TestUserOp', 'Test03', [11, 44], 'foobar', $entity, []]
249
+        );
250
+        $this->invokePrivate($this->manager, 'addScope', [$opId3, $userScope]);
251
+
252
+        $opId4 = $this->invokePrivate(
253
+            $this->manager,
254
+            'insertOperation',
255
+            ['OCA\WFE\TestAdminOp', 'Test04', [41, 10, 4], 'NoBar', $entity, []]
256
+        );
257
+        $this->invokePrivate($this->manager, 'addScope', [$opId4, $userScope]);
258
+
259
+        $adminOps = $this->manager->getAllOperations($adminScope);
260
+        $userOps = $this->manager->getAllOperations($userScope);
261
+
262
+        $this->assertSame(1, count($adminOps));
263
+        $this->assertTrue(array_key_exists('OCA\WFE\TestAdminOp', $adminOps));
264
+        $this->assertFalse(array_key_exists('OCA\WFE\TestUserOp', $adminOps));
265
+
266
+        $this->assertSame(1, count($userOps));
267
+        $this->assertFalse(array_key_exists('OCA\WFE\TestAdminOp', $userOps));
268
+        $this->assertTrue(array_key_exists('OCA\WFE\TestUserOp', $userOps));
269
+        $this->assertSame(2, count($userOps['OCA\WFE\TestUserOp']));
270
+    }
271
+
272
+    public function testGetOperations(): void {
273
+        $adminScope = $this->buildScope();
274
+        $userScope = $this->buildScope('jackie');
275
+        $entity = File::class;
276
+
277
+        $opId1 = $this->invokePrivate(
278
+            $this->manager,
279
+            'insertOperation',
280
+            ['OCA\WFE\TestOp', 'Test01', [11, 22], 'foo', $entity, []]
281
+        );
282
+        $this->invokePrivate($this->manager, 'addScope', [$opId1, $adminScope]);
283
+        $opId4 = $this->invokePrivate(
284
+            $this->manager,
285
+            'insertOperation',
286
+            ['OCA\WFE\OtherTestOp', 'Test04', [5], 'foo', $entity, []]
287
+        );
288
+        $this->invokePrivate($this->manager, 'addScope', [$opId4, $adminScope]);
289
+
290
+        $opId2 = $this->invokePrivate(
291
+            $this->manager,
292
+            'insertOperation',
293
+            ['OCA\WFE\TestOp', 'Test02', [33, 22], 'bar', $entity, []]
294
+        );
295
+        $this->invokePrivate($this->manager, 'addScope', [$opId2, $userScope]);
296
+        $opId3 = $this->invokePrivate(
297
+            $this->manager,
298
+            'insertOperation',
299
+            ['OCA\WFE\TestOp', 'Test03', [11, 44], 'foobar', $entity, []]
300
+        );
301
+        $this->invokePrivate($this->manager, 'addScope', [$opId3, $userScope]);
302
+        $opId5 = $this->invokePrivate(
303
+            $this->manager,
304
+            'insertOperation',
305
+            ['OCA\WFE\OtherTestOp', 'Test05', [5], 'foobar', $entity, []]
306
+        );
307
+        $this->invokePrivate($this->manager, 'addScope', [$opId5, $userScope]);
308
+
309
+        $operation = $this->createMock(IOperation::class);
310
+        $operation->expects($this->any())
311
+            ->method('isAvailableForScope')
312
+            ->willReturnMap([
313
+                [IManager::SCOPE_ADMIN, true],
314
+                [IManager::SCOPE_USER, true],
315
+            ]);
316
+
317
+        $this->container->expects($this->any())
318
+            ->method('get')
319
+            ->willReturnCallback(function ($className) use ($operation) {
320
+                switch ($className) {
321
+                    case 'OCA\WFE\TestOp':
322
+                        return $operation;
323
+                    case 'OCA\WFE\OtherTestOp':
324
+                        throw new QueryException();
325
+                }
326
+            });
327
+
328
+        $adminOps = $this->manager->getOperations('OCA\WFE\TestOp', $adminScope);
329
+        $userOps = $this->manager->getOperations('OCA\WFE\TestOp', $userScope);
330
+
331
+        $this->assertSame(1, count($adminOps));
332
+        array_walk($adminOps, function ($op): void {
333
+            $this->assertTrue($op['class'] === 'OCA\WFE\TestOp');
334
+        });
335
+
336
+        $this->assertSame(2, count($userOps));
337
+        array_walk($userOps, function ($op): void {
338
+            $this->assertTrue($op['class'] === 'OCA\WFE\TestOp');
339
+        });
340
+    }
341
+
342
+    public function testGetAllConfiguredEvents(): void {
343
+        $adminScope = $this->buildScope();
344
+        $userScope = $this->buildScope('jackie');
345
+        $entity = File::class;
346
+
347
+        $opId5 = $this->invokePrivate(
348
+            $this->manager,
349
+            'insertOperation',
350
+            ['OCA\WFE\OtherTestOp', 'Test04', [], 'foo', $entity, [NodeCreatedEvent::class]]
351
+        );
352
+        $this->invokePrivate($this->manager, 'addScope', [$opId5, $userScope]);
353
+
354
+        $allOperations = null;
355
+
356
+        $cache = $this->createMock(ICache::class);
357
+        $cache
358
+            ->method('get')
359
+            ->willReturnCallback(function () use (&$allOperations) {
360
+                if ($allOperations) {
361
+                    return $allOperations;
362
+                }
363
+
364
+                return null;
365
+            });
366
+
367
+        $this->cacheFactory->method('createDistributed')->willReturn($cache);
368
+        $allOperations = $this->manager->getAllConfiguredEvents();
369
+        $this->assertCount(1, $allOperations);
370
+
371
+        $allOperationsCached = $this->manager->getAllConfiguredEvents();
372
+        $this->assertCount(1, $allOperationsCached);
373
+        $this->assertEquals($allOperationsCached, $allOperations);
374
+    }
375
+
376
+    public function testUpdateOperation(): void {
377
+        $adminScope = $this->buildScope();
378
+        $userScope = $this->buildScope('jackie');
379
+        $entity = File::class;
380
+
381
+        $cache = $this->createMock(ICache::class);
382
+        $cache->expects($this->exactly(4))
383
+            ->method('remove')
384
+            ->with('events');
385
+        $this->cacheFactory->method('createDistributed')
386
+            ->willReturn($cache);
387
+
388
+        $expectedCalls = [
389
+            [IManager::SCOPE_ADMIN],
390
+            [IManager::SCOPE_USER],
391
+        ];
392
+        $i = 0;
393
+        $operationMock = $this->createMock(IOperation::class);
394
+        $operationMock->expects($this->any())
395
+            ->method('isAvailableForScope')
396
+            ->willReturnCallback(function () use (&$expectedCalls, &$i): bool {
397
+                $this->assertEquals($expectedCalls[$i], func_get_args());
398
+                $i++;
399
+                return true;
400
+            });
401
+
402
+        $this->container->expects($this->any())
403
+            ->method('get')
404
+            ->willReturnCallback(function ($class) use ($operationMock) {
405
+                if (substr($class, -2) === 'Op') {
406
+                    return $operationMock;
407
+                } elseif ($class === File::class) {
408
+                    return $this->getMockBuilder(File::class)
409
+                        ->setConstructorArgs([
410
+                            $this->l,
411
+                            $this->createMock(IURLGenerator::class),
412
+                            $this->createMock(IRootFolder::class),
413
+                            $this->createMock(IUserSession::class),
414
+                            $this->createMock(ISystemTagManager::class),
415
+                            $this->createMock(IUserManager::class),
416
+                            $this->createMock(UserMountCache::class),
417
+                            $this->createMock(IMountManager::class),
418
+                        ])
419
+                        ->onlyMethods($this->filterClassMethods(File::class, ['getEvents']))
420
+                        ->getMock();
421
+                }
422
+                return $this->createMock(ICheck::class);
423
+            });
424
+
425
+        $opId1 = $this->invokePrivate(
426
+            $this->manager,
427
+            'insertOperation',
428
+            [TestAdminOp::class, 'Test01', [11, 22], 'foo', $entity, []]
429
+        );
430
+        $this->invokePrivate($this->manager, 'addScope', [$opId1, $adminScope]);
431
+
432
+        $opId2 = $this->invokePrivate(
433
+            $this->manager,
434
+            'insertOperation',
435
+            [TestUserOp::class, 'Test02', [33, 22], 'bar', $entity, []]
436
+        );
437
+        $this->invokePrivate($this->manager, 'addScope', [$opId2, $userScope]);
438
+
439
+        $check1 = ['class' => ICheck::class, 'operator' => 'eq', 'value' => 'asdf'];
440
+        $check2 = ['class' => ICheck::class, 'operator' => 'eq', 'value' => 23456];
441
+
442
+        /** @noinspection PhpUnhandledExceptionInspection */
443
+        $op = $this->manager->updateOperation($opId1, 'Test01a', [$check1, $check2], 'foohur', $adminScope, $entity, ['\OCP\Files::postDelete']);
444
+        $this->assertSame('Test01a', $op['name']);
445
+        $this->assertSame('foohur', $op['operation']);
446
+
447
+        /** @noinspection PhpUnhandledExceptionInspection */
448
+        $op = $this->manager->updateOperation($opId2, 'Test02a', [$check1], 'barfoo', $userScope, $entity, ['\OCP\Files::postDelete']);
449
+        $this->assertSame('Test02a', $op['name']);
450
+        $this->assertSame('barfoo', $op['operation']);
451
+
452
+        foreach ([[$adminScope, $opId2], [$userScope, $opId1]] as $run) {
453
+            try {
454
+                /** @noinspection PhpUnhandledExceptionInspection */
455
+                $this->manager->updateOperation($run[1], 'Evil', [$check2], 'hackx0r', $run[0], $entity, []);
456
+                $this->assertTrue(false, 'DomainException not thrown');
457
+            } catch (\DomainException $e) {
458
+                $this->assertTrue(true);
459
+            }
460
+        }
461
+    }
462
+
463
+    public function testDeleteOperation(): void {
464
+        $adminScope = $this->buildScope();
465
+        $userScope = $this->buildScope('jackie');
466
+        $entity = File::class;
467
+
468
+        $cache = $this->createMock(ICache::class);
469
+        $cache->expects($this->exactly(4))
470
+            ->method('remove')
471
+            ->with('events');
472
+        $this->cacheFactory->method('createDistributed')->willReturn($cache);
473
+
474
+        $opId1 = $this->invokePrivate(
475
+            $this->manager,
476
+            'insertOperation',
477
+            ['OCA\WFE\TestAdminOp', 'Test01', [11, 22], 'foo', $entity, []]
478
+        );
479
+        $this->invokePrivate($this->manager, 'addScope', [$opId1, $adminScope]);
480
+
481
+        $opId2 = $this->invokePrivate(
482
+            $this->manager,
483
+            'insertOperation',
484
+            ['OCA\WFE\TestUserOp', 'Test02', [33, 22], 'bar', $entity, []]
485
+        );
486
+        $this->invokePrivate($this->manager, 'addScope', [$opId2, $userScope]);
487
+
488
+        foreach ([[$adminScope, $opId2], [$userScope, $opId1]] as $run) {
489
+            try {
490
+                /** @noinspection PhpUnhandledExceptionInspection */
491
+                $this->manager->deleteOperation($run[1], $run[0]);
492
+                $this->assertTrue(false, 'DomainException not thrown');
493
+            } catch (\Exception $e) {
494
+                $this->assertInstanceOf(\DomainException::class, $e);
495
+            }
496
+        }
497
+
498
+        /** @noinspection PhpUnhandledExceptionInspection */
499
+        $this->manager->deleteOperation($opId1, $adminScope);
500
+        /** @noinspection PhpUnhandledExceptionInspection */
501
+        $this->manager->deleteOperation($opId2, $userScope);
502
+
503
+        foreach ([$opId1, $opId2] as $opId) {
504
+            try {
505
+                $this->invokePrivate($this->manager, 'getOperation', [$opId]);
506
+                $this->assertTrue(false, 'UnexpectedValueException not thrown');
507
+            } catch (\Exception $e) {
508
+                $this->assertInstanceOf(\UnexpectedValueException::class, $e);
509
+            }
510
+        }
511
+    }
512
+
513
+    public function testGetEntitiesListBuildInOnly(): void {
514
+        $fileEntityMock = $this->createMock(File::class);
515
+
516
+        $this->container->expects($this->once())
517
+            ->method('get')
518
+            ->with(File::class)
519
+            ->willReturn($fileEntityMock);
520
+
521
+        $entities = $this->manager->getEntitiesList();
522
+
523
+        $this->assertCount(1, $entities);
524
+        $this->assertInstanceOf(IEntity::class, $entities[0]);
525
+    }
526
+
527
+    public function testGetEntitiesList(): void {
528
+        $fileEntityMock = $this->createMock(File::class);
529
+
530
+        $this->container->expects($this->once())
531
+            ->method('get')
532
+            ->with(File::class)
533
+            ->willReturn($fileEntityMock);
534
+
535
+        $extraEntity = $this->createMock(IEntity::class);
536
+
537
+        $this->dispatcher->expects($this->once())
538
+            ->method('dispatchTyped')
539
+            ->willReturnCallback(function (RegisterEntitiesEvent $e) use ($extraEntity): void {
540
+                $this->manager->registerEntity($extraEntity);
541
+            });
542
+
543
+        $entities = $this->manager->getEntitiesList();
544
+
545
+        $this->assertCount(2, $entities);
546
+
547
+        $entityTypeCounts = array_reduce($entities, function (array $carry, IEntity $entity) {
548
+            if ($entity instanceof File) {
549
+                $carry[0]++;
550
+            } elseif ($entity instanceof IEntity) {
551
+                $carry[1]++;
552
+            }
553
+            return $carry;
554
+        }, [0, 0]);
555
+
556
+        $this->assertSame(1, $entityTypeCounts[0]);
557
+        $this->assertSame(1, $entityTypeCounts[1]);
558
+    }
559
+
560
+    public function testValidateOperationOK(): void {
561
+        $check = [
562
+            'class' => ICheck::class,
563
+            'operator' => 'is',
564
+            'value' => 'barfoo',
565
+        ];
566
+
567
+        $operationMock = $this->createMock(IOperation::class);
568
+        $entityMock = $this->createMock(IEntity::class);
569
+        $eventEntityMock = $this->createMock(IEntityEvent::class);
570
+        $checkMock = $this->createMock(ICheck::class);
571
+        $scopeMock = $this->createMock(ScopeContext::class);
572
+
573
+        $scopeMock->expects($this->any())
574
+            ->method('getScope')
575
+            ->willReturn(IManager::SCOPE_ADMIN);
576
+
577
+        $operationMock->expects($this->once())
578
+            ->method('isAvailableForScope')
579
+            ->with(IManager::SCOPE_ADMIN)
580
+            ->willReturn(true);
581
+
582
+        $operationMock->expects($this->once())
583
+            ->method('validateOperation')
584
+            ->with('test', [$check], 'operationData');
585
+
586
+        $entityMock->expects($this->any())
587
+            ->method('getEvents')
588
+            ->willReturn([$eventEntityMock]);
589
+
590
+        $eventEntityMock->expects($this->any())
591
+            ->method('getEventName')
592
+            ->willReturn('MyEvent');
593
+
594
+        $checkMock->expects($this->any())
595
+            ->method('supportedEntities')
596
+            ->willReturn([IEntity::class]);
597
+        $checkMock->expects($this->atLeastOnce())
598
+            ->method('validateCheck');
599
+
600
+        $this->container->expects($this->any())
601
+            ->method('get')
602
+            ->willReturnCallback(function ($className) use ($operationMock, $entityMock, $eventEntityMock, $checkMock) {
603
+                switch ($className) {
604
+                    case IOperation::class:
605
+                        return $operationMock;
606
+                    case IEntity::class:
607
+                        return $entityMock;
608
+                    case IEntityEvent::class:
609
+                        return $eventEntityMock;
610
+                    case ICheck::class:
611
+                        return $checkMock;
612
+                    default:
613
+                        return $this->createMock($className);
614
+                }
615
+            });
616
+
617
+        $this->manager->validateOperation(IOperation::class, 'test', [$check], 'operationData', $scopeMock, IEntity::class, ['MyEvent']);
618
+    }
619
+
620
+    public function testValidateOperationCheckInputLengthError(): void {
621
+        $check = [
622
+            'class' => ICheck::class,
623
+            'operator' => 'is',
624
+            'value' => str_pad('', IManager::MAX_CHECK_VALUE_BYTES + 1, 'FooBar'),
625
+        ];
626
+
627
+        $operationMock = $this->createMock(IOperation::class);
628
+        $entityMock = $this->createMock(IEntity::class);
629
+        $eventEntityMock = $this->createMock(IEntityEvent::class);
630
+        $checkMock = $this->createMock(ICheck::class);
631
+        $scopeMock = $this->createMock(ScopeContext::class);
632
+
633
+        $scopeMock->expects($this->any())
634
+            ->method('getScope')
635
+            ->willReturn(IManager::SCOPE_ADMIN);
636
+
637
+        $operationMock->expects($this->once())
638
+            ->method('isAvailableForScope')
639
+            ->with(IManager::SCOPE_ADMIN)
640
+            ->willReturn(true);
641
+
642
+        $operationMock->expects($this->once())
643
+            ->method('validateOperation')
644
+            ->with('test', [$check], 'operationData');
645
+
646
+        $entityMock->expects($this->any())
647
+            ->method('getEvents')
648
+            ->willReturn([$eventEntityMock]);
649
+
650
+        $eventEntityMock->expects($this->any())
651
+            ->method('getEventName')
652
+            ->willReturn('MyEvent');
653
+
654
+        $checkMock->expects($this->any())
655
+            ->method('supportedEntities')
656
+            ->willReturn([IEntity::class]);
657
+        $checkMock->expects($this->never())
658
+            ->method('validateCheck');
659
+
660
+        $this->container->expects($this->any())
661
+            ->method('get')
662
+            ->willReturnCallback(function ($className) use ($operationMock, $entityMock, $eventEntityMock, $checkMock) {
663
+                switch ($className) {
664
+                    case IOperation::class:
665
+                        return $operationMock;
666
+                    case IEntity::class:
667
+                        return $entityMock;
668
+                    case IEntityEvent::class:
669
+                        return $eventEntityMock;
670
+                    case ICheck::class:
671
+                        return $checkMock;
672
+                    default:
673
+                        return $this->createMock($className);
674
+                }
675
+            });
676
+
677
+        try {
678
+            $this->manager->validateOperation(IOperation::class, 'test', [$check], 'operationData', $scopeMock, IEntity::class, ['MyEvent']);
679
+        } catch (\UnexpectedValueException $e) {
680
+            $this->assertSame('The provided check value is too long', $e->getMessage());
681
+        }
682
+    }
683
+
684
+    public function testValidateOperationDataLengthError(): void {
685
+        $check = [
686
+            'class' => ICheck::class,
687
+            'operator' => 'is',
688
+            'value' => 'barfoo',
689
+        ];
690
+        $operationData = str_pad('', IManager::MAX_OPERATION_VALUE_BYTES + 1, 'FooBar');
691
+
692
+        $operationMock = $this->createMock(IOperation::class);
693
+        $entityMock = $this->createMock(IEntity::class);
694
+        $eventEntityMock = $this->createMock(IEntityEvent::class);
695
+        $checkMock = $this->createMock(ICheck::class);
696
+        $scopeMock = $this->createMock(ScopeContext::class);
697
+
698
+        $scopeMock->expects($this->any())
699
+            ->method('getScope')
700
+            ->willReturn(IManager::SCOPE_ADMIN);
701
+
702
+        $operationMock->expects($this->never())
703
+            ->method('validateOperation');
704
+
705
+        $entityMock->expects($this->any())
706
+            ->method('getEvents')
707
+            ->willReturn([$eventEntityMock]);
708
+
709
+        $eventEntityMock->expects($this->any())
710
+            ->method('getEventName')
711
+            ->willReturn('MyEvent');
712
+
713
+        $checkMock->expects($this->any())
714
+            ->method('supportedEntities')
715
+            ->willReturn([IEntity::class]);
716
+        $checkMock->expects($this->never())
717
+            ->method('validateCheck');
718
+
719
+        $this->container->expects($this->any())
720
+            ->method('get')
721
+            ->willReturnCallback(function ($className) use ($operationMock, $entityMock, $eventEntityMock, $checkMock) {
722
+                switch ($className) {
723
+                    case IOperation::class:
724
+                        return $operationMock;
725
+                    case IEntity::class:
726
+                        return $entityMock;
727
+                    case IEntityEvent::class:
728
+                        return $eventEntityMock;
729
+                    case ICheck::class:
730
+                        return $checkMock;
731
+                    default:
732
+                        return $this->createMock($className);
733
+                }
734
+            });
735
+
736
+        try {
737
+            $this->manager->validateOperation(IOperation::class, 'test', [$check], $operationData, $scopeMock, IEntity::class, ['MyEvent']);
738
+        } catch (\UnexpectedValueException $e) {
739
+            $this->assertSame('The provided operation data is too long', $e->getMessage());
740
+        }
741
+    }
742
+
743
+    public function testValidateOperationScopeNotAvailable(): void {
744
+        $check = [
745
+            'class' => ICheck::class,
746
+            'operator' => 'is',
747
+            'value' => 'barfoo',
748
+        ];
749
+        $operationData = str_pad('', IManager::MAX_OPERATION_VALUE_BYTES - 1, 'FooBar');
750
+
751
+        $operationMock = $this->createMock(IOperation::class);
752
+        $entityMock = $this->createMock(IEntity::class);
753
+        $eventEntityMock = $this->createMock(IEntityEvent::class);
754
+        $checkMock = $this->createMock(ICheck::class);
755
+        $scopeMock = $this->createMock(ScopeContext::class);
756
+
757
+        $scopeMock->expects($this->any())
758
+            ->method('getScope')
759
+            ->willReturn(IManager::SCOPE_ADMIN);
760
+
761
+        $operationMock->expects($this->once())
762
+            ->method('isAvailableForScope')
763
+            ->with(IManager::SCOPE_ADMIN)
764
+            ->willReturn(false);
765
+
766
+        $operationMock->expects($this->never())
767
+            ->method('validateOperation');
768
+
769
+        $entityMock->expects($this->any())
770
+            ->method('getEvents')
771
+            ->willReturn([$eventEntityMock]);
772
+
773
+        $eventEntityMock->expects($this->any())
774
+            ->method('getEventName')
775
+            ->willReturn('MyEvent');
776
+
777
+        $checkMock->expects($this->any())
778
+            ->method('supportedEntities')
779
+            ->willReturn([IEntity::class]);
780
+        $checkMock->expects($this->never())
781
+            ->method('validateCheck');
782
+
783
+        $this->container->expects($this->any())
784
+            ->method('get')
785
+            ->willReturnCallback(function ($className) use ($operationMock, $entityMock, $eventEntityMock, $checkMock) {
786
+                switch ($className) {
787
+                    case IOperation::class:
788
+                        return $operationMock;
789
+                    case IEntity::class:
790
+                        return $entityMock;
791
+                    case IEntityEvent::class:
792
+                        return $eventEntityMock;
793
+                    case ICheck::class:
794
+                        return $checkMock;
795
+                    default:
796
+                        return $this->createMock($className);
797
+                }
798
+            });
799
+
800
+        try {
801
+            $this->manager->validateOperation(IOperation::class, 'test', [$check], $operationData, $scopeMock, IEntity::class, ['MyEvent']);
802
+        } catch (\UnexpectedValueException $e) {
803
+            $this->assertSame('Operation OCP\WorkflowEngine\IOperation is invalid', $e->getMessage());
804
+        }
805
+    }
806 806
 }
Please login to merge, or discard this patch.