Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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*
Expand Down
4 changes: 4 additions & 0 deletions lib/AppInfo/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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]
Expand Down
50 changes: 50 additions & 0 deletions lib/Listener/ApprovalStateChangedListener.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?php

declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\WorkflowScript\Listener;

use OCA\Approval\Events\ApprovalStateChangedEvent;
use OCA\WorkflowScript\RequesterContext;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventListener;

/**
* Picks up the requester of approval state changes provided by the
* approval app, so the %r placeholder can be substituted when a flow
* is triggered by the tag assignment following the state change.
*
* The event class belongs to the approval app. The registration happens
* by class name, so nothing breaks when the approval app is not
* installed: the event is then simply never emitted.
*
* @template-implements IEventListener<Event>
* @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);
}
}
15 changes: 15 additions & 0 deletions lib/Operation.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ public function __construct(
private IRootFolder $rootFolder,
private LoggerInterface $logger,
private IURLGenerator $urlGenerator,
private RequesterContext $requesterContext,
) {
}

Expand Down Expand Up @@ -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'] = '';
Expand Down
31 changes: 31 additions & 0 deletions lib/RequesterContext.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\WorkflowScript;

/**
* Request-scoped storage for the approval requester of files.
*
* The approval app (when installed) emits an ApprovalStateChangedEvent
* carrying the requester right before it (un)assigns its system tags.
* The workflow engine reacts to the tag change synchronously within the
* same request, so the requester is picked up here when substituting
* the %r placeholder while building the command.
*/
class RequesterContext {
/** @var array<int, string> */
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;
}
}
1 change: 1 addition & 0 deletions psalm.xml
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,6 @@
</extraFiles>
<stubs>
<file name="tests/stubs/oc_hooks_emitter.php" />
<file name="tests/stubs/oca_approval_events_approvalstatechangedevent.php" />
</stubs>
</psalm>
46 changes: 46 additions & 0 deletions tests/stubs/oca_approval_events_approvalstatechangedevent.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php

declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\Approval\Events {
/**
* Stub of the event emitted by the approval app (since its version
* shipping OCA\Approval\Events\ApprovalStateChangedEvent). The approval
* app is an optional integration, so its classes are not available to
* the autoloader; this stub only exists for static analysis.
*/
class ApprovalStateChangedEvent extends \OCP\EventDispatcher\Event {
public function __construct(
private int $fileId,
private int $ruleId,
private int $newState,
private ?string $actorUserId,
private ?string $requesterUserId,
) {
}

public function getFileId(): int {
return $this->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;
}
}
}