From adcf573d38e12453e01aea7d4d25566743bbf386 Mon Sep 17 00:00:00 2001 From: eli Date: Sun, 27 Sep 2026 12:17:47 +0800 Subject: [PATCH] feat: add %r placeholder for the approval requester When a flow reacts to a tag assignment of the approval app, the actor placeholder %a resolves to the approver on approval and rejection, while scripts typically need to act for the user who requested the approval (e.g. move the file into their space). The approval app emits an ApprovalStateChangedEvent carrying the requester right before changing its tags. The new listener stores it in the request-scoped RequesterContext, so it is substituted when the workflow engine reacts to the tag change within the same request. When no requester is known, the command is not executed and a warning is logged, consistent with the %n placeholder. Requires the approval app to emit the event, see https://github.com/nextcloud/workflow_script/issues/363 Signed-off-by: eli --- README.md | 1 + lib/AppInfo/Application.php | 4 ++ lib/Listener/ApprovalStateChangedListener.php | 50 +++++++++++++++++++ lib/Operation.php | 15 ++++++ lib/RequesterContext.php | 31 ++++++++++++ psalm.xml | 1 + ...roval_events_approvalstatechangedevent.php | 46 +++++++++++++++++ 7 files changed, 148 insertions(+) create mode 100644 lib/Listener/ApprovalStateChangedListener.php create mode 100644 lib/RequesterContext.php create mode 100644 tests/stubs/oca_approval_events_approvalstatechangedevent.php diff --git a/README.md b/README.md index 07d4347..1ecbc97 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,7 @@ When defining the script, you can specify one of the following placeholders that %i | file id | *142430* %a | actor's user id | *bob* %o | owner's user id | *alice* +%r | approval requester's user id (requires the [approval app](https://github.com/nextcloud/approval), only substituted when the flow is triggered by an approval state change; if no requester is known, the command is not executed and a warning is logged) | *alice* %n | nextcloud-relative path | *alice/files/Pictures/Wonderland/20180717_192103.jpg* %f | locally available file | */tmp/oc_tmp_m6E6OO-.jpg* %x | old nextcloud-relative file path (only on *rename* and *copy*) | *alice/files/Workbench/20180717_192103.jpg* diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index bcd2356..a29f230 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -7,6 +7,8 @@ namespace OCA\WorkflowScript\AppInfo; +use OCA\Approval\Events\ApprovalStateChangedEvent; +use OCA\WorkflowScript\Listener\ApprovalStateChangedListener; use OCA\WorkflowScript\Listener\RegisterFlowOperationsListener; use OCP\AppFramework\App; use OCP\AppFramework\Bootstrap\IBootContext; @@ -30,6 +32,8 @@ public function __construct() { #[\Override] public function register(IRegistrationContext $context): void { $context->registerEventListener(RegisterOperationsEvent::class, RegisterFlowOperationsListener::class); + // the approval app is optional; if it is absent, its event is simply never emitted + $context->registerEventListener(ApprovalStateChangedEvent::class, ApprovalStateChangedListener::class); } #[\Override] diff --git a/lib/Listener/ApprovalStateChangedListener.php b/lib/Listener/ApprovalStateChangedListener.php new file mode 100644 index 0000000..04ab4ff --- /dev/null +++ b/lib/Listener/ApprovalStateChangedListener.php @@ -0,0 +1,50 @@ + + * @psalm-api + */ +class ApprovalStateChangedListener implements IEventListener { + public function __construct( + private RequesterContext $requesterContext, + ) { + } + + /** + * @inheritDoc + */ + #[\Override] + public function handle(Event $event): void { + if (!$event instanceof ApprovalStateChangedEvent) { + return; + } + + $requesterId = $event->getRequesterUserId(); + if ($requesterId === null || $requesterId === '') { + return; + } + + $this->requesterContext->setRequester($event->getFileId(), $requesterId); + } +} diff --git a/lib/Operation.php b/lib/Operation.php index 81f8957..54506a1 100644 --- a/lib/Operation.php +++ b/lib/Operation.php @@ -46,6 +46,7 @@ public function __construct( private IRootFolder $rootFolder, private LoggerInterface $logger, private IURLGenerator $urlGenerator, + private RequesterContext $requesterContext, ) { } @@ -211,6 +212,20 @@ protected function buildCommand(string $template, Node $node, string $event, arr $command = str_replace('%o', escapeshellarg($userID), $command); } + if (strpos($command, '%r')) { + // filled by ApprovalStateChangedListener when the flow was + // triggered by an approval state change of the approval app + $requesterID = null; + try { + $requesterID = $this->requesterContext->getRequester($node->getId()); + } catch (InvalidPathException|NotFoundException) { + } + if ($requesterID === null) { + throw new PlaceholderNotSubstituted('r'); + } + $command = str_replace('%r', escapeshellarg($requesterID), $command); + } + if (strpos($command, '%x')) { if (!isset($extra['oldFilePath'])) { $extra['oldFilePath'] = ''; diff --git a/lib/RequesterContext.php b/lib/RequesterContext.php new file mode 100644 index 0000000..dd366c9 --- /dev/null +++ b/lib/RequesterContext.php @@ -0,0 +1,31 @@ + */ + private array $requesters = []; + + public function setRequester(int $fileId, string $requesterId): void { + $this->requesters[$fileId] = $requesterId; + } + + public function getRequester(int $fileId): ?string { + return $this->requesters[$fileId] ?? null; + } +} diff --git a/psalm.xml b/psalm.xml index c178bf5..d0be197 100644 --- a/psalm.xml +++ b/psalm.xml @@ -18,5 +18,6 @@ + diff --git a/tests/stubs/oca_approval_events_approvalstatechangedevent.php b/tests/stubs/oca_approval_events_approvalstatechangedevent.php new file mode 100644 index 0000000..07f8604 --- /dev/null +++ b/tests/stubs/oca_approval_events_approvalstatechangedevent.php @@ -0,0 +1,46 @@ +fileId; + } + + public function getRuleId(): int { + return $this->ruleId; + } + + public function getNewState(): int { + return $this->newState; + } + + public function getActorUserId(): ?string { + return $this->actorUserId; + } + + public function getRequesterUserId(): ?string { + return $this->requesterUserId; + } + } +}