Browse Source

fix: serialize FormData in content

tophf 3 years ago
parent
commit
25ee40e368

+ 31 - 1
src/injected/content/requests.js

@@ -3,15 +3,21 @@ import { getFullUrl, makeElem, sendCmd } from './util-content';
 
 const {
   fetch: safeFetch,
+  FileReader: SafeFileReader,
+  FormData: SafeFormData,
 } = global;
 const { arrayBuffer: getArrayBuffer, blob: getBlob } = ResponseProto;
 const { createObjectURL, revokeObjectURL } = URL;
+const getBlobType = describeProperty(SafeBlob[PROTO], 'type').get;
+const getReaderResult = describeProperty(SafeFileReader[PROTO], 'result').get;
+const readAsDataURL = SafeFileReader[PROTO].readAsDataURL;
+const fdAppend = SafeFormData[PROTO].append;
 
 const requests = createNullObj();
 let downloadChain = promiseResolve();
 
 bridge.addHandlers({
-  HttpRequest(msg, realm) {
+  async HttpRequest(msg, realm) {
     requests[msg.id] = {
       __proto__: null,
       realm,
@@ -21,6 +27,10 @@ bridge.addHandlers({
       'fileName',
     ]);
     msg.url = getFullUrl(msg.url);
+    if (msg.data[1]) {
+      // TODO: support huge data by splitting it to multiple messages
+      msg.data = await encodeBody(msg.data[0], msg.data[1]);
+    }
     sendCmd('HttpRequest', msg);
   },
   AbortRequest: true,
@@ -137,3 +147,23 @@ function finishChunks(req) {
     ? new SafeBlob([arr], { type: req.contentType })
     : arr.buffer;
 }
+
+/** Doing it here because vault's SafeResponse+blob() doesn't work in injected-web */
+async function encodeBody(body, mode) {
+  if (mode === 'fd') {
+    const fd = new SafeFormData();
+    body::forEach(entry => fd::fdAppend(entry[0], entry[1]));
+    body = fd;
+  }
+  const wasBlob = body instanceof SafeBlob;
+  const blob = wasBlob ? body : await new SafeResponse(body)::getBlob();
+  const reader = new SafeFileReader();
+  return new SafePromise(resolve => {
+    reader::on('load', () => resolve([
+      reader::getReaderResult(),
+      blob::getBlobType(),
+      wasBlob,
+    ]));
+    reader::readAsDataURL(blob);
+  });
+}

+ 2 - 1
src/injected/content/safe-globals-content.js

@@ -12,6 +12,7 @@ export const {
   MouseEvent: SafeMouseEvent,
   Object, // for minification and guarding webpack Object(import) calls
   Promise: SafePromise,
+  Response: SafeResponse,
   TextDecoder: SafeTextDecoder,
   Uint8Array: SafeUint8Array,
   atob: safeAtob,
@@ -20,7 +21,7 @@ export const {
   removeEventListener: off,
 } = global;
 export const SafeError = Error;
-export const ResponseProto = Response[PROTO];
+export const ResponseProto = SafeResponse[PROTO];
 export const { hasOwnProperty, toString: objectToString } = {};
 export const { apply, call } = hasOwnProperty;
 export const safeCall = call.bind(call);

+ 6 - 22
src/injected/web/requests.js

@@ -93,7 +93,7 @@ function callback(req, msg) {
   if (msg.type === 'loadend') delete idMap[req.id];
 }
 
-async function start(req, context, fileName) {
+function start(req, context, fileName) {
   const { id, scriptId } = req;
   const opts = assign(createNullObj(), req.opts);
   // withCredentials is for GM4 compatibility and used only if `anonymous` is not set,
@@ -111,8 +111,11 @@ async function start(req, context, fileName) {
       || (opts.binary || !isObject(data)) && [`${data}`]
       // FF56+ can send any cloneable data directly, FF52-55 can't due to https://bugzil.la/1371246
       || IS_FIREFOX && bridge.ua.browserVersion >= 56 && [data]
-      // TODO: support huge data by splitting it to multiple messages
-      || await encodeBody(data),
+      /* Chrome can't directly transfer FormData to isolated world so we explode it,
+       * trusting its iterator is usable because the only reason for a site to break it
+       * is to fight a userscript, which it can do by breaking FormData constructor anyway */
+      // eslint-disable-next-line no-restricted-syntax
+      || (getObjectTypeTag(data) === 'FormData' ? [[...data], 'fd'] : [data, 'bin']),
     eventsToNotify: [
       'abort',
       'error',
@@ -151,22 +154,3 @@ function getResponseType(responseType = '') {
   }
   return '';
 }
-
-/**
- * Polyfill for Chrome's inability to send complex types over extension messaging.
- * We're encoding the body here, not in content, because we want to support FormData
- * and ReadableStream, which Chrome can't transfer to isolated world via CustomEvent.
- */
-async function encodeBody(body) {
-  const wasBlob = getObjectTypeTag(body) === 'Blob';
-  const blob = wasBlob ? body : await new SafeResponse(body)::safeResponseBlob();
-  const reader = new SafeFileReader();
-  return new SafePromise(resolve => {
-    reader::on('load', () => resolve([
-      reader::getReaderResult(),
-      blob::getBlobType(),
-      wasBlob,
-    ]));
-    reader::readAsDataURL(blob);
-  });
-}

+ 0 - 12
src/injected/web/safe-globals-web.js

@@ -12,13 +12,11 @@ export let
   SafeDOMParser,
   SafeError,
   SafeEventTarget,
-  SafeFileReader,
   SafeKeyboardEvent,
   SafeMouseEvent,
   Object,
   SafePromise,
   SafeProxy,
-  SafeResponse,
   SafeSymbol,
   fire,
   off,
@@ -63,15 +61,11 @@ export let
   logging,
   mathRandom,
   parseFromString, // DOMParser
-  readAsDataURL, // FileReader
-  safeResponseBlob, // Response - safe = "safe global" to disambiguate the name
   stopImmediatePropagation,
   then,
   // various getters
-  getBlobType, // Blob
   getCurrentScript, // Document
   getDetail, // CustomEvent
-  getReaderResult, // FileReader
   getRelatedTarget; // MouseEvent
 
 /**
@@ -107,7 +101,6 @@ export const VAULT = (() => {
     SafeDOMParser = res[i += 1] || src.DOMParser,
     SafeError = res[i += 1] || src.Error,
     SafeEventTarget = res[i += 1] || src.EventTarget,
-    SafeFileReader = res[i += 1] || src.FileReader,
     SafeKeyboardEvent = res[i += 1] || src.KeyboardEvent,
     SafeMouseEvent = res[i += 1] || src.MouseEvent,
     Object = res[i += 1] || src.Object,
@@ -115,7 +108,6 @@ export const VAULT = (() => {
     SafeSymbol = res[i += 1] || src.Symbol,
     // In FF content mode global.Proxy !== window.Proxy
     SafeProxy = res[i += 1] || src.Proxy,
-    SafeResponse = res[i += 1] || src.Response,
     fire = res[i += 1] || src.dispatchEvent,
     off = res[i += 1] || src.removeEventListener,
     on = res[i += 1] || src.addEventListener,
@@ -155,15 +147,11 @@ export const VAULT = (() => {
     logging = res[i += 1] || assign(createNullObj(), src.console),
     mathRandom = res[i += 1] || src.Math.random,
     parseFromString = res[i += 1] || SafeDOMParser[PROTO].parseFromString,
-    readAsDataURL = res[i += 1] || SafeFileReader[PROTO].readAsDataURL,
-    safeResponseBlob = res[i += 1] || SafeResponse[PROTO].blob,
     stopImmediatePropagation = res[i += 1] || src.Event[PROTO].stopImmediatePropagation,
     then = res[i += 1] || SafeObject.freeze(SafePromise[PROTO]).then,
     // various getters
-    getBlobType = res[i += 1] || describeProperty(src.Blob[PROTO], 'type').get,
     getCurrentScript = res[i += 1] || describeProperty(src.Document[PROTO], 'currentScript').get,
     getDetail = res[i += 1] || describeProperty(SafeCustomEvent[PROTO], 'detail').get,
-    getReaderResult = res[i += 1] || describeProperty(SafeFileReader[PROTO], 'result').get,
     getRelatedTarget = res[i += 1] || describeProperty(SafeMouseEvent[PROTO], 'relatedTarget').get,
     // various values
     builtinGlobals = res[i += 1] || [