Passed
Push — master ( f83492...6d96b3 )
by Roeland
10:43 queued 11s
created
apps/workflowengine/lib/Manager.php 1 patch
Indentation   +624 added lines, -624 removed lines patch added patch discarded remove patch
@@ -55,632 +55,632 @@
 block discarded – undo
55 55
 
56 56
 class Manager implements IManager {
57 57
 
58
-	/** @var IStorage */
59
-	protected $storage;
58
+    /** @var IStorage */
59
+    protected $storage;
60 60
 
61
-	/** @var string */
62
-	protected $path;
61
+    /** @var string */
62
+    protected $path;
63 63
 
64
-	/** @var object */
65
-	protected $entity;
66
-
67
-	/** @var array[] */
68
-	protected $operations = [];
69
-
70
-	/** @var array[] */
71
-	protected $checks = [];
64
+    /** @var object */
65
+    protected $entity;
66
+
67
+    /** @var array[] */
68
+    protected $operations = [];
69
+
70
+    /** @var array[] */
71
+    protected $checks = [];
72 72
 
73
-	/** @var IDBConnection */
74
-	protected $connection;
75
-
76
-	/** @var IServerContainer|\OC\Server */
77
-	protected $container;
78
-
79
-	/** @var IL10N */
80
-	protected $l;
81
-
82
-	/** @var EventDispatcherInterface */
83
-	protected $eventDispatcher;
84
-
85
-	/** @var IEntity[] */
86
-	protected $registeredEntities = [];
73
+    /** @var IDBConnection */
74
+    protected $connection;
75
+
76
+    /** @var IServerContainer|\OC\Server */
77
+    protected $container;
78
+
79
+    /** @var IL10N */
80
+    protected $l;
81
+
82
+    /** @var EventDispatcherInterface */
83
+    protected $eventDispatcher;
84
+
85
+    /** @var IEntity[] */
86
+    protected $registeredEntities = [];
87 87
 
88
-	/** @var IOperation[] */
89
-	protected $registeredOperators = [];
90
-
91
-	/** @var ICheck[] */
92
-	protected $registeredChecks = [];
93
-
94
-	/** @var ILogger */
95
-	protected $logger;
96
-
97
-	/** @var CappedMemoryCache */
98
-	protected $operationsByScope = [];
99
-
100
-	/** @var IUserSession */
101
-	protected $session;
102
-
103
-	/**
104
-	 * @param IDBConnection $connection
105
-	 * @param IServerContainer $container
106
-	 * @param IL10N $l
107
-	 */
108
-	public function __construct(
109
-		IDBConnection $connection,
110
-		IServerContainer $container,
111
-		IL10N $l,
112
-		EventDispatcherInterface $eventDispatcher,
113
-		ILogger $logger,
114
-		IUserSession $session
115
-	) {
116
-		$this->connection = $connection;
117
-		$this->container = $container;
118
-		$this->l = $l;
119
-		$this->eventDispatcher = $eventDispatcher;
120
-		$this->logger = $logger;
121
-		$this->operationsByScope = new CappedMemoryCache(64);
122
-		$this->session = $session;
123
-	}
124
-
125
-	public function getRuleMatcher(): IRuleMatcher {
126
-		return new RuleMatcher($this->session, $this->container, $this->l, $this);
127
-	}
128
-
129
-	public function getAllConfiguredEvents() {
130
-		$query = $this->connection->getQueryBuilder();
131
-
132
-		$query->selectDistinct('class')
133
-			->addSelect('entity', 'events')
134
-			->from('flow_operations')
135
-			->where($query->expr()->neq('events', $query->createNamedParameter('[]'), IQueryBuilder::PARAM_STR));
136
-
137
-		$result = $query->execute();
138
-		$operations = [];
139
-		while($row = $result->fetch()) {
140
-			$eventNames = \json_decode($row['events']);
141
-
142
-			$operation = $row['class'];
143
-			$entity =  $row['entity'];
144
-
145
-			$operations[$operation] = $operations[$row['class']] ?? [];
146
-			$operations[$operation][$entity] = $operations[$operation][$entity] ?? [];
147
-
148
-			$operations[$operation][$entity] = array_unique(array_merge($operations[$operation][$entity], $eventNames ?? []));
149
-		}
150
-		$result->closeCursor();
151
-
152
-		return $operations;
153
-	}
154
-
155
-	public function getAllConfiguredScopesForOperation(string $operationClass): array {
156
-		static $scopesByOperation = [];
157
-		if (isset($scopesByOperation[$operationClass])) {
158
-			return $scopesByOperation[$operationClass];
159
-		}
160
-
161
-		$query = $this->connection->getQueryBuilder();
162
-
163
-		$query->selectDistinct('s.type')
164
-			->addSelect('s.value')
165
-			->from('flow_operations', 'o')
166
-			->leftJoin('o', 'flow_operations_scope', 's', $query->expr()->eq('o.id', 's.operation_id'))
167
-			->where($query->expr()->eq('o.class', $query->createParameter('operationClass')));
168
-
169
-		$query->setParameters(['operationClass' => $operationClass]);
170
-		$result = $query->execute();
171
-
172
-		$scopesByOperation[$operationClass] = [];
173
-		while ($row = $result->fetch()) {
174
-			$scope = new ScopeContext($row['type'], $row['value']);
175
-			$scopesByOperation[$operationClass][$scope->getHash()] = $scope;
176
-		}
177
-
178
-		return $scopesByOperation[$operationClass];
179
-	}
180
-
181
-	public function getAllOperations(ScopeContext $scopeContext): array {
182
-		if(isset($this->operations[$scopeContext->getHash()])) {
183
-			return $this->operations[$scopeContext->getHash()];
184
-		}
185
-
186
-		$query = $this->connection->getQueryBuilder();
187
-
188
-		$query->select('o.*')
189
-			->selectAlias('s.type', 'scope_type')
190
-			->selectAlias('s.value', 'scope_actor_id')
191
-			->from('flow_operations', 'o')
192
-			->leftJoin('o', 'flow_operations_scope', 's', $query->expr()->eq('o.id', 's.operation_id'))
193
-			->where($query->expr()->eq('s.type', $query->createParameter('scope')));
194
-
195
-		if($scopeContext->getScope() === IManager::SCOPE_USER) {
196
-			$query->andWhere($query->expr()->eq('s.value', $query->createParameter('scopeId')));
197
-		}
198
-
199
-		$query->setParameters(['scope' => $scopeContext->getScope(), 'scopeId' => $scopeContext->getScopeId()]);
200
-		$result = $query->execute();
201
-
202
-		$this->operations[$scopeContext->getHash()] = [];
203
-		while ($row = $result->fetch()) {
204
-			if(!isset($this->operations[$scopeContext->getHash()][$row['class']])) {
205
-				$this->operations[$scopeContext->getHash()][$row['class']] = [];
206
-			}
207
-			$this->operations[$scopeContext->getHash()][$row['class']][] = $row;
208
-		}
209
-
210
-		return $this->operations[$scopeContext->getHash()];
211
-	}
212
-
213
-	public function getOperations(string $class, ScopeContext $scopeContext): array {
214
-		if (!isset($this->operations[$scopeContext->getHash()])) {
215
-			$this->getAllOperations($scopeContext);
216
-		}
217
-		return $this->operations[$scopeContext->getHash()][$class] ?? [];
218
-	}
219
-
220
-	/**
221
-	 * @param int $id
222
-	 * @return array
223
-	 * @throws \UnexpectedValueException
224
-	 */
225
-	protected function getOperation($id) {
226
-		$query = $this->connection->getQueryBuilder();
227
-		$query->select('*')
228
-			->from('flow_operations')
229
-			->where($query->expr()->eq('id', $query->createNamedParameter($id)));
230
-		$result = $query->execute();
231
-		$row = $result->fetch();
232
-		$result->closeCursor();
233
-
234
-		if ($row) {
235
-			return $row;
236
-		}
237
-
238
-		throw new \UnexpectedValueException($this->l->t('Operation #%s does not exist', [$id]));
239
-	}
240
-
241
-	protected function insertOperation(
242
-		string $class,
243
-		string $name,
244
-		array $checkIds,
245
-		string $operation,
246
-		string $entity,
247
-		array $events
248
-	): int {
249
-		$query = $this->connection->getQueryBuilder();
250
-		$query->insert('flow_operations')
251
-			->values([
252
-				'class' => $query->createNamedParameter($class),
253
-				'name' => $query->createNamedParameter($name),
254
-				'checks' => $query->createNamedParameter(json_encode(array_unique($checkIds))),
255
-				'operation' => $query->createNamedParameter($operation),
256
-				'entity' => $query->createNamedParameter($entity),
257
-				'events' => $query->createNamedParameter(json_encode($events))
258
-			]);
259
-		$query->execute();
260
-
261
-		return $query->getLastInsertId();
262
-	}
263
-
264
-	/**
265
-	 * @param string $class
266
-	 * @param string $name
267
-	 * @param array[] $checks
268
-	 * @param string $operation
269
-	 * @return array The added operation
270
-	 * @throws \UnexpectedValueException
271
-	 * @throws DBALException
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, $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 (DBALException $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->where($qb->expr()->eq('s.value', $qb->createParameter('scopeId')));
317
-		}
318
-
319
-		$qb->setParameters(['scope' => $scopeContext->getScope(), 'scopeId' => $scopeContext->getScopeId()]);
320
-		$result = $qb->execute();
321
-
322
-		$this->operationsByScope[$scopeContext->getHash()] = [];
323
-		while($opId = $result->fetchColumn(0)) {
324
-			$this->operationsByScope[$scopeContext->getHash()][] = (int)$opId;
325
-		}
326
-		$result->closeCursor();
327
-
328
-		return in_array($id, $this->operationsByScope[$scopeContext->getHash()], true);
329
-	}
330
-
331
-	/**
332
-	 * @param int $id
333
-	 * @param string $name
334
-	 * @param array[] $checks
335
-	 * @param string $operation
336
-	 * @return array The updated operation
337
-	 * @throws \UnexpectedValueException
338
-	 * @throws \DomainException
339
-	 * @throws DBALException
340
-	 */
341
-	public function updateOperation(
342
-		int $id,
343
-		string $name,
344
-		array $checks,
345
-		string $operation,
346
-		ScopeContext $scopeContext,
347
-		string $entity,
348
-		array $events
349
-	): array {
350
-		if(!$this->canModify($id, $scopeContext)) {
351
-			throw new \DomainException('Target operation not within scope');
352
-		};
353
-		$row = $this->getOperation($id);
354
-		$this->validateOperation($row['class'], $name, $checks, $operation, $entity, $events);
355
-
356
-		$checkIds = [];
357
-		try {
358
-			$this->connection->beginTransaction();
359
-			foreach ($checks as $check) {
360
-				$checkIds[] = $this->addCheck($check['class'], $check['operator'], $check['value']);
361
-			}
362
-
363
-			$query = $this->connection->getQueryBuilder();
364
-			$query->update('flow_operations')
365
-				->set('name', $query->createNamedParameter($name))
366
-				->set('checks', $query->createNamedParameter(json_encode(array_unique($checkIds))))
367
-				->set('operation', $query->createNamedParameter($operation))
368
-				->set('entity', $query->createNamedParameter($entity))
369
-				->set('events', $query->createNamedParameter(json_encode($events)))
370
-				->where($query->expr()->eq('id', $query->createNamedParameter($id)));
371
-			$query->execute();
372
-			$this->connection->commit();
373
-		} catch (DBALException $e) {
374
-			$this->connection->rollBack();
375
-			throw $e;
376
-		}
377
-		unset($this->operations[$scopeContext->getHash()]);
378
-
379
-		return $this->getOperation($id);
380
-	}
381
-
382
-	/**
383
-	 * @param int $id
384
-	 * @return bool
385
-	 * @throws \UnexpectedValueException
386
-	 * @throws DBALException
387
-	 * @throws \DomainException
388
-	 */
389
-	public function deleteOperation($id, ScopeContext $scopeContext) {
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
-				->execute();
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
-					->execute();
404
-			}
405
-			$this->connection->commit();
406
-		} catch (DBALException $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
-		return $result;
416
-	}
417
-
418
-	protected function validateEvents(string $entity, array $events, IOperation $operation) {
419
-		try {
420
-			/** @var IEntity $instance */
421
-			$instance = $this->container->query($entity);
422
-		} catch (QueryException $e) {
423
-			throw new \UnexpectedValueException($this->l->t('Entity %s does not exist', [$entity]));
424
-		}
425
-
426
-		if(!$instance instanceof IEntity) {
427
-			throw new \UnexpectedValueException($this->l->t('Entity %s is invalid', [$entity]));
428
-		}
429
-
430
-		if(empty($events)) {
431
-			if(!$operation instanceof IComplexOperation) {
432
-				throw new \UnexpectedValueException($this->l->t('No events are chosen.'));
433
-			}
434
-			return;
435
-		}
436
-
437
-		$availableEvents = [];
438
-		foreach ($instance->getEvents() as $event) {
439
-			/** @var IEntityEvent $event */
440
-			$availableEvents[] = $event->getEventName();
441
-		}
442
-
443
-		$diff = array_diff($events, $availableEvents);
444
-		if(!empty($diff)) {
445
-			throw new \UnexpectedValueException($this->l->t('Entity %s has no event %s', [$entity, array_shift($diff)]));
446
-		}
447
-	}
448
-
449
-	/**
450
-	 * @param string $class
451
-	 * @param string $name
452
-	 * @param array[] $checks
453
-	 * @param string $operation
454
-	 * @throws \UnexpectedValueException
455
-	 */
456
-	public function validateOperation($class, $name, array $checks, $operation, string $entity, array $events) {
457
-		try {
458
-			/** @var IOperation $instance */
459
-			$instance = $this->container->query($class);
460
-		} catch (QueryException $e) {
461
-			throw new \UnexpectedValueException($this->l->t('Operation %s does not exist', [$class]));
462
-		}
463
-
464
-		if (!($instance instanceof IOperation)) {
465
-			throw new \UnexpectedValueException($this->l->t('Operation %s is invalid', [$class]));
466
-		}
467
-
468
-		$this->validateEvents($entity, $events, $instance);
469
-
470
-		$instance->validateOperation($name, $checks, $operation);
471
-
472
-		foreach ($checks as $check) {
473
-			try {
474
-				/** @var ICheck $instance */
475
-				$instance = $this->container->query($check['class']);
476
-			} catch (QueryException $e) {
477
-				throw new \UnexpectedValueException($this->l->t('Check %s does not exist', [$class]));
478
-			}
479
-
480
-			if (!($instance instanceof ICheck)) {
481
-				throw new \UnexpectedValueException($this->l->t('Check %s is invalid', [$class]));
482
-			}
483
-
484
-			if (!empty($instance->supportedEntities())
485
-				&& !in_array($entity, $instance->supportedEntities())
486
-			) {
487
-				throw new \UnexpectedValueException($this->l->t('Check %s is not allowed with this entity', [$class]));
488
-			}
489
-
490
-			$instance->validateCheck($check['operator'], $check['value']);
491
-		}
492
-	}
493
-
494
-	/**
495
-	 * @param int[] $checkIds
496
-	 * @return array[]
497
-	 */
498
-	public function getChecks(array $checkIds) {
499
-		$checkIds = array_map('intval', $checkIds);
500
-
501
-		$checks = [];
502
-		foreach ($checkIds as $i => $checkId) {
503
-			if (isset($this->checks[$checkId])) {
504
-				$checks[$checkId] = $this->checks[$checkId];
505
-				unset($checkIds[$i]);
506
-			}
507
-		}
508
-
509
-		if (empty($checkIds)) {
510
-			return $checks;
511
-		}
512
-
513
-		$query = $this->connection->getQueryBuilder();
514
-		$query->select('*')
515
-			->from('flow_checks')
516
-			->where($query->expr()->in('id', $query->createNamedParameter($checkIds, IQueryBuilder::PARAM_INT_ARRAY)));
517
-		$result = $query->execute();
518
-
519
-		while ($row = $result->fetch()) {
520
-			$this->checks[(int) $row['id']] = $row;
521
-			$checks[(int) $row['id']] = $row;
522
-		}
523
-		$result->closeCursor();
524
-
525
-		$checkIds = array_diff($checkIds, array_keys($checks));
526
-
527
-		if (!empty($checkIds)) {
528
-			$missingCheck = array_pop($checkIds);
529
-			throw new \UnexpectedValueException($this->l->t('Check #%s does not exist', $missingCheck));
530
-		}
531
-
532
-		return $checks;
533
-	}
534
-
535
-	/**
536
-	 * @param string $class
537
-	 * @param string $operator
538
-	 * @param string $value
539
-	 * @return int Check unique ID
540
-	 */
541
-	protected function addCheck($class, $operator, $value) {
542
-		$hash = md5($class . '::' . $operator . '::' . $value);
543
-
544
-		$query = $this->connection->getQueryBuilder();
545
-		$query->select('id')
546
-			->from('flow_checks')
547
-			->where($query->expr()->eq('hash', $query->createNamedParameter($hash)));
548
-		$result = $query->execute();
549
-
550
-		if ($row = $result->fetch()) {
551
-			$result->closeCursor();
552
-			return (int) $row['id'];
553
-		}
554
-
555
-		$query = $this->connection->getQueryBuilder();
556
-		$query->insert('flow_checks')
557
-			->values([
558
-				'class' => $query->createNamedParameter($class),
559
-				'operator' => $query->createNamedParameter($operator),
560
-				'value' => $query->createNamedParameter($value),
561
-				'hash' => $query->createNamedParameter($hash),
562
-			]);
563
-		$query->execute();
564
-
565
-		return $query->getLastInsertId();
566
-	}
567
-
568
-	protected function addScope(int $operationId, ScopeContext $scope): void {
569
-		$query = $this->connection->getQueryBuilder();
570
-
571
-		$insertQuery = $query->insert('flow_operations_scope');
572
-		$insertQuery->values([
573
-			'operation_id' => $query->createNamedParameter($operationId),
574
-			'type' => $query->createNamedParameter($scope->getScope()),
575
-			'value' => $query->createNamedParameter($scope->getScopeId()),
576
-		]);
577
-		$insertQuery->execute();
578
-	}
579
-
580
-	public function formatOperation(array $operation): array {
581
-		$checkIds = json_decode($operation['checks'], true);
582
-		$checks = $this->getChecks($checkIds);
583
-
584
-		$operation['checks'] = [];
585
-		foreach ($checks as $check) {
586
-			// Remove internal values
587
-			unset($check['id']);
588
-			unset($check['hash']);
589
-
590
-			$operation['checks'][] = $check;
591
-		}
592
-		$operation['events'] = json_decode($operation['events'], true) ?? [];
593
-
594
-
595
-		return $operation;
596
-	}
597
-
598
-	/**
599
-	 * @return IEntity[]
600
-	 */
601
-	public function getEntitiesList(): array {
602
-		$this->eventDispatcher->dispatch(IManager::EVENT_NAME_REG_ENTITY, new GenericEvent($this));
603
-
604
-		return array_values(array_merge($this->getBuildInEntities(), $this->registeredEntities));
605
-	}
606
-
607
-	/**
608
-	 * @return IOperation[]
609
-	 */
610
-	public function getOperatorList(): array {
611
-		$this->eventDispatcher->dispatch(IManager::EVENT_NAME_REG_OPERATION, new GenericEvent($this));
612
-
613
-		return array_merge($this->getBuildInOperators(), $this->registeredOperators);
614
-	}
615
-
616
-	/**
617
-	 * @return ICheck[]
618
-	 */
619
-	public function getCheckList(): array {
620
-		$this->eventDispatcher->dispatch(IManager::EVENT_NAME_REG_CHECK, new GenericEvent($this));
621
-
622
-		return array_merge($this->getBuildInChecks(), $this->registeredChecks);
623
-	}
624
-
625
-	public function registerEntity(IEntity $entity): void {
626
-		$this->registeredEntities[get_class($entity)] = $entity;
627
-	}
628
-
629
-	public function registerOperation(IOperation $operator): void {
630
-		$this->registeredOperators[get_class($operator)] = $operator;
631
-	}
632
-
633
-	public function registerCheck(ICheck $check): void {
634
-		$this->registeredChecks[get_class($check)] = $check;
635
-	}
636
-
637
-	/**
638
-	 * @return IEntity[]
639
-	 */
640
-	protected function getBuildInEntities(): array {
641
-		try {
642
-			return [
643
-				File::class => $this->container->query(File::class),
644
-			];
645
-		} catch (QueryException $e) {
646
-			$this->logger->logException($e);
647
-			return [];
648
-		}
649
-	}
650
-
651
-	/**
652
-	 * @return IOperation[]
653
-	 */
654
-	protected function getBuildInOperators(): array {
655
-		try {
656
-			return [
657
-				// None yet
658
-			];
659
-		} catch (QueryException $e) {
660
-			$this->logger->logException($e);
661
-			return [];
662
-		}
663
-	}
664
-
665
-	/**
666
-	 * @return IEntity[]
667
-	 */
668
-	protected function getBuildInChecks(): array {
669
-		try {
670
-			return [
671
-				$this->container->query(FileMimeType::class),
672
-				$this->container->query(FileName::class),
673
-				$this->container->query(FileSize::class),
674
-				$this->container->query(FileSystemTags::class),
675
-				$this->container->query(RequestRemoteAddress::class),
676
-				$this->container->query(RequestTime::class),
677
-				$this->container->query(RequestURL::class),
678
-				$this->container->query(RequestUserAgent::class),
679
-				$this->container->query(UserGroupMembership::class),
680
-			];
681
-		} catch (QueryException $e) {
682
-			$this->logger->logException($e);
683
-			return [];
684
-		}
685
-	}
88
+    /** @var IOperation[] */
89
+    protected $registeredOperators = [];
90
+
91
+    /** @var ICheck[] */
92
+    protected $registeredChecks = [];
93
+
94
+    /** @var ILogger */
95
+    protected $logger;
96
+
97
+    /** @var CappedMemoryCache */
98
+    protected $operationsByScope = [];
99
+
100
+    /** @var IUserSession */
101
+    protected $session;
102
+
103
+    /**
104
+     * @param IDBConnection $connection
105
+     * @param IServerContainer $container
106
+     * @param IL10N $l
107
+     */
108
+    public function __construct(
109
+        IDBConnection $connection,
110
+        IServerContainer $container,
111
+        IL10N $l,
112
+        EventDispatcherInterface $eventDispatcher,
113
+        ILogger $logger,
114
+        IUserSession $session
115
+    ) {
116
+        $this->connection = $connection;
117
+        $this->container = $container;
118
+        $this->l = $l;
119
+        $this->eventDispatcher = $eventDispatcher;
120
+        $this->logger = $logger;
121
+        $this->operationsByScope = new CappedMemoryCache(64);
122
+        $this->session = $session;
123
+    }
124
+
125
+    public function getRuleMatcher(): IRuleMatcher {
126
+        return new RuleMatcher($this->session, $this->container, $this->l, $this);
127
+    }
128
+
129
+    public function getAllConfiguredEvents() {
130
+        $query = $this->connection->getQueryBuilder();
131
+
132
+        $query->selectDistinct('class')
133
+            ->addSelect('entity', 'events')
134
+            ->from('flow_operations')
135
+            ->where($query->expr()->neq('events', $query->createNamedParameter('[]'), IQueryBuilder::PARAM_STR));
136
+
137
+        $result = $query->execute();
138
+        $operations = [];
139
+        while($row = $result->fetch()) {
140
+            $eventNames = \json_decode($row['events']);
141
+
142
+            $operation = $row['class'];
143
+            $entity =  $row['entity'];
144
+
145
+            $operations[$operation] = $operations[$row['class']] ?? [];
146
+            $operations[$operation][$entity] = $operations[$operation][$entity] ?? [];
147
+
148
+            $operations[$operation][$entity] = array_unique(array_merge($operations[$operation][$entity], $eventNames ?? []));
149
+        }
150
+        $result->closeCursor();
151
+
152
+        return $operations;
153
+    }
154
+
155
+    public function getAllConfiguredScopesForOperation(string $operationClass): array {
156
+        static $scopesByOperation = [];
157
+        if (isset($scopesByOperation[$operationClass])) {
158
+            return $scopesByOperation[$operationClass];
159
+        }
160
+
161
+        $query = $this->connection->getQueryBuilder();
162
+
163
+        $query->selectDistinct('s.type')
164
+            ->addSelect('s.value')
165
+            ->from('flow_operations', 'o')
166
+            ->leftJoin('o', 'flow_operations_scope', 's', $query->expr()->eq('o.id', 's.operation_id'))
167
+            ->where($query->expr()->eq('o.class', $query->createParameter('operationClass')));
168
+
169
+        $query->setParameters(['operationClass' => $operationClass]);
170
+        $result = $query->execute();
171
+
172
+        $scopesByOperation[$operationClass] = [];
173
+        while ($row = $result->fetch()) {
174
+            $scope = new ScopeContext($row['type'], $row['value']);
175
+            $scopesByOperation[$operationClass][$scope->getHash()] = $scope;
176
+        }
177
+
178
+        return $scopesByOperation[$operationClass];
179
+    }
180
+
181
+    public function getAllOperations(ScopeContext $scopeContext): array {
182
+        if(isset($this->operations[$scopeContext->getHash()])) {
183
+            return $this->operations[$scopeContext->getHash()];
184
+        }
185
+
186
+        $query = $this->connection->getQueryBuilder();
187
+
188
+        $query->select('o.*')
189
+            ->selectAlias('s.type', 'scope_type')
190
+            ->selectAlias('s.value', 'scope_actor_id')
191
+            ->from('flow_operations', 'o')
192
+            ->leftJoin('o', 'flow_operations_scope', 's', $query->expr()->eq('o.id', 's.operation_id'))
193
+            ->where($query->expr()->eq('s.type', $query->createParameter('scope')));
194
+
195
+        if($scopeContext->getScope() === IManager::SCOPE_USER) {
196
+            $query->andWhere($query->expr()->eq('s.value', $query->createParameter('scopeId')));
197
+        }
198
+
199
+        $query->setParameters(['scope' => $scopeContext->getScope(), 'scopeId' => $scopeContext->getScopeId()]);
200
+        $result = $query->execute();
201
+
202
+        $this->operations[$scopeContext->getHash()] = [];
203
+        while ($row = $result->fetch()) {
204
+            if(!isset($this->operations[$scopeContext->getHash()][$row['class']])) {
205
+                $this->operations[$scopeContext->getHash()][$row['class']] = [];
206
+            }
207
+            $this->operations[$scopeContext->getHash()][$row['class']][] = $row;
208
+        }
209
+
210
+        return $this->operations[$scopeContext->getHash()];
211
+    }
212
+
213
+    public function getOperations(string $class, ScopeContext $scopeContext): array {
214
+        if (!isset($this->operations[$scopeContext->getHash()])) {
215
+            $this->getAllOperations($scopeContext);
216
+        }
217
+        return $this->operations[$scopeContext->getHash()][$class] ?? [];
218
+    }
219
+
220
+    /**
221
+     * @param int $id
222
+     * @return array
223
+     * @throws \UnexpectedValueException
224
+     */
225
+    protected function getOperation($id) {
226
+        $query = $this->connection->getQueryBuilder();
227
+        $query->select('*')
228
+            ->from('flow_operations')
229
+            ->where($query->expr()->eq('id', $query->createNamedParameter($id)));
230
+        $result = $query->execute();
231
+        $row = $result->fetch();
232
+        $result->closeCursor();
233
+
234
+        if ($row) {
235
+            return $row;
236
+        }
237
+
238
+        throw new \UnexpectedValueException($this->l->t('Operation #%s does not exist', [$id]));
239
+    }
240
+
241
+    protected function insertOperation(
242
+        string $class,
243
+        string $name,
244
+        array $checkIds,
245
+        string $operation,
246
+        string $entity,
247
+        array $events
248
+    ): int {
249
+        $query = $this->connection->getQueryBuilder();
250
+        $query->insert('flow_operations')
251
+            ->values([
252
+                'class' => $query->createNamedParameter($class),
253
+                'name' => $query->createNamedParameter($name),
254
+                'checks' => $query->createNamedParameter(json_encode(array_unique($checkIds))),
255
+                'operation' => $query->createNamedParameter($operation),
256
+                'entity' => $query->createNamedParameter($entity),
257
+                'events' => $query->createNamedParameter(json_encode($events))
258
+            ]);
259
+        $query->execute();
260
+
261
+        return $query->getLastInsertId();
262
+    }
263
+
264
+    /**
265
+     * @param string $class
266
+     * @param string $name
267
+     * @param array[] $checks
268
+     * @param string $operation
269
+     * @return array The added operation
270
+     * @throws \UnexpectedValueException
271
+     * @throws DBALException
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, $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 (DBALException $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->where($qb->expr()->eq('s.value', $qb->createParameter('scopeId')));
317
+        }
318
+
319
+        $qb->setParameters(['scope' => $scopeContext->getScope(), 'scopeId' => $scopeContext->getScopeId()]);
320
+        $result = $qb->execute();
321
+
322
+        $this->operationsByScope[$scopeContext->getHash()] = [];
323
+        while($opId = $result->fetchColumn(0)) {
324
+            $this->operationsByScope[$scopeContext->getHash()][] = (int)$opId;
325
+        }
326
+        $result->closeCursor();
327
+
328
+        return in_array($id, $this->operationsByScope[$scopeContext->getHash()], true);
329
+    }
330
+
331
+    /**
332
+     * @param int $id
333
+     * @param string $name
334
+     * @param array[] $checks
335
+     * @param string $operation
336
+     * @return array The updated operation
337
+     * @throws \UnexpectedValueException
338
+     * @throws \DomainException
339
+     * @throws DBALException
340
+     */
341
+    public function updateOperation(
342
+        int $id,
343
+        string $name,
344
+        array $checks,
345
+        string $operation,
346
+        ScopeContext $scopeContext,
347
+        string $entity,
348
+        array $events
349
+    ): array {
350
+        if(!$this->canModify($id, $scopeContext)) {
351
+            throw new \DomainException('Target operation not within scope');
352
+        };
353
+        $row = $this->getOperation($id);
354
+        $this->validateOperation($row['class'], $name, $checks, $operation, $entity, $events);
355
+
356
+        $checkIds = [];
357
+        try {
358
+            $this->connection->beginTransaction();
359
+            foreach ($checks as $check) {
360
+                $checkIds[] = $this->addCheck($check['class'], $check['operator'], $check['value']);
361
+            }
362
+
363
+            $query = $this->connection->getQueryBuilder();
364
+            $query->update('flow_operations')
365
+                ->set('name', $query->createNamedParameter($name))
366
+                ->set('checks', $query->createNamedParameter(json_encode(array_unique($checkIds))))
367
+                ->set('operation', $query->createNamedParameter($operation))
368
+                ->set('entity', $query->createNamedParameter($entity))
369
+                ->set('events', $query->createNamedParameter(json_encode($events)))
370
+                ->where($query->expr()->eq('id', $query->createNamedParameter($id)));
371
+            $query->execute();
372
+            $this->connection->commit();
373
+        } catch (DBALException $e) {
374
+            $this->connection->rollBack();
375
+            throw $e;
376
+        }
377
+        unset($this->operations[$scopeContext->getHash()]);
378
+
379
+        return $this->getOperation($id);
380
+    }
381
+
382
+    /**
383
+     * @param int $id
384
+     * @return bool
385
+     * @throws \UnexpectedValueException
386
+     * @throws DBALException
387
+     * @throws \DomainException
388
+     */
389
+    public function deleteOperation($id, ScopeContext $scopeContext) {
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
+                ->execute();
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
+                    ->execute();
404
+            }
405
+            $this->connection->commit();
406
+        } catch (DBALException $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
+        return $result;
416
+    }
417
+
418
+    protected function validateEvents(string $entity, array $events, IOperation $operation) {
419
+        try {
420
+            /** @var IEntity $instance */
421
+            $instance = $this->container->query($entity);
422
+        } catch (QueryException $e) {
423
+            throw new \UnexpectedValueException($this->l->t('Entity %s does not exist', [$entity]));
424
+        }
425
+
426
+        if(!$instance instanceof IEntity) {
427
+            throw new \UnexpectedValueException($this->l->t('Entity %s is invalid', [$entity]));
428
+        }
429
+
430
+        if(empty($events)) {
431
+            if(!$operation instanceof IComplexOperation) {
432
+                throw new \UnexpectedValueException($this->l->t('No events are chosen.'));
433
+            }
434
+            return;
435
+        }
436
+
437
+        $availableEvents = [];
438
+        foreach ($instance->getEvents() as $event) {
439
+            /** @var IEntityEvent $event */
440
+            $availableEvents[] = $event->getEventName();
441
+        }
442
+
443
+        $diff = array_diff($events, $availableEvents);
444
+        if(!empty($diff)) {
445
+            throw new \UnexpectedValueException($this->l->t('Entity %s has no event %s', [$entity, array_shift($diff)]));
446
+        }
447
+    }
448
+
449
+    /**
450
+     * @param string $class
451
+     * @param string $name
452
+     * @param array[] $checks
453
+     * @param string $operation
454
+     * @throws \UnexpectedValueException
455
+     */
456
+    public function validateOperation($class, $name, array $checks, $operation, string $entity, array $events) {
457
+        try {
458
+            /** @var IOperation $instance */
459
+            $instance = $this->container->query($class);
460
+        } catch (QueryException $e) {
461
+            throw new \UnexpectedValueException($this->l->t('Operation %s does not exist', [$class]));
462
+        }
463
+
464
+        if (!($instance instanceof IOperation)) {
465
+            throw new \UnexpectedValueException($this->l->t('Operation %s is invalid', [$class]));
466
+        }
467
+
468
+        $this->validateEvents($entity, $events, $instance);
469
+
470
+        $instance->validateOperation($name, $checks, $operation);
471
+
472
+        foreach ($checks as $check) {
473
+            try {
474
+                /** @var ICheck $instance */
475
+                $instance = $this->container->query($check['class']);
476
+            } catch (QueryException $e) {
477
+                throw new \UnexpectedValueException($this->l->t('Check %s does not exist', [$class]));
478
+            }
479
+
480
+            if (!($instance instanceof ICheck)) {
481
+                throw new \UnexpectedValueException($this->l->t('Check %s is invalid', [$class]));
482
+            }
483
+
484
+            if (!empty($instance->supportedEntities())
485
+                && !in_array($entity, $instance->supportedEntities())
486
+            ) {
487
+                throw new \UnexpectedValueException($this->l->t('Check %s is not allowed with this entity', [$class]));
488
+            }
489
+
490
+            $instance->validateCheck($check['operator'], $check['value']);
491
+        }
492
+    }
493
+
494
+    /**
495
+     * @param int[] $checkIds
496
+     * @return array[]
497
+     */
498
+    public function getChecks(array $checkIds) {
499
+        $checkIds = array_map('intval', $checkIds);
500
+
501
+        $checks = [];
502
+        foreach ($checkIds as $i => $checkId) {
503
+            if (isset($this->checks[$checkId])) {
504
+                $checks[$checkId] = $this->checks[$checkId];
505
+                unset($checkIds[$i]);
506
+            }
507
+        }
508
+
509
+        if (empty($checkIds)) {
510
+            return $checks;
511
+        }
512
+
513
+        $query = $this->connection->getQueryBuilder();
514
+        $query->select('*')
515
+            ->from('flow_checks')
516
+            ->where($query->expr()->in('id', $query->createNamedParameter($checkIds, IQueryBuilder::PARAM_INT_ARRAY)));
517
+        $result = $query->execute();
518
+
519
+        while ($row = $result->fetch()) {
520
+            $this->checks[(int) $row['id']] = $row;
521
+            $checks[(int) $row['id']] = $row;
522
+        }
523
+        $result->closeCursor();
524
+
525
+        $checkIds = array_diff($checkIds, array_keys($checks));
526
+
527
+        if (!empty($checkIds)) {
528
+            $missingCheck = array_pop($checkIds);
529
+            throw new \UnexpectedValueException($this->l->t('Check #%s does not exist', $missingCheck));
530
+        }
531
+
532
+        return $checks;
533
+    }
534
+
535
+    /**
536
+     * @param string $class
537
+     * @param string $operator
538
+     * @param string $value
539
+     * @return int Check unique ID
540
+     */
541
+    protected function addCheck($class, $operator, $value) {
542
+        $hash = md5($class . '::' . $operator . '::' . $value);
543
+
544
+        $query = $this->connection->getQueryBuilder();
545
+        $query->select('id')
546
+            ->from('flow_checks')
547
+            ->where($query->expr()->eq('hash', $query->createNamedParameter($hash)));
548
+        $result = $query->execute();
549
+
550
+        if ($row = $result->fetch()) {
551
+            $result->closeCursor();
552
+            return (int) $row['id'];
553
+        }
554
+
555
+        $query = $this->connection->getQueryBuilder();
556
+        $query->insert('flow_checks')
557
+            ->values([
558
+                'class' => $query->createNamedParameter($class),
559
+                'operator' => $query->createNamedParameter($operator),
560
+                'value' => $query->createNamedParameter($value),
561
+                'hash' => $query->createNamedParameter($hash),
562
+            ]);
563
+        $query->execute();
564
+
565
+        return $query->getLastInsertId();
566
+    }
567
+
568
+    protected function addScope(int $operationId, ScopeContext $scope): void {
569
+        $query = $this->connection->getQueryBuilder();
570
+
571
+        $insertQuery = $query->insert('flow_operations_scope');
572
+        $insertQuery->values([
573
+            'operation_id' => $query->createNamedParameter($operationId),
574
+            'type' => $query->createNamedParameter($scope->getScope()),
575
+            'value' => $query->createNamedParameter($scope->getScopeId()),
576
+        ]);
577
+        $insertQuery->execute();
578
+    }
579
+
580
+    public function formatOperation(array $operation): array {
581
+        $checkIds = json_decode($operation['checks'], true);
582
+        $checks = $this->getChecks($checkIds);
583
+
584
+        $operation['checks'] = [];
585
+        foreach ($checks as $check) {
586
+            // Remove internal values
587
+            unset($check['id']);
588
+            unset($check['hash']);
589
+
590
+            $operation['checks'][] = $check;
591
+        }
592
+        $operation['events'] = json_decode($operation['events'], true) ?? [];
593
+
594
+
595
+        return $operation;
596
+    }
597
+
598
+    /**
599
+     * @return IEntity[]
600
+     */
601
+    public function getEntitiesList(): array {
602
+        $this->eventDispatcher->dispatch(IManager::EVENT_NAME_REG_ENTITY, new GenericEvent($this));
603
+
604
+        return array_values(array_merge($this->getBuildInEntities(), $this->registeredEntities));
605
+    }
606
+
607
+    /**
608
+     * @return IOperation[]
609
+     */
610
+    public function getOperatorList(): array {
611
+        $this->eventDispatcher->dispatch(IManager::EVENT_NAME_REG_OPERATION, new GenericEvent($this));
612
+
613
+        return array_merge($this->getBuildInOperators(), $this->registeredOperators);
614
+    }
615
+
616
+    /**
617
+     * @return ICheck[]
618
+     */
619
+    public function getCheckList(): array {
620
+        $this->eventDispatcher->dispatch(IManager::EVENT_NAME_REG_CHECK, new GenericEvent($this));
621
+
622
+        return array_merge($this->getBuildInChecks(), $this->registeredChecks);
623
+    }
624
+
625
+    public function registerEntity(IEntity $entity): void {
626
+        $this->registeredEntities[get_class($entity)] = $entity;
627
+    }
628
+
629
+    public function registerOperation(IOperation $operator): void {
630
+        $this->registeredOperators[get_class($operator)] = $operator;
631
+    }
632
+
633
+    public function registerCheck(ICheck $check): void {
634
+        $this->registeredChecks[get_class($check)] = $check;
635
+    }
636
+
637
+    /**
638
+     * @return IEntity[]
639
+     */
640
+    protected function getBuildInEntities(): array {
641
+        try {
642
+            return [
643
+                File::class => $this->container->query(File::class),
644
+            ];
645
+        } catch (QueryException $e) {
646
+            $this->logger->logException($e);
647
+            return [];
648
+        }
649
+    }
650
+
651
+    /**
652
+     * @return IOperation[]
653
+     */
654
+    protected function getBuildInOperators(): array {
655
+        try {
656
+            return [
657
+                // None yet
658
+            ];
659
+        } catch (QueryException $e) {
660
+            $this->logger->logException($e);
661
+            return [];
662
+        }
663
+    }
664
+
665
+    /**
666
+     * @return IEntity[]
667
+     */
668
+    protected function getBuildInChecks(): array {
669
+        try {
670
+            return [
671
+                $this->container->query(FileMimeType::class),
672
+                $this->container->query(FileName::class),
673
+                $this->container->query(FileSize::class),
674
+                $this->container->query(FileSystemTags::class),
675
+                $this->container->query(RequestRemoteAddress::class),
676
+                $this->container->query(RequestTime::class),
677
+                $this->container->query(RequestURL::class),
678
+                $this->container->query(RequestUserAgent::class),
679
+                $this->container->query(UserGroupMembership::class),
680
+            ];
681
+        } catch (QueryException $e) {
682
+            $this->logger->logException($e);
683
+            return [];
684
+        }
685
+    }
686 686
 }
Please login to merge, or discard this patch.