/* 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"
),
},
};
}
};
Messung V0.5 C=98 H=98 G=97
¤ Dauer der Verarbeitung: 0.4 Sekunden
¤
*© Formatika GbR, Deutschland