FIX: Fallback to file size to lookup file info

FIX: Fallback to file size to lookup file info

There is no direct way to share information between the the pre-upload and post-upload stages of the upload pipeline. To workaround this limitation, the plugin uses a global map indexed by filename, where it holds file information.

For certain browsers (Safari) and files (with non-ASCII characters in filename) this method does not work because the filename read by the browser and the one returned by the server use a different representation and keys do not match (eg. “fi%CC%88le” vs “f%C3%AFle”). For these files, the plugin will attempt to find the information it needs by looking up by file size.

diff --git a/assets/javascripts/discourse/initializers/hook-encrypt-upload.js b/assets/javascripts/discourse/initializers/hook-encrypt-upload.js
index f8e23de..223a808 100644
--- a/assets/javascripts/discourse/initializers/hook-encrypt-upload.js
+++ b/assets/javascripts/discourse/initializers/hook-encrypt-upload.js
@@ -2,15 +2,15 @@ import { withPluginApi } from "discourse/lib/plugin-api";
 import { getUploadMarkdown } from "discourse/lib/uploads";
 import { bufferToBase64 } from "discourse/plugins/discourse-encrypt/lib/base64";
 import {
-  generateUploadKey,
-  getMetadata,
-  readFile,
-} from "discourse/plugins/discourse-encrypt/lib/uploads";
-import {
   ENCRYPT_ACTIVE,
   getEncryptionStatus,
   hasTopicKey,
 } from "discourse/plugins/discourse-encrypt/lib/discourse";
+import {
+  generateUploadKey,
+  getMetadata,
+  readFile,
+} from "discourse/plugins/discourse-encrypt/lib/uploads";
 import { DEFAULT_LIST } from "pretty-text/white-lister";
 import { Promise } from "rsvp";
 
@@ -30,10 +30,7 @@ export default {
       DEFAULT_LIST.push("img[data-key]");
       DEFAULT_LIST.push("img[data-type]");
 
-      const uploadsKeys = {};
-      const uploadsType = {};
-      const uploadsData = {};
-      const uploadsUrl = {};
+      const uploads = {};
 
       api.addComposerUploadHandler([".*"], (file, editor) => {
         const controller = container.lookup("controller:composer");
@@ -42,7 +39,7 @@ export default {
           return true;
         }
 
-        const metadataPromise = getMetadata(file, uploadsUrl);
+        const metadataPromise = getMetadata(file, siteSettings);
         const plaintextPromise = readFile(file);
         const keyPromise = generateUploadKey();
         const exportedKeyPromise = keyPromise.then((key) => {
@@ -68,46 +65,43 @@ export default {
           ciphertextPromise,
           exportedKeyPromise,
           metadataPromise,
-        ]).then(([ciphertext, exportedKey, data]) => {
-          uploadsKeys[file.name] = exportedKey;
-          uploadsType[file.name] = file.type;
-          uploadsData[file.name] = data;
-
+        ]).then(([ciphertext, exportedKey, metadata]) => {
           const blob = new Blob([iv, ciphertext], {
             type: "application/x-binary",
           });
-          const f = new File([blob], `${file.name}.encrypted`);
+          const encryptedFile = new File([blob], `${file.name}.encrypted`);
           editor.$().fileupload("send", {
-            files: [f],
-            originalFiles: [f],
+            files: [encryptedFile],
+            originalFiles: [encryptedFile],
             formData: { type: "composer" },
           });
+
+          uploads[file.name] = {
+            key: exportedKey,
+            metadata,
+            type: file.type,
+            filesize: encryptedFile.size,
+          };
         });
+
         return false;
       });
 
       api.addComposerUploadMarkdownResolver((upload) => {
-        const filename = upload.original_filename.replace(/\.encrypted$/, "");
-        if (!uploadsKeys[filename]) {
+        const encryptedUpload =
+          uploads[upload.original_filename.replace(/\.encrypted$/, "")] ||
+          Object.values(uploads).find((u) => u.filesize === upload.filesize);
+        if (!encryptedUpload) {
           return;
         }
 
-        const realUpload = {};
-        Object.assign(realUpload, upload);
-        Object.assign(realUpload, uploadsData[filename]);
-        const key = uploadsKeys[filename];
-        const type = uploadsType[filename];
-        upload.url = uploadsUrl[filename];
-
-        delete uploadsData[filename];
-        delete uploadsKeys[filename];
-        delete uploadsType[filename];
-        delete uploadsUrl[filename];
-
-        return getUploadMarkdown(realUpload).replace(
+        const uploadData = Object.assign({}, upload, encryptedUpload.metadata);
+        const markdown = getUploadMarkdown(uploadData).replace(
           "](",
-          `|type=${type}|key=${key}](`
+          `|type=${encryptedUpload.type}|key=${encryptedUpload.key}](`
         );
+        delete uploads[encryptedUpload.original_filename];
+        return markdown;
       });
     });
   },
diff --git a/assets/javascripts/lib/uploads.js b/assets/javascripts/lib/uploads.js
index 361c77b..5dc500e 100644
--- a/assets/javascripts/lib/uploads.js
+++ b/assets/javascripts/lib/uploads.js
@@ -1,7 +1,7 @@
 import { isImage } from "discourse/lib/uploads";
 import { Promise } from "rsvp";
 
-export function getMetadata(file, uploadsUrl) {
+export function getMetadata(file, siteSettings) {
   if (!isImage(file.name)) {
     return Promise.resolve({ original_filename: file.name });
   }
@@ -11,15 +11,15 @@ export function getMetadata(file, uploadsUrl) {
     img.onload = () => resolve(img);
     img.onerror = (err) => reject(err);
     img.src = window.URL.createObjectURL(file);
-    uploadsUrl[file.name] = img.src;
   }).then((img) => {
     const ratio = Math.min(
-      Discourse.SiteSettings.max_image_width / img.width,
-      Discourse.SiteSettings.max_image_height / img.height
+      siteSettings.max_image_width / img.width,
+      siteSettings.max_image_height / img.height
     );
 
     return {
       original_filename: file.name,
+      url: img.src,
       width: img.width,
       height: img.height,
       thumbnail_width: Math.floor(img.width * ratio),
diff --git a/test/javascripts/lib/uploads-test.js b/test/javascripts/lib/uploads-test.js
index 1e7e4d4..c96652b 100644
--- a/test/javascripts/lib/uploads-test.js
+++ b/test/javascripts/lib/uploads-test.js
@@ -1,30 +1,24 @@
+import { base64ToBuffer } from "discourse/plugins/discourse-encrypt/lib/base64";
 import { getMetadata } from "discourse/plugins/discourse-encrypt/lib/uploads";
 
 QUnit.module("discourse-encrypt:lib:uploadHander");
 
-let testImageBase64 =
+const TEST_IMG_BASE64 =
   "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==";
 
+const SITE_SETTINGS = { max_image_width: 100, max_image_height: 100 };
+
 test("getMetadata - image file", async (assert) => {
-  let uploadsUrl = {};
-  let file = new File([window.atob(testImageBase64)], "test.png", {
+  const file = new File([base64ToBuffer(TEST_IMG_BASE64)], "test.png", {
     type: "image/png",
-    encoding: "utf-8",
   });
-
-  // suppress the image onerror, it is not important
-  getMetadata(file, uploadsUrl).catch(() => null);
-  assert.ok(
-    uploadsUrl[file.name],
-    "it loads the image and adds it to uploadsUrl"
-  );
+  const data = await getMetadata(file, SITE_SETTINGS);
+  assert.equal(data.original_filename, "test.png");
+  assert.ok(data.url);
 });
 
 test("getMetadata - other file", async (assert) => {
-  let uploadsUrl = {};
-  let file = new File(["test"], "test.txt", { type: "text/plain" });
-
-  getMetadata(file, uploadsUrl).then((result) => {
-    assert.equal(result.original_filename, "test.txt");
-  });
+  const file = new File(["test"], "test.txt", { type: "text/plain" });
+  const data = await getMetadata(file, SITE_SETTINGS);
+  assert.equal(data.original_filename, "test.txt");
 });

GitHub sha: 3983a10a10c868b0948529e702017feb16ba5e42

This commit appears in #109 which was approved by ZogStriP. It was merged by udan11.