Validate the file size before querying the server
authorAlexander Ebert <ebert@woltlab.com>
Sat, 18 May 2024 11:55:19 +0000 (13:55 +0200)
committerAlexander Ebert <ebert@woltlab.com>
Sat, 8 Jun 2024 10:19:39 +0000 (12:19 +0200)
ts/WoltLabSuite/Core/Component/File/Upload.ts
ts/WoltLabSuite/WebComponent/woltlab-core-file-upload.ts
ts/global.d.ts
wcfsetup/install/files/js/WoltLabSuite/Core/Component/File/Upload.js
wcfsetup/install/files/js/WoltLabSuite/WebComponent.min.js
wcfsetup/install/files/lib/system/file/processor/AbstractFileProcessor.class.php
wcfsetup/install/files/lib/system/file/processor/AttachmentFileProcessor.class.php
wcfsetup/install/files/lib/system/file/processor/FileProcessor.class.php
wcfsetup/install/files/lib/system/file/processor/IFileProcessor.class.php

index ab3c6a354420537f0e7c3ea5cb7529c39c0d8d1e..eb29f2ac226c9f4879682cc7333d39cc716ca464 100644 (file)
@@ -195,7 +195,37 @@ function validateFileLimit(element: WoltlabCoreFileUploadElement): boolean {
   return false;
 }
 
-function validateFile(element: WoltlabCoreFileUploadElement, file: File): boolean {
+function validateFileSize(element: WoltlabCoreFileUploadElement, file: File): boolean {
+  let isImage = false;
+  switch (file.type) {
+    case "image/gif":
+    case "image/jpeg":
+    case "image/png":
+    case "image/webp":
+      isImage = true;
+      break;
+  }
+
+  // Skip the file size validation for images, they can potentially be resized.
+  if (isImage) {
+    return true;
+  }
+
+  const maximumSize = element.maximumSize;
+  if (maximumSize === -1) {
+    return true;
+  }
+
+  if (file.size <= maximumSize) {
+    return true;
+  }
+
+  innerError(element, getPhrase("wcf.upload.error.fileSizeTooLarge", { filename: file.name }));
+
+  return false;
+}
+
+function validateFileExtension(element: WoltlabCoreFileUploadElement, file: File): boolean {
   const fileExtensions = (element.dataset.fileExtensions || "*").split(",");
   for (const fileExtension of fileExtensions) {
     if (fileExtension === "*") {
@@ -219,7 +249,9 @@ export function setup(): void {
 
       if (!validateFileLimit(element)) {
         return;
-      } else if (!validateFile(element, file)) {
+      } else if (!validateFileExtension(element, file)) {
+        return;
+      } else if (!validateFileSize(element, file)) {
         return;
       }
 
@@ -240,7 +272,7 @@ export function setup(): void {
 
       clearPreviousErrors(element);
 
-      if (!validateFile(element, file)) {
+      if (!validateFileExtension(element, file)) {
         promiseReject!();
 
         return;
index a9e3a3984ad280e3dc83d2ada5fede3b7d7ad9ac..7aadf87536a8ea71f56d429459324e065f982efa 100644 (file)
       return parseInt(this.dataset.maximumCount || "1");
     }
 
+    get maximumSize(): number {
+      return parseInt(this.dataset.maximumSize || "-1");
+    }
+
     get disabled(): boolean {
       return this.#element.disabled;
     }
index ab017351584f4c6a6bbda748c18bed7a43ece2aa..a4b7648c0b91d298e0b44419ac2f4ece9d539225 100644 (file)
@@ -96,6 +96,7 @@ declare global {
     get disabled(): boolean;
     set disabled(disabled: boolean);
     get maximumCount(): number;
+    get maximumSize(): number;
   }
 
   interface WoltlabCoreLoadingIndicatorElement extends HTMLElement {
index 2bee244df1120bb6cbf4c85bd6a3e36006ca0f48..4f3a5e1e3fd59249af719e77965b358cc0eecaac 100644 (file)
@@ -122,7 +122,31 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Helper/Selector", "Wol
         (0, Util_1.innerError)(element, (0, Language_1.getPhrase)("wcf.upload.error.maximumCountReached", { maximumCount }));
         return false;
     }
-    function validateFile(element, file) {
+    function validateFileSize(element, file) {
+        let isImage = false;
+        switch (file.type) {
+            case "image/gif":
+            case "image/jpeg":
+            case "image/png":
+            case "image/webp":
+                isImage = true;
+                break;
+        }
+        // Skip the file size validation for images, they can potentially be resized.
+        if (isImage) {
+            return true;
+        }
+        const maximumSize = element.maximumSize;
+        if (maximumSize === -1) {
+            return true;
+        }
+        if (file.size <= maximumSize) {
+            return true;
+        }
+        (0, Util_1.innerError)(element, (0, Language_1.getPhrase)("wcf.upload.error.fileSizeTooLarge", { filename: file.name }));
+        return false;
+    }
+    function validateFileExtension(element, file) {
         const fileExtensions = (element.dataset.fileExtensions || "*").split(",");
         for (const fileExtension of fileExtensions) {
             if (fileExtension === "*") {
@@ -143,7 +167,10 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Helper/Selector", "Wol
                 if (!validateFileLimit(element)) {
                     return;
                 }
-                else if (!validateFile(element, file)) {
+                else if (!validateFileExtension(element, file)) {
+                    return;
+                }
+                else if (!validateFileSize(element, file)) {
                     return;
                 }
                 void resizeImage(element, file).then((resizedFile) => {
@@ -159,7 +186,7 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Helper/Selector", "Wol
                     promiseReject = reject;
                 });
                 clearPreviousErrors(element);
-                if (!validateFile(element, file)) {
+                if (!validateFileExtension(element, file)) {
                     promiseReject();
                     return;
                 }
index 7666691b3c7cc4652fbbc7a5fa756cfed8246538..c8e441d2c2994680d254a0f89bbdc41adf5da01c 100644 (file)
@@ -70,7 +70,7 @@ Expecting `+Z.join(", ")+", got '"+(this.terminals_[T]||T)+"'":ae="Parse error o
             position: absolute;
             visibility: hidden;
         }
-      `}get maximumCount(){return parseInt(this.dataset.maximumCount||"1")}get disabled(){return this.#e.disabled}set disabled(i){this.#e.disabled=!!i}}window.customElements.define("woltlab-core-file-upload",e)}{let r=[24,48,96];class i extends HTMLElement{#e;#a;connectedCallback(){this.#e===void 0&&this.#t()}attributeChangedCallback(t,a,l){if(t==="size"){let d=parseInt(l||"");if(!r.includes(d)){let g=parseInt(a||"");r.includes(g)||(g=24),this.setAttribute(t,g.toString())}}}#t(){this.classList.add("loading-indicator"),this.hasAttribute("size")||this.setAttribute("size",24 .toString()),this.#e=document.createElement("fa-icon"),this.#e.size=this.size,this.#e.setIcon("spinner"),this.#a=document.createElement("span"),this.#a.classList.add("loading-indicator__text"),this.#a.textContent=window.WoltLabLanguage.getPhrase("wcf.global.loading"),this.#a.hidden=this.hideText;let t=document.createElement("div");t.classList.add("loading-indicator__wrapper"),t.append(this.#e,this.#a),this.append(t)}get size(){return parseInt(this.getAttribute("size"))}set size(t){if(!r.includes(t))throw new TypeError(`The size ${t} is unrecognized, permitted values are ${r.join(", ")}.`);this.setAttribute("size",t.toString()),this.#e&&(this.#e.size=t)}get hideText(){return this.hasAttribute("hide-text")}set hideText(t){t?this.setAttribute("hide-text",""):this.removeAttribute("hide-text"),this.#a&&(this.#a.hidden=t)}static get observedAttributes(){return["size"]}}window.customElements.define("woltlab-core-loading-indicator",i)}{let e;(l=>(l.Info="info",l.Success="success",l.Warning="warning",l.Error="error"))(e||={});class r extends HTMLElement{#e;#a;connectedCallback(){let c=this.attachShadow({mode:"open"});this.#e=document.createElement("fa-icon"),this.#e.size=24,this.#e.setIcon(this.icon,!0),this.#e.slot="icon",this.append(this.#e);let t=document.createElement("style");t.textContent=`
+      `}get maximumCount(){return parseInt(this.dataset.maximumCount||"1")}get maximumSize(){return parseInt(this.dataset.maximumSize||"-1")}get disabled(){return this.#e.disabled}set disabled(i){this.#e.disabled=!!i}}window.customElements.define("woltlab-core-file-upload",e)}{let r=[24,48,96];class i extends HTMLElement{#e;#a;connectedCallback(){this.#e===void 0&&this.#t()}attributeChangedCallback(t,a,l){if(t==="size"){let d=parseInt(l||"");if(!r.includes(d)){let g=parseInt(a||"");r.includes(g)||(g=24),this.setAttribute(t,g.toString())}}}#t(){this.classList.add("loading-indicator"),this.hasAttribute("size")||this.setAttribute("size",24 .toString()),this.#e=document.createElement("fa-icon"),this.#e.size=this.size,this.#e.setIcon("spinner"),this.#a=document.createElement("span"),this.#a.classList.add("loading-indicator__text"),this.#a.textContent=window.WoltLabLanguage.getPhrase("wcf.global.loading"),this.#a.hidden=this.hideText;let t=document.createElement("div");t.classList.add("loading-indicator__wrapper"),t.append(this.#e,this.#a),this.append(t)}get size(){return parseInt(this.getAttribute("size"))}set size(t){if(!r.includes(t))throw new TypeError(`The size ${t} is unrecognized, permitted values are ${r.join(", ")}.`);this.setAttribute("size",t.toString()),this.#e&&(this.#e.size=t)}get hideText(){return this.hasAttribute("hide-text")}set hideText(t){t?this.setAttribute("hide-text",""):this.removeAttribute("hide-text"),this.#a&&(this.#a.hidden=t)}static get observedAttributes(){return["size"]}}window.customElements.define("woltlab-core-loading-indicator",i)}{let e;(l=>(l.Info="info",l.Success="success",l.Warning="warning",l.Error="error"))(e||={});class r extends HTMLElement{#e;#a;connectedCallback(){let c=this.attachShadow({mode:"open"});this.#e=document.createElement("fa-icon"),this.#e.size=24,this.#e.setIcon(this.icon,!0),this.#e.slot="icon",this.append(this.#e);let t=document.createElement("style");t.textContent=`
         :host {
           align-items: center;
           display: grid;
index 418340b750ac23f0795a73eb2d65c3f5f8cae73c..8784a477bd3713c8e29799927e21f39236c040b2 100644 (file)
@@ -51,6 +51,13 @@ abstract class AbstractFileProcessor implements IFileProcessor
         return 1;
     }
 
+    #[\Override]
+    public function getMaximumSize(array $context): ?int
+    {
+        // Do not limit the maximum size of an uploaded file.
+        return null;
+    }
+
     #[\Override]
     public function getResizeConfiguration(): ResizeConfiguration
     {
index 42c56e39187876229d8b1fd61e8e37995317c6ef..cc6b7211315c905919a211134f4aec325c63535e 100644 (file)
@@ -248,6 +248,13 @@ final class AttachmentFileProcessor extends AbstractFileProcessor
         return $attachmentHandler?->count();
     }
 
+    #[\Override]
+    public function getMaximumSize(array $context): ?int
+    {
+        $attachmentHandler = $this->getAttachmentHandlerFromContext($context);
+        return $attachmentHandler?->getMaxSize();
+    }
+
     private function getAttachmentHandlerFromContext(array $context): ?AttachmentHandler
     {
         try {
index 17f3bbd0a8a99ad41f943a274b586b8dcd7660ba..32c430a546ab4f09c1e5c441630e169de58a74cd 100644 (file)
@@ -81,6 +81,11 @@ final class FileProcessor extends SingletonFactory
             $maximumCount = -1;
         }
 
+        $maximumSize = $fileProcessor->getMaximumSize($context);
+        if ($maximumSize === null) {
+            $maximumSize = -1;
+        }
+
         return \sprintf(
             <<<'HTML'
                 <woltlab-core-file-upload
@@ -89,6 +94,7 @@ final class FileProcessor extends SingletonFactory
                     data-file-extensions="%s"
                     data-resize-configuration="%s"
                     data-maximum-count="%d"
+                    data-maximum-size="%d"
                 ></woltlab-core-file-upload>
                 HTML,
             StringUtil::encodeHTML($fileProcessor->getObjectTypeName()),
@@ -96,6 +102,7 @@ final class FileProcessor extends SingletonFactory
             StringUtil::encodeHTML($allowedFileExtensions),
             StringUtil::encodeHTML(JSON::encode($fileProcessor->getResizeConfiguration())),
             $maximumCount,
+            $maximumSize,
         );
     }
 
index 6ddfcfd9f54fad8c60fcc0b4e6f3ee13e955053c..9b6d926b6a4ad0e13abefa43d09c75dc46654cf6 100644 (file)
@@ -65,7 +65,7 @@ interface IFileProcessor
      * it does not track this for whatever reason.
      *
      * @param array<string,string> $context
-     * @return null|int number of existing files or `null` if this should not be enforced
+     * @return null|int Number of existing files or `null` if this should not be enforced
      */
     public function countExistingFiles(array $context): ?int;
 
@@ -101,6 +101,14 @@ interface IFileProcessor
      */
     public function getMaximumCount(array $context): ?int;
 
+    /**
+     * Limits the maximum size of an uploade file.
+     *
+     * @param array<string,string> $context
+     * @return null|int Maximum size in bytes or null to disable the limit.
+     */
+    public function getMaximumSize(array $context): ?int;
+
     /**
      * Controls the client-side resizing of some types of images before they are
      * being uploaded to the server.