Persistently track the mime type of uploaded files
authorAlexander Ebert <ebert@woltlab.com>
Sat, 2 Mar 2024 16:54:31 +0000 (17:54 +0100)
committerAlexander Ebert <ebert@woltlab.com>
Sat, 20 Apr 2024 14:30:14 +0000 (16:30 +0200)
ts/WoltLabSuite/Core/Component/Attachment/List.ts
ts/WoltLabSuite/Core/Component/File/Upload.ts
ts/WoltLabSuite/Core/Component/File/woltlab-core-file.ts
wcfsetup/install/files/js/WoltLabSuite/Core/Component/Attachment/List.js
wcfsetup/install/files/js/WoltLabSuite/Core/Component/File/Upload.js
wcfsetup/install/files/js/WoltLabSuite/Core/Component/File/woltlab-core-file.js
wcfsetup/install/files/lib/action/FileUploadAction.class.php
wcfsetup/install/files/lib/data/file/File.class.php
wcfsetup/install/files/lib/data/file/FileEditor.class.php
wcfsetup/setup/db/install.sql

index 36f31a69935ef42d7b12fe8d9da0a13197d935b5..4d8bac24d623393a585a8c8073f06ccc1fdf843b 100644 (file)
@@ -7,12 +7,14 @@ function upload(fileList: HTMLElement, file: WoltlabCoreFileElement): void {
   fileList.append(element);
 
   void file.ready.then(() => {
-    const thumbnail = file.thumbnails.find((thumbnail) => {
-      return thumbnail.identifier === "tiny";
-    });
+    if (file.isImage()) {
+      const thumbnail = file.thumbnails.find((thumbnail) => {
+        return thumbnail.identifier === "tiny";
+      });
 
-    if (thumbnail !== undefined) {
-      file.thumbnail = thumbnail;
+      if (thumbnail !== undefined) {
+        file.thumbnail = thumbnail;
+      }
     }
   });
 }
index 0bd958598eee758a7a329d1e7ccac4f2b2932c5d..54f5879625e0ec73ecc6107586de7efecdabafde 100644 (file)
@@ -18,6 +18,7 @@ export type UploadCompleted = {
   endpointThumbnails: string;
   fileID: number;
   typeName: string;
+  mimeType: string;
   data: Record<string, unknown>;
 };
 
@@ -110,17 +111,14 @@ async function chunkUploadCompleted(fileElement: WoltlabCoreFileElement, respons
   }
 
   const hasThumbnails = response.endpointThumbnails !== "";
-  fileElement.uploadCompleted(response.fileID, hasThumbnails);
+  fileElement.uploadCompleted(response.fileID, response.mimeType, hasThumbnails);
 
   if (hasThumbnails) {
     await generateThumbnails(fileElement, response.endpointThumbnails);
   }
 }
 
-async function generateThumbnails(
-  fileElement: WoltlabCoreFileElement,
-  endpoint: string,
-): Promise<void> {
+async function generateThumbnails(fileElement: WoltlabCoreFileElement, endpoint: string): Promise<void> {
   let response: GenerateThumbnailsResponse;
 
   try {
index 3f66d707a1d0f99c0dc9faecf5af59dc198cfb16..bcb183a890906bdb091dd232ddfe7a2efeb071bc 100644 (file)
@@ -32,6 +32,7 @@ export class Thumbnail {
 export class WoltlabCoreFileElement extends HTMLElement {
   #filename: string = "";
   #fileId: number | undefined = undefined;
+  #mimeType: string | undefined = undefined;
   #state: State = State.Initial;
   readonly #thumbnails: Thumbnail[] = [];
 
@@ -60,9 +61,12 @@ export class WoltlabCoreFileElement extends HTMLElement {
     // Files that exist at page load have a valid file id, otherwise a new
     // file element can only be the result of an upload attempt.
     if (this.#fileId === undefined) {
-      this.#filename = this.dataset.filename || "";
+      this.#filename = this.dataset.filename || "bogus.bin";
       delete this.dataset.filename;
 
+      this.#mimeType = this.dataset.mimeType || "application/octet-stream";
+      delete this.dataset.mimeType;
+
       const fileId = parseInt(this.getAttribute("file-id") || "0");
       if (fileId) {
         this.#fileId = fileId;
@@ -184,6 +188,27 @@ export class WoltlabCoreFileElement extends HTMLElement {
     return this.#filename;
   }
 
+  get mimeType(): string | undefined {
+    return this.#mimeType;
+  }
+
+  isImage(): boolean {
+    if (this.mimeType === undefined) {
+      return false;
+    }
+
+    switch (this.mimeType) {
+      case "image/gif":
+      case "image/jpeg":
+      case "image/png":
+      case "image/webp":
+        return true;
+
+      default:
+        return false;
+    }
+  }
+
   uploadFailed(): void {
     if (this.#state !== State.Uploading) {
       return;
@@ -195,9 +220,10 @@ export class WoltlabCoreFileElement extends HTMLElement {
     this.#readyReject();
   }
 
-  uploadCompleted(fileId: number, hasThumbnails: boolean): void {
+  uploadCompleted(fileId: number, mimeType: string, hasThumbnails: boolean): void {
     if (this.#state === State.Uploading) {
       this.#fileId = fileId;
+      this.#mimeType = mimeType;
       this.setAttribute("file-id", fileId.toString());
 
       if (hasThumbnails) {
index af4e0241f576f3e80c51f90f93613d7dd1005d4d..6a5a0aa3eb237462af0cfc29546a02d43b9f03c3 100644 (file)
@@ -8,11 +8,13 @@ define(["require", "exports"], function (require, exports) {
         element.append(file);
         fileList.append(element);
         void file.ready.then(() => {
-            const thumbnail = file.thumbnails.find((thumbnail) => {
-                return thumbnail.identifier === "tiny";
-            });
-            if (thumbnail !== undefined) {
-                file.thumbnail = thumbnail;
+            if (file.isImage()) {
+                const thumbnail = file.thumbnails.find((thumbnail) => {
+                    return thumbnail.identifier === "tiny";
+                });
+                if (thumbnail !== undefined) {
+                    file.thumbnail = thumbnail;
+                }
             }
         });
     }
index 9be236d22ae4f06b6b0868f3b36dcf902e466f25..a454d57ac34aff8dbba33d809b0661184bee14b8 100644 (file)
@@ -69,7 +69,7 @@ define(["require", "exports", "WoltLabSuite/Core/Ajax/Backend", "WoltLabSuite/Co
             return;
         }
         const hasThumbnails = response.endpointThumbnails !== "";
-        fileElement.uploadCompleted(response.fileID, hasThumbnails);
+        fileElement.uploadCompleted(response.fileID, response.mimeType, hasThumbnails);
         if (hasThumbnails) {
             await generateThumbnails(fileElement, response.endpointThumbnails);
         }
index 5339e47a56eb1a81a699e5a4de905bf06389565f..4786b6a7faba21475fa961ede6b589d997cfa445 100644 (file)
@@ -20,6 +20,7 @@ define(["require", "exports"], function (require, exports) {
     class WoltlabCoreFileElement extends HTMLElement {
         #filename = "";
         #fileId = undefined;
+        #mimeType = undefined;
         #state = 0 /* State.Initial */;
         #thumbnails = [];
         #readyReject;
@@ -42,8 +43,10 @@ define(["require", "exports"], function (require, exports) {
             // Files that exist at page load have a valid file id, otherwise a new
             // file element can only be the result of an upload attempt.
             if (this.#fileId === undefined) {
-                this.#filename = this.dataset.filename || "";
+                this.#filename = this.dataset.filename || "bogus.bin";
                 delete this.dataset.filename;
+                this.#mimeType = this.dataset.mimeType || "application/octet-stream";
+                delete this.dataset.mimeType;
                 const fileId = parseInt(this.getAttribute("file-id") || "0");
                 if (fileId) {
                     this.#fileId = fileId;
@@ -146,6 +149,23 @@ define(["require", "exports"], function (require, exports) {
         get filename() {
             return this.#filename;
         }
+        get mimeType() {
+            return this.#mimeType;
+        }
+        isImage() {
+            if (this.mimeType === undefined) {
+                return false;
+            }
+            switch (this.mimeType) {
+                case "image/gif":
+                case "image/jpeg":
+                case "image/png":
+                case "image/webp":
+                    return true;
+                default:
+                    return false;
+            }
+        }
         uploadFailed() {
             if (this.#state !== 1 /* State.Uploading */) {
                 return;
@@ -154,9 +174,10 @@ define(["require", "exports"], function (require, exports) {
             this.#rebuildElement();
             this.#readyReject();
         }
-        uploadCompleted(fileId, hasThumbnails) {
+        uploadCompleted(fileId, mimeType, hasThumbnails) {
             if (this.#state === 1 /* State.Uploading */) {
                 this.#fileId = fileId;
+                this.#mimeType = mimeType;
                 this.setAttribute("file-id", fileId.toString());
                 if (hasThumbnails) {
                     this.#state = 2 /* State.GeneratingThumbnails */;
index d08bbec8f99aa49902d92ecdee7b59c2efc693f2..9e0595ceafebb39ca190c546576afbbfa72ba2ef 100644 (file)
@@ -148,6 +148,7 @@ final class FileUploadAction implements RequestHandlerInterface
                 'endpointThumbnails' => $endpointThumbnails,
                 'fileID' => $file->fileID,
                 'typeName' => $file->typeName,
+                'mimeType' => $file->mimeType,
                 'data' => $processor->getUploadResponse($file),
             ]);
         }
index 6650af413138e847518faa5f4ecb3957c0d4c246..1c7aa7bbffb78bcbc56e76224b9bb9d1c5b09cd8 100644 (file)
@@ -7,7 +7,6 @@ use wcf\data\DatabaseObject;
 use wcf\system\file\processor\FileProcessor;
 use wcf\system\file\processor\IFileProcessor;
 use wcf\system\request\LinkHandler;
-use wcf\util\FileUtil;
 
 /**
  * @author Alexander Ebert
@@ -20,6 +19,7 @@ use wcf\util\FileUtil;
  * @property-read int $fileSize
  * @property-read string $fileHash
  * @property-read string $typeName
+ * @property-read string $mimeType
  */
 class File extends DatabaseObject
 {
@@ -59,11 +59,9 @@ class File extends DatabaseObject
 
     public function isImage(): bool
     {
-        $mimeType = FileUtil::getMimeType($this->getPath() . $this->getSourceFilename());
-
-        return match ($mimeType) {
+        return match ($this->mimeType) {
             'image/gif' => true,
-            'image/jpg', 'image/jpeg' => true,
+            'image/jpeg' => true,
             'image/png' => true,
             'image/webp' => true,
             default => false,
index a70d78b8c6f26cecb075ebb9e19b2808939c69a5..88af30cd7b1c6975fc215d27cad6a26edc8695a4 100644 (file)
@@ -4,6 +4,7 @@ namespace wcf\data\file;
 
 use wcf\data\DatabaseObjectEditor;
 use wcf\data\file\temporary\FileTemporary;
+use wcf\util\FileUtil;
 
 /**
  * @author Alexander Ebert
@@ -24,11 +25,14 @@ class FileEditor extends DatabaseObjectEditor
 
     public static function createFromTemporary(FileTemporary $fileTemporary): File
     {
+        $mimeType = FileUtil::getMimeType($fileTemporary->getPath() . $fileTemporary->getFilename());
+
         $fileAction = new FileAction([], 'create', ['data' => [
             'filename' => $fileTemporary->filename,
             'fileSize' => $fileTemporary->fileSize,
             'fileHash' => $fileTemporary->fileHash,
             'typeName' => $fileTemporary->typeName,
+            'mimeType' => $mimeType,
         ]]);
         $file = $fileAction->executeAction()['returnValues'];
         \assert($file instanceof File);
index 5fe2e8fbac4b1bd4f0bd3cc164dca05809b6113d..95a179cc5e35346f95579cee4841aa433dbce152 100644 (file)
@@ -604,7 +604,8 @@ CREATE TABLE wcf1_file (
        filename VARCHAR(255) NOT NULL,
        fileSize BIGINT NOT NULL,
        fileHash CHAR(64) NOT NULL,
-       typeName VARCHAR(255) NOT NULL
+       typeName VARCHAR(255) NOT NULL,
+       mimeType VARCHAR(255) NOT NULL,
 );
 
 DROP TABLE IF EXISTS wcf1_file_temporary;