Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
3 changes: 3 additions & 0 deletions lib/AppInfo/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
use OCA\Approval\Dav\ApprovalPlugin;
use OCA\Approval\Listener\LoadAdditionalScriptsListener;
use OCA\Approval\Listener\LoadSidebarScripts;
use OCA\Approval\Listener\UpdateFilesListener;
use OCA\Approval\Notification\Notifier;
use OCA\Approval\Service\ApprovalService;

Expand All @@ -22,6 +23,7 @@
use OCP\AppFramework\Bootstrap\IRegistrationContext;
use OCP\EventDispatcher\IEventDispatcher;

use OCP\FilesMetadata\Event\MetadataBackgroundEvent;
use OCP\SabrePluginEvent;
use OCP\SystemTag\MapperEvent;

Expand Down Expand Up @@ -63,6 +65,7 @@ public function register(IRegistrationContext $context): void {
$context->registerEventListener(LoadSidebar::class, LoadSidebarScripts::class);
$context->registerNotifierService(Notifier::class);
$context->registerDashboardWidget(ApprovalPendingWidget::class);
$context->registerEventListener(MetadataBackgroundEvent::class, UpdateFilesListener::class);
}

public function boot(IBootContext $context): void {
Expand Down
10 changes: 6 additions & 4 deletions lib/Controller/ConfigController.php
Original file line number Diff line number Diff line change
Expand Up @@ -110,11 +110,12 @@ public function getRules(): DataResponse {
* @param array $approvers
* @param array $requesters
* @param string $description
* @param bool $unapproveWhenModified
* @return DataResponse
*/
public function createRule(int $tagPending, int $tagApproved, int $tagRejected,
array $approvers, array $requesters, string $description): DataResponse {
$result = $this->ruleService->createRule($tagPending, $tagApproved, $tagRejected, $approvers, $requesters, $description);
array $approvers, array $requesters, string $description, bool $unapproveWhenModified): DataResponse {
$result = $this->ruleService->createRule($tagPending, $tagApproved, $tagRejected, $approvers, $requesters, $description, $unapproveWhenModified);
return isset($result['error'])
? new DataResponse($result, 400)
: new DataResponse($result['id']);
Expand All @@ -128,11 +129,12 @@ public function createRule(int $tagPending, int $tagApproved, int $tagRejected,
* @param array $approvers
* @param array $requesters
* @param string $description
* @param bool $unapproveWhenModified
* @return DataResponse
*/
public function saveRule(int $id, int $tagPending, int $tagApproved, int $tagRejected,
array $approvers, array $requesters, string $description): DataResponse {
$result = $this->ruleService->saveRule($id, $tagPending, $tagApproved, $tagRejected, $approvers, $requesters, $description);
array $approvers, array $requesters, string $description, bool $unapproveWhenModified): DataResponse {
$result = $this->ruleService->saveRule($id, $tagPending, $tagApproved, $tagRejected, $approvers, $requesters, $description, $unapproveWhenModified);
return isset($result['error'])
? new DataResponse($result, 400)
: new DataResponse($result['id']);
Expand Down
32 changes: 32 additions & 0 deletions lib/Listener/UpdateFilesListener.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Approval\Listener;

use OCA\Approval\Service\ApprovalService;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventListener;
use OCP\FilesMetadata\Event\MetadataBackgroundEvent;

/** @template-implements IEventListener<MetadataBackgroundEvent> */
class UpdateFilesListener implements IEventListener {

public function __construct(
private ApprovalService $approvalService,
) {
}

/**
* @inheritDoc
*/
public function handle(Event $event): void {
if (!($event instanceof MetadataBackgroundEvent)) {
return;
}
$fileNode = $event->getNode();
$this->approvalService->removeApprovalTags($fileNode);
}
}
66 changes: 66 additions & 0 deletions lib/Migration/Version020301Date20250618110518.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<?php

declare(strict_types=1);

/**
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\Approval\Migration;

use Closure;
use OCP\DB\ISchemaWrapper;
use OCP\DB\Types;
use OCP\IDBConnection;
use OCP\Migration\IOutput;
use OCP\Migration\SimpleMigrationStep;

/**
* Auto-generated migration step: Please modify to your needs!
*/
class Version020301Date20250618110518 extends SimpleMigrationStep {

public function __construct(
private IDBConnection $connection,
) {
}

/**
* @param IOutput $output
* @param Closure(): ISchemaWrapper $schemaClosure
* @param array $options
*/
public function preSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void {
}

/**
* @param IOutput $output
* @param Closure(): ISchemaWrapper $schemaClosure
* @param array $options
* @return null|ISchemaWrapper
*/
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
/** @var ISchemaWrapper $schema */
$schema = $schemaClosure();
if ($schema->hasTable('approval_rules')) {
$table = $schema->getTable('approval_rules');
if (!$table->hasColumn('unapprove_when_modified')) {
$table->addColumn('unapprove_when_modified', Types::SMALLINT, ['default' => 0, 'notnull' => true]);
}
}

return $schema;
}

/**
* @param IOutput $output
* @param Closure(): ISchemaWrapper $schemaClosure
* @param array $options
*/
public function postSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void {
// Not sure if this is needed, but it can't hurt
$qbUpdate = $this->connection->getQueryBuilder();
$qbUpdate->update('approval_rules')->set('unapprove_when_modified', $qbUpdate->expr()->literal(0))->executeStatement();
}
}
21 changes: 21 additions & 0 deletions lib/Service/ApprovalService.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
use OCP\App\IAppManager;
use OCP\Files\FileInfo;
use OCP\Files\IRootFolder;
use OCP\Files\Node;
use OCP\IGroupManager;
use OCP\IL10N;
use OCP\IUser;
Expand Down Expand Up @@ -815,4 +816,24 @@ public function propFind(PropFind $propFind, INode $node): void {
}
);
}

/**
* Remove approval tag from a file
*
* @param Node $file
*/
public function removeApprovalTags(Node $file): void {
$fileId = $file->getId();
$fileTags = $this->tagObjectMapper->getTagIdsForObjects([$fileId], 'files');
$fileTags = $fileTags[$fileId] ?? [];
if (count($fileTags) > 0) {
$tags = $this->ruleService->filterApprovalTags($fileTags);
foreach ($tags as $tag) {
$mTime = $file->getMTime();
if ($this->ruleService->wasApprovedAfter($fileId, $mTime)) {
$this->tagObjectMapper->unassignTags((string)$fileId, 'files', $tag);
}
}
}
}
}
88 changes: 86 additions & 2 deletions lib/Service/RuleService.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
use OCA\Approval\AppInfo\Application;
use OCP\App\IAppManager;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\ICacheFactory;
use OCP\IDBConnection;

use OCP\IUserManager;
Expand All @@ -27,6 +28,7 @@ public function __construct(
private IDBConnection $db,
private IUserManager $userManager,
private IAppManager $appManager,
private ICacheFactory $cacheFactory,
) {
$this->strTypeToInt = [
'user' => Application::TYPE_USER,
Expand Down Expand Up @@ -102,11 +104,12 @@ private function hasConflict(?int $id, int $tagPending): bool {
* @param array $approvers
* @param array $requesters
* @param string $description
* @param bool $unapproveWhenModified
* @return array Error string or id of saved rule
* @throws \OCP\DB\Exception
*/
public function saveRule(int $id, int $tagPending, int $tagApproved, int $tagRejected,
array $approvers, array $requesters, string $description): array {
array $approvers, array $requesters, string $description, bool $unapproveWhenModified): array {
$this->cachedRules = null;
if (!$this->isValid($tagPending, $tagApproved, $tagRejected)) {
return ['error' => 'Invalid rule'];
Expand All @@ -122,11 +125,13 @@ public function saveRule(int $id, int $tagPending, int $tagApproved, int $tagRej
$qb->set('tag_approved', $qb->createNamedParameter($tagApproved, IQueryBuilder::PARAM_INT));
$qb->set('tag_rejected', $qb->createNamedParameter($tagRejected, IQueryBuilder::PARAM_INT));
$qb->set('description', $qb->createNamedParameter($description, IQueryBuilder::PARAM_STR));
$qb->set('unapprove_when_modified', $qb->createNamedParameter($unapproveWhenModified ? 1 : 0, IQueryBuilder::PARAM_INT));
$qb->where(
$qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT))
);
$qb->executeStatement();
$qb = $qb->resetQueryParts();
$this->clearRuleCaches();

$rule = $this->getRule($id);

Expand Down Expand Up @@ -211,10 +216,11 @@ public function saveRule(int $id, int $tagPending, int $tagApproved, int $tagRej
* @param array $approvers
* @param array $requesters
* @param string $description
* @param bool $unapproveWhenModified
* @return array id of created rule or error string
*/
public function createRule(int $tagPending, int $tagApproved, int $tagRejected,
array $approvers, array $requesters, string $description): array {
array $approvers, array $requesters, string $description, bool $unapproveWhenModified): array {
$this->cachedRules = null;
if (!$this->isValid($tagPending, $tagApproved, $tagRejected)) {
return ['error' => 'Rule is invalid'];
Expand All @@ -231,9 +237,11 @@ public function createRule(int $tagPending, int $tagApproved, int $tagRejected,
'tag_approved' => $qb->createNamedParameter($tagApproved, IQueryBuilder::PARAM_INT),
'tag_rejected' => $qb->createNamedParameter($tagRejected, IQueryBuilder::PARAM_INT),
'description' => $qb->createNamedParameter($description, IQueryBuilder::PARAM_STR),
'unapprove_when_modified' => $qb->createNamedParameter($unapproveWhenModified ? 1 : 0, IQueryBuilder::PARAM_INT),
]);
$qb->executeStatement();
$qb = $qb->resetQueryParts();
$this->clearRuleCaches();

$insertedRuleId = $qb->getLastInsertId();

Expand Down Expand Up @@ -302,6 +310,7 @@ public function deleteRule(int $id): array {
);
$qb->executeStatement();
$qb->resetQueryParts();
$this->clearRuleCaches();

return [];
}
Expand Down Expand Up @@ -377,6 +386,7 @@ public function getRules(): array {
'description' => $description,
'approvers' => [],
'requesters' => [],
'unapproveWhenModified' => (int)$row['unapprove_when_modified'] === 1,
];
}
$req->closeCursor();
Expand Down Expand Up @@ -500,4 +510,78 @@ public function getLastAction(int $fileId, int $ruleId, int $newState): ?array {
}
return $activity;
}

/**
* Gets all approval tags that should be unapproved when a file is modified
*
* @param array $tags
* @return array of filtered approval tags
*/
public function getApprovalTags(): array {
$cache = $this->cacheFactory->createDistributed('integration_overleaf');
if ($cached = $cache->get('approval_tags')) {
return $cached;
}
$qb = $this->db->getQueryBuilder();
$qb->selectDistinct('tag_approved')->from('approval_rules')
->where($qb->expr()
->eq('unapprove_when_modified', $qb->expr()->literal(1))
);
$req = $qb->executeQuery();
$approvalTags = $req->fetchAll();
$approvalTags = array_map(function ($tag) {
return $tag['tag_approved'];
}, $approvalTags);
$req->closeCursor();
$cache->set('approval_tags', $approvalTags, 3600);
return $approvalTags;
}

/**
* Check if a list of tags contains an approval tags that should be unapproved
* when the file is modified
*
* @param array $tags
* @return array of filtered approval tags
*/
public function filterApprovalTags(array $tags): array {
$approvalTags = $this->getApprovalTags();
return array_filter($tags, function ($tag) use ($approvalTags) {
return in_array($tag, $approvalTags);
});
}
/**
* Clear caches based on rule changes
*/
public function clearRuleCaches(): void {
$cache = $this->cacheFactory->createDistributed('integration_overleaf');
$cache->remove('approval_tags');
}

/**
* Checks that the approval of the file was after the time given.
* This does not verify that the file was actually approved.
*
* @param int $fileId
* @param int $time
* @return bool
*/
public function wasApprovedAfter(int $fileId, int $time): bool {
$qb = $this->db->getQueryBuilder();
$qb->select('timestamp')
->from('approval_activity')
->where(
$qb->expr()->eq('file_id', $qb->createNamedParameter($fileId, IQueryBuilder::PARAM_INT))
)
->andWhere(
$qb->expr()->eq('new_state', $qb->createNamedParameter(Application::STATE_APPROVED, IQueryBuilder::PARAM_INT))
);
$req = $qb->executeQuery();
$timestamp = $req->fetchOne();
$req->closeCursor();
if (!$timestamp) {
return true;
}
return $timestamp < $time;
}
}
3 changes: 3 additions & 0 deletions src/components/AdminSettings.vue
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,7 @@ export default {
entityId: u.entityId,
}
}),
unapproveWhenModified: rule.unapproveWhenModified,
}
const url = generateUrl('/apps/approval/rule/' + id)
axios.put(url, req).then((response) => {
Expand All @@ -279,6 +280,7 @@ export default {
description: '',
approvers: [],
requesters: [],
unapproveWhenModified: 'false',
}
},
onNewRuleDelete() {
Expand Down Expand Up @@ -306,6 +308,7 @@ export default {
entityId: u.entityId,
}
}),
unapproveWhenModified: rule.unapproveWhenModified,
}
const url = generateUrl('/apps/approval/rule')
axios.post(url, req).then((response) => {
Expand Down
Loading
Loading