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