DEV: Use new core addComposerUploadPreProcessor API (#131)

DEV: Use new core addComposerUploadPreProcessor API (#131)

See https://github.com/discourse/discourse/pull/14222

Now that we are using Uppy in core, we need a new way to preprocess files for uploads. This is done with an uppy plugin by convention. This commit introduces the plugin and uses it along with the new addComposerUploadPreProcessor API only if enable_experimental_composer_uploader is true for the site.

diff --git a/assets/javascripts/discourse/initializers/encrypt-uploads.js b/assets/javascripts/discourse/initializers/encrypt-uploads.js
index 0b77480..228e4a1 100644
--- a/assets/javascripts/discourse/initializers/encrypt-uploads.js
+++ b/assets/javascripts/discourse/initializers/encrypt-uploads.js
@@ -1,4 +1,5 @@
 import { withPluginApi } from "discourse/lib/plugin-api";
+import UppyUploadEncrypt from "discourse/plugins/discourse-encrypt/lib/uppy-upload-encrypt-plugin";
 import { getUploadMarkdown } from "discourse/lib/uploads";
 import { bufferToBase64 } from "discourse/plugins/discourse-encrypt/lib/base64";
 import {
@@ -32,60 +33,16 @@ export default {
 
       const uploads = {};
 
-      api.addComposerUploadHandler([".*"], (file, editor) => {
+      if (!siteSettings.enable_experimental_composer_uploader) {
         const controller = container.lookup("controller:composer");
-        const topicId = controller.get("model.topic.id");
-        if (!controller.get("model.isEncrypted") && !hasTopicKey(topicId)) {
-          return true;
-        }
-
-        const metadataPromise = getMetadata(file, siteSettings);
-        const plaintextPromise = readFile(file);
-        const keyPromise = generateUploadKey();
-        const exportedKeyPromise = keyPromise.then((key) => {
-          return window.crypto.subtle
-            .exportKey("raw", key)
-            .then((wrapped) => bufferToBase64(wrapped));
-        });
-
-        const iv = window.crypto.getRandomValues(new Uint8Array(12));
-
-        const ciphertextPromise = Promise.all([
-          plaintextPromise,
-          keyPromise,
-        ]).then(([plaintext, key]) => {
-          return window.crypto.subtle.encrypt(
-            { name: "AES-GCM", iv, tagLength: 128 },
-            key,
-            plaintext
-          );
-        });
-
-        Promise.all([
-          ciphertextPromise,
-          exportedKeyPromise,
-          metadataPromise,
-        ]).then(([ciphertext, exportedKey, metadata]) => {
-          const blob = new Blob([iv, ciphertext], {
-            type: "application/x-binary",
-          });
-          const encryptedFile = new File([blob], `${file.name}.encrypted`);
-          editor.$().fileupload("send", {
-            files: [encryptedFile],
-            originalFiles: [encryptedFile],
-            formData: { type: "composer" },
-          });
-
-          uploads[file.name] = {
-            key: exportedKey,
-            metadata,
-            type: file.type,
-            filesize: encryptedFile.size,
-          };
-        });
-
-        return false;
-      });
+        this._useStableUploadProcessor(api, siteSettings, uploads, controller);
+      } else {
+        this._useExperimentalComposerUploadProcessor(
+          api,
+          siteSettings,
+          uploads
+        );
+      }
 
       api.addComposerUploadMarkdownResolver((upload) => {
         const encryptedUpload =
@@ -105,4 +62,75 @@ export default {
       });
     });
   },
+
+  _useStableUploadProcessor(api, siteSettings, uploads, controller) {
+    api.addComposerUploadHandler([".*"], (file, editor) => {
+      const topicId = controller.get("model.topic.id");
+      if (!controller.get("model.isEncrypted") && !hasTopicKey(topicId)) {
+        return true;
+      }
+
+      const metadataPromise = getMetadata(file, siteSettings);
+      const plaintextPromise = readFile(file);
+      const keyPromise = generateUploadKey();
+      const exportedKeyPromise = keyPromise.then((key) => {
+        return window.crypto.subtle
+          .exportKey("raw", key)
+          .then((wrapped) => bufferToBase64(wrapped));
+      });
+
+      const iv = window.crypto.getRandomValues(new Uint8Array(12));
+
+      const ciphertextPromise = Promise.all([
+        plaintextPromise,
+        keyPromise,
+      ]).then(([plaintext, key]) => {
+        return window.crypto.subtle.encrypt(
+          { name: "AES-GCM", iv, tagLength: 128 },
+          key,
+          plaintext
+        );
+      });
+
+      Promise.all([
+        ciphertextPromise,
+        exportedKeyPromise,
+        metadataPromise,
+      ]).then(([ciphertext, exportedKey, metadata]) => {
+        const blob = new Blob([iv, ciphertext], {
+          type: "application/x-binary",
+        });
+        const encryptedFile = new File([blob], `${file.name}.encrypted`);
+        editor.$().fileupload("send", {
+          files: [encryptedFile],
+          originalFiles: [encryptedFile],
+          formData: { type: "composer" },
+        });
+
+        uploads[file.name] = {
+          key: exportedKey,
+          metadata,
+          type: file.type,
+          filesize: encryptedFile.size,
+        };
+      });
+
+      return false;
+    });
+  },
+
+  _useExperimentalComposerUploadProcessor(api, siteSettings, uploads) {
+    api.addComposerUploadPreProcessor(
+      UppyUploadEncrypt,
+      ({ composerModel }) => {
+        return {
+          composerModel,
+          siteSettings,
+          storeEncryptedUpload: (fileName, data) => {
+            uploads[fileName] = data;
+          },
+        };
+      }
+    );
+  },
 };
diff --git a/assets/javascripts/lib/uppy-upload-encrypt-plugin.js b/assets/javascripts/lib/uppy-upload-encrypt-plugin.js
new file mode 100644
index 0000000..7a97dfe
--- /dev/null
+++ b/assets/javascripts/lib/uppy-upload-encrypt-plugin.js
@@ -0,0 +1,87 @@
+import { BasePlugin } from "@uppy/core";
+import { Promise } from "rsvp";
+import { hasTopicKey } from "discourse/plugins/discourse-encrypt/lib/discourse";
+import { bufferToBase64 } from "discourse/plugins/discourse-encrypt/lib/base64";
+
+import {
+  generateUploadKey,
+  getMetadata,
+  readFile,
+} from "discourse/plugins/discourse-encrypt/lib/uploads";
+
+export default class UppyUploadEncrypt extends BasePlugin {
+  constructor(uppy, opts) {
+    super(uppy, opts);
+    this.id = opts.id || "uppy-upload-encrypt";
+
+    this.type = "preprocessor";
+    this.pluginClass = this.constructor.name;
+
+    this.composerModel = opts.composerModel;
+    this.storeEncryptedUpload = opts.storeEncryptedUpload;
+    this.siteSettings = opts.siteSettings;
+  }
+
+  async _encryptFile(fileId) {
+    let file = this.uppy.getFile(fileId);
+    this.uppy.emit("preprocess-progress", this.pluginClass, file);
+
+    const key = await generateUploadKey();
+    let exportedKey = await window.crypto.subtle.exportKey("raw", key);
+    exportedKey = bufferToBase64(exportedKey);
+
+    const metadata = await getMetadata(file.data, this.siteSettings);
+    const plaintext = await readFile(file.data);
+
+    const iv = window.crypto.getRandomValues(new Uint8Array(12));
+
+    const ciphertext = await window.crypto.subtle.encrypt(
+      { name: "AES-GCM", iv, tagLength: 128 },
+      key,
+      plaintext
+    );
+
+    const blob = new Blob([iv, ciphertext], {
+      type: "application/x-binary",
+    });
+
+    this.uppy.setFileState(fileId, {
+      data: blob,
+      size: blob.size,
+      name: `${file.name}.encrypted`,
+    });
+
+    this.storeEncryptedUpload(file.name, {
+      key: exportedKey,
+      metadata,
+      type: file.type,
+      filesize: blob.size,
+    });
+    this.uppy.emit("preprocess-complete", this.pluginClass, file);
+  }
+
+  async _encryptFiles(fileIds) {
+    if (
+      !this.composerModel.isEncrypted &&
+      !hasTopicKey(this.composerModel.get("topic.id"))
+    ) {
+      return Promise.resolve();
+    }
+
+    let encryptTasks = fileIds.map((fileId) => () =>
+      this._encryptFile.call(this, fileId)
+    );
+
+    for (const task of encryptTasks) {
+      await task();
+    }
+  }
+
+  install() {
+    this.uppy.addPreProcessor(this._encryptFiles.bind(this));
+  }
+
+  uninstall() {
+    this.uppy.removePreProcessor(this._encryptFiles.bind(this));
+  }
+}

GitHub sha: de20c460770ac37a530518ab3e6d2a5fa2a8627d

This commit appears in #131 which was approved by eviltrout. It was merged by martin.