/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ /* vim: set sts=2 sw=2 et tw=80: */ "use strict"; /* global OS */ Cu.import("resource://gre/modules/osfile.jsm"); Cu.import("resource://gre/modules/Downloads.jsm"); const gServer = createHttpServer(); gServer.registerDirectory("/data/", do_get_file("data")); const WINDOWS = AppConstants.platform == "win"; const BASE = `http://localhost:${gServer.identity.primaryPort}/data`; const FILE_NAME = "file_download.txt"; const FILE_URL = BASE + "/" + FILE_NAME; const FILE_NAME_UNIQUE = "file_download(1).txt"; const FILE_LEN = 46; let downloadDir; function setup() { downloadDir = FileUtils.getDir("TmpD", ["downloads"]); downloadDir.createUnique(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY); do_print(`Using download directory ${downloadDir.path}`); Services.prefs.setIntPref("browser.download.folderList", 2); Services.prefs.setComplexValue("browser.download.dir", Ci.nsIFile, downloadDir); do_register_cleanup(() => { Services.prefs.clearUserPref("browser.download.folderList"); Services.prefs.clearUserPref("browser.download.dir"); let entries = downloadDir.directoryEntries; while (entries.hasMoreElements()) { let entry = entries.getNext().QueryInterface(Ci.nsIFile); ok(false, `Leftover file ${entry.path} in download directory`); entry.remove(false); } downloadDir.remove(false); }); } function backgroundScript() { let blobUrl; browser.test.onMessage.addListener(async (msg, ...args) => { if (msg == "download.request") { let options = args[0]; if (options.blobme) { let blob = new Blob(options.blobme); delete options.blobme; blobUrl = options.url = window.URL.createObjectURL(blob); } try { let id = await browser.downloads.download(options); browser.test.sendMessage("download.done", {status: "success", id}); } catch (error) { browser.test.sendMessage("download.done", {status: "error", errmsg: error.message}); } } else if (msg == "killTheBlob") { window.URL.revokeObjectURL(blobUrl); blobUrl = null; } }); browser.test.sendMessage("ready"); } // This function is a bit of a sledgehammer, it looks at every download // the browser knows about and waits for all active downloads to complete. // But we only start one at a time and only do a handful in total, so // this lets us test download() without depending on anything else. async function waitForDownloads() { let list = await Downloads.getList(Downloads.ALL); let downloads = await list.getAll(); let inprogress = downloads.filter(dl => !dl.stopped); return Promise.all(inprogress.map(dl => dl.whenSucceeded())); } // Create a file in the downloads directory. function touch(filename) { let file = downloadDir.clone(); file.append(filename); file.create(Ci.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE); } // Remove a file in the downloads directory. function remove(filename, recursive = false) { let file = downloadDir.clone(); file.append(filename); file.remove(recursive); } add_task(function* test_downloads() { setup(); let extension = ExtensionTestUtils.loadExtension({ background: `(${backgroundScript})()`, manifest: { permissions: ["downloads"], }, }); function download(options) { extension.sendMessage("download.request", options); return extension.awaitMessage("download.done"); } async function testDownload(options, localFile, expectedSize, description) { let msg = await download(options); equal(msg.status, "success", `downloads.download() works with ${description}`); await waitForDownloads(); let localPath = downloadDir.clone(); let parts = Array.isArray(localFile) ? localFile : [localFile]; parts.map(p => localPath.append(p)); equal(localPath.fileSize, expectedSize, "Downloaded file has expected size"); localPath.remove(false); } yield extension.startup(); yield extension.awaitMessage("ready"); do_print("extension started"); // Call download() with just the url property. yield testDownload({url: FILE_URL}, FILE_NAME, FILE_LEN, "just source"); // Call download() with a filename property. yield testDownload({ url: FILE_URL, filename: "newpath.txt", }, "newpath.txt", FILE_LEN, "source and filename"); // Call download() with a filename with subdirs. yield testDownload({ url: FILE_URL, filename: "sub/dir/file", }, ["sub", "dir", "file"], FILE_LEN, "source and filename with subdirs"); // Call download() with a filename with existing subdirs. yield testDownload({ url: FILE_URL, filename: "sub/dir/file2", }, ["sub", "dir", "file2"], FILE_LEN, "source and filename with existing subdirs"); // Only run Windows path separator test on Windows. if (WINDOWS) { // Call download() with a filename with Windows path separator. yield testDownload({ url: FILE_URL, filename: "sub\\dir\\file3", }, ["sub", "dir", "file3"], FILE_LEN, "filename with Windows path separator"); } remove("sub", true); // Call download(), filename with subdir, skipping parts. yield testDownload({ url: FILE_URL, filename: "skip//part", }, ["skip", "part"], FILE_LEN, "source, filename, with subdir, skipping parts"); remove("skip", true); // Check conflictAction of "uniquify". touch(FILE_NAME); yield testDownload({ url: FILE_URL, conflictAction: "uniquify", }, FILE_NAME_UNIQUE, FILE_LEN, "conflictAction=uniquify"); // todo check that preexisting file was not modified? remove(FILE_NAME); // Check conflictAction of "overwrite". touch(FILE_NAME); yield testDownload({ url: FILE_URL, conflictAction: "overwrite", }, FILE_NAME, FILE_LEN, "conflictAction=overwrite"); // Try to download in invalid url yield download({url: "this is not a valid URL"}).then(msg => { equal(msg.status, "error", "downloads.download() fails with invalid url"); ok(/not a valid URL/.test(msg.errmsg), "error message for invalid url is correct"); }); // Try to download to an empty path. yield download({ url: FILE_URL, filename: "", }).then(msg => { equal(msg.status, "error", "downloads.download() fails with empty filename"); equal(msg.errmsg, "filename must not be empty", "error message for empty filename is correct"); }); // Try to download to an absolute path. const absolutePath = OS.Path.join(WINDOWS ? "\\tmp" : "/tmp", "file_download.txt"); yield download({ url: FILE_URL, filename: absolutePath, }).then(msg => { equal(msg.status, "error", "downloads.download() fails with absolute filename"); equal(msg.errmsg, "filename must not be an absolute path", `error message for absolute path (${absolutePath}) is correct`); }); if (WINDOWS) { yield download({ url: FILE_URL, filename: "C:\\file_download.txt", }).then(msg => { equal(msg.status, "error", "downloads.download() fails with absolute filename"); equal(msg.errmsg, "filename must not be an absolute path", "error message for absolute path with drive letter is correct"); }); } // Try to download to a relative path containing .. yield download({ url: FILE_URL, filename: OS.Path.join("..", "file_download.txt"), }).then(msg => { equal(msg.status, "error", "downloads.download() fails with back-references"); equal(msg.errmsg, "filename must not contain back-references (..)", "error message for back-references is correct"); }); // Try to download to a long relative path containing .. yield download({ url: FILE_URL, filename: OS.Path.join("foo", "..", "..", "file_download.txt"), }).then(msg => { equal(msg.status, "error", "downloads.download() fails with back-references"); equal(msg.errmsg, "filename must not contain back-references (..)", "error message for back-references is correct"); }); // Try to download a blob url const BLOB_STRING = "Hello, world"; yield testDownload({ blobme: [BLOB_STRING], filename: FILE_NAME, }, FILE_NAME, BLOB_STRING.length, "blob url"); extension.sendMessage("killTheBlob"); // Try to download a blob url without a given filename yield testDownload({ blobme: [BLOB_STRING], }, "download", BLOB_STRING.length, "blob url with no filename"); extension.sendMessage("killTheBlob"); yield extension.unload(); }); add_task(function* test_download_post() { const server = createHttpServer(); const url = `http://localhost:${server.identity.primaryPort}/post-log`; let received; server.registerPathHandler("/post-log", request => { received = request; }); // Confirm received vs. expected values. function confirm(method, headers = {}, body) { equal(received.method, method, "method is correct"); for (let name in headers) { ok(received.hasHeader(name), `header ${name} received`); equal(received.getHeader(name), headers[name], `header ${name} is correct`); } if (body) { const str = NetUtil.readInputStreamToString(received.bodyInputStream, received.bodyInputStream.available()); equal(str, body, "body is correct"); } } function background() { browser.test.onMessage.addListener(async options => { try { await 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) { options.url = url; options.conflictAction = "overwrite"; extension.sendMessage(options); return extension.awaitMessage("done"); } // Test method option. let result = yield download({}); ok(result.ok, "download works without the method option, defaults to GET"); confirm("GET"); result = yield download({method: "PUT"}); ok(!result.ok, "download rejected with PUT method"); ok(/method: Invalid enumeration/.test(result.err), "descriptive error message"); result = yield download({method: "POST"}); ok(result.ok, "download works with POST method"); confirm("POST"); // Test body option values. result = yield download({body: []}); ok(!result.ok, "download rejected because of non-string body"); ok(/body: Expected string/.test(result.err), "descriptive error message"); result = yield download({method: "POST", body: "of work"}); ok(result.ok, "download works with POST method and body"); confirm("POST", {"Content-Length": 7}, "of work"); // Test custom headers. result = yield download({headers: [{name: "X-Custom"}]}); ok(!result.ok, "download rejected because of missing header value"); ok(/"value" is required/.test(result.err), "descriptive error message"); result = yield download({headers: [{name: "X-Custom", value: "13"}]}); ok(result.ok, "download works with a custom header"); confirm("GET", {"X-Custom": "13"}); // Test forbidden headers. result = yield download({headers: [{name: "DNT", value: "1"}]}); ok(!result.ok, "download rejected because of forbidden header name DNT"); ok(/Forbidden request header/.test(result.err), "descriptive error message"); result = yield download({headers: [{name: "Proxy-Connection", value: "keep"}]}); ok(!result.ok, "download rejected because of forbidden header name prefix Proxy-"); ok(/Forbidden request header/.test(result.err), "descriptive error message"); result = yield download({headers: [{name: "Sec-ret", value: "13"}]}); ok(!result.ok, "download rejected because of forbidden header name prefix Sec-"); ok(/Forbidden request header/.test(result.err), "descriptive error message"); remove("post-log"); yield extension.unload(); });