feat(files_versions): allow to block version creation using WFE

This allows users to create workflows to block the versions creation
for some files, based on tags or other conditions using the workflow
engine.
The usecase would be compliance to allow configure some rules to prevent
versioning.

Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
This commit is contained in:
Ferdinand Thiessen 2026-04-13 16:06:35 +02:00
parent c8b29c9dac
commit fe7bbd6bcd
No known key found for this signature in database
GPG key ID: 7E849AE05218500F
8 changed files with 196 additions and 1 deletions

View file

@ -20,13 +20,16 @@ return array(
'OCA\\Files_Versions\\Events\\VersionCreatedEvent' => $baseDir . '/../lib/Events/VersionCreatedEvent.php',
'OCA\\Files_Versions\\Events\\VersionRestoredEvent' => $baseDir . '/../lib/Events/VersionRestoredEvent.php',
'OCA\\Files_Versions\\Expiration' => $baseDir . '/../lib/Expiration.php',
'OCA\\Files_Versions\\Listener\\CreateVersionListenerForWorkflow' => $baseDir . '/../lib/Listener/CreateVersionListenerForWorkflow.php',
'OCA\\Files_Versions\\Listener\\FileEventsListener' => $baseDir . '/../lib/Listener/FileEventsListener.php',
'OCA\\Files_Versions\\Listener\\LegacyRollbackListener' => $baseDir . '/../lib/Listener/LegacyRollbackListener.php',
'OCA\\Files_Versions\\Listener\\LoadAdditionalListener' => $baseDir . '/../lib/Listener/LoadAdditionalListener.php',
'OCA\\Files_Versions\\Listener\\LoadSidebarListener' => $baseDir . '/../lib/Listener/LoadSidebarListener.php',
'OCA\\Files_Versions\\Listener\\RegisterWorkflowIntegrationListener' => $baseDir . '/../lib/Listener/RegisterWorkflowIntegrationListener.php',
'OCA\\Files_Versions\\Listener\\VersionAuthorListener' => $baseDir . '/../lib/Listener/VersionAuthorListener.php',
'OCA\\Files_Versions\\Listener\\VersionStorageMoveListener' => $baseDir . '/../lib/Listener/VersionStorageMoveListener.php',
'OCA\\Files_Versions\\Migration\\Version1020Date20221114144058' => $baseDir . '/../lib/Migration/Version1020Date20221114144058.php',
'OCA\\Files_Versions\\Operation' => $baseDir . '/../lib/Operation.php',
'OCA\\Files_Versions\\Sabre\\Plugin' => $baseDir . '/../lib/Sabre/Plugin.php',
'OCA\\Files_Versions\\Sabre\\RestoreFolder' => $baseDir . '/../lib/Sabre/RestoreFolder.php',
'OCA\\Files_Versions\\Sabre\\RootCollection' => $baseDir . '/../lib/Sabre/RootCollection.php',

View file

@ -35,13 +35,16 @@ class ComposerStaticInitFiles_Versions
'OCA\\Files_Versions\\Events\\VersionCreatedEvent' => __DIR__ . '/..' . '/../lib/Events/VersionCreatedEvent.php',
'OCA\\Files_Versions\\Events\\VersionRestoredEvent' => __DIR__ . '/..' . '/../lib/Events/VersionRestoredEvent.php',
'OCA\\Files_Versions\\Expiration' => __DIR__ . '/..' . '/../lib/Expiration.php',
'OCA\\Files_Versions\\Listener\\CreateVersionListenerForWorkflow' => __DIR__ . '/..' . '/../lib/Listener/CreateVersionListenerForWorkflow.php',
'OCA\\Files_Versions\\Listener\\FileEventsListener' => __DIR__ . '/..' . '/../lib/Listener/FileEventsListener.php',
'OCA\\Files_Versions\\Listener\\LegacyRollbackListener' => __DIR__ . '/..' . '/../lib/Listener/LegacyRollbackListener.php',
'OCA\\Files_Versions\\Listener\\LoadAdditionalListener' => __DIR__ . '/..' . '/../lib/Listener/LoadAdditionalListener.php',
'OCA\\Files_Versions\\Listener\\LoadSidebarListener' => __DIR__ . '/..' . '/../lib/Listener/LoadSidebarListener.php',
'OCA\\Files_Versions\\Listener\\RegisterWorkflowIntegrationListener' => __DIR__ . '/..' . '/../lib/Listener/RegisterWorkflowIntegrationListener.php',
'OCA\\Files_Versions\\Listener\\VersionAuthorListener' => __DIR__ . '/..' . '/../lib/Listener/VersionAuthorListener.php',
'OCA\\Files_Versions\\Listener\\VersionStorageMoveListener' => __DIR__ . '/..' . '/../lib/Listener/VersionStorageMoveListener.php',
'OCA\\Files_Versions\\Migration\\Version1020Date20221114144058' => __DIR__ . '/..' . '/../lib/Migration/Version1020Date20221114144058.php',
'OCA\\Files_Versions\\Operation' => __DIR__ . '/..' . '/../lib/Operation.php',
'OCA\\Files_Versions\\Sabre\\Plugin' => __DIR__ . '/..' . '/../lib/Sabre/Plugin.php',
'OCA\\Files_Versions\\Sabre\\RestoreFolder' => __DIR__ . '/..' . '/../lib/Sabre/RestoreFolder.php',
'OCA\\Files_Versions\\Sabre\\RootCollection' => __DIR__ . '/..' . '/../lib/Sabre/RootCollection.php',

View file

@ -11,11 +11,14 @@ use OCA\DAV\Connector\Sabre\Principal;
use OCA\Files\Event\LoadAdditionalScriptsEvent;
use OCA\Files\Event\LoadSidebar;
use OCA\Files_Versions\Capabilities;
use OCA\Files_Versions\Events\CreateVersionEvent;
use OCA\Files_Versions\Events\VersionRestoredEvent;
use OCA\Files_Versions\Listener\CreateVersionListenerForWorkflow;
use OCA\Files_Versions\Listener\FileEventsListener;
use OCA\Files_Versions\Listener\LegacyRollbackListener;
use OCA\Files_Versions\Listener\LoadAdditionalListener;
use OCA\Files_Versions\Listener\LoadSidebarListener;
use OCA\Files_Versions\Listener\RegisterWorkflowIntegrationListener;
use OCA\Files_Versions\Listener\VersionAuthorListener;
use OCA\Files_Versions\Listener\VersionStorageMoveListener;
use OCA\Files_Versions\Versions\IVersionManager;
@ -36,6 +39,8 @@ use OCP\Files\Events\Node\NodeDeletedEvent;
use OCP\Files\Events\Node\NodeRenamedEvent;
use OCP\Files\Events\Node\NodeTouchedEvent;
use OCP\Files\Events\Node\NodeWrittenEvent;
use OCP\WorkflowEngine\Events\LoadSettingsScriptsEvent;
use OCP\WorkflowEngine\Events\RegisterOperationsEvent;
use Psr\Container\ContainerInterface;
use Psr\Log\LoggerInterface;
@ -85,8 +90,12 @@ class Application extends App implements IBootstrap {
// we add the version author listener with lower priority to make sure new versions already are created by FileEventsListener
$context->registerEventListener(NodeWrittenEvent::class, VersionAuthorListener::class, -1);
$context->registerEventListener(VersionRestoredEvent::class, LegacyRollbackListener::class);
// WFE integration
$context->registerEventListener(RegisterOperationsEvent::class, RegisterWorkflowIntegrationListener::class);
$context->registerEventListener(LoadSettingsScriptsEvent::class, RegisterWorkflowIntegrationListener::class);
$context->registerEventListener(CreateVersionEvent::class, CreateVersionListenerForWorkflow::class);
}
#[\Override]

View file

@ -0,0 +1,95 @@
<?php
declare(strict_types=1);
/*!
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Files_Versions;
use OCA\Files_Versions\Events\CreateVersionEvent;
use OCA\WorkflowEngine\Entity\File as FileEntity;
use OCP\EventDispatcher\Event;
use OCP\Files\Folder;
use OCP\IL10N;
use OCP\IURLGenerator;
use OCP\WorkflowEngine\IComplexOperation;
use OCP\WorkflowEngine\IManager;
use OCP\WorkflowEngine\IRuleMatcher;
use OCP\WorkflowEngine\ISpecificOperation;
use Psr\Log\LoggerInterface;
class BlockVersioningOperation implements ISpecificOperation, IComplexOperation {
public function __construct(
private readonly IL10N $l10n,
private readonly FileEntity $fileEntity,
private readonly LoggerInterface $logger,
private readonly IURLGenerator $urlGenerator,
) {
}
#[\Override]
public function getEntityId(): string {
return FileEntity::class;
}
#[\Override]
public function getDisplayName(): string {
return $this->l10n->t('Block file versioning');
}
#[\Override]
public function getDescription(): string {
return $this->l10n->t('Automatic tag based blocking of file version creation.');
}
#[\Override]
public function getIcon(): string {
return $this->urlGenerator->imagePath('files_versions', 'app.svg');
}
#[\Override]
public function isAvailableForScope(int $scope): bool {
return $scope === IManager::SCOPE_ADMIN;
}
#[\Override]
public function validateOperation(string $name, array $checks, string $operation): void {
if (empty($checks)) {
throw new \UnexpectedValueException($this->l10n->t('No rule given'));
}
}
#[\Override]
public function onEvent(string $eventName, Event $event, IRuleMatcher $ruleMatcher): void {
if ($eventName !== CreateVersionEvent::class || !($event instanceof CreateVersionEvent)) {
return;
}
$node = $event->getNode();
$path = $node->getInternalPath();
$ruleMatcher->setFileInfo(
$node->getStorage(),
$path,
$node instanceof Folder,
);
$ruleMatcher->setEntitySubject($this->fileEntity, $node);
$ruleMatcher->setOperation($this);
$flows = $ruleMatcher->getFlows();
if ($flows !== []) {
$this->logger->debug('Blocking version creation due to matching workflow rules', [
'path' => $path,
]);
$event->disableVersions();
}
}
#[\Override]
public function getTriggerHint(): string {
return $this->l10n->t('A new version is created'); // TRANSLATORS: This will be shown as "When: " "A new version is created"
}
}

View file

@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
/*!
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Files_Versions\Listener;
use OCA\Files_Versions\BlockVersioningOperation;
use OCA\Files_Versions\Events\CreateVersionEvent;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventListener;
use OCP\WorkflowEngine\IManager;
/** @template-implements IEventListener<CreateVersionEvent> */
class CreateVersionListenerForWorkflow implements IEventListener {
public function __construct(
private IManager $manager,
private BlockVersioningOperation $operation,
) {
}
#[\Override]
public function handle(Event $event): void {
if (!($event instanceof CreateVersionEvent)) {
return;
}
$this->operation->onEvent(
$event::class,
$event,
$this->manager->getRuleMatcher(),
);
}
}

View file

@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
/*!
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Files_Versions\Listener;
use OCA\Files_Versions\BlockVersioningOperation;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventListener;
use OCP\Util;
use OCP\WorkflowEngine\Events\LoadSettingsScriptsEvent;
use OCP\WorkflowEngine\Events\RegisterOperationsEvent;
/** @template-implements IEventListener<RegisterOperationsEvent|LoadSettingsScriptsEvent> */
class RegisterWorkflowIntegrationListener implements IEventListener {
public function __construct(
private readonly BlockVersioningOperation $operation,
) {
}
#[\Override]
public function handle(Event $event): void {
if ($event instanceof RegisterOperationsEvent) {
$event->registerOperation($this->operation);
} elseif ($event instanceof LoadSettingsScriptsEvent) {
Util::addScript('files_versions', 'workflow', 'workflowengine');
}
}
}

View file

@ -0,0 +1,12 @@
/*!
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
window.addEventListener('DOMContentLoaded', () => {
globalThis.OCA.WorkflowEngine.registerOperator({
id: 'OCA\\Files_Versions\\BlockVersioningOperation',
color: '#ff5900',
operation: 'deny',
})
})

View file

@ -48,6 +48,7 @@ const modules = {
},
files_versions: {
'sidebar-tab': resolve(import.meta.dirname, 'apps/files_versions/src', 'sidebar_tab.ts'),
workflow: resolve(import.meta.dirname, 'apps/files_versions/src', 'workflow.ts'),
},
oauth2: {
'settings-admin': resolve(import.meta.dirname, 'apps/oauth2/src', 'settings-admin.ts'),