Skip to content

Commit

Permalink
Merge pull request #50077 from nextcloud/feat/files_trashbin/allow-pr…
Browse files Browse the repository at this point in the history
…eventing-trash-permanently
  • Loading branch information
provokateurin authored Jan 13, 2025
2 parents f1ea284 + 769b38f commit 87d7bbf
Show file tree
Hide file tree
Showing 23 changed files with 189 additions and 20 deletions.
25 changes: 25 additions & 0 deletions apps/files/src/actions/deleteAction.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,22 @@ describe('Delete action conditions tests', () => {
})

describe('Delete action enabled tests', () => {
let initialState: HTMLInputElement

afterEach(() => {
document.body.removeChild(initialState)
})

beforeEach(() => {
initialState = document.createElement('input')
initialState.setAttribute('type', 'hidden')
initialState.setAttribute('id', 'initial-state-files_trashbin-config')
initialState.setAttribute('value', btoa(JSON.stringify({
allow_delete: true,
})))
document.body.appendChild(initialState)
})

test('Enabled with DELETE permissions', () => {
const file = new File({
id: 1,
Expand Down Expand Up @@ -177,6 +193,15 @@ describe('Delete action enabled tests', () => {
expect(action.enabled!([folder2], view)).toBe(false)
expect(action.enabled!([folder1, folder2], view)).toBe(false)
})

test('Disabled if not allowed', () => {
initialState.setAttribute('value', btoa(JSON.stringify({
allow_delete: false,
})))

expect(action.enabled).toBeDefined()
expect(action.enabled!([], view)).toBe(false)
})
})

describe('Delete action execute tests', () => {
Expand Down
8 changes: 8 additions & 0 deletions apps/files/src/actions/deleteAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { FilesTrashbinConfigState } from '../../../files_trashbin/src/fileListActions/emptyTrashAction.ts'

import { loadState } from '@nextcloud/initial-state'
import { Permission, Node, View, FileAction } from '@nextcloud/files'
import { showInfo } from '@nextcloud/dialogs'
import { translate as t } from '@nextcloud/l10n'
Expand Down Expand Up @@ -34,6 +37,11 @@ export const action = new FileAction({
},

enabled(nodes: Node[]) {
const config = loadState<FilesTrashbinConfigState>('files_trashbin', 'config')
if (!config.allow_delete) {
return false
}

return nodes.length > 0 && nodes
.map(node => node.permissions)
.every(permission => (permission & Permission.DELETE) !== 0)
Expand Down
16 changes: 15 additions & 1 deletion apps/files/src/services/HotKeysService.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import { describe, it, vi, expect, beforeEach, beforeAll } from 'vitest'
import { describe, it, vi, expect, beforeEach, beforeAll, afterEach } from 'vitest'
import { File, Permission, View } from '@nextcloud/files'
import axios from '@nextcloud/axios'

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

const goToRouteMock = vi.fn()

let initialState: HTMLInputElement

afterEach(() => {
document.body.removeChild(initialState)
})

beforeAll(() => {
registerHotkeys()
})
Expand All @@ -57,6 +63,14 @@ describe('HotKeysService testing', () => {
window.OCA = { Files: { Sidebar: { open: () => {}, setActiveTab: () => {} } } }
// @ts-expect-error We only mock what needed, we do not need Files.Router.goTo or Files.Navigation
window.OCP = { Files: { Router: { goToRoute: goToRouteMock, params: {}, query: {} } } }

initialState = document.createElement('input')
initialState.setAttribute('type', 'hidden')
initialState.setAttribute('id', 'initial-state-files_trashbin-config')
initialState.setAttribute('value', btoa(JSON.stringify({
allow_delete: true,
})))
document.body.appendChild(initialState)
})

it('Pressing d should open the sidebar once', () => {
Expand Down
2 changes: 2 additions & 0 deletions apps/files_trashbin/composer/composer/autoload_classmap.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
'OCA\\Files_Trashbin\\Expiration' => $baseDir . '/../lib/Expiration.php',
'OCA\\Files_Trashbin\\Helper' => $baseDir . '/../lib/Helper.php',
'OCA\\Files_Trashbin\\Listener\\EventListener' => $baseDir . '/../lib/Listener/EventListener.php',
'OCA\\Files_Trashbin\\Listeners\\BeforeTemplateRendered' => $baseDir . '/../lib/Listeners/BeforeTemplateRendered.php',
'OCA\\Files_Trashbin\\Listeners\\LoadAdditionalScripts' => $baseDir . '/../lib/Listeners/LoadAdditionalScripts.php',
'OCA\\Files_Trashbin\\Listeners\\SyncLivePhotosListener' => $baseDir . '/../lib/Listeners/SyncLivePhotosListener.php',
'OCA\\Files_Trashbin\\Migration\\Version1010Date20200630192639' => $baseDir . '/../lib/Migration/Version1010Date20200630192639.php',
Expand All @@ -40,6 +41,7 @@
'OCA\\Files_Trashbin\\Sabre\\TrashHome' => $baseDir . '/../lib/Sabre/TrashHome.php',
'OCA\\Files_Trashbin\\Sabre\\TrashRoot' => $baseDir . '/../lib/Sabre/TrashRoot.php',
'OCA\\Files_Trashbin\\Sabre\\TrashbinPlugin' => $baseDir . '/../lib/Sabre/TrashbinPlugin.php',
'OCA\\Files_Trashbin\\Service\\ConfigService' => $baseDir . '/../lib/Service/ConfigService.php',
'OCA\\Files_Trashbin\\Storage' => $baseDir . '/../lib/Storage.php',
'OCA\\Files_Trashbin\\Trash\\BackendNotFoundException' => $baseDir . '/../lib/Trash/BackendNotFoundException.php',
'OCA\\Files_Trashbin\\Trash\\ITrashBackend' => $baseDir . '/../lib/Trash/ITrashBackend.php',
Expand Down
2 changes: 2 additions & 0 deletions apps/files_trashbin/composer/composer/autoload_static.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ class ComposerStaticInitFiles_Trashbin
'OCA\\Files_Trashbin\\Expiration' => __DIR__ . '/..' . '/../lib/Expiration.php',
'OCA\\Files_Trashbin\\Helper' => __DIR__ . '/..' . '/../lib/Helper.php',
'OCA\\Files_Trashbin\\Listener\\EventListener' => __DIR__ . '/..' . '/../lib/Listener/EventListener.php',
'OCA\\Files_Trashbin\\Listeners\\BeforeTemplateRendered' => __DIR__ . '/..' . '/../lib/Listeners/BeforeTemplateRendered.php',
'OCA\\Files_Trashbin\\Listeners\\LoadAdditionalScripts' => __DIR__ . '/..' . '/../lib/Listeners/LoadAdditionalScripts.php',
'OCA\\Files_Trashbin\\Listeners\\SyncLivePhotosListener' => __DIR__ . '/..' . '/../lib/Listeners/SyncLivePhotosListener.php',
'OCA\\Files_Trashbin\\Migration\\Version1010Date20200630192639' => __DIR__ . '/..' . '/../lib/Migration/Version1010Date20200630192639.php',
Expand All @@ -55,6 +56,7 @@ class ComposerStaticInitFiles_Trashbin
'OCA\\Files_Trashbin\\Sabre\\TrashHome' => __DIR__ . '/..' . '/../lib/Sabre/TrashHome.php',
'OCA\\Files_Trashbin\\Sabre\\TrashRoot' => __DIR__ . '/..' . '/../lib/Sabre/TrashRoot.php',
'OCA\\Files_Trashbin\\Sabre\\TrashbinPlugin' => __DIR__ . '/..' . '/../lib/Sabre/TrashbinPlugin.php',
'OCA\\Files_Trashbin\\Service\\ConfigService' => __DIR__ . '/..' . '/../lib/Service/ConfigService.php',
'OCA\\Files_Trashbin\\Storage' => __DIR__ . '/..' . '/../lib/Storage.php',
'OCA\\Files_Trashbin\\Trash\\BackendNotFoundException' => __DIR__ . '/..' . '/../lib/Trash/BackendNotFoundException.php',
'OCA\\Files_Trashbin\\Trash\\ITrashBackend' => __DIR__ . '/..' . '/../lib/Trash/ITrashBackend.php',
Expand Down
7 changes: 7 additions & 0 deletions apps/files_trashbin/lib/AppInfo/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@

use OCA\DAV\Connector\Sabre\Principal;
use OCA\Files\Event\LoadAdditionalScriptsEvent;
use OCA\Files_Sharing\Event\BeforeTemplateRenderedEvent;
use OCA\Files_Trashbin\Capabilities;
use OCA\Files_Trashbin\Events\BeforeNodeRestoredEvent;
use OCA\Files_Trashbin\Expiration;
use OCA\Files_Trashbin\Listener\EventListener;
use OCA\Files_Trashbin\Listeners\BeforeTemplateRendered;
use OCA\Files_Trashbin\Listeners\LoadAdditionalScripts;
use OCA\Files_Trashbin\Listeners\SyncLivePhotosListener;
use OCA\Files_Trashbin\Trash\ITrashManager;
Expand Down Expand Up @@ -52,6 +54,11 @@ public function register(IRegistrationContext $context): void {
LoadAdditionalScripts::class
);

$context->registerEventListener(
BeforeTemplateRenderedEvent::class,
BeforeTemplateRendered::class
);

$context->registerEventListener(BeforeNodeRestoredEvent::class, SyncLivePhotosListener::class);

$context->registerEventListener(NodeWrittenEvent::class, EventListener::class);
Expand Down
11 changes: 9 additions & 2 deletions apps/files_trashbin/lib/Capabilities.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
*/
namespace OCA\Files_Trashbin;

use OCA\Files_Trashbin\Service\ConfigService;
use OCP\Capabilities\ICapability;

/**
Expand All @@ -18,12 +19,18 @@ class Capabilities implements ICapability {
/**
* Return this classes capabilities
*
* @return array{files: array{undelete: bool}}
* @return array{
* files: array{
* undelete: bool,
* delete_from_trash: bool
* }
* }
*/
public function getCapabilities() {
return [
'files' => [
'undelete' => true
'undelete' => true,
'delete_from_trash' => ConfigService::getDeleteFromTrashEnabled(),
]
];
}
Expand Down
32 changes: 32 additions & 0 deletions apps/files_trashbin/lib/Listeners/BeforeTemplateRendered.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

declare(strict_types=1);

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

namespace OCA\Files_Trashbin\Listeners;

use OCA\Files_Sharing\Event\BeforeTemplateRenderedEvent;
use OCA\Files_Trashbin\Service\ConfigService;
use OCP\AppFramework\Services\IInitialState;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventListener;

/** @template-implements IEventListener<BeforeTemplateRenderedEvent> */
class BeforeTemplateRendered implements IEventListener {
public function __construct(
private IInitialState $initialState,
) {
}

public function handle(Event $event): void {
if (!($event instanceof BeforeTemplateRenderedEvent)) {
return;
}

ConfigService::injectInitialState($this->initialState);
}
}
9 changes: 9 additions & 0 deletions apps/files_trashbin/lib/Listeners/LoadAdditionalScripts.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,26 @@

use OCA\Files\Event\LoadAdditionalScriptsEvent;
use OCA\Files_Trashbin\AppInfo\Application;
use OCA\Files_Trashbin\Service\ConfigService;
use OCP\AppFramework\Services\IInitialState;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventListener;
use OCP\Util;

/** @template-implements IEventListener<LoadAdditionalScriptsEvent> */
class LoadAdditionalScripts implements IEventListener {
public function __construct(
private IInitialState $initialState,
) {
}

public function handle(Event $event): void {
if (!($event instanceof LoadAdditionalScriptsEvent)) {
return;
}

Util::addInitScript(Application::APP_ID, 'init');

ConfigService::injectInitialState($this->initialState);
}
}
6 changes: 6 additions & 0 deletions apps/files_trashbin/lib/Sabre/AbstractTrash.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@
*/
namespace OCA\Files_Trashbin\Sabre;

use OCA\Files_Trashbin\Service\ConfigService;
use OCA\Files_Trashbin\Trash\ITrashItem;
use OCA\Files_Trashbin\Trash\ITrashManager;
use OCP\Files\FileInfo;
use OCP\IUser;
use Sabre\DAV\Exception\Forbidden;

abstract class AbstractTrash implements ITrash {
public function __construct(
Expand Down Expand Up @@ -73,6 +75,10 @@ public function getDeletedBy(): ?IUser {
}

public function delete() {
if (!ConfigService::getDeleteFromTrashEnabled()) {
throw new Forbidden('Not allowed to delete items from the trash bin');
}

$this->trashManager->removeItem($this->data);
}

Expand Down
5 changes: 5 additions & 0 deletions apps/files_trashbin/lib/Sabre/TrashRoot.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
*/
namespace OCA\Files_Trashbin\Sabre;

use OCA\Files_Trashbin\Service\ConfigService;
use OCA\Files_Trashbin\Trash\ITrashItem;
use OCA\Files_Trashbin\Trash\ITrashManager;
use OCA\Files_Trashbin\Trashbin;
Expand All @@ -26,6 +27,10 @@ public function __construct(
}

public function delete() {
if (!ConfigService::getDeleteFromTrashEnabled()) {
throw new Forbidden('Not allowed to delete items from the trash bin');
}

Trashbin::deleteAll();
foreach ($this->trashManager->listTrashRoot($this->user) as $trashItem) {
$this->trashManager->removeItem($trashItem);
Expand Down
27 changes: 27 additions & 0 deletions apps/files_trashbin/lib/Service/ConfigService.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

declare(strict_types=1);

namespace OCA\Files_Trashbin\Service;

use OCP\AppFramework\Services\IInitialState;
use OCP\IConfig;
use OCP\Server;

/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
class ConfigService {
public static function getDeleteFromTrashEnabled(): bool {
return Server::get(IConfig::class)->getSystemValueBool('files.trash.delete', true);
}

public static function injectInitialState(IInitialState $initialState): void {
$initialState->provideLazyInitialState('config', function () {
return [
'allow_delete' => ConfigService::getDeleteFromTrashEnabled(),
];
});
}
}
6 changes: 5 additions & 1 deletion apps/files_trashbin/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,15 @@
"files": {
"type": "object",
"required": [
"undelete"
"undelete",
"delete_from_trash"
],
"properties": {
"undelete": {
"type": "boolean"
},
"delete_from_trash": {
"type": "boolean"
}
}
}
Expand Down
16 changes: 14 additions & 2 deletions apps/files_trashbin/src/fileListActions/emptyTrashAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ import { logger } from '../logger.ts'
import { generateRemoteUrl } from '@nextcloud/router'
import { getCurrentUser } from '@nextcloud/auth'
import { emit } from '@nextcloud/event-bus'
import { loadState } from '@nextcloud/initial-state'

export type FilesTrashbinConfigState = {
allow_delete: boolean;
}

const emptyTrash = async (): Promise<boolean> => {
try {
Expand All @@ -42,6 +47,12 @@ export const emptyTrashAction = new FileListAction({
if (view.id !== 'trashbin') {
return false
}

const config = loadState<FilesTrashbinConfigState>('files_trashbin', 'config')
if (!config.allow_delete) {
return false
}

return nodes.length > 0 && folder.path === '/'
},

Expand Down Expand Up @@ -71,8 +82,9 @@ export const emptyTrashAction = new FileListAction({

const result = await askConfirmation
if (result === true) {
await emptyTrash()
nodes.forEach((node) => emit('files:node:deleted', node))
if (await emptyTrash()) {
nodes.forEach((node) => emit('files:node:deleted', node))
}
return
}

Expand Down
5 changes: 3 additions & 2 deletions apps/files_trashbin/tests/CapabilitiesTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,12 @@ protected function setUp(): void {
parent::setUp();
$this->capabilities = new Capabilities();
}

public function testGetCapabilities(): void {
$capabilities = [
'files' => [
'undelete' => true
'undelete' => true,
'delete_from_trash' => true,
]
];

Expand Down
Loading

0 comments on commit 87d7bbf

Please sign in to comment.