bug 915036 - Implement DownloadSource.adjustChannel callback to support POST requests
MozReview-Commit-ID: 1RplqGhjtn6
--- 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.