From 83a5e8486bfc7cb965e1a120a5f245af2603d379 Mon Sep 17 00:00:00 2001 From: Alexander Ebert Date: Sun, 24 Mar 2024 16:47:45 +0100 Subject: [PATCH] Add an API endpoint to delete files --- ts/WoltLabSuite/Core/Ajax/Backend.ts | 2 + ts/WoltLabSuite/Core/Api/Files/DeleteFile.ts | 12 ++++++ .../Core/Component/Attachment/List.ts | 40 +++++++++++++++++++ ts/WoltLabSuite/Core/Component/File/Upload.ts | 1 - .../js/WoltLabSuite/Core/Ajax/Backend.js | 3 ++ .../WoltLabSuite/Core/Api/Files/DeleteFile.js | 15 +++++++ .../Core/Component/Attachment/List.js | 30 +++++++++++++- .../files/lib/bootstrap/com.woltlab.wcf.php | 1 + .../files/lib/data/file/File.class.php | 10 +++++ .../core/files/DeleteFile.class.php | 36 +++++++++++++++++ .../AttachmentFileProcessor.class.php | 7 ++++ .../file/processor/IFileProcessor.class.php | 2 + 12 files changed, 156 insertions(+), 3 deletions(-) create mode 100644 ts/WoltLabSuite/Core/Api/Files/DeleteFile.ts create mode 100644 wcfsetup/install/files/js/WoltLabSuite/Core/Api/Files/DeleteFile.js create mode 100644 wcfsetup/install/files/lib/system/endpoint/controller/core/files/DeleteFile.class.php diff --git a/ts/WoltLabSuite/Core/Ajax/Backend.ts b/ts/WoltLabSuite/Core/Ajax/Backend.ts index 46d9e8779b..40b6a9cb6f 100644 --- a/ts/WoltLabSuite/Core/Ajax/Backend.ts +++ b/ts/WoltLabSuite/Core/Ajax/Backend.ts @@ -154,6 +154,8 @@ class BackendRequest { init.body = JSON.stringify(this.#payload); } } + } else if (this.#type === RequestType.DELETE) { + init.method = "DELETE"; } else { init.method = "GET"; } diff --git a/ts/WoltLabSuite/Core/Api/Files/DeleteFile.ts b/ts/WoltLabSuite/Core/Api/Files/DeleteFile.ts new file mode 100644 index 0000000000..b6a1b7f603 --- /dev/null +++ b/ts/WoltLabSuite/Core/Api/Files/DeleteFile.ts @@ -0,0 +1,12 @@ +import { prepareRequest } from "WoltLabSuite/Core/Ajax/Backend"; +import { ApiResult, apiResultFromError, apiResultFromValue } from "../Result"; + +export async function deleteFile(fileId: number): Promise> { + try { + await prepareRequest(`${window.WSC_API_URL}index.php?api/rpc/core/files/${fileId}`).delete().fetchAsJson(); + } catch (e) { + return apiResultFromError(e); + } + + return apiResultFromValue([]); +} diff --git a/ts/WoltLabSuite/Core/Component/Attachment/List.ts b/ts/WoltLabSuite/Core/Component/Attachment/List.ts index 8b2b47b520..bedf2d13a3 100644 --- a/ts/WoltLabSuite/Core/Component/Attachment/List.ts +++ b/ts/WoltLabSuite/Core/Component/Attachment/List.ts @@ -1,3 +1,4 @@ +import { deleteFile } from "WoltLabSuite/Core/Api/Files/DeleteFile"; import { dispatchToCkeditor } from "../Ckeditor/Event"; import WoltlabCoreFileElement from "../File/woltlab-core-file"; @@ -18,7 +19,14 @@ function upload(fileList: HTMLElement, file: WoltlabCoreFileElement, editorId: s return; } + const fileId = file.fileId; + if (fileId === undefined) { + // TODO: error handling + return; + } + element.append( + getDeleteAttachButton(fileId, (data as FileProcessorData).attachmentID, editorId, element), getInsertAttachBbcodeButton( (data as FileProcessorData).attachmentID, file.isImage() && file.link ? file.link : "", @@ -40,6 +48,38 @@ function upload(fileList: HTMLElement, file: WoltlabCoreFileElement, editorId: s }); } +function getDeleteAttachButton( + fileId: number, + attachmentId: number, + editorId: string, + element: HTMLElement, +): HTMLButtonElement { + const button = document.createElement("button"); + button.type = "button"; + button.classList.add("button", "small"); + button.textContent = "TODO: delete"; + + button.addEventListener("click", () => { + const editor = document.getElementById(editorId); + if (editor === null) { + // TODO: error handling + return; + } + + void deleteFile(fileId).then((result) => { + result.unwrap(); + + dispatchToCkeditor(editor).removeAttachment({ + attachmentId, + }); + + element.remove(); + }); + }); + + return button; +} + function getInsertAttachBbcodeButton(attachmentId: number, url: string, editorId: string): HTMLButtonElement { const button = document.createElement("button"); button.type = "button"; diff --git a/ts/WoltLabSuite/Core/Component/File/Upload.ts b/ts/WoltLabSuite/Core/Component/File/Upload.ts index 0916346a1b..6369decf74 100644 --- a/ts/WoltLabSuite/Core/Component/File/Upload.ts +++ b/ts/WoltLabSuite/Core/Component/File/Upload.ts @@ -1,4 +1,3 @@ -import { prepareRequest } from "WoltLabSuite/Core/Ajax/Backend"; import { wheneverFirstSeen } from "WoltLabSuite/Core/Helper/Selector"; import { upload as filesUpload } from "WoltLabSuite/Core/Api/Files/Upload"; import WoltlabCoreFileElement from "./woltlab-core-file"; diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Ajax/Backend.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Ajax/Backend.js index e423cf4fa2..5f4e4a47c3 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Ajax/Backend.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Ajax/Backend.js @@ -114,6 +114,9 @@ define(["require", "exports", "tslib", "./Status", "./Error", "../Core"], functi } } } + else if (this.#type === 0 /* RequestType.DELETE */) { + init.method = "DELETE"; + } else { init.method = "GET"; } diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Api/Files/DeleteFile.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Api/Files/DeleteFile.js new file mode 100644 index 0000000000..6d6c419df7 --- /dev/null +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Api/Files/DeleteFile.js @@ -0,0 +1,15 @@ +define(["require", "exports", "WoltLabSuite/Core/Ajax/Backend", "../Result"], function (require, exports, Backend_1, Result_1) { + "use strict"; + Object.defineProperty(exports, "__esModule", { value: true }); + exports.deleteFile = void 0; + async function deleteFile(fileId) { + try { + await (0, Backend_1.prepareRequest)(`${window.WSC_API_URL}index.php?api/rpc/core/files/${fileId}`).delete().fetchAsJson(); + } + catch (e) { + return (0, Result_1.apiResultFromError)(e); + } + return (0, Result_1.apiResultFromValue)([]); + } + exports.deleteFile = deleteFile; +}); diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Attachment/List.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Attachment/List.js index 241a0a82fd..d84d83ad82 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Attachment/List.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Attachment/List.js @@ -1,4 +1,4 @@ -define(["require", "exports", "../Ckeditor/Event"], function (require, exports, Event_1) { +define(["require", "exports", "WoltLabSuite/Core/Api/Files/DeleteFile", "../Ckeditor/Event"], function (require, exports, DeleteFile_1, Event_1) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.setup = void 0; @@ -13,7 +13,12 @@ define(["require", "exports", "../Ckeditor/Event"], function (require, exports, // TODO: error handling return; } - element.append(getInsertAttachBbcodeButton(data.attachmentID, file.isImage() && file.link ? file.link : "", editorId)); + const fileId = file.fileId; + if (fileId === undefined) { + // TODO: error handling + return; + } + element.append(getDeleteAttachButton(fileId, data.attachmentID, editorId, element), getInsertAttachBbcodeButton(data.attachmentID, file.isImage() && file.link ? file.link : "", editorId)); if (file.isImage()) { const thumbnail = file.thumbnails.find((thumbnail) => thumbnail.identifier === "tiny"); if (thumbnail !== undefined) { @@ -26,6 +31,27 @@ define(["require", "exports", "../Ckeditor/Event"], function (require, exports, } }); } + function getDeleteAttachButton(fileId, attachmentId, editorId, element) { + const button = document.createElement("button"); + button.type = "button"; + button.classList.add("button", "small"); + button.textContent = "TODO: delete"; + button.addEventListener("click", () => { + const editor = document.getElementById(editorId); + if (editor === null) { + // TODO: error handling + return; + } + void (0, DeleteFile_1.deleteFile)(fileId).then((result) => { + result.unwrap(); + (0, Event_1.dispatchToCkeditor)(editor).removeAttachment({ + attachmentId, + }); + element.remove(); + }); + }); + return button; + } function getInsertAttachBbcodeButton(attachmentId, url, editorId) { const button = document.createElement("button"); button.type = "button"; diff --git a/wcfsetup/install/files/lib/bootstrap/com.woltlab.wcf.php b/wcfsetup/install/files/lib/bootstrap/com.woltlab.wcf.php index dda6c7d7ed..c4a44d9d1c 100644 --- a/wcfsetup/install/files/lib/bootstrap/com.woltlab.wcf.php +++ b/wcfsetup/install/files/lib/bootstrap/com.woltlab.wcf.php @@ -116,6 +116,7 @@ return static function (): void { $eventHandler->register( \wcf\event\endpoint\ControllerCollecting::class, static function (\wcf\event\endpoint\ControllerCollecting $event) { + $event->register(new \wcf\system\endpoint\controller\core\files\DeleteFile); $event->register(new \wcf\system\endpoint\controller\core\files\PostGenerateThumbnails); $event->register(new \wcf\system\endpoint\controller\core\files\PostUpload); $event->register(new \wcf\system\endpoint\controller\core\files\upload\PostChunk); diff --git a/wcfsetup/install/files/lib/data/file/File.class.php b/wcfsetup/install/files/lib/data/file/File.class.php index 1c7aa7bbff..fcba2c03a0 100644 --- a/wcfsetup/install/files/lib/data/file/File.class.php +++ b/wcfsetup/install/files/lib/data/file/File.class.php @@ -67,4 +67,14 @@ class File extends DatabaseObject default => false, }; } + + public function canDelete(): bool + { + $processor = $this->getProcessor(); + if ($processor === null) { + return true; + } + + return $processor->canDelete($this); + } } diff --git a/wcfsetup/install/files/lib/system/endpoint/controller/core/files/DeleteFile.class.php b/wcfsetup/install/files/lib/system/endpoint/controller/core/files/DeleteFile.class.php new file mode 100644 index 0000000000..9c686ec1d3 --- /dev/null +++ b/wcfsetup/install/files/lib/system/endpoint/controller/core/files/DeleteFile.class.php @@ -0,0 +1,36 @@ +fileID) { + throw new UserInputException('id'); + } + + if (!$file->canDelete()) { + throw new PermissionDeniedException(); + } + + // TODO: How do we handle the cleanup of files? + $fileAction = new FileAction([$file], 'delete'); + $fileAction->executeAction(); + + return new JsonResponse([]); + } +} diff --git a/wcfsetup/install/files/lib/system/file/processor/AttachmentFileProcessor.class.php b/wcfsetup/install/files/lib/system/file/processor/AttachmentFileProcessor.class.php index 3c24055005..5e954d9fd6 100644 --- a/wcfsetup/install/files/lib/system/file/processor/AttachmentFileProcessor.class.php +++ b/wcfsetup/install/files/lib/system/file/processor/AttachmentFileProcessor.class.php @@ -97,6 +97,13 @@ final class AttachmentFileProcessor implements IFileProcessor return FileProcessorPreflightResult::Passed; } + #[\Override] + public function canDelete(File $file): bool + { + // TODO + return true; + } + #[\Override] public function canDownload(File $file): bool { diff --git a/wcfsetup/install/files/lib/system/file/processor/IFileProcessor.class.php b/wcfsetup/install/files/lib/system/file/processor/IFileProcessor.class.php index 3f624c1ad3..d18a484598 100644 --- a/wcfsetup/install/files/lib/system/file/processor/IFileProcessor.class.php +++ b/wcfsetup/install/files/lib/system/file/processor/IFileProcessor.class.php @@ -19,6 +19,8 @@ interface IFileProcessor public function adoptThumbnail(FileThumbnail $thumbnail): void; + public function canDelete(File $file): bool; + public function canDownload(File $file): bool; public function getAllowedFileExtensions(array $context): array; -- 2.20.1