bug 1247793 part 2 - Implement download() options: method, headers and body, r?aswan
MozReview-Commit-ID: 2muhcweY8Fo
--- a/toolkit/components/extensions/ext-downloads.js
+++ b/toolkit/components/extensions/ext-downloads.js
@@ -33,16 +33,21 @@ const DOWNLOAD_ITEM_FIELDS = ["id", "url
"bytesReceived", "totalBytes",
"fileSize", "exists",
"byExtensionId", "byExtensionName"];
// Fields that we generate onChanged events for.
const DOWNLOAD_ITEM_CHANGE_FIELDS = ["endTime", "state", "paused", "canResume",
"error", "exists"];
+const FORBIDDEN_HEADERS = ["ACCEPT-CHARSET", "ACCEPT-ENCODING",
+ "ACCESS-CONTROL-REQUEST-HEADERS", "ACCESS-CONTROL-REQUEST-METHOD",
+ "CONNECTION", "CONTENT-LENGTH", "COOKIE", "COOKIE2", "DATE", "DNT",
+ "EXPECT", "HOST", "KEEP-ALIVE", "ORIGIN", "REFERER", "TE", "TRAILER",
+ "TRANSFER-ENCODING", "UPGRADE", "VIA"];
class DownloadItem {
constructor(id, download, extension) {
this.id = id;
this.download = download;
this.extension = extension;
this.prechange = {};
}
@@ -476,23 +481,47 @@ extensions.registerSchemaAPI("downloads"
});
});
});
}
let download;
return Downloads.getPreferredDownloadsDirectory()
.then(downloadsDir => createTarget(downloadsDir))
- .then(target => Downloads.createDownload({
- source: options.url,
- target: {
- path: target,
- partFilePath: target + ".part",
- },
- })).then(dl => {
+ .then(target => {
+ const source = {
+ url: options.url,
+ method: options.method || "GET",
+ };
+
+ if (typeof options.body === "string") {
+ source.body = options.body;
+ }
+
+ if (options.headers) {
+ source.headers = [];
+
+ for (let header of options.headers) {
+ const name = header.name.toUpperCase();
+
+ // Rules from https://fetch.spec.whatwg.org/#forbidden-header-name
+ if (!FORBIDDEN_HEADERS.includes(name) && !name.startsWith("PROXY-") && !name.startsWith("SEC-")) {
+ source.headers.push(header);
+ }
+ }
+ }
+
+ return Downloads.createDownload({
+ source,
+ target: {
+ path: target,
+ partFilePath: target + ".part",
+ },
+ });
+ }).then(dl => {
download = dl;
return DownloadMap.getDownloadList();
}).then(list => {
list.add(download);
// This is necessary to make pause/resume work.
download.tryToKeepPartialData = true;
download.start();
--- a/toolkit/components/extensions/schemas/downloads.json
+++ b/toolkit/components/extensions/schemas/downloads.json
@@ -381,27 +381,25 @@
"optional": true
},
"saveAs": {
"description": "Use a file-chooser to allow the user to select a filename.",
"optional": true,
"type": "boolean"
},
"method": {
- "unsupported": true,
"description": "The HTTP method to use if the URL uses the HTTP[S] protocol.",
"enum": [
"GET",
"POST"
],
"optional": true,
"type": "string"
},
"headers": {
- "unsupported": true,
"optional": true,
"type": "array",
"description": "Extra HTTP headers to send with the request if the URL uses the HTTP[s] protocol. Each header is represented as a dictionary containing the keys <code>name</code> and either <code>value</code> or <code>binaryValue</code>, restricted to those allowed by XMLHttpRequest.",
"items": {
"type": "object",
"properties": {
"name": {
"description": "Name of the HTTP header.",
@@ -410,17 +408,16 @@
"value": {
"description": "Value of the HTTP header.",
"type": "string"
}
}
}
},
"body": {
- "unsupported": true,
"description": "Post body.",
"optional": true,
"type": "string"
}
}
},
{
"name": "callback",
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_downloads_post.js
@@ -0,0 +1,117 @@
+/* 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";
+
+add_task(function* setup() {
+ const dir = FileUtils.getDir("TmpD", ["downloads"]);
+ dir.createUnique(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
+
+ Services.prefs.setIntPref("browser.download.folderList", 2);
+ Services.prefs.setComplexValue("browser.download.dir", Ci.nsIFile, dir);
+
+ do_register_cleanup(() => {
+ Services.prefs.clearUserPref("browser.download.folderList");
+ Services.prefs.clearUserPref("browser.download.dir");
+ return cleanupDir(dir);
+ });
+});
+
+add_task(function* test_download_post() {
+ const server = createHttpServer();
+ const url = `http://localhost:${server.identity.primaryPort}/log`;
+
+ let received;
+ server.registerPathHandler("/log", request => {
+ received = request;
+ });
+
+ function background() {
+ browser.test.onMessage.addListener(options => {
+ Promise.resolve()
+ .then(() => browser.downloads.download(options))
+ .catch(err => browser.test.sendMessage("done", {err: err.message}));
+ });
+ browser.downloads.onChanged.addListener(({state}) => {
+ if (state && state.current === "complete") {
+ browser.test.sendMessage("done", {ok: true});
+ }
+ });
+ }
+
+ const manifest = {permissions: ["downloads"]};
+ const extension = ExtensionTestUtils.loadExtension({background, manifest});
+ yield extension.startup();
+
+ function download(options) {
+ extension.sendMessage(options);
+ return extension.awaitMessage("done");
+ }
+
+ // Confirm received vs. expected values.
+ function confirm(method, headers, body) {
+ equal(received.method, method, "method is correct");
+
+ for (let name in (headers || {})) {
+ if (received.hasHeader(name)) {
+ equal(received.getHeader(name), headers[name], `header ${name} is correct`);
+ } else {
+ // Expected header value null means we expect the header to be missing.
+ equal(headers[name], null, `header ${name} missing, as expected`);
+ }
+ }
+
+ if (body) {
+ const str = NetUtil.readInputStreamToString(received.bodyInputStream,
+ received.bodyInputStream.available());
+ equal(str, body, "body is correct");
+ }
+ }
+
+ // Test method option.
+ let done = yield download({url});
+ ok(done.ok, "download works without the method option, defaults to GET");
+ confirm("GET");
+
+ done = yield download({url, method: "PUT"});
+ ok(!done.ok, `download rejected with PUT method - ${done.err}`);
+
+ done = yield download({url, method: "POST"});
+ ok(done.ok, "download works with POST method");
+ confirm("POST");
+
+ // Test body option values.
+ done = yield download({url, body: []});
+ ok(!done.ok, `download rejected because of non-string body - ${done.err}`);
+
+ done = yield download({url, method: "POST", body: "of work"});
+ ok(done.ok, "download works with POST method and body");
+ confirm("POST", {"Content-Length": 7}, "of work");
+
+ // Test custom headers.
+ done = yield download({url, headers: [{name: "X-Custom"}]});
+ ok(!done.ok, `download rejected because of missing header value - ${done.err}`);
+
+ done = yield download({url, headers: [{name: "X-Custom", value: "13"}]});
+ ok(done.ok, "download works with a custom header");
+ confirm("GET", {"X-Custom": "13"});
+
+ // Test forbidden headers.
+ done = yield download({url, headers: [{name: "DNT", value: "1"}]});
+ ok(done.ok, "download works, but can't set a forbidden header DNT");
+ confirm("GET", {"DNT": null});
+
+ done = yield download({url, headers: [{name: "Accept-Encoding", value: "gzip"}]});
+ ok(done.ok, "download works, but can't override a forbidden header value Accept-Encoding");
+ confirm("GET", {"Accept-Encoding": "gzip, deflate"});
+
+ done = yield download({url, headers: [{name: "Proxy-Connection", value: "keep-alive"}]});
+ ok(done.ok, "download works, but can't set a Proxy- header");
+ confirm("GET", {"Proxy-Connection": null});
+
+ done = yield download({url, headers: [{name: "Sec-ret", value: "1"}]});
+ ok(done.ok, "download works, but can't set a Sec-urity header");
+ confirm("GET", {"Sec-ret": null});
+
+ yield extension.unload();
+});
--- a/toolkit/components/extensions/test/xpcshell/xpcshell.ini
+++ b/toolkit/components/extensions/test/xpcshell/xpcshell.ini
@@ -24,16 +24,18 @@ skip-if = os == "android" # Android does
[test_ext_background_window_properties.js]
skip-if = os == "android"
[test_ext_contexts.js]
[test_ext_downloads.js]
[test_ext_downloads_download.js]
skip-if = os == "android"
[test_ext_downloads_misc.js]
skip-if = os == "android"
+[test_ext_downloads_post.js]
+skip-if = os == "android"
[test_ext_downloads_search.js]
skip-if = os == "android"
[test_ext_experiments.js]
skip-if = release_or_beta
[test_ext_extension.js]
[test_ext_idle.js]
[test_ext_json_parser.js]
[test_ext_localStorage.js]