Add an API endpoint to delete files
authorAlexander Ebert <ebert@woltlab.com>
Sun, 24 Mar 2024 15:47:45 +0000 (16:47 +0100)
committerAlexander Ebert <ebert@woltlab.com>
Sat, 8 Jun 2024 10:19:38 +0000 (12:19 +0200)
12 files changed:
ts/WoltLabSuite/Core/Ajax/Backend.ts
ts/WoltLabSuite/Core/Api/Files/DeleteFile.ts [new file with mode: 0644]
ts/WoltLabSuite/Core/Component/Attachment/List.ts
ts/WoltLabSuite/Core/Component/File/Upload.ts
wcfsetup/install/files/js/WoltLabSuite/Core/Ajax/Backend.js
wcfsetup/install/files/js/WoltLabSuite/Core/Api/Files/DeleteFile.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/Component/Attachment/List.js
wcfsetup/install/files/lib/bootstrap/com.woltlab.wcf.php
wcfsetup/install/files/lib/data/file/File.class.php
wcfsetup/install/files/lib/system/endpoint/controller/core/files/DeleteFile.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/file/processor/AttachmentFileProcessor.class.php
wcfsetup/install/files/lib/system/file/processor/IFileProcessor.class.php

index 46d9e8779b30d3fb3f2f59764e0b27c4d64d1a83..40b6a9cb6f84b9e43d65e30945a0dffe4524d6e6 100644 (file)
@@ -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 (file)
index 0000000..b6a1b7f
--- /dev/null
@@ -0,0 +1,12 @@
+import { prepareRequest } from "WoltLabSuite/Core/Ajax/Backend";
+import { ApiResult, apiResultFromError, apiResultFromValue } from "../Result";
+
+export async function deleteFile(fileId: number): Promise<ApiResult<[]>> {
+  try {
+    await prepareRequest(`${window.WSC_API_URL}index.php?api/rpc/core/files/${fileId}`).delete().fetchAsJson();
+  } catch (e) {
+    return apiResultFromError(e);
+  }
+
+  return apiResultFromValue([]);
+}
index 8b2b47b52088c968bc901f69a0bc21882ccf84c5..bedf2d13a3d79696375c6f5753c17e523baf8d27 100644 (file)
@@ -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";
index 0916346a1b462d3b9d19f091641052d5a3066000..6369decf7472f6105806ff258771963c4f56a025 100644 (file)
@@ -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";
index e423cf4fa2331f201a4c5eba360d3491457ee9a7..5f4e4a47c32eaebbd95af6291b38fa351df1263b 100644 (file)
@@ -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 (file)
index 0000000..6d6c419
--- /dev/null
@@ -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;
+});
index 241a0a82fdba8b65e04349455288ca7e5d9c1b44..d84d83ad825a9631b38fdca71ac23e428e3d7531 100644 (file)
@@ -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";
index dda6c7d7edc2c6d6eeeaed95f3448c9e30e40be9..c4a44d9d1c5bf6738afcbe889852f14ad5e76d9e 100644 (file)
@@ -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);
index 1c7aa7bbffb78bcbc56e76224b9bb9d1c5b09cd8..fcba2c03a07ed903055e42318876f9946f2c736c 100644 (file)
@@ -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 (file)
index 0000000..9c686ec
--- /dev/null
@@ -0,0 +1,36 @@
+<?php
+
+namespace wcf\system\endpoint\controller\core\files;
+
+use Laminas\Diactoros\Response\JsonResponse;
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\ServerRequestInterface;
+use wcf\data\file\File;
+use wcf\data\file\FileAction;
+use wcf\system\endpoint\DeleteRequest;
+use wcf\system\endpoint\IController;
+use wcf\system\exception\PermissionDeniedException;
+use wcf\system\exception\UserInputException;
+
+#[DeleteRequest('/core/files/{id}')]
+final class DeleteFile implements IController
+{
+    #[\Override]
+    public function __invoke(ServerRequestInterface $request, array $variables): ResponseInterface
+    {
+        $file = new File($variables['id']);
+        if (!$file->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([]);
+    }
+}
index 3c24055005aa7dfe9b3a89413162a21c505ea272..5e954d9fd6555200971ca7f5e205296316e4f16c 100644 (file)
@@ -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
     {
index 3f624c1ad3f16744f58b9933b5b49d9fa1cf07ef..d18a484598614f3949ce46057b83b3aef73cf39d 100644 (file)
@@ -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;