Bug 1201979 - Support requestBody in onBeforeRequest. r=kmag
authorGiorgio Maone <g.maone@informaction.com>
Fri, 29 Jul 2016 23:38:43 +0200
changeset 349568 3b04c5fb4e57e684fd7ff59231c059ba650ab2b0
parent 349567 5469a910b4a1119e3a00394db6668e3bf6e72baf
child 349569 c761bfb5fd83eb0d767adbf34012b7dda384dce9
push id1230
push userjlund@mozilla.com
push dateMon, 31 Oct 2016 18:13:35 +0000
treeherdermozilla-release@5e06e3766db2 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerskmag
bugs1201979
milestone50.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1201979 - Support requestBody in onBeforeRequest. r=kmag MozReview-Commit-ID: LAHKN5uZO0
modules/libpref/init/all.js
toolkit/components/extensions/ext-webRequest.js
toolkit/components/extensions/test/mochitest/file_WebRequest_page1.html
toolkit/components/extensions/test/mochitest/test_ext_webrequest.html
toolkit/modules/addons/WebRequest.jsm
toolkit/modules/addons/WebRequestUpload.jsm
toolkit/modules/moz.build
--- a/modules/libpref/init/all.js
+++ b/modules/libpref/init/all.js
@@ -5446,16 +5446,19 @@ pref("media.useAudioChannelAPI", false);
 pref("dom.requestcontext.enabled", false);
 
 pref("toolkit.pageThumbs.screenSizeDivisor", 7);
 pref("toolkit.pageThumbs.minWidth", 0);
 pref("toolkit.pageThumbs.minHeight", 0);
 
 pref("webextensions.tests", false);
 
+// 16MB default non-parseable upload limit for requestBody.raw.bytes
+pref("webextensions.webRequest.requestBodyMaxRawBytes", 16777216);
+
 // Allow customization of the fallback directory for file uploads
 pref("dom.input.fallbackUploadDir", "");
 
 // Turn rewriting of youtube embeds on/off
 pref("plugins.rewrite_youtube_embeds", true);
 
 // Disable browser frames by default
 pref("dom.mozBrowserFramesEnabled", false);
--- a/toolkit/components/extensions/ext-webRequest.js
+++ b/toolkit/components/extensions/ext-webRequest.js
@@ -48,17 +48,18 @@ function WebRequestEventManager(context,
 
       // Fills in tabId typically.
       let result = {};
       extensions.emit("fill-browser-data", data.browser, data2, result);
       if (result.cancel) {
         return;
       }
 
-      let optional = ["requestHeaders", "responseHeaders", "statusCode", "statusLine", "error", "redirectUrl"];
+      let optional = ["requestHeaders", "responseHeaders", "statusCode", "statusLine", "error", "redirectUrl",
+                      "requestBody"];
       for (let opt of optional) {
         if (opt in data) {
           data2[opt] = data[opt];
         }
       }
 
       return runSafeSync(context, callback, data2);
     };
--- a/toolkit/components/extensions/test/mochitest/file_WebRequest_page1.html
+++ b/toolkit/components/extensions/test/mochitest/file_WebRequest_page1.html
@@ -24,20 +24,110 @@
 <script src="nonexistent_script_url.js"></script>
 
 <iframe src="file_WebRequest_page2.html" width="200" height="200"></iframe>
 <iframe src="redirection.sjs" width="200" height="200"></iframe>
 <iframe src="data:text/plain,webRequestTest" width="200" height="200"></iframe>
 <iframe src="data:text/plain,webRequestTest_bad" width="200" height="200"></iframe>
 <iframe src="https://invalid.localhost/" width="200" height="200"></iframe>
 <a href="file_WebRequest_page3.html?trigger=a" target="webrequest_link">link</a>
-<form method="post" action="file_WebRequest_page3.html?trigger=form" target="webrequest_form"></form>
+<form method="post"
+  action="file_WebRequest_page3.html?trigger=form"
+  target="_blank"
+  enctype="multipart/form-data"
+  >
+<input type="text" name="&quot;special&quot; chàrs" value="sp€cial">
+<input type="file" name="testFile">
+<input type="file" name="emptyFile">
+<input type="text" name="textInput1" value="value1">
+</form>
+<form method="post"
+  action="file_WebRequest_page3.html?trigger=form"
+  target="_blank"
+  enctype="multipart/form-data"
+  >
+<input type="text" name="textInput2" value="value2">
+<input type="file" name="testFile">
+<input type="file" name="emptyFile">
+
+</form>
+<form method="post"
+  action="file_WebRequest_page3.html?trigger=form"
+  target="_blank"
+  >
+<input type="text" name="textInput" value="value1">
+<input type="text" name="textInput" value="value2">
+</form>
 <script>
 "use strict";
 for (let a of document.links) {
   a.click();
 }
-for (let f of document.forms) {
-  f.submit();
-}
+
+SpecialPowers.createFiles([{name: "testFile.pdf", data: "Not really a PDF file :)", "type": "application/x-pdf"}], (files) => {
+  let testFile = files[0];
+  let blob = {
+    name: "blobAsFile",
+    content: new Blob(["A blob sent as a file"], {type: "text/csv"}),
+    fileName: "blobAsFile.csv",
+  };
+  let file = {
+    name: "testFile",
+    fileName: testFile.name,
+  };
+  let uploads = {
+    [blob.name]: blob,
+    [file.name]: file,
+  };
+
+  for (let form of document.forms) {
+    if (file.name in form.elements) {
+      SpecialPowers.wrap(form.elements[file.name]).mozSetFileArray(files);
+    }
+    let action = new URL(form.action);
+    let formData = new FormData(form);
+    let webRequestFD = {};
+
+    let updateActionURL = () => {
+      for (let name of formData.keys()) {
+        webRequestFD[name] = name in uploads ? [uploads[name].fileName] : formData.getAll(name);
+      }
+      action.searchParams.set("upload", JSON.stringify(webRequestFD));
+      action.searchParams.set("enctype", form.enctype);
+    };
+
+    updateActionURL();
+
+    form.action = action;
+    form.submit();
+
+    if (form.enctype === "multipart/form-data") {
+      let post = (data) => {
+        let xhr = new XMLHttpRequest();
+        action.searchParams.set("xhr", "1");
+        xhr.open("POST", action.href);
+        xhr.send(data);
+        action.searchParams.delete("xhr");
+      };
+
+      formData.append(blob.name, blob.content, blob.fileName);
+      formData.append("formDataField", "some value");
+      updateActionURL(true);
+      post(formData);
+
+      action.searchParams.set("upload", JSON.stringify([{file: "<file>"}]));
+      post(testFile);
+
+      let blobReader = new FileReader();
+      blobReader.readAsArrayBuffer(blob.content);
+      blobReader.onload = () => {
+        action.searchParams.set("upload", `${blobReader.result.byteLength} bytes`);
+        post(blob.content);
+      };
+      let byteLength = 16;
+      action.searchParams.set("upload", `${byteLength} bytes`);
+      post(new ArrayBuffer(byteLength));
+    }
+  }
+});
 </script>
 </body>
 </html>
--- a/toolkit/components/extensions/test/mochitest/test_ext_webrequest.html
+++ b/toolkit/components/extensions/test/mochitest/test_ext_webrequest.html
@@ -313,16 +313,41 @@ function backgroundScript() {
       }
     }
     if (details.url.includes("_bad")) {
       return {cancel: true};
     }
     return {};
   }
 
+  function onUpload(details) {
+    let url = new URL(details.url);
+    let upload = url.searchParams.get("upload");
+    if (!upload) {
+      return;
+    }
+    let requestBody = details.requestBody;
+    browser.test.log(`onUpload ${details.url} ${JSON.stringify(details.requestBody)}`);
+    browser.test.assertTrue(!!requestBody, `Intercepted upload ${details.url} #${details.requestId} ${upload} have a requestBody`);
+    if (!requestBody) {
+      return;
+    }
+    let byteLength = parseInt(upload, 10);
+    if (byteLength) {
+      browser.test.assertTrue(!!requestBody.raw, `Binary upload ${details.url} #${details.requestId} ${upload} have a raw attribute`);
+      browser.test.assertEq(byteLength, requestBody.raw && requestBody.raw.map(r => r.bytes && r.bytes.byteLength || 0).reduce((a, b) => a + b), `Binary upload size matches`);
+      return;
+    }
+    if ("raw" in requestBody) {
+      browser.test.assertEq(upload, JSON.stringify(requestBody.raw).replace(/(\bfile: ")[^"]+/, "$1<file>"), `Upload ${details.url} #${details.requestId} matches raw data`);
+    } else {
+      browser.test.assertEq(upload, JSON.stringify(requestBody.formData), `Upload ${details.url} #${details.requestId} matches form data.`);
+    }
+  }
+
   function onBeforeSendHeaders(details) {
     browser.test.log(`onBeforeSendHeaders ${details.url}`);
     checkRequestId(details);
     checkOrigin(details);
     checkResourceType(details.type);
     processHeaders("request", details);
     if (shouldRecord(details.url)) {
       recorded.beforeSendHeaders.push(details.url);
@@ -409,16 +434,24 @@ function backgroundScript() {
   }
 
   function onCompleted(details) {
     checkIpAndRecord("completed", details);
     checkHeaders("response", details);
   }
 
   browser.webRequest.onBeforeRequest.addListener(onBeforeRequest, {urls: ["<all_urls>"]}, ["blocking"]);
+  try {
+    browser.webRequest.onBeforeRequest.addListener(onUpload, {urls: ["http://*/*"]}, ["blocking", "requestBody"]);
+  } catch (e) {
+    // requestBody is disabled in release builds
+    if (!/\brequestBody\b/.test(e.message)) {
+      throw e;
+    }
+  }
   browser.webRequest.onBeforeSendHeaders.addListener(onBeforeSendHeaders, {urls: ["<all_urls>"]}, ["blocking", "requestHeaders"]);
   browser.webRequest.onSendHeaders.addListener(onSendHeaders, {urls: ["<all_urls>"]}, ["requestHeaders"]);
   browser.webRequest.onBeforeRedirect.addListener(onBeforeRedirect, {urls: ["<all_urls>"]});
   browser.webRequest.onHeadersReceived.addListener(onHeadersReceived, {urls: ["<all_urls>"]}, ["blocking", "responseHeaders"]);
   browser.webRequest.onResponseStarted.addListener(checkIpAndRecord.bind(null, "responseStarted"), {urls: ["<all_urls>"]});
   browser.webRequest.onResponseStarted.addListener(checkIpAndRecord.bind(null, "responseStarted2"), {urls: ["<all_urls>"]});
   browser.webRequest.onErrorOccurred.addListener(onErrorOccurred, {urls: ["<all_urls>"]});
   browser.webRequest.onCompleted.addListener(onCompleted, {urls: ["<all_urls>"]}, ["responseHeaders"]);
--- a/toolkit/modules/addons/WebRequest.jsm
+++ b/toolkit/modules/addons/WebRequest.jsm
@@ -13,20 +13,24 @@ const Cc = Components.classes;
 const Cu = Components.utils;
 const Cr = Components.results;
 
 const {nsIHttpActivityObserver, nsISocketTransport} = Ci;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 
+XPCOMUtils.defineLazyModuleGetter(this, "AppConstants",
+                                  "resource://gre/modules/AppConstants.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "BrowserUtils",
                                   "resource://gre/modules/BrowserUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "WebRequestCommon",
                                   "resource://gre/modules/WebRequestCommon.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "WebRequestUpload",
+                                  "resource://gre/modules/WebRequestUpload.jsm");
 
 function attachToChannel(channel, key, data) {
   if (channel instanceof Ci.nsIWritablePropertyBag2) {
     let wrapper = {wrappedJSObject: data};
     channel.setPropertyAsInterface(key, wrapper);
   }
   return data;
 }
@@ -500,16 +504,18 @@ HttpObserverManager = {
     let loadInfo = channel.loadInfo;
     let policyType = loadInfo ?
                      loadInfo.externalContentPolicyType :
                      Ci.nsIContentPolicy.TYPE_OTHER;
 
     let requestHeaderNames;
     let responseHeaderNames;
 
+    let requestBody;
+
     let includeStatus = (
                           kind === "headersReceived" ||
                           kind === "onRedirect" ||
                           kind === "onStart" ||
                           kind === "onStop"
                         ) && channel instanceof Ci.nsIHttpChannel;
 
     let commonData = null;
@@ -562,16 +568,24 @@ HttpObserverManager = {
       if (opts.requestHeaders) {
         data.requestHeaders = this.getHeaders(channel, "visitRequestHeaders", kind);
         requestHeaderNames = data.requestHeaders.map(h => h.name);
       }
       if (opts.responseHeaders) {
         data.responseHeaders = this.getHeaders(channel, "visitResponseHeaders", kind);
         responseHeaderNames = data.responseHeaders.map(h => h.name);
       }
+      if (opts.requestBody) {
+        if (requestBody === undefined) {
+          requestBody = WebRequestUpload.createRequestBody(channel);
+        }
+        if (requestBody) {
+          data.requestBody = requestBody;
+        }
+      }
       if (includeStatus) {
         mergeStatus(data, channel, kind);
       }
 
       let result = null;
       try {
         result = callback(data);
       } catch (e) {
@@ -644,19 +658,26 @@ HttpObserverManager = {
   },
 
   onStopRequest(channel, loadContext) {
     this.runChannelListener(channel, loadContext, "onStop");
   },
 };
 
 var onBeforeRequest = {
+  get allowedOptions() {
+    delete this.allowedOptions;
+    this.allowedOptions = ["blocking"];
+    if (!AppConstants.RELEASE_BUILD) {
+      this.allowedOptions.push("requestBody");
+    }
+    return this.allowedOptions;
+  },
   addListener(callback, filter = null, opt_extraInfoSpec = null) {
-    // FIXME: Add requestBody support.
-    let opts = parseExtra(opt_extraInfoSpec, ["blocking"]);
+    let opts = parseExtra(opt_extraInfoSpec, this.allowedOptions);
     opts.filter = parseFilter(filter);
     ContentPolicyManager.addListener(callback, opts);
     HttpObserverManager.addListener("opening", callback, opts);
   },
 
   removeListener(callback) {
     HttpObserverManager.removeListener("opening", callback);
     ContentPolicyManager.removeListener(callback);
new file mode 100644
--- /dev/null
+++ b/toolkit/modules/addons/WebRequestUpload.jsm
@@ -0,0 +1,321 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const EXPORTED_SYMBOLS = ["WebRequestUpload"];
+
+/* exported WebRequestUpload */
+
+const Ci = Components.interfaces;
+const Cc = Components.classes;
+const Cu = Components.utils;
+const Cr = Components.results;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+var WebRequestUpload;
+
+function rewind(stream) {
+  try {
+    stream.seek(0, 0);
+  } catch (e) {
+    // It might be already closed, e.g. because of a previous error.
+  }
+}
+
+function parseFormData(stream, channel, lenient = false) {
+  const BUFFER_SIZE = 8192; // Empirically it seemed a good compromise.
+
+  let mimeStream = null;
+
+  if (stream instanceof Ci.nsIMIMEInputStream && stream.data) {
+    mimeStream = stream;
+    stream = stream.data;
+  }
+  let multiplexStream = null;
+  if (stream instanceof Ci.nsIMultiplexInputStream) {
+    multiplexStream = stream;
+  }
+
+  let touchedStreams = new Set();
+
+  function createTextStream(stream) {
+    let textStream = Cc["@mozilla.org/intl/converter-input-stream;1"].createInstance(Ci.nsIConverterInputStream);
+    textStream.init(stream, "UTF-8", 0, lenient ? textStream.DEFAULT_REPLACEMENT_CHARACTER : 0);
+    if (stream instanceof Ci.nsISeekableStream) {
+      touchedStreams.add(stream);
+    }
+    return textStream;
+  }
+
+  let streamIdx = 0;
+  function nextTextStream() {
+    for (; streamIdx < multiplexStream.count;) {
+      let currentStream = multiplexStream.getStream(streamIdx++);
+      if (currentStream instanceof Ci.nsIStringInputStream) {
+        touchedStreams.add(multiplexStream);
+        return createTextStream(currentStream);
+      }
+    }
+    return null;
+  }
+
+  let textStream;
+  if (multiplexStream) {
+    textStream = nextTextStream();
+  } else {
+    textStream = createTextStream(mimeStream || stream);
+  }
+
+  if (!textStream) {
+    return null;
+  }
+
+  function readString() {
+    if (textStream) {
+      let textBuffer = {};
+      textStream.readString(BUFFER_SIZE, textBuffer);
+      return textBuffer.value;
+    }
+    return "";
+  }
+
+  function multiplexRead() {
+    let str = readString();
+    if (!str) {
+      textStream = nextTextStream();
+      if (textStream) {
+        str = multiplexRead();
+      }
+    }
+    return str;
+  }
+
+  let readChunk;
+  if (multiplexStream) {
+    readChunk = multiplexRead;
+  } else {
+    readChunk = readString;
+  }
+
+  function appendFormData(formData, name, value) {
+    if (name in formData) {
+      formData[name].push(value);
+    } else {
+      formData[name] = [value];
+    }
+  }
+
+  function parseMultiPart(firstChunk, boundary = "") {
+    let formData = Object.create(null);
+
+    if (!boundary) {
+      let match = firstChunk.match(/^--\S+/);
+      if (!match) {
+        return null;
+      }
+      boundary = match[0];
+    }
+
+    let unslash = (s) => s.replace(/\\"/g, '"');
+    let tail = "";
+    for (let chunk = firstChunk;
+         chunk || tail;
+         chunk = readChunk()) {
+      let parts;
+      if (chunk) {
+        chunk = tail + chunk;
+        parts = chunk.split(boundary);
+        tail = parts.pop();
+      } else {
+        parts = [tail];
+        tail = "";
+      }
+
+      for (let part of parts) {
+        let match = part.match(/^\r\nContent-Disposition: form-data; name="(.*)"\r\n(?:Content-Type: (\S+))?.*\r\n/i);
+        if (!match) {
+          continue;
+        }
+        let [header, name, contentType] = match;
+        if (contentType) {
+          let fileName;
+          // Since escaping inside Content-Disposition subfields is still poorly defined and buggy (see Bug 136676),
+          // currently we always consider backslash-prefixed quotes as escaped even if that's not generally true
+          // (i.e. in a field whose value actually ends with a backslash).
+          // Therefore in this edge case we may end coalescing name and filename, which is marginally better than
+          // potentially truncating the name field at the wrong point, at least from a XSS filter POV.
+          match = name.match(/^(.*[^\\])"; filename="(.*)/);
+          if (match) {
+            [, name, fileName] = match;
+          }
+          appendFormData(formData, unslash(name), fileName ? unslash(fileName) : "");
+        } else {
+          appendFormData(formData, unslash(name), part.slice(header.length, -2));
+        }
+      }
+    }
+
+    return formData;
+  }
+
+  function parseUrlEncoded(firstChunk) {
+    let formData = Object.create(null);
+
+    let tail = "";
+    for (let chunk = firstChunk;
+         chunk || tail;
+         chunk = readChunk()) {
+      let pairs;
+      if (chunk) {
+        chunk = tail + chunk.trim();
+        pairs = chunk.split("&");
+        tail = pairs.pop();
+      } else {
+        chunk = tail;
+        tail = "";
+        pairs = [chunk];
+      }
+      for (let pair of pairs) {
+        let [name, value] = pair.replace(/\+/g, " ").split("=").map(decodeURIComponent);
+        appendFormData(formData, name, value);
+      }
+    }
+
+    return formData;
+  }
+
+  try {
+    let chunk = readChunk();
+
+    if (multiplexStream) {
+      touchedStreams.add(multiplexStream);
+      return parseMultiPart(chunk);
+    } else {
+      let contentType;
+      if (/^Content-Type:/i.test(chunk)) {
+        contentType = chunk.replace(/^Content-Type:\s*/i, "");
+        chunk = chunk.slice(chunk.indexOf("\r\n\r\n") + 4);
+      } else {
+        try {
+          contentType = channel.getRequestHeader("Content-Type");
+        } catch (e) {
+          Cu.reportError(e);
+          return null;
+        }
+      }
+
+      let match = contentType.match(/^(?:multipart\/form-data;\s*boundary=(\S*)|application\/x-www-form-urlencoded\s)/i);
+      if (match) {
+        let boundary = match[1];
+        if (boundary) {
+          return parseMultiPart(chunk, boundary);
+        } else {
+          return parseUrlEncoded(chunk);
+        }
+      }
+    }
+  } finally {
+    for (let stream of touchedStreams) {
+      rewind(stream);
+    }
+  }
+
+  return null;
+}
+
+function createFormData(stream, channel) {
+  try {
+    rewind(stream);
+    return parseFormData(stream, channel);
+  } catch (e) {
+    Cu.reportError(e);
+  } finally {
+    rewind(stream);
+  }
+  return null;
+}
+
+function convertRawData(outerStream) {
+  let raw = [];
+  let totalBytes = 0;
+
+  // Here we read the stream up to WebRequestUpload.MAX_RAW_BYTES, returning false if we had to truncate the result.
+  function readAll(stream) {
+    let unbuffered = stream.unbufferedStream || stream;
+    if (unbuffered instanceof Ci.nsIFileInputStream) {
+      raw.push({file: "<file>"}); // Full paths not supported yet for naked files (follow up bug)
+      return true;
+    }
+    rewind(stream);
+
+    let binaryStream = Cc["@mozilla.org/binaryinputstream;1"].createInstance(Ci.nsIBinaryInputStream);
+    binaryStream.setInputStream(stream);
+    const MAX_BYTES = WebRequestUpload.MAX_RAW_BYTES;
+    try {
+      for (let available; (available = binaryStream.available());) {
+        let size = Math.min(MAX_BYTES - totalBytes, available);
+        let bytes = new ArrayBuffer(size);
+        binaryStream.readArrayBuffer(size, bytes);
+        let chunk = {bytes};
+        raw.push(chunk);
+        totalBytes += size;
+
+        if (totalBytes >= MAX_BYTES) {
+          if (size < available) {
+            chunk.truncated = true;
+            chunk.originalSize = available;
+            return false;
+          }
+          break;
+        }
+      }
+    } finally {
+      rewind(stream);
+    }
+    return true;
+  }
+
+  let unbuffered = outerStream;
+  if (outerStream instanceof Ci.nsIStreamBufferAccess) {
+    unbuffered = outerStream.unbufferedStream;
+  }
+
+  if (unbuffered instanceof Ci.nsIMultiplexInputStream) {
+    for (let i = 0, count = unbuffered.count; i < count; i++) {
+      if (!readAll(unbuffered.getStream(i))) {
+        break;
+      }
+    }
+  } else {
+    readAll(outerStream);
+  }
+
+  return raw;
+}
+
+WebRequestUpload = {
+  createRequestBody(channel) {
+    let requestBody = null;
+    if (channel instanceof Ci.nsIUploadChannel && channel.uploadStream) {
+      try {
+        let stream = channel.uploadStream.QueryInterface(Ci.nsISeekableStream);
+        let formData = createFormData(stream, channel);
+        if (formData) {
+          requestBody = {formData};
+        } else {
+          requestBody = {raw: convertRawData(stream), lenientFormData: createFormData(stream, channel, true)};
+        }
+      } catch (e) {
+        Cu.reportError(e);
+        requestBody = {error: e.message || String(e)};
+      }
+      requestBody = Object.freeze(requestBody);
+    }
+    return requestBody;
+  },
+};
+
+XPCOMUtils.defineLazyPreferenceGetter(WebRequestUpload, "MAX_RAW_BYTES", "webextensions.webRequest.requestBodyMaxRawBytes");
--- a/toolkit/modules/moz.build
+++ b/toolkit/modules/moz.build
@@ -19,16 +19,17 @@ SPHINX_TREES['toolkit_modules'] = 'docs'
 EXTRA_JS_MODULES += [
     'addons/MatchPattern.jsm',
     'addons/WebNavigation.jsm',
     'addons/WebNavigationContent.js',
     'addons/WebNavigationFrames.jsm',
     'addons/WebRequest.jsm',
     'addons/WebRequestCommon.jsm',
     'addons/WebRequestContent.js',
+    'addons/WebRequestUpload.jsm',
     'AsyncPrefs.jsm',
     'Battery.jsm',
     'BinarySearch.jsm',
     'BrowserUtils.jsm',
     'CanonicalJSON.jsm',
     'CertUtils.jsm',
     'CharsetMenu.jsm',
     'ClientID.jsm',