diff options
Diffstat (limited to 'apps/workflowengine/lib/Service')
-rw-r--r-- | apps/workflowengine/lib/Service/Logger.php | 152 | ||||
-rw-r--r-- | apps/workflowengine/lib/Service/RuleMatcher.php | 211 |
2 files changed, 363 insertions, 0 deletions
diff --git a/apps/workflowengine/lib/Service/Logger.php b/apps/workflowengine/lib/Service/Logger.php new file mode 100644 index 00000000000..494240bc403 --- /dev/null +++ b/apps/workflowengine/lib/Service/Logger.php @@ -0,0 +1,152 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\WorkflowEngine\Service; + +use OCA\WorkflowEngine\AppInfo\Application; +use OCA\WorkflowEngine\Helper\LogContext; +use OCP\IConfig; +use OCP\ILogger; +use OCP\Log\IDataLogger; +use OCP\Log\ILogFactory; +use Psr\Log\LoggerInterface; + +class Logger { + protected ?LoggerInterface $flowLogger = null; + + public function __construct( + protected LoggerInterface $generalLogger, + private IConfig $config, + private ILogFactory $logFactory, + ) { + $this->initLogger(); + } + + protected function initLogger(): void { + $default = $this->config->getSystemValue('datadirectory', \OC::$SERVERROOT . '/data') . '/flow.log'; + $logFile = trim((string)$this->config->getAppValue(Application::APP_ID, 'logfile', $default)); + if ($logFile !== '') { + $this->flowLogger = $this->logFactory->getCustomPsrLogger($logFile); + } + } + + public function logFlowRequests(LogContext $logContext) { + $message = 'Flow activation: rules were requested for operation {op}'; + $context = ['op' => $logContext->getDetails()['operation']['name'], 'level' => ILogger::DEBUG]; + + $logContext->setDescription('Flow activation: rules were requested'); + + $this->log($message, $context, $logContext); + } + + public function logScopeExpansion(LogContext $logContext) { + $message = 'Flow rule of a different user is legit for operation {op}'; + $context = ['op' => $logContext->getDetails()['operation']['name']]; + + $logContext->setDescription('Flow rule of a different user is legit'); + + $this->log($message, $context, $logContext); + } + + public function logPassedCheck(LogContext $logContext) { + $message = 'Flow rule qualified to run {op}, config: {config}'; + $context = [ + 'op' => $logContext->getDetails()['operation']['name'], + 'config' => $logContext->getDetails()['configuration'], + 'level' => ILogger::DEBUG, + ]; + + $logContext->setDescription('Flow rule qualified to run'); + + $this->log($message, $context, $logContext); + } + + public function logRunSingle(LogContext $logContext) { + $message = 'Last qualified flow configuration is going to run {op}'; + $context = [ + 'op' => $logContext->getDetails()['operation']['name'], + ]; + + $logContext->setDescription('Last qualified flow configuration is going to run'); + + $this->log($message, $context, $logContext); + } + + public function logRunAll(LogContext $logContext) { + $message = 'All qualified flow configurations are going to run {op}'; + $context = [ + 'op' => $logContext->getDetails()['operation']['name'], + ]; + + $logContext->setDescription('All qualified flow configurations are going to run'); + + $this->log($message, $context, $logContext); + } + + public function logRunNone(LogContext $logContext) { + $message = 'No flow configurations is going to run {op}'; + $context = [ + 'op' => $logContext->getDetails()['operation']['name'], + 'level' => ILogger::DEBUG, + ]; + + $logContext->setDescription('No flow configurations is going to run'); + + $this->log($message, $context, $logContext); + } + + public function logEventInit(LogContext $logContext) { + $message = 'Flow activated by event {ev}'; + + $context = [ + 'ev' => $logContext->getDetails()['eventName'], + 'level' => ILogger::DEBUG, + ]; + + $logContext->setDescription('Flow activated by event'); + + $this->log($message, $context, $logContext); + } + + public function logEventDone(LogContext $logContext) { + $message = 'Flow handling done for event {ev}'; + + $context = [ + 'ev' => $logContext->getDetails()['eventName'], + ]; + + $logContext->setDescription('Flow handling for event done'); + + $this->log($message, $context, $logContext); + } + + protected function log( + string $message, + array $context, + LogContext $logContext, + ): void { + if (!isset($context['app'])) { + $context['app'] = Application::APP_ID; + } + if (!isset($context['level'])) { + $context['level'] = ILogger::INFO; + } + $this->generalLogger->log($context['level'], $message, $context); + + if (!$this->flowLogger instanceof IDataLogger) { + return; + } + + $details = $logContext->getDetails(); + $this->flowLogger->logData( + $details['message'], + $details, + ['app' => Application::APP_ID, 'level' => $context['level']] + ); + } +} diff --git a/apps/workflowengine/lib/Service/RuleMatcher.php b/apps/workflowengine/lib/Service/RuleMatcher.php new file mode 100644 index 00000000000..c95387e14ee --- /dev/null +++ b/apps/workflowengine/lib/Service/RuleMatcher.php @@ -0,0 +1,211 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\WorkflowEngine\Service; + +use OCA\WorkflowEngine\Helper\LogContext; +use OCA\WorkflowEngine\Helper\ScopeContext; +use OCA\WorkflowEngine\Manager; +use OCP\AppFramework\QueryException; +use OCP\Files\Storage\IStorage; +use OCP\IL10N; +use OCP\IServerContainer; +use OCP\IUserSession; +use OCP\WorkflowEngine\ICheck; +use OCP\WorkflowEngine\IEntity; +use OCP\WorkflowEngine\IEntityCheck; +use OCP\WorkflowEngine\IFileCheck; +use OCP\WorkflowEngine\IManager; +use OCP\WorkflowEngine\IOperation; +use OCP\WorkflowEngine\IRuleMatcher; +use RuntimeException; + +class RuleMatcher implements IRuleMatcher { + + /** @var array */ + protected $contexts; + /** @var array */ + protected $fileInfo = []; + /** @var IOperation */ + protected $operation; + /** @var IEntity */ + protected $entity; + /** @var string */ + protected $eventName; + + public function __construct( + protected IUserSession $session, + protected IServerContainer $container, + protected IL10N $l, + protected Manager $manager, + protected Logger $logger, + ) { + } + + public function setFileInfo(IStorage $storage, string $path, bool $isDir = false): void { + $this->fileInfo['storage'] = $storage; + $this->fileInfo['path'] = $path; + $this->fileInfo['isDir'] = $isDir; + } + + public function setEntitySubject(IEntity $entity, $subject): void { + $this->contexts[get_class($entity)] = [$entity, $subject]; + } + + public function setOperation(IOperation $operation): void { + if ($this->operation !== null) { + throw new RuntimeException('This method must not be called more than once'); + } + $this->operation = $operation; + } + + public function setEntity(IEntity $entity): void { + if ($this->entity !== null) { + throw new RuntimeException('This method must not be called more than once'); + } + $this->entity = $entity; + } + + public function setEventName(string $eventName): void { + if ($this->eventName !== null) { + throw new RuntimeException('This method must not be called more than once'); + } + $this->eventName = $eventName; + } + + public function getEntity(): IEntity { + if ($this->entity === null) { + throw new \LogicException('Entity was not set yet'); + } + return $this->entity; + } + + public function getFlows(bool $returnFirstMatchingOperationOnly = true): array { + if (!$this->operation) { + throw new RuntimeException('Operation is not set'); + } + return $this->getMatchingOperations(get_class($this->operation), $returnFirstMatchingOperationOnly); + } + + public function getMatchingOperations(string $class, bool $returnFirstMatchingOperationOnly = true): array { + $scopes[] = new ScopeContext(IManager::SCOPE_ADMIN); + $user = $this->session->getUser(); + if ($user !== null && $this->manager->isUserScopeEnabled()) { + $scopes[] = new ScopeContext(IManager::SCOPE_USER, $user->getUID()); + } + + $ctx = new LogContext(); + $ctx + ->setScopes($scopes) + ->setEntity($this->entity) + ->setOperation($this->operation); + $this->logger->logFlowRequests($ctx); + + $operations = []; + foreach ($scopes as $scope) { + $operations = array_merge($operations, $this->manager->getOperations($class, $scope)); + } + + if ($this->entity instanceof IEntity) { + /** @var ScopeContext[] $additionalScopes */ + $additionalScopes = $this->manager->getAllConfiguredScopesForOperation($class); + foreach ($additionalScopes as $hash => $scopeCandidate) { + if ($scopeCandidate->getScope() !== IManager::SCOPE_USER || in_array($scopeCandidate, $scopes)) { + continue; + } + if ($this->entity->isLegitimatedForUserId($scopeCandidate->getScopeId())) { + $ctx = new LogContext(); + $ctx + ->setScopes([$scopeCandidate]) + ->setEntity($this->entity) + ->setOperation($this->operation); + $this->logger->logScopeExpansion($ctx); + $operations = array_merge($operations, $this->manager->getOperations($class, $scopeCandidate)); + } + } + } + + $matches = []; + foreach ($operations as $operation) { + $configuredEvents = json_decode($operation['events'], true); + if ($this->eventName !== null && !in_array($this->eventName, $configuredEvents)) { + continue; + } + + $checkIds = json_decode($operation['checks'], true); + $checks = $this->manager->getChecks($checkIds); + + foreach ($checks as $check) { + if (!$this->check($check)) { + // Check did not match, continue with the next operation + continue 2; + } + } + + $ctx = new LogContext(); + $ctx + ->setEntity($this->entity) + ->setOperation($this->operation) + ->setConfiguration($operation); + $this->logger->logPassedCheck($ctx); + + if ($returnFirstMatchingOperationOnly) { + $ctx = new LogContext(); + $ctx + ->setEntity($this->entity) + ->setOperation($this->operation) + ->setConfiguration($operation); + $this->logger->logRunSingle($ctx); + return $operation; + } + $matches[] = $operation; + } + + $ctx = new LogContext(); + $ctx + ->setEntity($this->entity) + ->setOperation($this->operation); + if (!empty($matches)) { + $ctx->setConfiguration($matches); + $this->logger->logRunAll($ctx); + } else { + $this->logger->logRunNone($ctx); + } + + return $matches; + } + + /** + * @param array $check + * @return bool + */ + public function check(array $check) { + try { + $checkInstance = $this->container->query($check['class']); + } catch (QueryException $e) { + // Check does not exist, assume it matches. + return true; + } + + if ($checkInstance instanceof IFileCheck) { + if (empty($this->fileInfo)) { + throw new RuntimeException('Must set file info before running the check'); + } + $checkInstance->setFileInfo($this->fileInfo['storage'], $this->fileInfo['path'], $this->fileInfo['isDir']); + } elseif ($checkInstance instanceof IEntityCheck) { + foreach ($this->contexts as $entityInfo) { + [$entity, $subject] = $entityInfo; + $checkInstance->setEntitySubject($entity, $subject); + } + } elseif (!$checkInstance instanceof ICheck) { + // Check is invalid + throw new \UnexpectedValueException($this->l->t('Check %s is invalid or does not exist', $check['class'])); + } + return $checkInstance->executeCheck($check['operator'], $check['value']); + } +} |