Skip to content

Commit 87d7bbf

Browse files
Merge pull request #50077 from nextcloud/feat/files_trashbin/allow-preventing-trash-permanently
2 parents f1ea284 + 769b38f commit 87d7bbf

23 files changed

+189
-20
lines changed

apps/files/src/actions/deleteAction.spec.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,22 @@ describe('Delete action conditions tests', () => {
127127
})
128128

129129
describe('Delete action enabled tests', () => {
130+
let initialState: HTMLInputElement
131+
132+
afterEach(() => {
133+
document.body.removeChild(initialState)
134+
})
135+
136+
beforeEach(() => {
137+
initialState = document.createElement('input')
138+
initialState.setAttribute('type', 'hidden')
139+
initialState.setAttribute('id', 'initial-state-files_trashbin-config')
140+
initialState.setAttribute('value', btoa(JSON.stringify({
141+
allow_delete: true,
142+
})))
143+
document.body.appendChild(initialState)
144+
})
145+
130146
test('Enabled with DELETE permissions', () => {
131147
const file = new File({
132148
id: 1,
@@ -177,6 +193,15 @@ describe('Delete action enabled tests', () => {
177193
expect(action.enabled!([folder2], view)).toBe(false)
178194
expect(action.enabled!([folder1, folder2], view)).toBe(false)
179195
})
196+
197+
test('Disabled if not allowed', () => {
198+
initialState.setAttribute('value', btoa(JSON.stringify({
199+
allow_delete: false,
200+
})))
201+
202+
expect(action.enabled).toBeDefined()
203+
expect(action.enabled!([], view)).toBe(false)
204+
})
180205
})
181206

182207
describe('Delete action execute tests', () => {

apps/files/src/actions/deleteAction.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
33
* SPDX-License-Identifier: AGPL-3.0-or-later
44
*/
5+
import type { FilesTrashbinConfigState } from '../../../files_trashbin/src/fileListActions/emptyTrashAction.ts'
6+
7+
import { loadState } from '@nextcloud/initial-state'
58
import { Permission, Node, View, FileAction } from '@nextcloud/files'
69
import { showInfo } from '@nextcloud/dialogs'
710
import { translate as t } from '@nextcloud/l10n'
@@ -34,6 +37,11 @@ export const action = new FileAction({
3437
},
3538

3639
enabled(nodes: Node[]) {
40+
const config = loadState<FilesTrashbinConfigState>('files_trashbin', 'config')
41+
if (!config.allow_delete) {
42+
return false
43+
}
44+
3745
return nodes.length > 0 && nodes
3846
.map(node => node.permissions)
3947
.every(permission => (permission & Permission.DELETE) !== 0)

apps/files/src/services/HotKeysService.spec.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
33
* SPDX-License-Identifier: AGPL-3.0-or-later
44
*/
5-
import { describe, it, vi, expect, beforeEach, beforeAll } from 'vitest'
5+
import { describe, it, vi, expect, beforeEach, beforeAll, afterEach } from 'vitest'
66
import { File, Permission, View } from '@nextcloud/files'
77
import axios from '@nextcloud/axios'
88

@@ -33,6 +33,12 @@ describe('HotKeysService testing', () => {
3333

3434
const goToRouteMock = vi.fn()
3535

36+
let initialState: HTMLInputElement
37+
38+
afterEach(() => {
39+
document.body.removeChild(initialState)
40+
})
41+
3642
beforeAll(() => {
3743
registerHotkeys()
3844
})
@@ -57,6 +63,14 @@ describe('HotKeysService testing', () => {
5763
window.OCA = { Files: { Sidebar: { open: () => {}, setActiveTab: () => {} } } }
5864
// @ts-expect-error We only mock what needed, we do not need Files.Router.goTo or Files.Navigation
5965
window.OCP = { Files: { Router: { goToRoute: goToRouteMock, params: {}, query: {} } } }
66+
67+
initialState = document.createElement('input')
68+
initialState.setAttribute('type', 'hidden')
69+
initialState.setAttribute('id', 'initial-state-files_trashbin-config')
70+
initialState.setAttribute('value', btoa(JSON.stringify({
71+
allow_delete: true,
72+
})))
73+
document.body.appendChild(initialState)
6074
})
6175

6276
it('Pressing d should open the sidebar once', () => {

apps/files_trashbin/composer/composer/autoload_classmap.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
'OCA\\Files_Trashbin\\Expiration' => $baseDir . '/../lib/Expiration.php',
2424
'OCA\\Files_Trashbin\\Helper' => $baseDir . '/../lib/Helper.php',
2525
'OCA\\Files_Trashbin\\Listener\\EventListener' => $baseDir . '/../lib/Listener/EventListener.php',
26+
'OCA\\Files_Trashbin\\Listeners\\BeforeTemplateRendered' => $baseDir . '/../lib/Listeners/BeforeTemplateRendered.php',
2627
'OCA\\Files_Trashbin\\Listeners\\LoadAdditionalScripts' => $baseDir . '/../lib/Listeners/LoadAdditionalScripts.php',
2728
'OCA\\Files_Trashbin\\Listeners\\SyncLivePhotosListener' => $baseDir . '/../lib/Listeners/SyncLivePhotosListener.php',
2829
'OCA\\Files_Trashbin\\Migration\\Version1010Date20200630192639' => $baseDir . '/../lib/Migration/Version1010Date20200630192639.php',
@@ -40,6 +41,7 @@
4041
'OCA\\Files_Trashbin\\Sabre\\TrashHome' => $baseDir . '/../lib/Sabre/TrashHome.php',
4142
'OCA\\Files_Trashbin\\Sabre\\TrashRoot' => $baseDir . '/../lib/Sabre/TrashRoot.php',
4243
'OCA\\Files_Trashbin\\Sabre\\TrashbinPlugin' => $baseDir . '/../lib/Sabre/TrashbinPlugin.php',
44+
'OCA\\Files_Trashbin\\Service\\ConfigService' => $baseDir . '/../lib/Service/ConfigService.php',
4345
'OCA\\Files_Trashbin\\Storage' => $baseDir . '/../lib/Storage.php',
4446
'OCA\\Files_Trashbin\\Trash\\BackendNotFoundException' => $baseDir . '/../lib/Trash/BackendNotFoundException.php',
4547
'OCA\\Files_Trashbin\\Trash\\ITrashBackend' => $baseDir . '/../lib/Trash/ITrashBackend.php',

apps/files_trashbin/composer/composer/autoload_static.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ class ComposerStaticInitFiles_Trashbin
3838
'OCA\\Files_Trashbin\\Expiration' => __DIR__ . '/..' . '/../lib/Expiration.php',
3939
'OCA\\Files_Trashbin\\Helper' => __DIR__ . '/..' . '/../lib/Helper.php',
4040
'OCA\\Files_Trashbin\\Listener\\EventListener' => __DIR__ . '/..' . '/../lib/Listener/EventListener.php',
41+
'OCA\\Files_Trashbin\\Listeners\\BeforeTemplateRendered' => __DIR__ . '/..' . '/../lib/Listeners/BeforeTemplateRendered.php',
4142
'OCA\\Files_Trashbin\\Listeners\\LoadAdditionalScripts' => __DIR__ . '/..' . '/../lib/Listeners/LoadAdditionalScripts.php',
4243
'OCA\\Files_Trashbin\\Listeners\\SyncLivePhotosListener' => __DIR__ . '/..' . '/../lib/Listeners/SyncLivePhotosListener.php',
4344
'OCA\\Files_Trashbin\\Migration\\Version1010Date20200630192639' => __DIR__ . '/..' . '/../lib/Migration/Version1010Date20200630192639.php',
@@ -55,6 +56,7 @@ class ComposerStaticInitFiles_Trashbin
5556
'OCA\\Files_Trashbin\\Sabre\\TrashHome' => __DIR__ . '/..' . '/../lib/Sabre/TrashHome.php',
5657
'OCA\\Files_Trashbin\\Sabre\\TrashRoot' => __DIR__ . '/..' . '/../lib/Sabre/TrashRoot.php',
5758
'OCA\\Files_Trashbin\\Sabre\\TrashbinPlugin' => __DIR__ . '/..' . '/../lib/Sabre/TrashbinPlugin.php',
59+
'OCA\\Files_Trashbin\\Service\\ConfigService' => __DIR__ . '/..' . '/../lib/Service/ConfigService.php',
5860
'OCA\\Files_Trashbin\\Storage' => __DIR__ . '/..' . '/../lib/Storage.php',
5961
'OCA\\Files_Trashbin\\Trash\\BackendNotFoundException' => __DIR__ . '/..' . '/../lib/Trash/BackendNotFoundException.php',
6062
'OCA\\Files_Trashbin\\Trash\\ITrashBackend' => __DIR__ . '/..' . '/../lib/Trash/ITrashBackend.php',

apps/files_trashbin/lib/AppInfo/Application.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,12 @@
88

99
use OCA\DAV\Connector\Sabre\Principal;
1010
use OCA\Files\Event\LoadAdditionalScriptsEvent;
11+
use OCA\Files_Sharing\Event\BeforeTemplateRenderedEvent;
1112
use OCA\Files_Trashbin\Capabilities;
1213
use OCA\Files_Trashbin\Events\BeforeNodeRestoredEvent;
1314
use OCA\Files_Trashbin\Expiration;
1415
use OCA\Files_Trashbin\Listener\EventListener;
16+
use OCA\Files_Trashbin\Listeners\BeforeTemplateRendered;
1517
use OCA\Files_Trashbin\Listeners\LoadAdditionalScripts;
1618
use OCA\Files_Trashbin\Listeners\SyncLivePhotosListener;
1719
use OCA\Files_Trashbin\Trash\ITrashManager;
@@ -52,6 +54,11 @@ public function register(IRegistrationContext $context): void {
5254
LoadAdditionalScripts::class
5355
);
5456

57+
$context->registerEventListener(
58+
BeforeTemplateRenderedEvent::class,
59+
BeforeTemplateRendered::class
60+
);
61+
5562
$context->registerEventListener(BeforeNodeRestoredEvent::class, SyncLivePhotosListener::class);
5663

5764
$context->registerEventListener(NodeWrittenEvent::class, EventListener::class);

apps/files_trashbin/lib/Capabilities.php

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
*/
77
namespace OCA\Files_Trashbin;
88

9+
use OCA\Files_Trashbin\Service\ConfigService;
910
use OCP\Capabilities\ICapability;
1011

1112
/**
@@ -18,12 +19,18 @@ class Capabilities implements ICapability {
1819
/**
1920
* Return this classes capabilities
2021
*
21-
* @return array{files: array{undelete: bool}}
22+
* @return array{
23+
* files: array{
24+
* undelete: bool,
25+
* delete_from_trash: bool
26+
* }
27+
* }
2228
*/
2329
public function getCapabilities() {
2430
return [
2531
'files' => [
26-
'undelete' => true
32+
'undelete' => true,
33+
'delete_from_trash' => ConfigService::getDeleteFromTrashEnabled(),
2734
]
2835
];
2936
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
7+
* SPDX-License-Identifier: AGPL-3.0-or-later
8+
*/
9+
10+
namespace OCA\Files_Trashbin\Listeners;
11+
12+
use OCA\Files_Sharing\Event\BeforeTemplateRenderedEvent;
13+
use OCA\Files_Trashbin\Service\ConfigService;
14+
use OCP\AppFramework\Services\IInitialState;
15+
use OCP\EventDispatcher\Event;
16+
use OCP\EventDispatcher\IEventListener;
17+
18+
/** @template-implements IEventListener<BeforeTemplateRenderedEvent> */
19+
class BeforeTemplateRendered implements IEventListener {
20+
public function __construct(
21+
private IInitialState $initialState,
22+
) {
23+
}
24+
25+
public function handle(Event $event): void {
26+
if (!($event instanceof BeforeTemplateRenderedEvent)) {
27+
return;
28+
}
29+
30+
ConfigService::injectInitialState($this->initialState);
31+
}
32+
}

apps/files_trashbin/lib/Listeners/LoadAdditionalScripts.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,26 @@
1010

1111
use OCA\Files\Event\LoadAdditionalScriptsEvent;
1212
use OCA\Files_Trashbin\AppInfo\Application;
13+
use OCA\Files_Trashbin\Service\ConfigService;
14+
use OCP\AppFramework\Services\IInitialState;
1315
use OCP\EventDispatcher\Event;
1416
use OCP\EventDispatcher\IEventListener;
1517
use OCP\Util;
1618

1719
/** @template-implements IEventListener<LoadAdditionalScriptsEvent> */
1820
class LoadAdditionalScripts implements IEventListener {
21+
public function __construct(
22+
private IInitialState $initialState,
23+
) {
24+
}
25+
1926
public function handle(Event $event): void {
2027
if (!($event instanceof LoadAdditionalScriptsEvent)) {
2128
return;
2229
}
2330

2431
Util::addInitScript(Application::APP_ID, 'init');
32+
33+
ConfigService::injectInitialState($this->initialState);
2534
}
2635
}

apps/files_trashbin/lib/Sabre/AbstractTrash.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,12 @@
88
*/
99
namespace OCA\Files_Trashbin\Sabre;
1010

11+
use OCA\Files_Trashbin\Service\ConfigService;
1112
use OCA\Files_Trashbin\Trash\ITrashItem;
1213
use OCA\Files_Trashbin\Trash\ITrashManager;
1314
use OCP\Files\FileInfo;
1415
use OCP\IUser;
16+
use Sabre\DAV\Exception\Forbidden;
1517

1618
abstract class AbstractTrash implements ITrash {
1719
public function __construct(
@@ -73,6 +75,10 @@ public function getDeletedBy(): ?IUser {
7375
}
7476

7577
public function delete() {
78+
if (!ConfigService::getDeleteFromTrashEnabled()) {
79+
throw new Forbidden('Not allowed to delete items from the trash bin');
80+
}
81+
7682
$this->trashManager->removeItem($this->data);
7783
}
7884

0 commit comments

Comments
 (0)