Skip to content

Commit 154ef0f

Browse files
Merge pull request #539 from nextcloud/backport/534/stable33
[stable33] feat: add %r placeholder for the approval requester
2 parents 7e3d0b7 + 344394f commit 154ef0f

7 files changed

Lines changed: 148 additions & 0 deletions

File tree

‎README.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ When defining the script, you can specify one of the following placeholders that
2424
%i | file id | *142430*
2525
%a | actor's user id | *bob*
2626
%o | owner's user id | *alice*
27+
%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*
2728
%n | nextcloud-relative path | *alice/files/Pictures/Wonderland/20180717_192103.jpg*
2829
%f | locally available file | */tmp/oc_tmp_m6E6OO-.jpg*
2930
%x | old nextcloud-relative file path (only on *rename* and *copy*) | *alice/files/Workbench/20180717_192103.jpg*

‎lib/AppInfo/Application.php‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77

88
namespace OCA\WorkflowScript\AppInfo;
99

10+
use OCA\Approval\Events\ApprovalStateChangedEvent;
11+
use OCA\WorkflowScript\Listener\ApprovalStateChangedListener;
1012
use OCA\WorkflowScript\Listener\RegisterFlowOperationsListener;
1113
use OCP\AppFramework\App;
1214
use OCP\AppFramework\Bootstrap\IBootContext;
@@ -30,6 +32,8 @@ public function __construct() {
3032
#[\Override]
3133
public function register(IRegistrationContext $context): void {
3234
$context->registerEventListener(RegisterOperationsEvent::class, RegisterFlowOperationsListener::class);
35+
// the approval app is optional; if it is absent, its event is simply never emitted
36+
$context->registerEventListener(ApprovalStateChangedEvent::class, ApprovalStateChangedListener::class);
3337
}
3438

3539
#[\Override]
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
/**
5+
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
6+
* SPDX-License-Identifier: AGPL-3.0-or-later
7+
*/
8+
9+
namespace OCA\WorkflowScript\Listener;
10+
11+
use OCA\Approval\Events\ApprovalStateChangedEvent;
12+
use OCA\WorkflowScript\RequesterContext;
13+
use OCP\EventDispatcher\Event;
14+
use OCP\EventDispatcher\IEventListener;
15+
16+
/**
17+
* Picks up the requester of approval state changes provided by the
18+
* approval app, so the %r placeholder can be substituted when a flow
19+
* is triggered by the tag assignment following the state change.
20+
*
21+
* The event class belongs to the approval app. The registration happens
22+
* by class name, so nothing breaks when the approval app is not
23+
* installed: the event is then simply never emitted.
24+
*
25+
* @template-implements IEventListener<Event>
26+
* @psalm-api
27+
*/
28+
class ApprovalStateChangedListener implements IEventListener {
29+
public function __construct(
30+
private RequesterContext $requesterContext,
31+
) {
32+
}
33+
34+
/**
35+
* @inheritDoc
36+
*/
37+
#[\Override]
38+
public function handle(Event $event): void {
39+
if (!$event instanceof ApprovalStateChangedEvent) {
40+
return;
41+
}
42+
43+
$requesterId = $event->getRequesterUserId();
44+
if ($requesterId === null || $requesterId === '') {
45+
return;
46+
}
47+
48+
$this->requesterContext->setRequester($event->getFileId(), $requesterId);
49+
}
50+
}

‎lib/Operation.php‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ public function __construct(
4646
private IRootFolder $rootFolder,
4747
private LoggerInterface $logger,
4848
private IURLGenerator $urlGenerator,
49+
private RequesterContext $requesterContext,
4950
) {
5051
}
5152

@@ -211,6 +212,20 @@ protected function buildCommand(string $template, Node $node, string $event, arr
211212
$command = str_replace('%o', escapeshellarg($userID), $command);
212213
}
213214

215+
if (strpos($command, '%r')) {
216+
// filled by ApprovalStateChangedListener when the flow was
217+
// triggered by an approval state change of the approval app
218+
$requesterID = null;
219+
try {
220+
$requesterID = $this->requesterContext->getRequester($node->getId());
221+
} catch (InvalidPathException|NotFoundException) {
222+
}
223+
if ($requesterID === null) {
224+
throw new PlaceholderNotSubstituted('r');
225+
}
226+
$command = str_replace('%r', escapeshellarg($requesterID), $command);
227+
}
228+
214229
if (strpos($command, '%x')) {
215230
if (!isset($extra['oldFilePath'])) {
216231
$extra['oldFilePath'] = '';

‎lib/RequesterContext.php‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
/**
5+
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
6+
* SPDX-License-Identifier: AGPL-3.0-or-later
7+
*/
8+
9+
namespace OCA\WorkflowScript;
10+
11+
/**
12+
* Request-scoped storage for the approval requester of files.
13+
*
14+
* The approval app (when installed) emits an ApprovalStateChangedEvent
15+
* carrying the requester right before it (un)assigns its system tags.
16+
* The workflow engine reacts to the tag change synchronously within the
17+
* same request, so the requester is picked up here when substituting
18+
* the %r placeholder while building the command.
19+
*/
20+
class RequesterContext {
21+
/** @var array<int, string> */
22+
private array $requesters = [];
23+
24+
public function setRequester(int $fileId, string $requesterId): void {
25+
$this->requesters[$fileId] = $requesterId;
26+
}
27+
28+
public function getRequester(int $fileId): ?string {
29+
return $this->requesters[$fileId] ?? null;
30+
}
31+
}

‎psalm.xml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,6 @@
1818
</extraFiles>
1919
<stubs>
2020
<file name="tests/stubs/oc_hooks_emitter.php" />
21+
<file name="tests/stubs/oca_approval_events_approvalstatechangedevent.php" />
2122
</stubs>
2223
</psalm>
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
/**
5+
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
6+
* SPDX-License-Identifier: AGPL-3.0-or-later
7+
*/
8+
9+
namespace OCA\Approval\Events {
10+
/**
11+
* Stub of the event emitted by the approval app (since its version
12+
* shipping OCA\Approval\Events\ApprovalStateChangedEvent). The approval
13+
* app is an optional integration, so its classes are not available to
14+
* the autoloader; this stub only exists for static analysis.
15+
*/
16+
class ApprovalStateChangedEvent extends \OCP\EventDispatcher\Event {
17+
public function __construct(
18+
private int $fileId,
19+
private int $ruleId,
20+
private int $newState,
21+
private ?string $actorUserId,
22+
private ?string $requesterUserId,
23+
) {
24+
}
25+
26+
public function getFileId(): int {
27+
return $this->fileId;
28+
}
29+
30+
public function getRuleId(): int {
31+
return $this->ruleId;
32+
}
33+
34+
public function getNewState(): int {
35+
return $this->newState;
36+
}
37+
38+
public function getActorUserId(): ?string {
39+
return $this->actorUserId;
40+
}
41+
42+
public function getRequesterUserId(): ?string {
43+
return $this->requesterUserId;
44+
}
45+
}
46+
}

0 commit comments

Comments
 (0)