123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713 |
- <?php
- /**
- * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
- * SPDX-License-Identifier: AGPL-3.0-or-later
- */
- namespace OCA\WorkflowEngine;
- use Doctrine\DBAL\Exception;
- use OCA\WorkflowEngine\AppInfo\Application;
- use OCA\WorkflowEngine\Check\FileMimeType;
- use OCA\WorkflowEngine\Check\FileName;
- use OCA\WorkflowEngine\Check\FileSize;
- use OCA\WorkflowEngine\Check\FileSystemTags;
- use OCA\WorkflowEngine\Check\RequestRemoteAddress;
- use OCA\WorkflowEngine\Check\RequestTime;
- use OCA\WorkflowEngine\Check\RequestURL;
- use OCA\WorkflowEngine\Check\RequestUserAgent;
- use OCA\WorkflowEngine\Check\UserGroupMembership;
- use OCA\WorkflowEngine\Entity\File;
- use OCA\WorkflowEngine\Helper\ScopeContext;
- use OCA\WorkflowEngine\Service\Logger;
- use OCA\WorkflowEngine\Service\RuleMatcher;
- use OCP\AppFramework\QueryException;
- use OCP\Cache\CappedMemoryCache;
- use OCP\DB\QueryBuilder\IQueryBuilder;
- use OCP\EventDispatcher\IEventDispatcher;
- use OCP\ICacheFactory;
- use OCP\IConfig;
- use OCP\IDBConnection;
- use OCP\IL10N;
- use OCP\IServerContainer;
- use OCP\IUserSession;
- use OCP\WorkflowEngine\Events\RegisterChecksEvent;
- use OCP\WorkflowEngine\Events\RegisterEntitiesEvent;
- use OCP\WorkflowEngine\Events\RegisterOperationsEvent;
- use OCP\WorkflowEngine\ICheck;
- use OCP\WorkflowEngine\IComplexOperation;
- use OCP\WorkflowEngine\IEntity;
- use OCP\WorkflowEngine\IEntityEvent;
- use OCP\WorkflowEngine\IManager;
- use OCP\WorkflowEngine\IOperation;
- use OCP\WorkflowEngine\IRuleMatcher;
- use Psr\Log\LoggerInterface;
- class Manager implements IManager {
- /** @var array[] */
- protected $operations = [];
- /** @var array[] */
- protected $checks = [];
- /** @var IEntity[] */
- protected $registeredEntities = [];
- /** @var IOperation[] */
- protected $registeredOperators = [];
- /** @var ICheck[] */
- protected $registeredChecks = [];
- /** @var CappedMemoryCache<int[]> */
- protected CappedMemoryCache $operationsByScope;
- public function __construct(
- protected IDBConnection $connection,
- protected IServerContainer $container,
- protected IL10N $l,
- protected LoggerInterface $logger,
- protected IUserSession $session,
- private IEventDispatcher $dispatcher,
- private IConfig $config,
- private ICacheFactory $cacheFactory,
- ) {
- $this->operationsByScope = new CappedMemoryCache(64);
- }
- public function getRuleMatcher(): IRuleMatcher {
- return new RuleMatcher(
- $this->session,
- $this->container,
- $this->l,
- $this,
- $this->container->query(Logger::class)
- );
- }
- public function getAllConfiguredEvents() {
- $cache = $this->cacheFactory->createDistributed('flow');
- $cached = $cache->get('events');
- if ($cached !== null) {
- return $cached;
- }
- $query = $this->connection->getQueryBuilder();
- $query->select('class', 'entity')
- ->selectAlias($query->expr()->castColumn('events', IQueryBuilder::PARAM_STR), 'events')
- ->from('flow_operations')
- ->where($query->expr()->neq('events', $query->createNamedParameter('[]'), IQueryBuilder::PARAM_STR))
- ->groupBy('class', 'entity', $query->expr()->castColumn('events', IQueryBuilder::PARAM_STR));
- $result = $query->executeQuery();
- $operations = [];
- while ($row = $result->fetch()) {
- $eventNames = \json_decode($row['events']);
- $operation = $row['class'];
- $entity = $row['entity'];
- $operations[$operation] = $operations[$row['class']] ?? [];
- $operations[$operation][$entity] = $operations[$operation][$entity] ?? [];
- $operations[$operation][$entity] = array_unique(array_merge($operations[$operation][$entity], $eventNames ?? []));
- }
- $result->closeCursor();
- $cache->set('events', $operations, 3600);
- return $operations;
- }
- /**
- * @param string $operationClass
- * @return ScopeContext[]
- */
- public function getAllConfiguredScopesForOperation(string $operationClass): array {
- static $scopesByOperation = [];
- if (isset($scopesByOperation[$operationClass])) {
- return $scopesByOperation[$operationClass];
- }
- try {
- /** @var IOperation $operation */
- $operation = $this->container->query($operationClass);
- } catch (QueryException $e) {
- return [];
- }
- $query = $this->connection->getQueryBuilder();
- $query->selectDistinct('s.type')
- ->addSelect('s.value')
- ->from('flow_operations', 'o')
- ->leftJoin('o', 'flow_operations_scope', 's', $query->expr()->eq('o.id', 's.operation_id'))
- ->where($query->expr()->eq('o.class', $query->createParameter('operationClass')));
- $query->setParameters(['operationClass' => $operationClass]);
- $result = $query->executeQuery();
- $scopesByOperation[$operationClass] = [];
- while ($row = $result->fetch()) {
- $scope = new ScopeContext($row['type'], $row['value']);
- if (!$operation->isAvailableForScope((int)$row['type'])) {
- continue;
- }
- $scopesByOperation[$operationClass][$scope->getHash()] = $scope;
- }
- return $scopesByOperation[$operationClass];
- }
- public function getAllOperations(ScopeContext $scopeContext): array {
- if (isset($this->operations[$scopeContext->getHash()])) {
- return $this->operations[$scopeContext->getHash()];
- }
- $query = $this->connection->getQueryBuilder();
- $query->select('o.*')
- ->selectAlias('s.type', 'scope_type')
- ->selectAlias('s.value', 'scope_actor_id')
- ->from('flow_operations', 'o')
- ->leftJoin('o', 'flow_operations_scope', 's', $query->expr()->eq('o.id', 's.operation_id'))
- ->where($query->expr()->eq('s.type', $query->createParameter('scope')));
- if ($scopeContext->getScope() === IManager::SCOPE_USER) {
- $query->andWhere($query->expr()->eq('s.value', $query->createParameter('scopeId')));
- }
- $query->setParameters(['scope' => $scopeContext->getScope(), 'scopeId' => $scopeContext->getScopeId()]);
- $result = $query->executeQuery();
- $this->operations[$scopeContext->getHash()] = [];
- while ($row = $result->fetch()) {
- try {
- /** @var IOperation $operation */
- $operation = $this->container->query($row['class']);
- } catch (QueryException $e) {
- continue;
- }
- if (!$operation->isAvailableForScope((int)$row['scope_type'])) {
- continue;
- }
- if (!isset($this->operations[$scopeContext->getHash()][$row['class']])) {
- $this->operations[$scopeContext->getHash()][$row['class']] = [];
- }
- $this->operations[$scopeContext->getHash()][$row['class']][] = $row;
- }
- return $this->operations[$scopeContext->getHash()];
- }
- public function getOperations(string $class, ScopeContext $scopeContext): array {
- if (!isset($this->operations[$scopeContext->getHash()])) {
- $this->getAllOperations($scopeContext);
- }
- return $this->operations[$scopeContext->getHash()][$class] ?? [];
- }
- /**
- * @param int $id
- * @return array
- * @throws \UnexpectedValueException
- */
- protected function getOperation($id) {
- $query = $this->connection->getQueryBuilder();
- $query->select('*')
- ->from('flow_operations')
- ->where($query->expr()->eq('id', $query->createNamedParameter($id)));
- $result = $query->executeQuery();
- $row = $result->fetch();
- $result->closeCursor();
- if ($row) {
- return $row;
- }
- throw new \UnexpectedValueException($this->l->t('Operation #%s does not exist', [$id]));
- }
- protected function insertOperation(
- string $class,
- string $name,
- array $checkIds,
- string $operation,
- string $entity,
- array $events,
- ): int {
- $query = $this->connection->getQueryBuilder();
- $query->insert('flow_operations')
- ->values([
- 'class' => $query->createNamedParameter($class),
- 'name' => $query->createNamedParameter($name),
- 'checks' => $query->createNamedParameter(json_encode(array_unique($checkIds))),
- 'operation' => $query->createNamedParameter($operation),
- 'entity' => $query->createNamedParameter($entity),
- 'events' => $query->createNamedParameter(json_encode($events))
- ]);
- $query->executeStatement();
- $this->cacheFactory->createDistributed('flow')->remove('events');
- return $query->getLastInsertId();
- }
- /**
- * @param string $class
- * @param string $name
- * @param array[] $checks
- * @param string $operation
- * @return array The added operation
- * @throws \UnexpectedValueException
- * @throws Exception
- */
- public function addOperation(
- string $class,
- string $name,
- array $checks,
- string $operation,
- ScopeContext $scope,
- string $entity,
- array $events,
- ) {
- $this->validateOperation($class, $name, $checks, $operation, $scope, $entity, $events);
- $this->connection->beginTransaction();
- try {
- $checkIds = [];
- foreach ($checks as $check) {
- $checkIds[] = $this->addCheck($check['class'], $check['operator'], $check['value']);
- }
- $id = $this->insertOperation($class, $name, $checkIds, $operation, $entity, $events);
- $this->addScope($id, $scope);
- $this->connection->commit();
- } catch (Exception $e) {
- $this->connection->rollBack();
- throw $e;
- }
- return $this->getOperation($id);
- }
- protected function canModify(int $id, ScopeContext $scopeContext):bool {
- if (isset($this->operationsByScope[$scopeContext->getHash()])) {
- return in_array($id, $this->operationsByScope[$scopeContext->getHash()], true);
- }
- $qb = $this->connection->getQueryBuilder();
- $qb = $qb->select('o.id')
- ->from('flow_operations', 'o')
- ->leftJoin('o', 'flow_operations_scope', 's', $qb->expr()->eq('o.id', 's.operation_id'))
- ->where($qb->expr()->eq('s.type', $qb->createParameter('scope')));
- if ($scopeContext->getScope() !== IManager::SCOPE_ADMIN) {
- $qb->andWhere($qb->expr()->eq('s.value', $qb->createParameter('scopeId')));
- }
- $qb->setParameters(['scope' => $scopeContext->getScope(), 'scopeId' => $scopeContext->getScopeId()]);
- $result = $qb->executeQuery();
- $operations = [];
- while (($opId = $result->fetchOne()) !== false) {
- $operations[] = (int)$opId;
- }
- $this->operationsByScope[$scopeContext->getHash()] = $operations;
- $result->closeCursor();
- return in_array($id, $this->operationsByScope[$scopeContext->getHash()], true);
- }
- /**
- * @param int $id
- * @param string $name
- * @param array[] $checks
- * @param string $operation
- * @return array The updated operation
- * @throws \UnexpectedValueException
- * @throws \DomainException
- * @throws Exception
- */
- public function updateOperation(
- int $id,
- string $name,
- array $checks,
- string $operation,
- ScopeContext $scopeContext,
- string $entity,
- array $events,
- ): array {
- if (!$this->canModify($id, $scopeContext)) {
- throw new \DomainException('Target operation not within scope');
- };
- $row = $this->getOperation($id);
- $this->validateOperation($row['class'], $name, $checks, $operation, $scopeContext, $entity, $events);
- $checkIds = [];
- try {
- $this->connection->beginTransaction();
- foreach ($checks as $check) {
- $checkIds[] = $this->addCheck($check['class'], $check['operator'], $check['value']);
- }
- $query = $this->connection->getQueryBuilder();
- $query->update('flow_operations')
- ->set('name', $query->createNamedParameter($name))
- ->set('checks', $query->createNamedParameter(json_encode(array_unique($checkIds))))
- ->set('operation', $query->createNamedParameter($operation))
- ->set('entity', $query->createNamedParameter($entity))
- ->set('events', $query->createNamedParameter(json_encode($events)))
- ->where($query->expr()->eq('id', $query->createNamedParameter($id)));
- $query->execute();
- $this->connection->commit();
- } catch (Exception $e) {
- $this->connection->rollBack();
- throw $e;
- }
- unset($this->operations[$scopeContext->getHash()]);
- $this->cacheFactory->createDistributed('flow')->remove('events');
- return $this->getOperation($id);
- }
- /**
- * @param int $id
- * @return bool
- * @throws \UnexpectedValueException
- * @throws Exception
- * @throws \DomainException
- */
- public function deleteOperation($id, ScopeContext $scopeContext) {
- if (!$this->canModify($id, $scopeContext)) {
- throw new \DomainException('Target operation not within scope');
- };
- $query = $this->connection->getQueryBuilder();
- try {
- $this->connection->beginTransaction();
- $result = (bool)$query->delete('flow_operations')
- ->where($query->expr()->eq('id', $query->createNamedParameter($id)))
- ->executeStatement();
- if ($result) {
- $qb = $this->connection->getQueryBuilder();
- $result &= (bool)$qb->delete('flow_operations_scope')
- ->where($qb->expr()->eq('operation_id', $qb->createNamedParameter($id)))
- ->executeStatement();
- }
- $this->connection->commit();
- } catch (Exception $e) {
- $this->connection->rollBack();
- throw $e;
- }
- if (isset($this->operations[$scopeContext->getHash()])) {
- unset($this->operations[$scopeContext->getHash()]);
- }
- $this->cacheFactory->createDistributed('flow')->remove('events');
- return $result;
- }
- protected function validateEvents(string $entity, array $events, IOperation $operation) {
- try {
- /** @var IEntity $instance */
- $instance = $this->container->query($entity);
- } catch (QueryException $e) {
- throw new \UnexpectedValueException($this->l->t('Entity %s does not exist', [$entity]));
- }
- if (!$instance instanceof IEntity) {
- throw new \UnexpectedValueException($this->l->t('Entity %s is invalid', [$entity]));
- }
- if (empty($events)) {
- if (!$operation instanceof IComplexOperation) {
- throw new \UnexpectedValueException($this->l->t('No events are chosen.'));
- }
- return;
- }
- $availableEvents = [];
- foreach ($instance->getEvents() as $event) {
- /** @var IEntityEvent $event */
- $availableEvents[] = $event->getEventName();
- }
- $diff = array_diff($events, $availableEvents);
- if (!empty($diff)) {
- throw new \UnexpectedValueException($this->l->t('Entity %s has no event %s', [$entity, array_shift($diff)]));
- }
- }
- /**
- * @param string $class
- * @param string $name
- * @param array[] $checks
- * @param string $operation
- * @param ScopeContext $scope
- * @param string $entity
- * @param array $events
- * @throws \UnexpectedValueException
- */
- public function validateOperation($class, $name, array $checks, $operation, ScopeContext $scope, string $entity, array $events) {
- try {
- /** @var IOperation $instance */
- $instance = $this->container->query($class);
- } catch (QueryException $e) {
- throw new \UnexpectedValueException($this->l->t('Operation %s does not exist', [$class]));
- }
- if (!($instance instanceof IOperation)) {
- throw new \UnexpectedValueException($this->l->t('Operation %s is invalid', [$class]));
- }
- if (!$instance->isAvailableForScope($scope->getScope())) {
- throw new \UnexpectedValueException($this->l->t('Operation %s is invalid', [$class]));
- }
- $this->validateEvents($entity, $events, $instance);
- if (count($checks) === 0) {
- throw new \UnexpectedValueException($this->l->t('At least one check needs to be provided'));
- }
- if (strlen((string)$operation) > IManager::MAX_OPERATION_VALUE_BYTES) {
- throw new \UnexpectedValueException($this->l->t('The provided operation data is too long'));
- }
- $instance->validateOperation($name, $checks, $operation);
- foreach ($checks as $check) {
- if (!is_string($check['class'])) {
- throw new \UnexpectedValueException($this->l->t('Invalid check provided'));
- }
- try {
- /** @var ICheck $instance */
- $instance = $this->container->query($check['class']);
- } catch (QueryException $e) {
- throw new \UnexpectedValueException($this->l->t('Check %s does not exist', [$class]));
- }
- if (!($instance instanceof ICheck)) {
- throw new \UnexpectedValueException($this->l->t('Check %s is invalid', [$class]));
- }
- if (!empty($instance->supportedEntities())
- && !in_array($entity, $instance->supportedEntities())
- ) {
- throw new \UnexpectedValueException($this->l->t('Check %s is not allowed with this entity', [$class]));
- }
- if (strlen((string)$check['value']) > IManager::MAX_CHECK_VALUE_BYTES) {
- throw new \UnexpectedValueException($this->l->t('The provided check value is too long'));
- }
- $instance->validateCheck($check['operator'], $check['value']);
- }
- }
- /**
- * @param int[] $checkIds
- * @return array[]
- */
- public function getChecks(array $checkIds) {
- $checkIds = array_map('intval', $checkIds);
- $checks = [];
- foreach ($checkIds as $i => $checkId) {
- if (isset($this->checks[$checkId])) {
- $checks[$checkId] = $this->checks[$checkId];
- unset($checkIds[$i]);
- }
- }
- if (empty($checkIds)) {
- return $checks;
- }
- $query = $this->connection->getQueryBuilder();
- $query->select('*')
- ->from('flow_checks')
- ->where($query->expr()->in('id', $query->createNamedParameter($checkIds, IQueryBuilder::PARAM_INT_ARRAY)));
- $result = $query->executeQuery();
- while ($row = $result->fetch()) {
- $this->checks[(int)$row['id']] = $row;
- $checks[(int)$row['id']] = $row;
- }
- $result->closeCursor();
- $checkIds = array_diff($checkIds, array_keys($checks));
- if (!empty($checkIds)) {
- $missingCheck = array_pop($checkIds);
- throw new \UnexpectedValueException($this->l->t('Check #%s does not exist', $missingCheck));
- }
- return $checks;
- }
- /**
- * @param string $class
- * @param string $operator
- * @param string $value
- * @return int Check unique ID
- */
- protected function addCheck($class, $operator, $value) {
- $hash = md5($class . '::' . $operator . '::' . $value);
- $query = $this->connection->getQueryBuilder();
- $query->select('id')
- ->from('flow_checks')
- ->where($query->expr()->eq('hash', $query->createNamedParameter($hash)));
- $result = $query->executeQuery();
- if ($row = $result->fetch()) {
- $result->closeCursor();
- return (int)$row['id'];
- }
- $query = $this->connection->getQueryBuilder();
- $query->insert('flow_checks')
- ->values([
- 'class' => $query->createNamedParameter($class),
- 'operator' => $query->createNamedParameter($operator),
- 'value' => $query->createNamedParameter($value),
- 'hash' => $query->createNamedParameter($hash),
- ]);
- $query->executeStatement();
- return $query->getLastInsertId();
- }
- protected function addScope(int $operationId, ScopeContext $scope): void {
- $query = $this->connection->getQueryBuilder();
- $insertQuery = $query->insert('flow_operations_scope');
- $insertQuery->values([
- 'operation_id' => $query->createNamedParameter($operationId),
- 'type' => $query->createNamedParameter($scope->getScope()),
- 'value' => $query->createNamedParameter($scope->getScopeId()),
- ]);
- $insertQuery->executeStatement();
- }
- public function formatOperation(array $operation): array {
- $checkIds = json_decode($operation['checks'], true);
- $checks = $this->getChecks($checkIds);
- $operation['checks'] = [];
- foreach ($checks as $check) {
- // Remove internal values
- unset($check['id']);
- unset($check['hash']);
- $operation['checks'][] = $check;
- }
- $operation['events'] = json_decode($operation['events'], true) ?? [];
- return $operation;
- }
- /**
- * @return IEntity[]
- */
- public function getEntitiesList(): array {
- $this->dispatcher->dispatchTyped(new RegisterEntitiesEvent($this));
- return array_values(array_merge($this->getBuildInEntities(), $this->registeredEntities));
- }
- /**
- * @return IOperation[]
- */
- public function getOperatorList(): array {
- $this->dispatcher->dispatchTyped(new RegisterOperationsEvent($this));
- return array_merge($this->getBuildInOperators(), $this->registeredOperators);
- }
- /**
- * @return ICheck[]
- */
- public function getCheckList(): array {
- $this->dispatcher->dispatchTyped(new RegisterChecksEvent($this));
- return array_merge($this->getBuildInChecks(), $this->registeredChecks);
- }
- public function registerEntity(IEntity $entity): void {
- $this->registeredEntities[get_class($entity)] = $entity;
- }
- public function registerOperation(IOperation $operator): void {
- $this->registeredOperators[get_class($operator)] = $operator;
- }
- public function registerCheck(ICheck $check): void {
- $this->registeredChecks[get_class($check)] = $check;
- }
- /**
- * @return IEntity[]
- */
- protected function getBuildInEntities(): array {
- try {
- return [
- File::class => $this->container->query(File::class),
- ];
- } catch (QueryException $e) {
- $this->logger->error($e->getMessage(), ['exception' => $e]);
- return [];
- }
- }
- /**
- * @return IOperation[]
- */
- protected function getBuildInOperators(): array {
- try {
- return [
- // None yet
- ];
- } catch (QueryException $e) {
- $this->logger->error($e->getMessage(), ['exception' => $e]);
- return [];
- }
- }
- /**
- * @return ICheck[]
- */
- protected function getBuildInChecks(): array {
- try {
- return [
- $this->container->query(FileMimeType::class),
- $this->container->query(FileName::class),
- $this->container->query(FileSize::class),
- $this->container->query(FileSystemTags::class),
- $this->container->query(RequestRemoteAddress::class),
- $this->container->query(RequestTime::class),
- $this->container->query(RequestURL::class),
- $this->container->query(RequestUserAgent::class),
- $this->container->query(UserGroupMembership::class),
- ];
- } catch (QueryException $e) {
- $this->logger->error($e->getMessage(), ['exception' => $e]);
- return [];
- }
- }
- public function isUserScopeEnabled(): bool {
- return $this->config->getAppValue(Application::APP_ID, 'user_scope_disabled', 'no') === 'no';
- }
- }
|