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
2 changes: 1 addition & 1 deletion apps/workflowengine/appinfo/info.xml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
<name>Nextcloud workflow engine</name>
<summary>Nextcloud workflow engine</summary>
<description>Nextcloud workflow engine</description>
<version>2.16.0</version>
<version>2.16.1</version>
<licence>agpl</licence>
<author>Arthur Schiwon</author>
<author>Julius Härtl</author>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
'OCA\\WorkflowEngine\\Migration\\PopulateNewlyIntroducedDatabaseFields' => $baseDir . '/../lib/Migration/PopulateNewlyIntroducedDatabaseFields.php',
'OCA\\WorkflowEngine\\Migration\\Version2000Date20190808074233' => $baseDir . '/../lib/Migration/Version2000Date20190808074233.php',
'OCA\\WorkflowEngine\\Migration\\Version2200Date20210805101925' => $baseDir . '/../lib/Migration/Version2200Date20210805101925.php',
'OCA\\WorkflowEngine\\Migration\\Version3400Date20260227000000' => $baseDir . '/../lib/Migration/Version3400Date20260227000000.php',
'OCA\\WorkflowEngine\\ResponseDefinitions' => $baseDir . '/../lib/ResponseDefinitions.php',
'OCA\\WorkflowEngine\\Service\\Logger' => $baseDir . '/../lib/Service/Logger.php',
'OCA\\WorkflowEngine\\Service\\RuleMatcher' => $baseDir . '/../lib/Service/RuleMatcher.php',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ class ComposerStaticInitWorkflowEngine
'OCA\\WorkflowEngine\\Migration\\PopulateNewlyIntroducedDatabaseFields' => __DIR__ . '/..' . '/../lib/Migration/PopulateNewlyIntroducedDatabaseFields.php',
'OCA\\WorkflowEngine\\Migration\\Version2000Date20190808074233' => __DIR__ . '/..' . '/../lib/Migration/Version2000Date20190808074233.php',
'OCA\\WorkflowEngine\\Migration\\Version2200Date20210805101925' => __DIR__ . '/..' . '/../lib/Migration/Version2200Date20210805101925.php',
'OCA\\WorkflowEngine\\Migration\\Version3400Date20260227000000' => __DIR__ . '/..' . '/../lib/Migration/Version3400Date20260227000000.php',
'OCA\\WorkflowEngine\\ResponseDefinitions' => __DIR__ . '/..' . '/../lib/ResponseDefinitions.php',
'OCA\\WorkflowEngine\\Service\\Logger' => __DIR__ . '/..' . '/../lib/Service/Logger.php',
'OCA\\WorkflowEngine\\Service\\RuleMatcher' => __DIR__ . '/..' . '/../lib/Service/RuleMatcher.php',
Expand Down
120 changes: 60 additions & 60 deletions apps/workflowengine/lib/Entity/File.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,15 @@
namespace OCA\WorkflowEngine\Entity;

use OC\Files\Config\UserMountCache;
use OC\SystemTag\Events\SingleTagAssignedEvent;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\GenericEvent;
use OCP\Files\Events\Node\AbstractNodeEvent;
use OCP\Files\Events\Node\NodeCopiedEvent;
use OCP\Files\Events\Node\NodeCreatedEvent;
use OCP\Files\Events\Node\NodeDeletedEvent;
use OCP\Files\Events\Node\NodeRenamedEvent;
use OCP\Files\Events\Node\NodeTouchedEvent;
use OCP\Files\Events\Node\NodeUpdatedEvent;
use OCP\Files\InvalidPathException;
use OCP\Files\IRootFolder;
use OCP\Files\Mount\IMountManager;
Expand All @@ -22,78 +29,85 @@
use OCP\IUserManager;
use OCP\IUserSession;
use OCP\SystemTag\ISystemTagManager;
use OCP\SystemTag\MapperEvent;
use OCP\WorkflowEngine\EntityContext\IContextPortation;
use OCP\WorkflowEngine\EntityContext\IDisplayText;
use OCP\WorkflowEngine\EntityContext\IIcon;
use OCP\WorkflowEngine\EntityContext\IUrl;
use OCP\WorkflowEngine\GenericEntityEvent;
use OCP\WorkflowEngine\IEntity;
use OCP\WorkflowEngine\IRuleMatcher;
use Override;

class File implements IEntity, IDisplayText, IUrl, IIcon, IContextPortation {
private const EVENT_NAMESPACE = '\OCP\Files::';
/** @var ?class-string<Event> $eventName */
protected ?string $eventName = null;
protected ?Event $event = null;
private ?Node $node = null;
private ?IUser $actingUser = null;

public function __construct(
protected IL10N $l10n,
protected IURLGenerator $urlGenerator,
protected IRootFolder $root,
private IUserSession $userSession,
private ISystemTagManager $tagManager,
private IUserManager $userManager,
private UserMountCache $userMountCache,
private IMountManager $mountManager,
protected readonly IL10N $l10n,
protected readonly IURLGenerator $urlGenerator,
protected readonly IRootFolder $root,
private readonly IUserSession $userSession,
private readonly ISystemTagManager $tagManager,
private readonly IUserManager $userManager,
private readonly UserMountCache $userMountCache,
private readonly IMountManager $mountManager,
) {
}

#[Override]
public function getName(): string {
return $this->l10n->t('File');
}

#[Override]
public function getIcon(): string {
return $this->urlGenerator->imagePath('core', 'categories/files.svg');
}

#[Override]
public function getEvents(): array {
return [
new GenericEntityEvent($this->l10n->t('File created'), self::EVENT_NAMESPACE . 'postCreate'),
new GenericEntityEvent($this->l10n->t('File updated'), self::EVENT_NAMESPACE . 'postWrite'),
new GenericEntityEvent($this->l10n->t('File renamed'), self::EVENT_NAMESPACE . 'postRename'),
new GenericEntityEvent($this->l10n->t('File deleted'), self::EVENT_NAMESPACE . 'postDelete'),
new GenericEntityEvent($this->l10n->t('File accessed'), self::EVENT_NAMESPACE . 'postTouch'),
new GenericEntityEvent($this->l10n->t('File copied'), self::EVENT_NAMESPACE . 'postCopy'),
new GenericEntityEvent($this->l10n->t('Tag assigned'), MapperEvent::EVENT_ASSIGN),
new GenericEntityEvent($this->l10n->t('File created'), NodeCreatedEvent::class),
new GenericEntityEvent($this->l10n->t('File updated'), NodeUpdatedEvent::class),
new GenericEntityEvent($this->l10n->t('File renamed'), NodeRenamedEvent::class),
new GenericEntityEvent($this->l10n->t('File deleted'), NodeDeletedEvent::class),
new GenericEntityEvent($this->l10n->t('File accessed'), NodeTouchedEvent::class),
new GenericEntityEvent($this->l10n->t('File copied'), NodeCopiedEvent::class),
new GenericEntityEvent($this->l10n->t('Tag assigned'), SingleTagAssignedEvent::class),
];
}

#[Override]
public function prepareRuleMatcher(IRuleMatcher $ruleMatcher, string $eventName, Event $event): void {
if (!$event instanceof GenericEvent && !$event instanceof MapperEvent) {
$isSupported = array_any($this->getEvents(), static fn (GenericEntityEvent $genericEvent): bool => is_a($event, $genericEvent->getEventName()));
if (!$isSupported) {
return;
}

$this->eventName = $eventName;
$this->event = $event;
$this->actingUser = $this->actingUser ?? $this->userSession->getUser();
try {
$node = $this->getNode();
$ruleMatcher->setEntitySubject($this, $node);
$ruleMatcher->setFileInfo($node->getStorage(), $node->getInternalPath());
} catch (NotFoundException $e) {
} catch (NotFoundException) {
// pass
}
}

#[Override]
public function isLegitimatedForUserId(string $userId): bool {
try {
$node = $this->getNode();
if ($node->getOwner()?->getUID() === $userId) {
return true;
}

if ($this->eventName === self::EVENT_NAMESPACE . 'postDelete') {
if ($this->eventName === NodeDeletedEvent::class) {
// At postDelete, the file no longer exists. Check for parent folder instead.
$fileId = $node->getParentId();
} else {
Expand All @@ -120,35 +134,27 @@ protected function getNode(): Node {
if ($this->node) {
return $this->node;
}
if (!$this->event instanceof GenericEvent && !$this->event instanceof MapperEvent) {

if ($this->event instanceof AbstractNodeEvent) {
return $this->event->getNode();
}

if (!$this->event instanceof SingleTagAssignedEvent || $this->event->getObjectType() !== 'files') {
throw new NotFoundException();
}
switch ($this->eventName) {
case self::EVENT_NAMESPACE . 'postCreate':
case self::EVENT_NAMESPACE . 'postWrite':
case self::EVENT_NAMESPACE . 'postDelete':
case self::EVENT_NAMESPACE . 'postTouch':
return $this->event->getSubject();
case self::EVENT_NAMESPACE . 'postRename':
case self::EVENT_NAMESPACE . 'postCopy':
return $this->event->getSubject()[1];
case MapperEvent::EVENT_ASSIGN:
if (!$this->event instanceof MapperEvent || $this->event->getObjectType() !== 'files') {
throw new NotFoundException();
}
$this->node = $this->root->getFirstNodeById((int)$this->event->getObjectId());
if ($this->node !== null) {
return $this->node;
}
break;

$this->node = $this->root->getFirstNodeById((int)$this->event->getObjectId());
if ($this->node === null) {
throw new NotFoundException();
}
throw new NotFoundException();

return $this->node;
}

public function getDisplayText(int $verbosity = 0): string {
try {
$node = $this->getNode();
} catch (NotFoundException $e) {
} catch (NotFoundException) {
return '';
}

Expand All @@ -158,21 +164,21 @@ public function getDisplayText(int $verbosity = 0): string {
];

switch ($this->eventName) {
case self::EVENT_NAMESPACE . 'postCreate':
case NodeCreatedEvent::class:
return $this->l10n->t('%s created %s', $options);
case self::EVENT_NAMESPACE . 'postWrite':
case NodeUpdatedEvent::class:
return $this->l10n->t('%s modified %s', $options);
case self::EVENT_NAMESPACE . 'postDelete':
case NodeDeletedEvent::class:
return $this->l10n->t('%s deleted %s', $options);
case self::EVENT_NAMESPACE . 'postTouch':
case NodeTouchedEvent::class:
return $this->l10n->t('%s accessed %s', $options);
case self::EVENT_NAMESPACE . 'postRename':
case NodeRenamedEvent::class:
return $this->l10n->t('%s renamed %s', $options);
case self::EVENT_NAMESPACE . 'postCopy':
case NodeCopiedEvent::class:
return $this->l10n->t('%s copied %s', $options);
case MapperEvent::EVENT_ASSIGN:
case SingleTagAssignedEvent::class:
$tagNames = [];
if ($this->event instanceof MapperEvent) {
if ($this->event instanceof SingleTagAssignedEvent) {
$tagIDs = $this->event->getTags();
$tagObjects = $this->tagManager->getTagsByIds($tagIDs);
foreach ($tagObjects as $systemTag) {
Expand Down Expand Up @@ -201,9 +207,7 @@ public function getUrl(): string {
}
}

/**
* @inheritDoc
*/
#[Override]
public function exportContextIDs(): array {
$nodeOwner = $this->getNode()->getOwner();
$actingUserId = null;
Expand All @@ -215,14 +219,12 @@ public function exportContextIDs(): array {
return [
'eventName' => $this->eventName,
'nodeId' => $this->getNode()->getId(),
'nodeOwnerId' => $nodeOwner ? $nodeOwner->getUID() : null,
'nodeOwnerId' => $nodeOwner?->getUID(),
'actingUserId' => $actingUserId,
];
}

/**
* @inheritDoc
*/
#[Override]
public function importContextIDs(array $contextIDs): void {
$this->eventName = $contextIDs['eventName'];
if ($contextIDs['nodeOwnerId'] !== null) {
Expand All @@ -237,9 +239,7 @@ public function importContextIDs(array $contextIDs): void {
}
}

/**
* @inheritDoc
*/
#[Override]
public function getIconUrl(): string {
return $this->getIcon();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
<?php

declare(strict_types=1);

/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH
* SPDX-FileContributor: Carl Schwan
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\WorkflowEngine\Migration;

use OC\SystemTag\Events\SingleTagAssignedEvent;
use OCP\Files\Events\Node\NodeCopiedEvent;
use OCP\Files\Events\Node\NodeCreatedEvent;
use OCP\Files\Events\Node\NodeDeletedEvent;
use OCP\Files\Events\Node\NodeRenamedEvent;
use OCP\Files\Events\Node\NodeTouchedEvent;
use OCP\Files\Events\Node\NodeUpdatedEvent;
use OCP\IDBConnection;
use OCP\Migration\Attributes\ModifyColumn;
use OCP\Migration\IOutput;
use OCP\Migration\SimpleMigrationStep;
use Override;

#[ModifyColumn(table: 'flow_operations', name: 'events', description: 'Use new event names')]
class Version3400Date20260227000000 extends SimpleMigrationStep {
public function __construct(
private readonly IDBConnection $connection,
) {
}

#[Override]
public function postSchemaChange(IOutput $output, \Closure $schemaClosure, array $options) {
$qb = $this->connection->getQueryBuilder();
$lastId = null;
while (true) {
$qb->select('*')
->from('flow_operations');
if ($lastId !== null) {
$qb->andWhere($qb->expr()->gt('id', $qb->createNamedParameter($lastId)));
}
$qb->setMaxResults(1000);
$newMapping = [];
$result = $qb->executeQuery();
while ($row = $result->fetchAssociative()) {
$events = json_decode($row['events'], true);
$newEvents = array_map(function (string $eventName): string {
return match ($eventName) {
'\OCP\Files::postCreate' => NodeCreatedEvent::class,
'\OCP\Files::postUpdate' => NodeUpdatedEvent::class,
'\OCP\Files::postRename' => NodeRenamedEvent::class,
'\OCP\Files::postDelete' => NodeDeletedEvent::class,
'\OCP\Files::postTouch' => NodeTouchedEvent::class,
'\OCP\Files::postCopy' => NodeCopiedEvent::class,
'OCP\SystemTag\ISystemTagObjectMapper::assignTags' => SingleTagAssignedEvent::class,
};
}, $events);

if ($newEvents !== $events) {
$newMapping[$row['id']] = json_encode($newEvents);
}
}
$result->closeCursor();

try {
if ($newMapping !== []) {
$this->connection->beginTransaction();
}
foreach ($newMapping as $id => $events) {
$update = $this->connection->getQueryBuilder();
$update->update('flow_operations')
->set('events', $update->createNamedParameter($events))
->where($qb->expr()->eq('id', $update->createNamedParameter($id)))
->executeStatement();
}
if ($newMapping !== []) {
$this->connection->commit();
}
} catch (\Exception $e) {
$this->connection->rollback();
throw $e;
}

if ($row !== false) {
$lastId = $row['id'];
} else {
break;
}
}

return $schemaClosure();
}
}
5 changes: 3 additions & 2 deletions apps/workflowengine/tests/ManagerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\Files\Events\Node\NodeCreatedEvent;
use OCP\Files\Events\Node\NodeDeletedEvent;
use OCP\Files\IRootFolder;
use OCP\Files\Mount\IMountManager;
use OCP\ICache;
Expand Down Expand Up @@ -441,12 +442,12 @@ public function testUpdateOperation(): void {
$check2 = ['class' => ICheck::class, 'operator' => 'eq', 'value' => 23456];

/** @noinspection PhpUnhandledExceptionInspection */
$op = $this->manager->updateOperation($opId1, 'Test01a', [$check1, $check2], 'foohur', $adminScope, $entity, ['\OCP\Files::postDelete']);
$op = $this->manager->updateOperation($opId1, 'Test01a', [$check1, $check2], 'foohur', $adminScope, $entity, [NodeDeletedEvent::class]);
$this->assertSame('Test01a', $op['name']);
$this->assertSame('foohur', $op['operation']);

/** @noinspection PhpUnhandledExceptionInspection */
$op = $this->manager->updateOperation($opId2, 'Test02a', [$check1], 'barfoo', $userScope, $entity, ['\OCP\Files::postDelete']);
$op = $this->manager->updateOperation($opId2, 'Test02a', [$check1], 'barfoo', $userScope, $entity, [NodeDeletedEvent::class]);
$this->assertSame('Test02a', $op['name']);
$this->assertSame('barfoo', $op['operation']);

Expand Down
Loading
Loading