313 lines
8.6 KiB
JavaScript
313 lines
8.6 KiB
JavaScript
/* 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";
|
|
|
|
ChromeUtils.defineESModuleGetters(this, {
|
|
DownloadPaths: "resource://gre/modules/DownloadPaths.sys.mjs",
|
|
DownloadTracker: "resource://gre/modules/GeckoViewWebExtension.sys.mjs",
|
|
});
|
|
|
|
Cu.importGlobalProperties(["PathUtils"]);
|
|
|
|
var { ignoreEvent } = ExtensionCommon;
|
|
|
|
const REQUEST_DOWNLOAD_MESSAGE = "GeckoView:WebExtension:Download";
|
|
|
|
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",
|
|
"TE",
|
|
"TRAILER",
|
|
"TRANSFER-ENCODING",
|
|
"UPGRADE",
|
|
"VIA",
|
|
];
|
|
|
|
const FORBIDDEN_PREFIXES = /^PROXY-|^SEC-/i;
|
|
|
|
const State = {
|
|
IN_PROGRESS: "in_progress",
|
|
INTERRUPTED: "interrupted",
|
|
COMPLETE: "complete",
|
|
};
|
|
|
|
const STATE_MAP = new Map([
|
|
[0, State.IN_PROGRESS],
|
|
[1, State.INTERRUPTED],
|
|
[2, State.COMPLETE],
|
|
]);
|
|
|
|
const INTERRUPT_REASON_MAP = new Map([
|
|
[0, undefined],
|
|
[1, "FILE_FAILED"],
|
|
[2, "FILE_ACCESS_DENIED"],
|
|
[3, "FILE_NO_SPACE"],
|
|
[4, "FILE_NAME_TOO_LONG"],
|
|
[5, "FILE_TOO_LARGE"],
|
|
[6, "FILE_VIRUS_INFECTED"],
|
|
[7, "FILE_TRANSIENT_ERROR"],
|
|
[8, "FILE_BLOCKED"],
|
|
[9, "FILE_SECURITY_CHECK_FAILED"],
|
|
[10, "FILE_TOO_SHORT"],
|
|
[11, "NETWORK_FAILED"],
|
|
[12, "NETWORK_TIMEOUT"],
|
|
[13, "NETWORK_DISCONNECTED"],
|
|
[14, "NETWORK_SERVER_DOWN"],
|
|
[15, "NETWORK_INVALID_REQUEST"],
|
|
[16, "SERVER_FAILED"],
|
|
[17, "SERVER_NO_RANGE"],
|
|
[18, "SERVER_BAD_CONTENT"],
|
|
[19, "SERVER_UNAUTHORIZED"],
|
|
[20, "SERVER_CERT_PROBLEM"],
|
|
[21, "SERVER_FORBIDDEN"],
|
|
[22, "USER_CANCELED"],
|
|
[23, "USER_SHUTDOWN"],
|
|
[24, "CRASH"],
|
|
]);
|
|
|
|
// TODO Bug 1247794: make id and extension info persistent
|
|
class DownloadItem {
|
|
/**
|
|
* Initializes an object that represents a download
|
|
*
|
|
* @param {object} downloadInfo - an object from Java when creating a download
|
|
* @param {object} options - an object passed in to download() function
|
|
* @param {Extension} extension - instance of an extension object
|
|
*/
|
|
constructor(downloadInfo, options, extension) {
|
|
this.id = downloadInfo.id;
|
|
this.url = options.url;
|
|
this.referrer = downloadInfo.referrer || "";
|
|
this.filename = downloadInfo.filename || "";
|
|
this.incognito = options.incognito;
|
|
this.danger = "safe"; // todo; not implemented in desktop either
|
|
this.mime = downloadInfo.mime || "";
|
|
this.startTime = downloadInfo.startTime;
|
|
this.state = STATE_MAP.get(downloadInfo.state);
|
|
this.paused = downloadInfo.paused;
|
|
this.canResume = downloadInfo.canResume;
|
|
this.bytesReceived = downloadInfo.bytesReceived;
|
|
this.totalBytes = downloadInfo.totalBytes;
|
|
this.fileSize = downloadInfo.fileSize;
|
|
this.exists = downloadInfo.exists;
|
|
this.byExtensionId = extension?.id;
|
|
this.byExtensionName = extension?.name;
|
|
}
|
|
|
|
/**
|
|
* This function updates the download item it was called on.
|
|
*
|
|
* @param {object} data that arrived from the app (Java)
|
|
* @returns {object | null} an object of <a href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/downloads/onChanged#downloaddelta>downloadDelta type</a>
|
|
*/
|
|
update(data) {
|
|
const { downloadItemId } = data;
|
|
const delta = {};
|
|
|
|
data.state = STATE_MAP.get(data.state);
|
|
data.error = INTERRUPT_REASON_MAP.get(data.error);
|
|
delete data.downloadItemId;
|
|
|
|
let changed = false;
|
|
for (const prop in data) {
|
|
const current = data[prop] ?? null;
|
|
const previous = this[prop] ?? null;
|
|
if (current !== previous) {
|
|
delta[prop] = { current, previous };
|
|
this[prop] = current;
|
|
changed = true;
|
|
}
|
|
}
|
|
|
|
// Don't send empty onChange events
|
|
if (!changed) {
|
|
return null;
|
|
}
|
|
|
|
delta.id = downloadItemId;
|
|
|
|
return delta;
|
|
}
|
|
}
|
|
|
|
this.downloads = class extends ExtensionAPIPersistent {
|
|
PERSISTENT_EVENTS = {
|
|
onChanged({ fire }) {
|
|
const listener = (eventName, event) => {
|
|
const { delta, downloadItem } = event;
|
|
const { extension } = this;
|
|
if (extension.privateBrowsingAllowed || !downloadItem.incognito) {
|
|
fire.async(delta);
|
|
}
|
|
};
|
|
DownloadTracker.on("download-changed", listener);
|
|
|
|
return {
|
|
unregister() {
|
|
DownloadTracker.off("download-changed", listener);
|
|
},
|
|
convert(_fire) {
|
|
fire = _fire;
|
|
},
|
|
};
|
|
},
|
|
};
|
|
|
|
getAPI(context) {
|
|
const { extension } = context;
|
|
return {
|
|
downloads: {
|
|
download(options) {
|
|
// the validation checks should be kept in sync with the toolkit implementation
|
|
let { filename } = options;
|
|
if (filename != null) {
|
|
if (!filename.length) {
|
|
return Promise.reject({ message: "filename must not be empty" });
|
|
}
|
|
|
|
if (PathUtils.isAbsolute(filename)) {
|
|
return Promise.reject({
|
|
message: "filename must not be an absolute path",
|
|
});
|
|
}
|
|
|
|
// % is not permitted but relatively common.
|
|
filename = filename.replaceAll("%", "_");
|
|
|
|
const pathComponents = PathUtils.splitRelative(filename, {
|
|
allowEmpty: true,
|
|
allowCurrentDir: true,
|
|
allowParentDir: true,
|
|
});
|
|
|
|
if (pathComponents.some(component => component == "..")) {
|
|
return Promise.reject({
|
|
message: "filename must not contain back-references (..)",
|
|
});
|
|
}
|
|
|
|
if (
|
|
pathComponents.some((component, i) => {
|
|
const sanitized = DownloadPaths.sanitize(component, {
|
|
compressWhitespaces: false,
|
|
allowDirectoryNames: i < pathComponents.length - 1,
|
|
});
|
|
return component != sanitized;
|
|
})
|
|
) {
|
|
return Promise.reject({
|
|
message: "filename must not contain illegal characters",
|
|
});
|
|
}
|
|
}
|
|
|
|
if (options.incognito && !context.privateBrowsingAllowed) {
|
|
return Promise.reject({
|
|
message: "Private browsing access not allowed",
|
|
});
|
|
}
|
|
|
|
if (options.cookieStoreId != null) {
|
|
// https://bugzilla.mozilla.org/show_bug.cgi?id=1721460
|
|
throw new ExtensionError("Not implemented");
|
|
}
|
|
|
|
if (options.headers) {
|
|
for (const { name } of options.headers) {
|
|
if (
|
|
FORBIDDEN_HEADERS.includes(name.toUpperCase()) ||
|
|
name.match(FORBIDDEN_PREFIXES)
|
|
) {
|
|
return Promise.reject({
|
|
message: "Forbidden request header name",
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
return EventDispatcher.instance
|
|
.sendRequestForResult({
|
|
type: REQUEST_DOWNLOAD_MESSAGE,
|
|
options,
|
|
extensionId: extension.id,
|
|
})
|
|
.then(value => {
|
|
const downloadItem = new DownloadItem(value, options, extension);
|
|
DownloadTracker.addDownloadItem(downloadItem);
|
|
return downloadItem.id;
|
|
});
|
|
},
|
|
|
|
removeFile() {
|
|
throw new ExtensionError("Not implemented");
|
|
},
|
|
|
|
search() {
|
|
throw new ExtensionError("Not implemented");
|
|
},
|
|
|
|
pause() {
|
|
throw new ExtensionError("Not implemented");
|
|
},
|
|
|
|
resume() {
|
|
throw new ExtensionError("Not implemented");
|
|
},
|
|
|
|
cancel() {
|
|
throw new ExtensionError("Not implemented");
|
|
},
|
|
|
|
showDefaultFolder() {
|
|
throw new ExtensionError("Not implemented");
|
|
},
|
|
|
|
erase() {
|
|
throw new ExtensionError("Not implemented");
|
|
},
|
|
|
|
open() {
|
|
throw new ExtensionError("Not implemented");
|
|
},
|
|
|
|
show() {
|
|
throw new ExtensionError("Not implemented");
|
|
},
|
|
|
|
getFileIcon() {
|
|
throw new ExtensionError("Not implemented");
|
|
},
|
|
|
|
onChanged: new EventManager({
|
|
context,
|
|
module: "downloads",
|
|
event: "onChanged",
|
|
extensionApi: this,
|
|
}).api(),
|
|
|
|
onCreated: ignoreEvent(context, "downloads.onCreated"),
|
|
|
|
onErased: ignoreEvent(context, "downloads.onErased"),
|
|
|
|
onDeterminingFilename: ignoreEvent(
|
|
context,
|
|
"downloads.onDeterminingFilename"
|
|
),
|
|
},
|
|
};
|
|
}
|
|
};
|