summaryrefslogtreecommitdiffstats
path: root/toolkit/components/webextensions/ext-downloads.js
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/webextensions/ext-downloads.js')
-rw-r--r--toolkit/components/webextensions/ext-downloads.js799
1 files changed, 0 insertions, 799 deletions
diff --git a/toolkit/components/webextensions/ext-downloads.js b/toolkit/components/webextensions/ext-downloads.js
deleted file mode 100644
index 132814ae4..000000000
--- a/toolkit/components/webextensions/ext-downloads.js
+++ /dev/null
@@ -1,799 +0,0 @@
-"use strict";
-
-var {classes: Cc, interfaces: Ci, utils: Cu} = Components;
-
-Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-
-XPCOMUtils.defineLazyModuleGetter(this, "Downloads",
- "resource://gre/modules/Downloads.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "DownloadPaths",
- "resource://gre/modules/DownloadPaths.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "OS",
- "resource://gre/modules/osfile.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
- "resource://gre/modules/FileUtils.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
- "resource://gre/modules/NetUtil.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "EventEmitter",
- "resource://devtools/shared/event-emitter.js");
-
-Cu.import("resource://gre/modules/ExtensionUtils.jsm");
-const {
- ignoreEvent,
- normalizeTime,
- runSafeSync,
- SingletonEventManager,
- PlatformInfo,
-} = ExtensionUtils;
-
-const DOWNLOAD_ITEM_FIELDS = ["id", "url", "referrer", "filename", "incognito",
- "danger", "mime", "startTime", "endTime",
- "estimatedEndTime", "state",
- "paused", "canResume", "error",
- "bytesReceived", "totalBytes",
- "fileSize", "exists",
- "byExtensionId", "byExtensionName"];
-
-// Fields that we generate onChanged events for.
-const DOWNLOAD_ITEM_CHANGE_FIELDS = ["endTime", "state", "paused", "canResume",
- "error", "exists"];
-
-// From https://fetch.spec.whatwg.org/#forbidden-header-name
-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"];
-
-const FORBIDDEN_PREFIXES = /^PROXY-|^SEC-/i;
-
-class DownloadItem {
- constructor(id, download, extension) {
- this.id = id;
- this.download = download;
- this.extension = extension;
- this.prechange = {};
- }
-
- get url() { return this.download.source.url; }
- get referrer() { return this.download.source.referrer; }
- get filename() { return this.download.target.path; }
- get incognito() { return this.download.source.isPrivate; }
- get danger() { return "safe"; } // TODO
- get mime() { return this.download.contentType; }
- get startTime() { return this.download.startTime; }
- get endTime() { return null; } // TODO
- get estimatedEndTime() { return null; } // TODO
- get state() {
- if (this.download.succeeded) {
- return "complete";
- }
- if (this.download.canceled) {
- return "interrupted";
- }
- return "in_progress";
- }
- get paused() {
- return this.download.canceled && this.download.hasPartialData && !this.download.error;
- }
- get canResume() {
- return (this.download.stopped || this.download.canceled) &&
- this.download.hasPartialData && !this.download.error;
- }
- get error() {
- if (!this.download.stopped || this.download.succeeded) {
- return null;
- }
- // TODO store this instead of calculating it
-
- if (this.download.error) {
- if (this.download.error.becauseSourceFailed) {
- return "NETWORK_FAILED"; // TODO
- }
- if (this.download.error.becauseTargetFailed) {
- return "FILE_FAILED"; // TODO
- }
- return "CRASH";
- }
- return "USER_CANCELED";
- }
- get bytesReceived() {
- return this.download.currentBytes;
- }
- get totalBytes() {
- return this.download.hasProgress ? this.download.totalBytes : -1;
- }
- get fileSize() {
- // todo: this is supposed to be post-compression
- return this.download.succeeded ? this.download.target.size : -1;
- }
- get exists() { return this.download.target.exists; }
- get byExtensionId() { return this.extension ? this.extension.id : undefined; }
- get byExtensionName() { return this.extension ? this.extension.name : undefined; }
-
- /**
- * Create a cloneable version of this object by pulling all the
- * fields into simple properties (instead of getters).
- *
- * @returns {object} A DownloadItem with flat properties,
- * suitable for cloning.
- */
- serialize() {
- let obj = {};
- for (let field of DOWNLOAD_ITEM_FIELDS) {
- obj[field] = this[field];
- }
- if (obj.startTime) {
- obj.startTime = obj.startTime.toISOString();
- }
- return obj;
- }
-
- // When a change event fires, handlers can look at how an individual
- // field changed by comparing item.fieldname with item.prechange.fieldname.
- // After all handlers have been invoked, this gets called to store the
- // current values of all fields ahead of the next event.
- _change() {
- for (let field of DOWNLOAD_ITEM_CHANGE_FIELDS) {
- this.prechange[field] = this[field];
- }
- }
-}
-
-
-// DownloadMap maps back and forth betwen the numeric identifiers used in
-// the downloads WebExtension API and a Download object from the Downloads jsm.
-// todo: make id and extension info persistent (bug 1247794)
-const DownloadMap = {
- currentId: 0,
- loadPromise: null,
-
- // Maps numeric id -> DownloadItem
- byId: new Map(),
-
- // Maps Download object -> DownloadItem
- byDownload: new WeakMap(),
-
- lazyInit() {
- if (this.loadPromise == null) {
- EventEmitter.decorate(this);
- this.loadPromise = Downloads.getList(Downloads.ALL).then(list => {
- let self = this;
- return list.addView({
- onDownloadAdded(download) {
- const item = self.newFromDownload(download, null);
- self.emit("create", item);
- },
-
- onDownloadRemoved(download) {
- const item = self.byDownload.get(download);
- if (item != null) {
- self.emit("erase", item);
- self.byDownload.delete(download);
- self.byId.delete(item.id);
- }
- },
-
- onDownloadChanged(download) {
- const item = self.byDownload.get(download);
- if (item == null) {
- Cu.reportError("Got onDownloadChanged for unknown download object");
- } else {
- // We get the first one of these when the download is started.
- // In this case, don't emit anything, just initialize prechange.
- if (Object.keys(item.prechange).length > 0) {
- self.emit("change", item);
- }
- item._change();
- }
- },
- }).then(() => list.getAll())
- .then(downloads => {
- downloads.forEach(download => {
- this.newFromDownload(download, null);
- });
- })
- .then(() => list);
- });
- }
- return this.loadPromise;
- },
-
- getDownloadList() {
- return this.lazyInit();
- },
-
- getAll() {
- return this.lazyInit().then(() => this.byId.values());
- },
-
- fromId(id) {
- const download = this.byId.get(id);
- if (!download) {
- throw new Error(`Invalid download id ${id}`);
- }
- return download;
- },
-
- newFromDownload(download, extension) {
- if (this.byDownload.has(download)) {
- return this.byDownload.get(download);
- }
-
- const id = ++this.currentId;
- let item = new DownloadItem(id, download, extension);
- this.byId.set(id, item);
- this.byDownload.set(download, item);
- return item;
- },
-
- erase(item) {
- // This will need to get more complicated for bug 1255507 but for now we
- // only work with downloads in the DownloadList from getAll()
- return this.getDownloadList().then(list => {
- list.remove(item.download);
- });
- },
-};
-
-// Create a callable function that filters a DownloadItem based on a
-// query object of the type passed to search() or erase().
-function downloadQuery(query) {
- let queryTerms = [];
- let queryNegativeTerms = [];
- if (query.query != null) {
- for (let term of query.query) {
- if (term[0] == "-") {
- queryNegativeTerms.push(term.slice(1).toLowerCase());
- } else {
- queryTerms.push(term.toLowerCase());
- }
- }
- }
-
- function normalizeDownloadTime(arg, before) {
- if (arg == null) {
- return before ? Number.MAX_VALUE : 0;
- }
- return normalizeTime(arg).getTime();
- }
-
- const startedBefore = normalizeDownloadTime(query.startedBefore, true);
- const startedAfter = normalizeDownloadTime(query.startedAfter, false);
- // const endedBefore = normalizeDownloadTime(query.endedBefore, true);
- // const endedAfter = normalizeDownloadTime(query.endedAfter, false);
-
- const totalBytesGreater = query.totalBytesGreater || 0;
- const totalBytesLess = (query.totalBytesLess != null)
- ? query.totalBytesLess : Number.MAX_VALUE;
-
- // Handle options for which we can have a regular expression and/or
- // an explicit value to match.
- function makeMatch(regex, value, field) {
- if (value == null && regex == null) {
- return input => true;
- }
-
- let re;
- try {
- re = new RegExp(regex || "", "i");
- } catch (err) {
- throw new Error(`Invalid ${field}Regex: ${err.message}`);
- }
- if (value == null) {
- return input => re.test(input);
- }
-
- value = value.toLowerCase();
- if (re.test(value)) {
- return input => (value == input);
- }
- return input => false;
- }
-
- const matchFilename = makeMatch(query.filenameRegex, query.filename, "filename");
- const matchUrl = makeMatch(query.urlRegex, query.url, "url");
-
- return function(item) {
- const url = item.url.toLowerCase();
- const filename = item.filename.toLowerCase();
-
- if (!queryTerms.every(term => url.includes(term) || filename.includes(term))) {
- return false;
- }
-
- if (queryNegativeTerms.some(term => url.includes(term) || filename.includes(term))) {
- return false;
- }
-
- if (!matchFilename(filename) || !matchUrl(url)) {
- return false;
- }
-
- if (!item.startTime) {
- if (query.startedBefore != null || query.startedAfter != null) {
- return false;
- }
- } else if (item.startTime > startedBefore || item.startTime < startedAfter) {
- return false;
- }
-
- // todo endedBefore, endedAfter
-
- if (item.totalBytes == -1) {
- if (query.totalBytesGreater != null || query.totalBytesLess != null) {
- return false;
- }
- } else if (item.totalBytes <= totalBytesGreater || item.totalBytes >= totalBytesLess) {
- return false;
- }
-
- // todo: include danger
- const SIMPLE_ITEMS = ["id", "mime", "startTime", "endTime", "state",
- "paused", "error",
- "bytesReceived", "totalBytes", "fileSize", "exists"];
- for (let field of SIMPLE_ITEMS) {
- if (query[field] != null && item[field] != query[field]) {
- return false;
- }
- }
-
- return true;
- };
-}
-
-function queryHelper(query) {
- let matchFn;
- try {
- matchFn = downloadQuery(query);
- } catch (err) {
- return Promise.reject({message: err.message});
- }
-
- let compareFn;
- if (query.orderBy != null) {
- const fields = query.orderBy.map(field => field[0] == "-"
- ? {reverse: true, name: field.slice(1)}
- : {reverse: false, name: field});
-
- for (let field of fields) {
- if (!DOWNLOAD_ITEM_FIELDS.includes(field.name)) {
- return Promise.reject({message: `Invalid orderBy field ${field.name}`});
- }
- }
-
- compareFn = (dl1, dl2) => {
- for (let field of fields) {
- const val1 = dl1[field.name];
- const val2 = dl2[field.name];
-
- if (val1 < val2) {
- return field.reverse ? 1 : -1;
- } else if (val1 > val2) {
- return field.reverse ? -1 : 1;
- }
- }
- return 0;
- };
- }
-
- return DownloadMap.getAll().then(downloads => {
- if (compareFn) {
- downloads = Array.from(downloads);
- downloads.sort(compareFn);
- }
- let results = [];
- for (let download of downloads) {
- if (query.limit && results.length >= query.limit) {
- break;
- }
- if (matchFn(download)) {
- results.push(download);
- }
- }
- return results;
- });
-}
-
-extensions.registerSchemaAPI("downloads", "addon_parent", context => {
- let {extension} = context;
- return {
- downloads: {
- download(options) {
- let {filename} = options;
- if (filename && PlatformInfo.os === "win") {
- // cross platform javascript code uses "/"
- filename = filename.replace(/\//g, "\\");
- }
-
- if (filename != null) {
- if (filename.length == 0) {
- return Promise.reject({message: "filename must not be empty"});
- }
-
- let path = OS.Path.split(filename);
- if (path.absolute) {
- return Promise.reject({message: "filename must not be an absolute path"});
- }
-
- if (path.components.some(component => component == "..")) {
- return Promise.reject({message: "filename must not contain back-references (..)"});
- }
- }
-
- if (options.conflictAction == "prompt") {
- // TODO
- return Promise.reject({message: "conflictAction prompt not yet implemented"});
- }
-
- if (options.headers) {
- for (let {name} of options.headers) {
- if (FORBIDDEN_HEADERS.includes(name.toUpperCase()) || name.match(FORBIDDEN_PREFIXES)) {
- return Promise.reject({message: "Forbidden request header name"});
- }
- }
- }
-
- // Handle method, headers and body options.
- function adjustChannel(channel) {
- if (channel instanceof Ci.nsIHttpChannel) {
- const method = options.method || "GET";
- channel.requestMethod = method;
-
- if (options.headers) {
- for (let {name, value} of options.headers) {
- channel.setRequestHeader(name, value, false);
- }
- }
-
- if (options.body != null) {
- const stream = Cc["@mozilla.org/io/string-input-stream;1"]
- .createInstance(Ci.nsIStringInputStream);
- stream.setData(options.body, options.body.length);
-
- channel.QueryInterface(Ci.nsIUploadChannel2);
- channel.explicitSetUploadStream(stream, null, -1, method, false);
- }
- }
- return Promise.resolve();
- }
-
- function createTarget(downloadsDir) {
- let target;
- if (filename) {
- target = OS.Path.join(downloadsDir, filename);
- } else {
- let uri = NetUtil.newURI(options.url);
-
- let remote = "download";
- if (uri instanceof Ci.nsIURL) {
- remote = uri.fileName;
- }
- target = OS.Path.join(downloadsDir, remote);
- }
-
- // Create any needed subdirectories if required by filename.
- const dir = OS.Path.dirname(target);
- return OS.File.makeDir(dir, {from: downloadsDir}).then(() => {
- return OS.File.exists(target);
- }).then(exists => {
- // This has a race, something else could come along and create
- // the file between this test and them time the download code
- // creates the target file. But we can't easily fix it without
- // modifying DownloadCore so we live with it for now.
- if (exists) {
- switch (options.conflictAction) {
- case "uniquify":
- default:
- target = DownloadPaths.createNiceUniqueFile(new FileUtils.File(target)).path;
- break;
-
- case "overwrite":
- break;
- }
- }
- }).then(() => {
- if (!options.saveAs) {
- return Promise.resolve(target);
- }
-
- // Setup the file picker Save As dialog.
- const picker = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
- const window = Services.wm.getMostRecentWindow("navigator:browser");
- picker.init(window, null, Ci.nsIFilePicker.modeSave);
- picker.displayDirectory = new FileUtils.File(dir);
- picker.appendFilters(Ci.nsIFilePicker.filterAll);
- picker.defaultString = OS.Path.basename(target);
-
- // Open the dialog and resolve/reject with the result.
- return new Promise((resolve, reject) => {
- picker.open(result => {
- if (result === Ci.nsIFilePicker.returnCancel) {
- reject({message: "Download canceled by the user"});
- } else {
- resolve(picker.file.path);
- }
- });
- });
- });
- }
-
- let download;
- return Downloads.getPreferredDownloadsDirectory()
- .then(downloadsDir => createTarget(downloadsDir))
- .then(target => {
- const source = {
- url: options.url,
- };
-
- if (options.method || options.headers || options.body) {
- source.adjustChannel = adjustChannel;
- }
-
- 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();
-
- const item = DownloadMap.newFromDownload(download, extension);
- return item.id;
- });
- },
-
- removeFile(id) {
- return DownloadMap.lazyInit().then(() => {
- let item;
- try {
- item = DownloadMap.fromId(id);
- } catch (err) {
- return Promise.reject({message: `Invalid download id ${id}`});
- }
- if (item.state !== "complete") {
- return Promise.reject({message: `Cannot remove incomplete download id ${id}`});
- }
- return OS.File.remove(item.filename, {ignoreAbsent: false}).catch((err) => {
- return Promise.reject({message: `Could not remove download id ${item.id} because the file doesn't exist`});
- });
- });
- },
-
- search(query) {
- return queryHelper(query)
- .then(items => items.map(item => item.serialize()));
- },
-
- pause(id) {
- return DownloadMap.lazyInit().then(() => {
- let item;
- try {
- item = DownloadMap.fromId(id);
- } catch (err) {
- return Promise.reject({message: `Invalid download id ${id}`});
- }
- if (item.state != "in_progress") {
- return Promise.reject({message: `Download ${id} cannot be paused since it is in state ${item.state}`});
- }
-
- return item.download.cancel();
- });
- },
-
- resume(id) {
- return DownloadMap.lazyInit().then(() => {
- let item;
- try {
- item = DownloadMap.fromId(id);
- } catch (err) {
- return Promise.reject({message: `Invalid download id ${id}`});
- }
- if (!item.canResume) {
- return Promise.reject({message: `Download ${id} cannot be resumed`});
- }
-
- return item.download.start();
- });
- },
-
- cancel(id) {
- return DownloadMap.lazyInit().then(() => {
- let item;
- try {
- item = DownloadMap.fromId(id);
- } catch (err) {
- return Promise.reject({message: `Invalid download id ${id}`});
- }
- if (item.download.succeeded) {
- return Promise.reject({message: `Download ${id} is already complete`});
- }
- return item.download.finalize(true);
- });
- },
-
- showDefaultFolder() {
- Downloads.getPreferredDownloadsDirectory().then(dir => {
- let dirobj = new FileUtils.File(dir);
- if (dirobj.isDirectory()) {
- dirobj.launch();
- } else {
- throw new Error(`Download directory ${dirobj.path} is not actually a directory`);
- }
- }).catch(Cu.reportError);
- },
-
- erase(query) {
- return queryHelper(query).then(items => {
- let results = [];
- let promises = [];
- for (let item of items) {
- promises.push(DownloadMap.erase(item));
- results.push(item.id);
- }
- return Promise.all(promises).then(() => results);
- });
- },
-
- open(downloadId) {
- return DownloadMap.lazyInit().then(() => {
- let download = DownloadMap.fromId(downloadId).download;
- if (download.succeeded) {
- return download.launch();
- }
- return Promise.reject({message: "Download has not completed."});
- }).catch((error) => {
- return Promise.reject({message: error.message});
- });
- },
-
- show(downloadId) {
- return DownloadMap.lazyInit().then(() => {
- let download = DownloadMap.fromId(downloadId);
- return download.download.showContainingDirectory();
- }).then(() => {
- return true;
- }).catch(error => {
- return Promise.reject({message: error.message});
- });
- },
-
- getFileIcon(downloadId, options) {
- return DownloadMap.lazyInit().then(() => {
- let size = options && options.size ? options.size : 32;
- let download = DownloadMap.fromId(downloadId).download;
- let pathPrefix = "";
- let path;
-
- if (download.succeeded) {
- let file = FileUtils.File(download.target.path);
- path = Services.io.newFileURI(file).spec;
- } else {
- path = OS.Path.basename(download.target.path);
- pathPrefix = "//";
- }
-
- return new Promise((resolve, reject) => {
- let chromeWebNav = Services.appShell.createWindowlessBrowser(true);
- chromeWebNav
- .QueryInterface(Ci.nsIInterfaceRequestor)
- .getInterface(Ci.nsIDocShell)
- .createAboutBlankContentViewer(Services.scriptSecurityManager.getSystemPrincipal());
-
- let img = chromeWebNav.document.createElement("img");
- img.width = size;
- img.height = size;
-
- let handleLoad;
- let handleError;
- const cleanup = () => {
- img.removeEventListener("load", handleLoad);
- img.removeEventListener("error", handleError);
- chromeWebNav.close();
- chromeWebNav = null;
- };
-
- handleLoad = () => {
- let canvas = chromeWebNav.document.createElement("canvas");
- canvas.width = size;
- canvas.height = size;
- let context = canvas.getContext("2d");
- context.drawImage(img, 0, 0, size, size);
- let dataURL = canvas.toDataURL("image/png");
- cleanup();
- resolve(dataURL);
- };
-
- handleError = (error) => {
- Cu.reportError(error);
- cleanup();
- reject(new Error("An unexpected error occurred"));
- };
-
- img.addEventListener("load", handleLoad);
- img.addEventListener("error", handleError);
- img.src = `moz-icon:${pathPrefix}${path}?size=${size}`;
- });
- }).catch((error) => {
- return Promise.reject({message: error.message});
- });
- },
-
- // When we do setShelfEnabled(), check for additional "downloads.shelf" permission.
- // i.e.:
- // setShelfEnabled(enabled) {
- // if (!extension.hasPermission("downloads.shelf")) {
- // throw new context.cloneScope.Error("Permission denied because 'downloads.shelf' permission is missing.");
- // }
- // ...
- // }
-
- onChanged: new SingletonEventManager(context, "downloads.onChanged", fire => {
- const handler = (what, item) => {
- let changes = {};
- const noundef = val => (val === undefined) ? null : val;
- DOWNLOAD_ITEM_CHANGE_FIELDS.forEach(fld => {
- if (item[fld] != item.prechange[fld]) {
- changes[fld] = {
- previous: noundef(item.prechange[fld]),
- current: noundef(item[fld]),
- };
- }
- });
- if (Object.keys(changes).length > 0) {
- changes.id = item.id;
- runSafeSync(context, fire, changes);
- }
- };
-
- let registerPromise = DownloadMap.getDownloadList().then(() => {
- DownloadMap.on("change", handler);
- });
- return () => {
- registerPromise.then(() => {
- DownloadMap.off("change", handler);
- });
- };
- }).api(),
-
- onCreated: new SingletonEventManager(context, "downloads.onCreated", fire => {
- const handler = (what, item) => {
- runSafeSync(context, fire, item.serialize());
- };
- let registerPromise = DownloadMap.getDownloadList().then(() => {
- DownloadMap.on("create", handler);
- });
- return () => {
- registerPromise.then(() => {
- DownloadMap.off("create", handler);
- });
- };
- }).api(),
-
- onErased: new SingletonEventManager(context, "downloads.onErased", fire => {
- const handler = (what, item) => {
- runSafeSync(context, fire, item.id);
- };
- let registerPromise = DownloadMap.getDownloadList().then(() => {
- DownloadMap.on("erase", handler);
- });
- return () => {
- registerPromise.then(() => {
- DownloadMap.off("erase", handler);
- });
- };
- }).api(),
-
- onDeterminingFilename: ignoreEvent(context, "downloads.onDeterminingFilename"),
- },
- };
-});