mirror of
https://github.com/nextcloud/server.git
synced 2026-06-11 01:30:50 -04:00
Merge pull request #60512 from nextcloud/backport/59966/stable34
Some checks are pending
CodeQL Advanced / Analyze (actions) (push) Waiting to run
CodeQL Advanced / Analyze (javascript-typescript) (push) Waiting to run
Integration sqlite / changes (push) Waiting to run
Integration sqlite / integration-sqlite (stable34, main, 8.4, stable34, --tags ~@large files_features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (stable34, main, 8.4, stable34, capabilities_features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (stable34, main, 8.4, stable34, collaboration_features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (stable34, main, 8.4, stable34, comments_features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (stable34, main, 8.4, stable34, dav_features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (stable34, main, 8.4, stable34, features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (stable34, main, 8.4, stable34, federation_features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (stable34, main, 8.4, stable34, file_conversions) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (stable34, main, 8.4, stable34, files_reminders) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (stable34, main, 8.4, stable34, filesdrop_features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (stable34, main, 8.4, stable34, guests_features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (stable34, main, 8.4, stable34, ldap_features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (stable34, main, 8.4, stable34, openldap_features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (stable34, main, 8.4, stable34, openldap_numerical_features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (stable34, main, 8.4, stable34, remoteapi_features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (stable34, main, 8.4, stable34, routing_features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (stable34, main, 8.4, stable34, setup_features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (stable34, main, 8.4, stable34, sharees_features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (stable34, main, 8.4, stable34, sharing_features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (stable34, main, 8.4, stable34, theming_features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (stable34, main, 8.4, stable34, videoverification_features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite-summary (push) Blocked by required conditions
Psalm static code analysis / changes (push) Waiting to run
Psalm static code analysis / static-code-analysis (push) Blocked by required conditions
Psalm static code analysis / static-code-analysis-security (push) Blocked by required conditions
Psalm static code analysis / static-code-analysis-ocp (push) Blocked by required conditions
Psalm static code analysis / static-code-analysis-ncu (push) Blocked by required conditions
Psalm static code analysis / static-code-analysis-strict (push) Blocked by required conditions
Psalm static code analysis / static-code-analysis-summary (push) Blocked by required conditions
Some checks are pending
CodeQL Advanced / Analyze (actions) (push) Waiting to run
CodeQL Advanced / Analyze (javascript-typescript) (push) Waiting to run
Integration sqlite / changes (push) Waiting to run
Integration sqlite / integration-sqlite (stable34, main, 8.4, stable34, --tags ~@large files_features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (stable34, main, 8.4, stable34, capabilities_features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (stable34, main, 8.4, stable34, collaboration_features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (stable34, main, 8.4, stable34, comments_features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (stable34, main, 8.4, stable34, dav_features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (stable34, main, 8.4, stable34, features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (stable34, main, 8.4, stable34, federation_features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (stable34, main, 8.4, stable34, file_conversions) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (stable34, main, 8.4, stable34, files_reminders) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (stable34, main, 8.4, stable34, filesdrop_features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (stable34, main, 8.4, stable34, guests_features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (stable34, main, 8.4, stable34, ldap_features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (stable34, main, 8.4, stable34, openldap_features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (stable34, main, 8.4, stable34, openldap_numerical_features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (stable34, main, 8.4, stable34, remoteapi_features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (stable34, main, 8.4, stable34, routing_features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (stable34, main, 8.4, stable34, setup_features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (stable34, main, 8.4, stable34, sharees_features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (stable34, main, 8.4, stable34, sharing_features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (stable34, main, 8.4, stable34, theming_features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (stable34, main, 8.4, stable34, videoverification_features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite-summary (push) Blocked by required conditions
Psalm static code analysis / changes (push) Waiting to run
Psalm static code analysis / static-code-analysis (push) Blocked by required conditions
Psalm static code analysis / static-code-analysis-security (push) Blocked by required conditions
Psalm static code analysis / static-code-analysis-ocp (push) Blocked by required conditions
Psalm static code analysis / static-code-analysis-ncu (push) Blocked by required conditions
Psalm static code analysis / static-code-analysis-strict (push) Blocked by required conditions
Psalm static code analysis / static-code-analysis-summary (push) Blocked by required conditions
[stable34] Add runtime operations in WFE
This commit is contained in:
commit
5b445c5cec
14 changed files with 619 additions and 9 deletions
|
|
@ -41,6 +41,7 @@
|
|||
|
||||
<commands>
|
||||
<command>OCA\WorkflowEngine\Command\Index</command>
|
||||
<command>OCA\WorkflowEngine\Command\Runtime</command>
|
||||
</commands>
|
||||
|
||||
<settings>
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ return array(
|
|||
'OCA\\WorkflowEngine\\Check\\TFileCheck' => $baseDir . '/../lib/Check/TFileCheck.php',
|
||||
'OCA\\WorkflowEngine\\Check\\UserGroupMembership' => $baseDir . '/../lib/Check/UserGroupMembership.php',
|
||||
'OCA\\WorkflowEngine\\Command\\Index' => $baseDir . '/../lib/Command/Index.php',
|
||||
'OCA\\WorkflowEngine\\Command\\Runtime' => $baseDir . '/../lib/Command/Runtime.php',
|
||||
'OCA\\WorkflowEngine\\Controller\\AWorkflowOCSController' => $baseDir . '/../lib/Controller/AWorkflowOCSController.php',
|
||||
'OCA\\WorkflowEngine\\Controller\\GlobalWorkflowsController' => $baseDir . '/../lib/Controller/GlobalWorkflowsController.php',
|
||||
'OCA\\WorkflowEngine\\Controller\\RequestTimeController' => $baseDir . '/../lib/Controller/RequestTimeController.php',
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ class ComposerStaticInitWorkflowEngine
|
|||
'OCA\\WorkflowEngine\\Check\\TFileCheck' => __DIR__ . '/..' . '/../lib/Check/TFileCheck.php',
|
||||
'OCA\\WorkflowEngine\\Check\\UserGroupMembership' => __DIR__ . '/..' . '/../lib/Check/UserGroupMembership.php',
|
||||
'OCA\\WorkflowEngine\\Command\\Index' => __DIR__ . '/..' . '/../lib/Command/Index.php',
|
||||
'OCA\\WorkflowEngine\\Command\\Runtime' => __DIR__ . '/..' . '/../lib/Command/Runtime.php',
|
||||
'OCA\\WorkflowEngine\\Controller\\AWorkflowOCSController' => __DIR__ . '/..' . '/../lib/Controller/AWorkflowOCSController.php',
|
||||
'OCA\\WorkflowEngine\\Controller\\GlobalWorkflowsController' => __DIR__ . '/..' . '/../lib/Controller/GlobalWorkflowsController.php',
|
||||
'OCA\\WorkflowEngine\\Controller\\RequestTimeController' => __DIR__ . '/..' . '/../lib/Controller/RequestTimeController.php',
|
||||
|
|
|
|||
|
|
@ -42,15 +42,23 @@ class Application extends App implements IBootstrap {
|
|||
|
||||
#[\Override]
|
||||
public function boot(IBootContext $context): void {
|
||||
$context->injectFn(Closure::fromCallable([$this, 'registerRuntimeOperations']));
|
||||
$context->injectFn(Closure::fromCallable([$this, 'registerRuleListeners']));
|
||||
}
|
||||
|
||||
private function registerRuntimeOperations(Manager $manager): void {
|
||||
$manager->reloadRuntimeOperations();
|
||||
}
|
||||
|
||||
private function registerRuleListeners(IEventDispatcher $dispatcher,
|
||||
ContainerInterface $container,
|
||||
LoggerInterface $logger): void {
|
||||
/** @var Manager $manager */
|
||||
$manager = $container->get(Manager::class);
|
||||
$configuredEvents = $manager->getAllConfiguredEvents();
|
||||
$configuredEvents = array_merge_recursive(
|
||||
$manager->getAllConfiguredEvents(),
|
||||
$manager->getAllConfiguredRuntimeEvents(),
|
||||
);
|
||||
|
||||
foreach ($configuredEvents as $operationClass => $events) {
|
||||
foreach ($events as $entityClass => $eventNames) {
|
||||
|
|
|
|||
105
apps/workflowengine/lib/Command/Runtime.php
Normal file
105
apps/workflowengine/lib/Command/Runtime.php
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
namespace OCA\WorkflowEngine\Command;
|
||||
|
||||
use OC\Core\Command\Base;
|
||||
use OC\User\NoUserException;
|
||||
use OCA\WorkflowEngine\Helper\ScopeContext;
|
||||
use OCA\WorkflowEngine\Manager;
|
||||
use OCP\IUserManager;
|
||||
use OCP\IUserSession;
|
||||
use OCP\WorkflowEngine\IManager;
|
||||
use Override;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
class Runtime extends Base {
|
||||
|
||||
public function __construct(
|
||||
private Manager $manager,
|
||||
private IUserManager $userManager,
|
||||
private IUserSession $userSession,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
#[Override]
|
||||
protected function configure() {
|
||||
parent::configure();
|
||||
$this
|
||||
->setName('workflows:runtime:list')
|
||||
->setDescription('Lists configured runtime workflows')
|
||||
// need to add an optional filtering by app
|
||||
->addArgument(
|
||||
'appId',
|
||||
InputArgument::OPTIONAL,
|
||||
'Filter runtime workflows by appId',
|
||||
null
|
||||
)
|
||||
->addArgument(
|
||||
'scope',
|
||||
InputArgument::OPTIONAL,
|
||||
'Lists workflows for "admin", "user"',
|
||||
'admin'
|
||||
)
|
||||
->addArgument(
|
||||
'userId',
|
||||
InputArgument::OPTIONAL,
|
||||
'User ID used for user scope and session',
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
protected function mappedScope(string $scope): int {
|
||||
return match($scope) {
|
||||
'admin' => IManager::SCOPE_ADMIN,
|
||||
'user' => IManager::SCOPE_USER,
|
||||
default => -1,
|
||||
};
|
||||
}
|
||||
|
||||
#[Override]
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int {
|
||||
$appId = $input->getArgument('appId');
|
||||
$userId = $input->getArgument('userId');
|
||||
|
||||
if ($userId !== null) {
|
||||
$user = $this->userManager->get($userId);
|
||||
if ($user === null) {
|
||||
throw new NoUserException("user $userId not found");
|
||||
}
|
||||
$this->userSession->setUser($user);
|
||||
$this->manager->reloadRuntimeOperations();
|
||||
}
|
||||
|
||||
$opsByClass = $this->manager->getAllRuntimeOperations(
|
||||
new ScopeContext(
|
||||
$this->mappedScope($input->getArgument('scope')),
|
||||
$input->getArgument('userId')
|
||||
),
|
||||
$appId,
|
||||
);
|
||||
|
||||
foreach ($opsByClass as &$operations) {
|
||||
foreach ($operations as &$operation) {
|
||||
$checks = $operation->checks;
|
||||
$appId = $operation->appId;
|
||||
$operation = $operation->toArray();
|
||||
$operation['checks'] = $this->manager->getRuntimeChecks($checks, $appId);
|
||||
}
|
||||
unset($operation);
|
||||
}
|
||||
unset($operations);
|
||||
|
||||
$this->writeArrayInOutputFormat($input, $output, $opsByClass);
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
|
@ -6,6 +6,9 @@
|
|||
*/
|
||||
namespace OCA\WorkflowEngine;
|
||||
|
||||
use NCU\WorkflowEngine\Events\RegisterRuntimeOperationsEvent;
|
||||
use NCU\WorkflowEngine\RuntimeOperation;
|
||||
use NCU\WorkflowEngine\RuntimeScope;
|
||||
use OCA\WorkflowEngine\Check\FileMimeType;
|
||||
use OCA\WorkflowEngine\Check\FileName;
|
||||
use OCA\WorkflowEngine\Check\FileSize;
|
||||
|
|
@ -19,6 +22,7 @@ use OCA\WorkflowEngine\Entity\File;
|
|||
use OCA\WorkflowEngine\Helper\ScopeContext;
|
||||
use OCA\WorkflowEngine\Service\Logger;
|
||||
use OCA\WorkflowEngine\Service\RuleMatcher;
|
||||
use OCP\App\IAppManager;
|
||||
use OCP\AppFramework\Services\IAppConfig;
|
||||
use OCP\Cache\CappedMemoryCache;
|
||||
use OCP\DB\Exception;
|
||||
|
|
@ -47,12 +51,29 @@ use Psr\Log\LoggerInterface;
|
|||
* @psalm-import-type WorkflowEngineRule from ResponseDefinitions
|
||||
*/
|
||||
class Manager implements IManager {
|
||||
/** @var array[] */
|
||||
/** @var array<string, array<string, array<int, WorkflowEngineCheck>>> */
|
||||
protected array $operations = [];
|
||||
|
||||
/** @var array<int, WorkflowEngineCheck> */
|
||||
protected array $checks = [];
|
||||
|
||||
/** @var array<string, array<string, WorkflowEngineCheck>> */
|
||||
protected array $registeredRuntimeChecks = [];
|
||||
|
||||
/**
|
||||
* Registered runtime operations, keyed by app ID and runtime operation ID.
|
||||
*
|
||||
* @var array<string, array<string, RuntimeOperation>>
|
||||
*/
|
||||
protected array $registeredRuntimeOperations = [];
|
||||
|
||||
/**
|
||||
* Registered runtime scopes, keyed by app ID and runtime operation ID.
|
||||
*
|
||||
* @var array<string, array<string, RuntimeScope>>
|
||||
*/
|
||||
protected array $registeredRuntimeScopes = [];
|
||||
|
||||
/** @var IEntity[] */
|
||||
protected array $registeredEntities = [];
|
||||
|
||||
|
|
@ -77,6 +98,7 @@ class Manager implements IManager {
|
|||
private readonly IEventDispatcher $dispatcher,
|
||||
private readonly IAppConfig $appConfig,
|
||||
private readonly ICacheFactory $cacheFactory,
|
||||
private readonly IAppManager $appManager,
|
||||
) {
|
||||
$this->operationsByScope = new CappedMemoryCache(64);
|
||||
}
|
||||
|
|
@ -92,7 +114,7 @@ class Manager implements IManager {
|
|||
);
|
||||
}
|
||||
|
||||
public function getAllConfiguredEvents() {
|
||||
public function getAllConfiguredEvents(): array {
|
||||
$cache = $this->cacheFactory->createDistributed('flow');
|
||||
$cached = $cache->get('events');
|
||||
if ($cached !== null) {
|
||||
|
|
@ -127,6 +149,30 @@ class Manager implements IManager {
|
|||
return $operations;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the events configured by runtime operations, in the same structure as getAllConfiguredEvents().
|
||||
*
|
||||
* @return array<class-string<IOperation>, array<class-string<IEntity>, list<string>>>
|
||||
*/
|
||||
public function getAllConfiguredRuntimeEvents(): array {
|
||||
$eventsByOperationAndEntity = [];
|
||||
foreach ($this->registeredRuntimeOperations as $appOperations) {
|
||||
foreach ($appOperations as $operation) {
|
||||
$operationClass = $operation->class;
|
||||
$entityClass = $operation->entity;
|
||||
$eventsByOperationAndEntity[$operationClass] ??= [];
|
||||
$eventsByOperationAndEntity[$operationClass][$entityClass] ??= [];
|
||||
/** @var list<string> $events */
|
||||
$events = array_unique(
|
||||
array_merge($eventsByOperationAndEntity[$operationClass][$entityClass], $operation->events)
|
||||
);
|
||||
$eventsByOperationAndEntity[$operationClass][$entityClass] = $events;
|
||||
}
|
||||
}
|
||||
|
||||
return $eventsByOperationAndEntity;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param class-string<IOperation> $operationClass
|
||||
* @return ScopeContext[]
|
||||
|
|
@ -168,6 +214,33 @@ class Manager implements IManager {
|
|||
return $this->scopesByOperation[$operationClass];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets configured scopes for operations registered at runtime.
|
||||
*
|
||||
* @param class-string<IOperation> $operationClass
|
||||
* @return ScopeContext[]
|
||||
*/
|
||||
public function getAllConfiguredScopesForRuntimeOperation(string $operationClass): array {
|
||||
$scopes = [];
|
||||
foreach ($this->registeredRuntimeOperations as $appId => $appOperations) {
|
||||
foreach ($appOperations as $operationId => $operation) {
|
||||
if ($operation->class !== $operationClass) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$runtimeScope = $this->registeredRuntimeScopes[$appId][$operationId] ?? null;
|
||||
if ($runtimeScope === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$scope = new ScopeContext($runtimeScope->type, $runtimeScope->value);
|
||||
$scopes[$scope->getHash()] = $scope;
|
||||
}
|
||||
}
|
||||
|
||||
return $scopes;
|
||||
}
|
||||
|
||||
public function getAllOperations(ScopeContext $scopeContext): array {
|
||||
if (isset($this->operations[$scopeContext->getHash()])) {
|
||||
return $this->operations[$scopeContext->getHash()];
|
||||
|
|
@ -264,6 +337,115 @@ class Manager implements IManager {
|
|||
return $query->getLastInsertId();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all operations registered at runtime
|
||||
*
|
||||
* @param ScopeContext $scopeContext
|
||||
* @return array<class-string<IOperation>, list<RuntimeOperation>>
|
||||
*/
|
||||
public function getAllRuntimeOperations(ScopeContext $scopeContext, ?string $appFilter = null): array {
|
||||
$result = [];
|
||||
foreach ($this->registeredRuntimeOperations as $appId => $appOperations) {
|
||||
if ($appFilter !== null && $appId !== $appFilter) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($appOperations as $operationId => $operation) {
|
||||
// scope stored per-app per-operation in registeredRuntimeScopes
|
||||
$runtimeScope = $this->registeredRuntimeScopes[$appId][$operationId] ?? null;
|
||||
if ($runtimeScope === null) {
|
||||
continue;
|
||||
}
|
||||
// filter by provided $scopeContext
|
||||
if ($runtimeScope->type !== $scopeContext->getScope()) {
|
||||
continue;
|
||||
}
|
||||
if ($scopeContext->getScope() === IManager::SCOPE_USER && $runtimeScope->value !== $scopeContext->getScopeId()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$runtimeOperation = new RuntimeOperation($operationId,
|
||||
$operation->class,
|
||||
$operation->name,
|
||||
$operation->checks,
|
||||
$operation->operation,
|
||||
$operation->entity,
|
||||
$operation->events,
|
||||
$appId,
|
||||
);
|
||||
|
||||
$result[$operation->class][] = $runtimeOperation;
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return operations registered at runtime, which are not persisted in the DB nor shown in the UI.
|
||||
*
|
||||
* @param class-string<IOperation> $class
|
||||
* @param ScopeContext $scopeContext
|
||||
* @return list<RuntimeOperation>
|
||||
*/
|
||||
public function getRuntimeOperations(string $class, ScopeContext $scopeContext): array {
|
||||
$operations = $this->getAllRuntimeOperations($scopeContext);
|
||||
|
||||
return $operations[$class] ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $appId
|
||||
* @param class-string<IOperation> $class
|
||||
* @param string $name
|
||||
* @param list<WorkflowEngineCheck> $checks
|
||||
* @param string $operation
|
||||
* @param class-string<IEntity> $entity
|
||||
* @param list<class-string<IEntityEvent>> $events
|
||||
*/
|
||||
public function addRuntimeOperation(
|
||||
string $appId,
|
||||
string $class,
|
||||
string $name,
|
||||
array $checks,
|
||||
string $operation,
|
||||
ScopeContext $scope,
|
||||
string $entity,
|
||||
array $events,
|
||||
): void {
|
||||
if (!$this->appManager->isEnabledForAnyone($appId)) {
|
||||
throw new \InvalidArgumentException("App {$appId} is not enabled");
|
||||
}
|
||||
|
||||
$this->validateOperation($class, $name, $checks, $operation, $scope, $entity, $events);
|
||||
|
||||
$checkHashes = [];
|
||||
foreach ($checks as $check) {
|
||||
$hash = md5($check['class'] . '::' . $check['operator'] . '::' . $check['value']);
|
||||
$checkHashes[] = $hash;
|
||||
$this->registeredRuntimeChecks[$appId] ??= [];
|
||||
$this->registeredRuntimeChecks[$appId][$hash] ??= $check;
|
||||
}
|
||||
|
||||
$operationId = uniqid($appId, true);
|
||||
$runtimeOperation = new RuntimeOperation(
|
||||
$operationId,
|
||||
$class,
|
||||
$name,
|
||||
$checkHashes,
|
||||
$operation,
|
||||
$entity,
|
||||
$events,
|
||||
$appId,
|
||||
);
|
||||
$this->registeredRuntimeOperations[$appId] ??= [];
|
||||
$this->registeredRuntimeOperations[$appId][$operationId] ??= $runtimeOperation;
|
||||
|
||||
$runtimeScope = new RuntimeScope($operationId, $scope->getScope(), $scope->getScopeId());
|
||||
$this->registeredRuntimeScopes[$appId] ??= [];
|
||||
$this->registeredRuntimeScopes[$appId][$operationId] ??= $runtimeScope;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $class
|
||||
* @param string $name
|
||||
|
|
@ -523,6 +705,22 @@ class Manager implements IManager {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<string> $checkHashes
|
||||
* @param string $appId
|
||||
* @return array<string, WorkflowEngineCheck> checks indexed by their ID
|
||||
*/
|
||||
public function getRuntimeChecks(array $checkHashes, string $appId): array {
|
||||
$checks = [];
|
||||
foreach ($checkHashes as $hash) {
|
||||
if (!isset($this->registeredRuntimeChecks[$appId][$hash])) {
|
||||
throw new \UnexpectedValueException("Runtime check {$hash} for app {$appId} missing");
|
||||
}
|
||||
$checks[$hash] = $this->registeredRuntimeChecks[$appId][$hash];
|
||||
}
|
||||
return $checks;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int[] $checkIds
|
||||
* @return array<int, WorkflowEngineCheck>
|
||||
|
|
@ -665,6 +863,14 @@ class Manager implements IManager {
|
|||
$this->registeredOperators[get_class($operator)] = $operator;
|
||||
}
|
||||
|
||||
public function reloadRuntimeOperations(): void {
|
||||
$this->registeredRuntimeOperations = [];
|
||||
$this->registeredRuntimeScopes = [];
|
||||
$this->registeredRuntimeChecks = [];
|
||||
|
||||
$this->dispatcher->dispatchTyped(new RegisterRuntimeOperationsEvent($this));
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function registerCheck(ICheck $check): void {
|
||||
$this->registeredChecks[get_class($check)] = $check;
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ declare(strict_types=1);
|
|||
*/
|
||||
namespace OCA\WorkflowEngine\Service;
|
||||
|
||||
use NCU\WorkflowEngine\RuntimeOperation;
|
||||
use OCA\WorkflowEngine\Helper\LogContext;
|
||||
use OCA\WorkflowEngine\Helper\ScopeContext;
|
||||
use OCA\WorkflowEngine\Manager;
|
||||
|
|
@ -112,10 +113,12 @@ class RuleMatcher implements IRuleMatcher {
|
|||
$operations = [];
|
||||
foreach ($scopes as $scope) {
|
||||
$operations = array_merge($operations, $this->manager->getOperations($class, $scope));
|
||||
$operations = array_merge($operations, $this->manager->getRuntimeOperations($class, $scope));
|
||||
}
|
||||
|
||||
if ($this->entity instanceof IEntity) {
|
||||
$additionalScopes = $this->manager->getAllConfiguredScopesForOperation($class);
|
||||
$additionalScopes = $this->manager->getAllConfiguredScopesForOperation($class)
|
||||
+ $this->manager->getAllConfiguredScopesForRuntimeOperation($class);
|
||||
foreach ($additionalScopes as $hash => $scopeCandidate) {
|
||||
if ($scopeCandidate->getScope() !== IManager::SCOPE_USER || in_array($scopeCandidate, $scopes)) {
|
||||
continue;
|
||||
|
|
@ -128,20 +131,29 @@ class RuleMatcher implements IRuleMatcher {
|
|||
->setOperation($this->operation);
|
||||
$this->logger->logScopeExpansion($ctx);
|
||||
$operations = array_merge($operations, $this->manager->getOperations($class, $scopeCandidate));
|
||||
$operations = array_merge($operations, $this->manager->getRuntimeOperations($class, $scopeCandidate));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$matches = [];
|
||||
foreach ($operations as $operation) {
|
||||
$configuredEvents = json_decode($operation['events'], true);
|
||||
if ($operation instanceof RuntimeOperation) {
|
||||
$configuredEvents = $operation->events;
|
||||
$checkIds = $operation->checks;
|
||||
$checks = $this->manager->getRuntimeChecks($checkIds, $operation->appId);
|
||||
// from now on, backwards compatibility is required
|
||||
$operation = $operation->toArray();
|
||||
} else {
|
||||
$configuredEvents = json_decode($operation['events'], true);
|
||||
$checkIds = json_decode($operation['checks'], true);
|
||||
$checks = $this->manager->getChecks($checkIds);
|
||||
}
|
||||
|
||||
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
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ use OC\Files\Config\UserMountCache;
|
|||
use OCA\WorkflowEngine\Entity\File;
|
||||
use OCA\WorkflowEngine\Helper\ScopeContext;
|
||||
use OCA\WorkflowEngine\Manager;
|
||||
use OCP\App\IAppManager;
|
||||
use OCP\AppFramework\QueryException;
|
||||
use OCP\AppFramework\Services\IAppConfig;
|
||||
use OCP\EventDispatcher\Event;
|
||||
|
|
@ -33,6 +34,7 @@ use OCP\WorkflowEngine\IEntityEvent;
|
|||
use OCP\WorkflowEngine\IManager;
|
||||
use OCP\WorkflowEngine\IOperation;
|
||||
use OCP\WorkflowEngine\IRuleMatcher;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use Psr\Container\ContainerInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
|
@ -84,6 +86,7 @@ class ManagerTest extends TestCase {
|
|||
protected IEventDispatcher&MockObject $dispatcher;
|
||||
protected IAppConfig&MockObject $config;
|
||||
protected ICacheFactory&MockObject $cacheFactory;
|
||||
protected IAppManager&MockObject $appManager;
|
||||
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
|
|
@ -101,6 +104,7 @@ class ManagerTest extends TestCase {
|
|||
$this->dispatcher = $this->createMock(IEventDispatcher::class);
|
||||
$this->config = $this->createMock(IAppConfig::class);
|
||||
$this->cacheFactory = $this->createMock(ICacheFactory::class);
|
||||
$this->appManager = $this->createMock(IAppManager::class);
|
||||
|
||||
$this->manager = new Manager(
|
||||
$this->db,
|
||||
|
|
@ -110,7 +114,8 @@ class ManagerTest extends TestCase {
|
|||
$this->session,
|
||||
$this->dispatcher,
|
||||
$this->config,
|
||||
$this->cacheFactory
|
||||
$this->cacheFactory,
|
||||
$this->appManager,
|
||||
);
|
||||
$this->clearTables();
|
||||
}
|
||||
|
|
@ -737,6 +742,125 @@ class ManagerTest extends TestCase {
|
|||
}
|
||||
}
|
||||
|
||||
private function prepareContainerForRuntimeOperation(): void {
|
||||
$operationMock = $this->createMock(IOperation::class);
|
||||
$operationMock->method('isAvailableForScope')->willReturn(true);
|
||||
|
||||
$myEventMock = $this->createMock(IEntityEvent::class);
|
||||
$myEventMock->method('getEventName')->willReturn('MyEvent');
|
||||
$otherEventMock = $this->createMock(IEntityEvent::class);
|
||||
$otherEventMock->method('getEventName')->willReturn('OtherEvent');
|
||||
|
||||
$entityMock = $this->createMock(IEntity::class);
|
||||
$entityMock->method('getEvents')->willReturn([$myEventMock, $otherEventMock]);
|
||||
|
||||
$checkMock = $this->createMock(ICheck::class);
|
||||
$checkMock->method('supportedEntities')->willReturn([]);
|
||||
|
||||
$this->container->expects($this->any())
|
||||
->method('get')
|
||||
->willReturnCallback(fn ($class) => match ($class) {
|
||||
IOperation::class => $operationMock,
|
||||
IEntity::class => $entityMock,
|
||||
ICheck::class => $checkMock,
|
||||
default => $this->createMock($class),
|
||||
});
|
||||
}
|
||||
|
||||
public function testAddRuntimeOperationRejectsUnknownApp(): void {
|
||||
$this->appManager->method('isEnabledForAnyone')->willReturn(false);
|
||||
$this->expectException(\InvalidArgumentException::class);
|
||||
|
||||
$scope = $this->buildScope();
|
||||
$check = ['class' => ICheck::class, 'operator' => 'is', 'value' => 'test'];
|
||||
$this->manager->addRuntimeOperation('unknownapp', IOperation::class, 'Test', [$check], '', $scope, IEntity::class, ['MyEvent']);
|
||||
}
|
||||
|
||||
public function testGetAllConfiguredRuntimeEventsEmpty(): void {
|
||||
$this->assertSame([], $this->manager->getAllConfiguredRuntimeEvents());
|
||||
}
|
||||
|
||||
public function testGetAllConfiguredRuntimeEvents(): void {
|
||||
$this->appManager->method('isEnabledForAnyone')->willReturn(true);
|
||||
$this->prepareContainerForRuntimeOperation();
|
||||
|
||||
$scope = $this->buildScope();
|
||||
$check = ['class' => ICheck::class, 'operator' => 'is', 'value' => 'test'];
|
||||
|
||||
$this->manager->addRuntimeOperation('testapp', IOperation::class, 'Op1', [$check], '', $scope, IEntity::class, ['MyEvent']);
|
||||
$this->manager->addRuntimeOperation('testapp', IOperation::class, 'Op2', [$check], '', $scope, IEntity::class, ['MyEvent', 'OtherEvent']);
|
||||
|
||||
$events = $this->manager->getAllConfiguredRuntimeEvents();
|
||||
|
||||
$this->assertArrayHasKey(IOperation::class, $events);
|
||||
$this->assertArrayHasKey(IEntity::class, $events[IOperation::class]);
|
||||
$eventNames = $events[IOperation::class][IEntity::class];
|
||||
$this->assertContains('MyEvent', $eventNames);
|
||||
$this->assertContains('OtherEvent', $eventNames);
|
||||
$this->assertCount(2, $eventNames);
|
||||
}
|
||||
|
||||
public function testRuntimeOperationScopeIsolation(): void {
|
||||
$this->appManager->method('isEnabledForAnyone')->willReturn(true);
|
||||
$this->prepareContainerForRuntimeOperation();
|
||||
|
||||
$adminScope = $this->buildScope();
|
||||
$userScope = $this->buildScope('alice');
|
||||
$check = ['class' => ICheck::class, 'operator' => 'is', 'value' => 'test'];
|
||||
|
||||
$this->manager->addRuntimeOperation('testapp', IOperation::class, 'AdminOp', [$check], '', $adminScope, IEntity::class, ['MyEvent']);
|
||||
$this->manager->addRuntimeOperation('testapp', IOperation::class, 'UserOp', [$check], '', $userScope, IEntity::class, ['MyEvent']);
|
||||
|
||||
$adminOps = $this->manager->getRuntimeOperations(IOperation::class, $adminScope);
|
||||
$userOps = $this->manager->getRuntimeOperations(IOperation::class, $userScope);
|
||||
$this->assertCount(1, $adminOps);
|
||||
$this->assertSame('AdminOp', $adminOps[0]->name);
|
||||
$this->assertCount(1, $userOps);
|
||||
$this->assertSame('UserOp', $userOps[0]->name);
|
||||
|
||||
$scopes = $this->manager->getAllConfiguredScopesForRuntimeOperation(IOperation::class);
|
||||
$this->assertCount(2, $scopes);
|
||||
$scopeTypes = array_map(fn ($s) => $s->getScope(), array_values($scopes));
|
||||
$this->assertContains(IManager::SCOPE_ADMIN, $scopeTypes);
|
||||
$this->assertContains(IManager::SCOPE_USER, $scopeTypes);
|
||||
}
|
||||
|
||||
public static function dataGetRuntimeChecks(): array {
|
||||
return [
|
||||
'single operation' => [1],
|
||||
'two operations with same check are deduplicated' => [2],
|
||||
];
|
||||
}
|
||||
|
||||
#[DataProvider(methodName: 'dataGetRuntimeChecks')]
|
||||
public function testGetRuntimeChecks(int $opCount): void {
|
||||
$this->appManager->method('isEnabledForAnyone')->willReturn(true);
|
||||
$this->prepareContainerForRuntimeOperation();
|
||||
|
||||
$scope = $this->buildScope();
|
||||
$check = ['class' => ICheck::class, 'operator' => 'is', 'value' => 'testvalue'];
|
||||
|
||||
for ($i = 0; $i < $opCount; $i++) {
|
||||
$this->manager->addRuntimeOperation('myapp', IOperation::class, "Op{$i}", [$check], '', $scope, IEntity::class, ['MyEvent']);
|
||||
}
|
||||
|
||||
$ops = $this->manager->getRuntimeOperations(IOperation::class, $scope);
|
||||
$this->assertCount($opCount, $ops);
|
||||
|
||||
$hash = md5(ICheck::class . '::is::testvalue');
|
||||
$resolved = $this->manager->getRuntimeChecks([$hash], 'myapp');
|
||||
$this->assertCount(1, $resolved);
|
||||
$this->assertArrayHasKey($hash, $resolved);
|
||||
$this->assertSame(ICheck::class, $resolved[$hash]['class']);
|
||||
$this->assertSame('is', $resolved[$hash]['operator']);
|
||||
$this->assertSame('testvalue', $resolved[$hash]['value']);
|
||||
}
|
||||
|
||||
public function testGetRuntimeChecksThrowsForUnknownHash(): void {
|
||||
$this->expectException(\UnexpectedValueException::class);
|
||||
$this->manager->getRuntimeChecks(['unknownhash'], 'myapp');
|
||||
}
|
||||
|
||||
public function testValidateOperationScopeNotAvailable(): void {
|
||||
$check = [
|
||||
'id' => 1,
|
||||
|
|
|
|||
|
|
@ -37,6 +37,9 @@ return array(
|
|||
'NCU\\Security\\Signature\\ISignatureManager' => $baseDir . '/lib/unstable/Security/Signature/ISignatureManager.php',
|
||||
'NCU\\Security\\Signature\\ISignedRequest' => $baseDir . '/lib/unstable/Security/Signature/ISignedRequest.php',
|
||||
'NCU\\Security\\Signature\\Model\\Signatory' => $baseDir . '/lib/unstable/Security/Signature/Model/Signatory.php',
|
||||
'NCU\\WorkflowEngine\\Events\\RegisterRuntimeOperationsEvent' => $baseDir . '/lib/unstable/WorkflowEngine/Events/RegisterRuntimeOperationsEvent.php',
|
||||
'NCU\\WorkflowEngine\\RuntimeOperation' => $baseDir . '/lib/unstable/WorkflowEngine/RuntimeOperation.php',
|
||||
'NCU\\WorkflowEngine\\RuntimeScope' => $baseDir . '/lib/unstable/WorkflowEngine/RuntimeScope.php',
|
||||
'OCP\\Accounts\\IAccount' => $baseDir . '/lib/public/Accounts/IAccount.php',
|
||||
'OCP\\Accounts\\IAccountManager' => $baseDir . '/lib/public/Accounts/IAccountManager.php',
|
||||
'OCP\\Accounts\\IAccountProperty' => $baseDir . '/lib/public/Accounts/IAccountProperty.php',
|
||||
|
|
|
|||
|
|
@ -78,6 +78,9 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
|
|||
'NCU\\Security\\Signature\\ISignatureManager' => __DIR__ . '/../../..' . '/lib/unstable/Security/Signature/ISignatureManager.php',
|
||||
'NCU\\Security\\Signature\\ISignedRequest' => __DIR__ . '/../../..' . '/lib/unstable/Security/Signature/ISignedRequest.php',
|
||||
'NCU\\Security\\Signature\\Model\\Signatory' => __DIR__ . '/../../..' . '/lib/unstable/Security/Signature/Model/Signatory.php',
|
||||
'NCU\\WorkflowEngine\\Events\\RegisterRuntimeOperationsEvent' => __DIR__ . '/../../..' . '/lib/unstable/WorkflowEngine/Events/RegisterRuntimeOperationsEvent.php',
|
||||
'NCU\\WorkflowEngine\\RuntimeOperation' => __DIR__ . '/../../..' . '/lib/unstable/WorkflowEngine/RuntimeOperation.php',
|
||||
'NCU\\WorkflowEngine\\RuntimeScope' => __DIR__ . '/../../..' . '/lib/unstable/WorkflowEngine/RuntimeScope.php',
|
||||
'OCP\\Accounts\\IAccount' => __DIR__ . '/../../..' . '/lib/public/Accounts/IAccount.php',
|
||||
'OCP\\Accounts\\IAccountManager' => __DIR__ . '/../../..' . '/lib/public/Accounts/IAccountManager.php',
|
||||
'OCP\\Accounts\\IAccountProperty' => __DIR__ . '/../../..' . '/lib/public/Accounts/IAccountProperty.php',
|
||||
|
|
|
|||
|
|
@ -0,0 +1,39 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
namespace NCU\WorkflowEngine\Events;
|
||||
|
||||
use OCP\EventDispatcher\Event;
|
||||
use OCP\WorkflowEngine\IManager;
|
||||
|
||||
/**
|
||||
* @experimental 34.0.0
|
||||
*/
|
||||
final class RegisterRuntimeOperationsEvent extends Event {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* @experimental 34.0.0
|
||||
*/
|
||||
public function __construct(
|
||||
private readonly IManager $manager,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* @experimental 34.0.0
|
||||
*/
|
||||
public function getManager(): IManager {
|
||||
return $this->manager;
|
||||
}
|
||||
}
|
||||
63
lib/unstable/WorkflowEngine/RuntimeOperation.php
Normal file
63
lib/unstable/WorkflowEngine/RuntimeOperation.php
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
namespace NCU\WorkflowEngine;
|
||||
|
||||
use OCP\WorkflowEngine\IEntity;
|
||||
use OCP\WorkflowEngine\IOperation;
|
||||
|
||||
/**
|
||||
* @experimental 34.0.0
|
||||
*/
|
||||
final readonly class RuntimeOperation {
|
||||
|
||||
private bool $runtime;
|
||||
|
||||
/**
|
||||
* @experimental 34.0.0
|
||||
*
|
||||
* @param string $id
|
||||
* @param class-string<IOperation> $class
|
||||
* @param string $name
|
||||
* @param list<string> $checks
|
||||
* @param string $operation
|
||||
* @param class-string<IEntity> $entity
|
||||
* @param list<string> $events
|
||||
* @param string $appId
|
||||
*/
|
||||
public function __construct(
|
||||
public string $id,
|
||||
public string $class,
|
||||
public string $name,
|
||||
public array $checks,
|
||||
public string $operation,
|
||||
public string $entity,
|
||||
public array $events,
|
||||
public string $appId,
|
||||
) {
|
||||
$this->runtime = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @experimental 34.0.0
|
||||
* @return array<key-of<properties-of<self>>, value-of<properties-of<self>>>
|
||||
*/
|
||||
public function toArray(): array {
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'class' => $this->class,
|
||||
'name' => $this->name,
|
||||
'checks' => $this->checks,
|
||||
'operation' => $this->operation,
|
||||
'entity' => $this->entity,
|
||||
'events' => $this->events,
|
||||
'appId' => $this->appId,
|
||||
'runtime' => $this->runtime,
|
||||
];
|
||||
}
|
||||
}
|
||||
41
lib/unstable/WorkflowEngine/RuntimeScope.php
Normal file
41
lib/unstable/WorkflowEngine/RuntimeScope.php
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
namespace NCU\WorkflowEngine;
|
||||
|
||||
/**
|
||||
* @experimental 34.0.0
|
||||
*/
|
||||
final readonly class RuntimeScope {
|
||||
|
||||
/**
|
||||
* @experimental 34.0.0
|
||||
*
|
||||
* @param string $operationId
|
||||
* @param int $type
|
||||
* @param string $value
|
||||
*/
|
||||
public function __construct(
|
||||
public string $operationId,
|
||||
public int $type,
|
||||
public string $value,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @experimental 34.0.0
|
||||
* @return array<key-of<properties-of<self>>, value-of<properties-of<self>>>
|
||||
*/
|
||||
public function toArray(): array {
|
||||
return [
|
||||
'operationId' => $this->operationId,
|
||||
'type' => $this->type,
|
||||
'value' => $this->value,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -19,4 +19,7 @@
|
|||
<projectFiles>
|
||||
<directory name="lib/unstable"/>
|
||||
</projectFiles>
|
||||
<extraFiles>
|
||||
<directory name="3rdparty"/>
|
||||
</extraFiles>
|
||||
</psalm>
|
||||
|
|
|
|||
Loading…
Reference in a new issue