diff --git a/apps/workflowengine/appinfo/info.xml b/apps/workflowengine/appinfo/info.xml index 5b94aaeb10d0a..43a6714f40be0 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..5080055bf2aea 100644 --- a/apps/workflowengine/lib/AppInfo/Application.php +++ b/apps/workflowengine/lib/AppInfo/Application.php @@ -42,15 +42,23 @@ public function register(IRegistrationContext $context): void { #[\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) { diff --git a/apps/workflowengine/lib/Command/Runtime.php b/apps/workflowengine/lib/Command/Runtime.php new file mode 100644 index 0000000000000..261c137b08ec4 --- /dev/null +++ b/apps/workflowengine/lib/Command/Runtime.php @@ -0,0 +1,105 @@ +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; + } +} diff --git a/apps/workflowengine/lib/Manager.php b/apps/workflowengine/lib/Manager.php index a23a1f55524a6..ea5d5f5ce04c5 100644 --- a/apps/workflowengine/lib/Manager.php +++ b/apps/workflowengine/lib/Manager.php @@ -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\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 @@ * @psalm-import-type WorkflowEngineRule from ResponseDefinitions */ class Manager implements IManager { - /** @var array[] */ + /** @var array>> */ protected array $operations = []; /** @var array */ protected array $checks = []; + /** @var array> */ + protected array $registeredRuntimeChecks = []; + + /** + * Registered runtime operations, keyed by app ID and runtime operation ID. + * + * @var array> + */ + protected array $registeredRuntimeOperations = []; + + /** + * Registered runtime scopes, keyed by app ID and runtime operation ID. + * + * @var array> + */ + protected array $registeredRuntimeScopes = []; + /** @var IEntity[] */ protected array $registeredEntities = []; @@ -77,6 +98,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 +114,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 +149,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 +214,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; + } + + $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 @@ 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 + $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 $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 = 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 @@ 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 +863,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..89c61ed4cb7dd 100644 --- a/apps/workflowengine/lib/Service/RuleMatcher.php +++ b/apps/workflowengine/lib/Service/RuleMatcher.php @@ -8,6 +8,7 @@ */ 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 @@ 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,20 +131,29 @@ 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)); } } } $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 diff --git a/apps/workflowengine/tests/ManagerTest.php b/apps/workflowengine/tests/ManagerTest.php index e5b32cd78f403..74c5663adfc84 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 17470c5b72c37..9c7357ea6aa0a 100644 --- a/lib/composer/composer/autoload_classmap.php +++ b/lib/composer/composer/autoload_classmap.php @@ -37,6 +37,9 @@ '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', diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php index 3e661000039b7..d25c72171fa99 100644 --- a/lib/composer/composer/autoload_static.php +++ b/lib/composer/composer/autoload_static.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', diff --git a/lib/unstable/WorkflowEngine/Events/RegisterRuntimeOperationsEvent.php b/lib/unstable/WorkflowEngine/Events/RegisterRuntimeOperationsEvent.php new file mode 100644 index 0000000000000..e66ba433c4baa --- /dev/null +++ b/lib/unstable/WorkflowEngine/Events/RegisterRuntimeOperationsEvent.php @@ -0,0 +1,39 @@ +manager; + } +} diff --git a/lib/unstable/WorkflowEngine/RuntimeOperation.php b/lib/unstable/WorkflowEngine/RuntimeOperation.php new file mode 100644 index 0000000000000..84764c0fe04b6 --- /dev/null +++ b/lib/unstable/WorkflowEngine/RuntimeOperation.php @@ -0,0 +1,63 @@ + $class + * @param string $name + * @param list $checks + * @param string $operation + * @param class-string $entity + * @param list $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>, value-of>> + */ + 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, + ]; + } +} diff --git a/lib/unstable/WorkflowEngine/RuntimeScope.php b/lib/unstable/WorkflowEngine/RuntimeScope.php new file mode 100644 index 0000000000000..cc149f0dd7281 --- /dev/null +++ b/lib/unstable/WorkflowEngine/RuntimeScope.php @@ -0,0 +1,41 @@ +>, value-of>> + */ + public function toArray(): array { + return [ + 'operationId' => $this->operationId, + 'type' => $this->type, + 'value' => $this->value, + ]; + } +} diff --git a/psalm-ncu.xml b/psalm-ncu.xml index 7b5a42c60af90..09e9c92094b37 100644 --- a/psalm-ncu.xml +++ b/psalm-ncu.xml @@ -19,4 +19,7 @@ + + +