From 57410663d93eb935337abfbee923ce93516f6d55 Mon Sep 17 00:00:00 2001 From: Salvatore Martire <4652631+salmart-dev@users.noreply.github.com> Date: Tue, 28 Apr 2026 16:28:57 +0200 Subject: [PATCH] feat(wfe): add runtime operations Signed-off-by: Salvatore Martire <4652631+salmart-dev@users.noreply.github.com> # Conflicts: # apps/workflowengine/lib/Manager.php --- apps/workflowengine/appinfo/info.xml | 1 + .../composer/composer/autoload_classmap.php | 1 + .../composer/composer/autoload_static.php | 1 + .../lib/AppInfo/Application.php | 12 +- apps/workflowengine/lib/Command/Runtime.php | 103 ++++++++ apps/workflowengine/lib/Manager.php | 235 +++++++++++++++++- .../lib/Service/RuleMatcher.php | 11 +- apps/workflowengine/tests/ManagerTest.php | 126 +++++++++- lib/composer/composer/autoload_classmap.php | 1 + lib/composer/composer/autoload_static.php | 1 + .../Events/RegisterRuntimeOperationsEvent.php | 28 +++ 11 files changed, 514 insertions(+), 6 deletions(-) create mode 100644 apps/workflowengine/lib/Command/Runtime.php create mode 100644 lib/private/WorkflowEngine/Events/RegisterRuntimeOperationsEvent.php diff --git a/apps/workflowengine/appinfo/info.xml b/apps/workflowengine/appinfo/info.xml index c6fce1d533439..92462330d3982 100644 --- a/apps/workflowengine/appinfo/info.xml +++ b/apps/workflowengine/appinfo/info.xml @@ -41,6 +41,7 @@ OCA\WorkflowEngine\Command\Index + OCA\WorkflowEngine\Command\Runtime diff --git a/apps/workflowengine/composer/composer/autoload_classmap.php b/apps/workflowengine/composer/composer/autoload_classmap.php index 0fd869905d4ee..71ae91689da17 100644 --- a/apps/workflowengine/composer/composer/autoload_classmap.php +++ b/apps/workflowengine/composer/composer/autoload_classmap.php @@ -21,6 +21,7 @@ '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', diff --git a/apps/workflowengine/composer/composer/autoload_static.php b/apps/workflowengine/composer/composer/autoload_static.php index dddcf5611dab1..1bb73b2f8b92e 100644 --- a/apps/workflowengine/composer/composer/autoload_static.php +++ b/apps/workflowengine/composer/composer/autoload_static.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', diff --git a/apps/workflowengine/lib/AppInfo/Application.php b/apps/workflowengine/lib/AppInfo/Application.php index 4f02b885adebb..ba1474fe636e9 100644 --- a/apps/workflowengine/lib/AppInfo/Application.php +++ b/apps/workflowengine/lib/AppInfo/Application.php @@ -42,15 +42,25 @@ public function register(IRegistrationContext $context): void { #[\Override] public function boot(IBootContext $context): void { + $context->injectFn(Closure::fromCallable([$this, 'emitRuntimeEvent'])); $context->injectFn(Closure::fromCallable([$this, 'registerRuleListeners'])); } + private function emitRuntimeEvent(IEventDispatcher $dispatcher, ContainerInterface $container): void { + /** @var Manager $manager */ + $manager = $container->get(Manager::class); + $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) { diff --git a/apps/workflowengine/lib/Command/Runtime.php b/apps/workflowengine/lib/Command/Runtime.php new file mode 100644 index 0000000000000..1e6008a2d9f8c --- /dev/null +++ b/apps/workflowengine/lib/Command/Runtime.php @@ -0,0 +1,103 @@ +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 (is_null($user)) { + 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']; + $decodedChecks = json_decode($checks, true); + $operation['checks'] = $this->manager->getRuntimeChecks($decodedChecks, $appId); + } + unset($operation); + } + unset($operations); + + $output->writeln(\json_encode($opsByClass, JSON_PRETTY_PRINT)); + return 0; + } +} diff --git a/apps/workflowengine/lib/Manager.php b/apps/workflowengine/lib/Manager.php index a23a1f55524a6..e0c4cdf100568 100644 --- a/apps/workflowengine/lib/Manager.php +++ b/apps/workflowengine/lib/Manager.php @@ -6,6 +6,7 @@ */ namespace OCA\WorkflowEngine; +use OC\WorkflowEngine\Events\RegisterRuntimeOperationsEvent; use OCA\WorkflowEngine\Check\FileMimeType; use OCA\WorkflowEngine\Check\FileName; use OCA\WorkflowEngine\Check\FileSize; @@ -19,6 +20,7 @@ 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; @@ -45,14 +47,51 @@ /** * @psalm-import-type WorkflowEngineCheck from ResponseDefinitions * @psalm-import-type WorkflowEngineRule from ResponseDefinitions + * + * @psalm-type RuntimeOperation = array{ + * id: string, + * class: class-string, + * name: string, + * checks: string, + * operation: string, + * entity: class-string, + * events: string, + * appId: string, + * runtime: true + * } */ class Manager implements IManager { - /** @var array[] */ + /** @var array>> */ protected array $operations = []; /** @var array */ protected array $checks = []; + /** @var array> */ + protected array $registeredRuntimeChecks = []; + + /** + * @var array, + * name: string, + * checks: list, + * operation: string, + * entity: class-string, + * events: list, + * }>> + */ + protected array $registeredRuntimeOperations = []; + + /** + * @var array> + */ + protected array $registeredRuntimeScopes = []; + /** @var IEntity[] */ protected array $registeredEntities = []; @@ -77,6 +116,7 @@ public function __construct( private readonly IEventDispatcher $dispatcher, private readonly IAppConfig $appConfig, private readonly ICacheFactory $cacheFactory, + private readonly IAppManager $appManager, ) { $this->operationsByScope = new CappedMemoryCache(64); } @@ -92,7 +132,7 @@ public function getRuleMatcher(): IRuleMatcher { ); } - public function getAllConfiguredEvents() { + public function getAllConfiguredEvents(): array { $cache = $this->cacheFactory->createDistributed('flow'); $cached = $cache->get('events'); if ($cached !== null) { @@ -127,6 +167,30 @@ public function getAllConfiguredEvents() { return $operations; } + /** + * Returns the events configured by runtime operations, in the same structure as getAllConfiguredEvents(). + * + * @return array, array, list>> + */ + 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 $events */ + $events = array_unique( + array_merge($eventsByOperationAndEntity[$operationClass][$entityClass], $operation['events']) + ); + $eventsByOperationAndEntity[$operationClass][$entityClass] = $events; + } + } + + return $eventsByOperationAndEntity; + } + /** * @param class-string $operationClass * @return ScopeContext[] @@ -168,6 +232,33 @@ public function getAllConfiguredScopesForOperation(string $operationClass): arra return $this->scopesByOperation[$operationClass]; } + /** + * Gets configured scopes for operations registered at runtime. + * + * @param class-string $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; + } + + $scopeInfo = $this->registeredRuntimeScopes[$appId][$operationId] ?? null; + if ($scopeInfo === null) { + continue; + } + + $scope = new ScopeContext($scopeInfo['type'], $scopeInfo['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 +355,122 @@ protected function insertOperation( return $query->getLastInsertId(); } + /** + * Get all operations registered at runtime + * + * @param ScopeContext $scopeContext + * @return array, list> + */ + 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 + $scopeInfo = $this->registeredRuntimeScopes[$appId][$operationId] ?? null; + if ($scopeInfo === null) { + continue; + } + // filter by provided $scopeContext + if ((int)$scopeInfo['type'] !== $scopeContext->getScope()) { + continue; + } + if ($scopeContext->getScope() === IManager::SCOPE_USER && (string)$scopeInfo['value'] !== $scopeContext->getScopeId()) { + continue; + } + + $encodedChecks = json_encode($operation['checks']); + $encodedEvents = json_encode($operation['events']); + $row = [ + 'id' => $operationId, // string uniqid + 'class' => $operation['class'], + 'name' => $operation['name'], + // encode checks as JSON of hashes to be resolved later + 'checks' => $encodedChecks !== false ? $encodedChecks : '', + 'operation' => $operation['operation'], + 'entity' => $operation['entity'], + 'events' => $encodedEvents !== false ? $encodedEvents : '', + 'appId' => $appId, + 'runtime' => true, + ]; + $result[$operation['class']][] = $row; + } + } + + return $result; + } + + /** + * Return operations registered at runtime, which are not persisted in the DB nor shown in the UI. + * + * @param class-string $class + * @param ScopeContext $scopeContext + * @return list + */ + public function getRuntimeOperations(string $class, ScopeContext $scopeContext): array { + $operations = $this->getAllRuntimeOperations($scopeContext); + + return $operations[$class] ?? []; + } + + /** + * @param string $appId + * @param class-string $class + * @param string $name + * @param list $checks + * @param string $operation + * @param class-string $entity + * @param list> $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 = [ + 'id' => $operationId, + 'class' => $class, + 'name' => $name, + 'checks' => $checkHashes, + 'operation' => $operation, + 'entity' => $entity, + 'events' => $events, + ]; + $this->registeredRuntimeOperations[$appId] ??= []; + $this->registeredRuntimeOperations[$appId][$operationId] ??= $runtimeOperation; + + $runtimeScope = [ + 'operationId' => $operationId, + 'type' => $scope->getScope(), + 'value' => $scope->getScopeId(), + ]; + $this->registeredRuntimeScopes[$appId] ??= []; + $this->registeredRuntimeScopes[$appId][$operationId] ??= $runtimeScope; + } + /** * @param string $class * @param string $name @@ -523,6 +730,22 @@ public function validateOperation(string $class, string $name, array $checks, st } } + /** + * @param list $checkHashes + * @param string $appId + * @return array 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 @@ -665,6 +888,14 @@ public function registerOperation(IOperation $operator): void { $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; diff --git a/apps/workflowengine/lib/Service/RuleMatcher.php b/apps/workflowengine/lib/Service/RuleMatcher.php index b92bfdcff7b4b..486d6196c868b 100644 --- a/apps/workflowengine/lib/Service/RuleMatcher.php +++ b/apps/workflowengine/lib/Service/RuleMatcher.php @@ -112,10 +112,12 @@ public function getFlows(bool $returnFirstMatchingOperationOnly = true): array { $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,6 +130,7 @@ public function getFlows(bool $returnFirstMatchingOperationOnly = true): array { ->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)); } } } @@ -140,7 +143,11 @@ public function getFlows(bool $returnFirstMatchingOperationOnly = true): array { } $checkIds = json_decode($operation['checks'], true); - $checks = $this->manager->getChecks($checkIds); + if (($operation['runtime'] ?? null) === true) { + $checks = $this->manager->getRuntimeChecks($checkIds, $operation['appId']); + } else { + $checks = $this->manager->getChecks($checkIds); + } foreach ($checks as $check) { if (!$this->check($check)) { diff --git a/apps/workflowengine/tests/ManagerTest.php b/apps/workflowengine/tests/ManagerTest.php index e5b32cd78f403..411475c6dca4e 100644 --- a/apps/workflowengine/tests/ManagerTest.php +++ b/apps/workflowengine/tests/ManagerTest.php @@ -10,6 +10,7 @@ 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\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 @@ protected function setUp(): void { $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 @@ protected function setUp(): void { $this->session, $this->dispatcher, $this->config, - $this->cacheFactory + $this->cacheFactory, + $this->appManager, ); $this->clearTables(); } @@ -737,6 +742,125 @@ public function testValidateOperationDataLengthError(): void { } } + 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, diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php index 6d57f1e487842..f74781925d3d6 100644 --- a/lib/composer/composer/autoload_classmap.php +++ b/lib/composer/composer/autoload_classmap.php @@ -2268,6 +2268,7 @@ 'OC\\User\\PartiallyDeletedUsersBackend' => $baseDir . '/lib/private/User/PartiallyDeletedUsersBackend.php', 'OC\\User\\Session' => $baseDir . '/lib/private/User/Session.php', 'OC\\User\\User' => $baseDir . '/lib/private/User/User.php', + 'OC\\WorkflowEngine\\Events\\RegisterRuntimeOperationsEvent' => $baseDir . '/lib/private/WorkflowEngine/Events/RegisterRuntimeOperationsEvent.php', 'OC_App' => $baseDir . '/lib/private/legacy/OC_App.php', 'OC_Defaults' => $baseDir . '/lib/private/legacy/OC_Defaults.php', 'OC_Helper' => $baseDir . '/lib/private/legacy/OC_Helper.php', diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php index 564995dbfa757..3a9f722dbe5c9 100644 --- a/lib/composer/composer/autoload_static.php +++ b/lib/composer/composer/autoload_static.php @@ -2309,6 +2309,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2 'OC\\User\\PartiallyDeletedUsersBackend' => __DIR__ . '/../../..' . '/lib/private/User/PartiallyDeletedUsersBackend.php', 'OC\\User\\Session' => __DIR__ . '/../../..' . '/lib/private/User/Session.php', 'OC\\User\\User' => __DIR__ . '/../../..' . '/lib/private/User/User.php', + 'OC\\WorkflowEngine\\Events\\RegisterRuntimeOperationsEvent' => __DIR__ . '/../../..' . '/lib/private/WorkflowEngine/Events/RegisterRuntimeOperationsEvent.php', 'OC_App' => __DIR__ . '/../../..' . '/lib/private/legacy/OC_App.php', 'OC_Defaults' => __DIR__ . '/../../..' . '/lib/private/legacy/OC_Defaults.php', 'OC_Helper' => __DIR__ . '/../../..' . '/lib/private/legacy/OC_Helper.php', diff --git a/lib/private/WorkflowEngine/Events/RegisterRuntimeOperationsEvent.php b/lib/private/WorkflowEngine/Events/RegisterRuntimeOperationsEvent.php new file mode 100644 index 0000000000000..0b918e2e549b8 --- /dev/null +++ b/lib/private/WorkflowEngine/Events/RegisterRuntimeOperationsEvent.php @@ -0,0 +1,28 @@ +manager; + } +}