bug 915036 - Implement DownloadSource.adjustChannel callback to support POST requests draft 915036-jsdownloads-adjustChannel
authorTomislav Jovanovic <tomica@gmail.com>
Fri, 21 Oct 2016 15:54:18 +0200
changeset 428296 51066668c6c4419f2d04f4bf10fa712a79863994
parent 425967 94b0fddf96b43942bdd851a3275042909ea37e09
child 534705 666584c1d41233c5c93e6f2913bb9b3fcfbd3a41
push id33278
push userbmo:tomica@gmail.com
push dateSat, 22 Oct 2016 06:17:04 +0000
bugs915036
milestone52.0a1
bug 915036 - Implement DownloadSource.adjustChannel callback to support POST requests MozReview-Commit-ID: 1RplqGhjtn6
toolkit/components/jsdownloads/src/DownloadCore.jsm
toolkit/components/jsdownloads/test/unit/common_test_Download.js
toolkit/components/jsdownloads/test/unit/test_DownloadStore.js
--- a/toolkit/components/jsdownloads/src/DownloadCore.jsm
+++ b/toolkit/components/jsdownloads/src/DownloadCore.jsm
@@ -1100,18 +1100,19 @@ this.Download.prototype = {
   toSerializable: function ()
   {
     let serializable = {
       source: this.source.toSerializable(),
       target: this.target.toSerializable(),
     };
 
     let saver = this.saver.toSerializable();
-    if (!saver) {
-      // If we are unable to serialize the saver, we won't persist the download.
+    if (!serializable.source || !saver) {
+      // If we are unable to serialize either the source or the saver,
+      // we won't persist the download.
       return null;
     }
 
     // Simplify the representation for the most common saver type.  If the saver
     // is an object instead of a simple string, we can't simplify it because we
     // need to persist all its properties, not only "type".  This may happen for
     // savers of type "copy" as well as other types.
     if (saver !== "copy") {
@@ -1271,22 +1272,44 @@ this.DownloadSource.prototype = {
 
   /**
    * String containing the referrer URI of the download source, or null if no
    * referrer should be sent or the download source is not HTTP.
    */
   referrer: null,
 
   /**
+   * For downloads handled by the (default) DownloadCopySaver, this function
+   * can adjust the network channel before it is opened, for example to change
+   * the HTTP headers or to upload a stream as POST data.
+   *
+   * @note If this is defined this object will not be serializable, thus the
+   *       Download object will not be persisted across sessions.
+   *
+   * @param aChannel
+   *        The nsIChannel to be adjusted.
+   *
+   * @return {Promise}
+   * @resolves When the channel has been adjusted and can be opened.
+   * @rejects JavaScript exception that will cause the download to fail.
+   */
+   adjustChannel: null,
+
+  /**
    * Returns a static representation of the current object state.
    *
    * @return A JavaScript object that can be serialized to JSON.
    */
   toSerializable: function ()
   {
+    if (this.adjustChannel) {
+      // If the callback was used, we can't reproduce this across sessions.
+      return null;
+    }
+
     // Simplify the representation if we don't have other details.
     if (!this.isPrivate && !this.referrer && !this._unknownProperties) {
       return this.url;
     }
 
     let serializable = { url: this.url };
     if (this.isPrivate) {
       serializable.isPrivate = true;
@@ -1309,16 +1332,20 @@ this.DownloadSource.prototype = {
  *        object with the following properties:
  *        {
  *          url: String containing the URI for the download source.
  *          isPrivate: Indicates whether the download originated from a private
  *                     window.  If omitted, the download is public.
  *          referrer: String containing the referrer URI of the download source.
  *                    Can be omitted or null if no referrer should be sent or
  *                    the download source is not HTTP.
+ *          adjustChannel: For downloads handled by (default) DownloadCopySaver,
+ *                         this function can adjust the network channel before
+ *                         it is opened, for example to change the HTTP headers
+ *                         or to upload a stream as POST data.  Optional.
  *        }
  *
  * @return The newly created DownloadSource object.
  */
 this.DownloadSource.fromSerializable = function (aSerializable) {
   let source = new DownloadSource();
   if (isString(aSerializable)) {
     // Convert String objects to primitive strings at this point.
@@ -1333,16 +1360,19 @@ this.DownloadSource.fromSerializable = f
     // Convert String objects to primitive strings at this point.
     source.url = aSerializable.url.toString();
     if ("isPrivate" in aSerializable) {
       source.isPrivate = aSerializable.isPrivate;
     }
     if ("referrer" in aSerializable) {
       source.referrer = aSerializable.referrer;
     }
+    if ("adjustChannel" in aSerializable) {
+      source.adjustChannel = aSerializable.adjustChannel;
+    }
 
     deserializeUnknownProperties(source, aSerializable, property =>
       property != "url" && property != "isPrivate" && property != "referrer");
   }
 
   return source;
 };
 
@@ -2007,16 +2037,21 @@ this.DownloadCopySaver.prototype = {
               let totalBytes = aProgressMax == -1 ? -1 : (resumeFromBytes +
                                                           aProgressMax);
               aSetProgressBytesFn(currentBytes, totalBytes, aProgress > 0 &&
                                   partFilePath && keepPartialData);
             },
             onStatus: function () { },
           };
 
+          // If the callback was set, handle it now before opening the channel.
+          if (download.source.adjustChannel) {
+            yield download.source.adjustChannel(channel);
+          }
+
           // Open the channel, directing output to the background file saver.
           backgroundFileSaver.QueryInterface(Ci.nsIStreamListener);
           channel.asyncOpen2({
             onStartRequest: function (aRequest, aContext) {
               backgroundFileSaver.onStartRequest(aRequest, aContext);
 
               // Check if the request's response has been blocked by Windows
               // Parental Controls with an HTTP 450 error code.
@@ -2838,9 +2873,8 @@ this.DownloadPDFSaver.prototype = {
  * @param aSerializable
  *        Serializable representation of a DownloadPDFSaver object.
  *
  * @return The newly created DownloadPDFSaver object.
  */
 this.DownloadPDFSaver.fromSerializable = function (aSerializable) {
   return new DownloadPDFSaver();
 };
-
--- a/toolkit/components/jsdownloads/test/unit/common_test_Download.js
+++ b/toolkit/components/jsdownloads/test/unit/common_test_Download.js
@@ -336,16 +336,68 @@ add_task(function* test_referrer()
   });
   do_check_eq(download.source.referrer, TEST_REFERRER_URL);
   yield download.start();
 
   cleanup();
 });
 
 /**
+ * Checks the adjustChannel callback for downloads.
+ */
+add_task(function* test_adjustChannel()
+{
+  const sourcePath = "/test_post.txt";
+  const sourceUrl = httpUrl("test_post.txt");
+  const targetPath = getTempFile(TEST_TARGET_FILE_NAME).path;
+  const customHeader = { name: "X-Answer", value: "42" };
+  const postData = "Don't Panic";
+
+  function cleanup() {
+    gHttpServer.registerPathHandler(sourcePath, null);
+  }
+  do_register_cleanup(cleanup);
+
+  gHttpServer.registerPathHandler(sourcePath, aRequest => {
+    do_check_eq(aRequest.method, "POST");
+
+    do_check_true(aRequest.hasHeader(customHeader.name));
+    do_check_eq(aRequest.getHeader(customHeader.name), customHeader.value);
+
+    const stream = aRequest.bodyInputStream;
+    const body = NetUtil.readInputStreamToString(stream, stream.available());
+    do_check_eq(body, postData);
+  });
+
+  function adjustChannel(channel) {
+    channel.QueryInterface(Ci.nsIHttpChannel);
+    channel.setRequestHeader(customHeader.name, customHeader.value, false);
+
+    const stream = Cc["@mozilla.org/io/string-input-stream;1"]
+                   .createInstance(Ci.nsIStringInputStream);
+    stream.setData(postData, postData.length);
+
+    channel.QueryInterface(Ci.nsIUploadChannel2);
+    channel.explicitSetUploadStream(stream, null, -1, "POST", false);
+
+    return Promise.resolve();
+  }
+
+  const download = yield Downloads.createDownload({
+    source: { url: sourceUrl, adjustChannel },
+    target: targetPath,
+  });
+  do_check_eq(download.source.adjustChannel, adjustChannel);
+  do_check_eq(download.toSerializable(), null);
+  yield download.start();
+
+  cleanup();
+});
+
+/**
  * Checks initial and final state and progress for a successful download.
  */
 add_task(function* test_initial_final_state()
 {
   let download;
   if (!gUseLegacySaver) {
     // When testing DownloadCopySaver, we have control over the download, thus
     // we can check its state before it starts.
--- a/toolkit/components/jsdownloads/test/unit/test_DownloadStore.js
+++ b/toolkit/components/jsdownloads/test/unit/test_DownloadStore.js
@@ -61,24 +61,34 @@ add_task(function* test_save_reload()
   let pdfDownload = yield Downloads.createDownload({
     source: { url: httpUrl("empty.txt"),
               referrer: TEST_REFERRER_URL },
     target: getTempFile(TEST_TARGET_FILE_NAME),
     saver: "pdf",
   });
   listForSave.add(pdfDownload);
 
+  // If we used a callback to adjust the channel, the download should
+  // not be serialized because we can't recreate it across sessions.
+  let adjustedDownload = yield Downloads.createDownload({
+    source: { url: httpUrl("empty.txt"),
+              adjustChannel: () => Promise.resolve() },
+    target: getTempFile(TEST_TARGET_FILE_NAME),
+  });
+  listForSave.add(adjustedDownload);
+
   let legacyDownload = yield promiseStartLegacyDownload();
   yield legacyDownload.cancel();
   listForSave.add(legacyDownload);
 
   yield storeForSave.save();
   yield storeForLoad.load();
 
-  // Remove the PDF download because it should not appear in this list.
+  // Remove the PDF and adjusted downloads because they should not appear here.
+  listForSave.remove(adjustedDownload);
   listForSave.remove(pdfDownload);
 
   let itemsForSave = yield listForSave.getAll();
   let itemsForLoad = yield listForLoad.getAll();
 
   do_check_eq(itemsForSave.length, itemsForLoad.length);
 
   // Downloads should be reloaded in the same order.