27 Commits

Author SHA1 Message Date
76992bd4f4 Version 4.0.11 2019-09-10 09:37:00 +02:00
dccd530475 Typo 2019-09-10 09:33:32 +02:00
f1fa01a0eb Add ja to locale list 2019-09-10 09:30:59 +02:00
7949142ef6 Japanese translation (#83) 2019-09-10 09:29:12 +02:00
af1da8fc0a Fix maximized state being refused with dims
Closes #84
2019-09-10 09:23:43 +02:00
39f4237cde Update Indonesian translation (#80) 2019-09-09 18:07:07 +02:00
b676ed74cd Update locale list 2019-09-08 21:30:21 +02:00
bf474877ca Add Italian translation (#49) 2019-09-08 21:27:42 +02:00
7ee13af238 Version 4.0.10 2019-09-07 22:51:06 +02:00
d488e5874a Correct license header 2019-09-07 22:51:06 +02:00
b1a7c22452 Dismiss tooltip when the selection changes 2019-09-07 22:46:24 +02:00
e928d202ee Use the same base font size on all platforms 2019-09-07 20:13:37 +02:00
c39961d253 Make sure application/octet-stream is not mime-able 2019-09-07 19:39:43 +02:00
c6d11fcd7f Give MimeDB a class name 2019-09-07 19:34:12 +02:00
eb96103478 Sanitiy check detected names if we got a mime 2019-09-07 19:32:51 +02:00
583ccfc7b1 Detect name from query string 2019-09-07 19:27:28 +02:00
e0437718a0 Do not register remove-complete twice 2019-09-07 18:58:27 +02:00
2126ae022b Move preroller to it's own class 2019-09-07 18:27:31 +02:00
2ef39dcb19 Add some minor tests for mime 2019-09-07 17:54:12 +02:00
047c865e76 The things you see only after yu push... 2019-09-07 10:27:01 +02:00
c586cd00cc Switch to @Rob--W's CD parser
I "cleaned" up the library according to my personal preferences.
I tend to do this because in the process I need to develop a deeper
understanding of the code, and not because there was nothing wrong
with it.
I deeply appreciate the work that went into creating this library
and releasing it open source 👍

Closes #72
2019-09-07 10:22:09 +02:00
ee7f470269 Use ranges in preroll to avoid large downloads
This will effectively request the first 2 bytes from a server?
Why 2 bytes? Because a lot of servers are broken when bytes=0-0

Related to #70
2019-09-06 21:56:00 +02:00
f04dda308b Adding Hungarian locale, translation (#69) 2019-09-06 20:55:46 +02:00
071458e262 Switch preroll to GET
Fixes #70
2019-09-06 20:42:00 +02:00
9ffc96de4d Purge ourselves from history and sessions
Closes #66
2019-09-06 09:57:24 +02:00
AC
26e9a5404a Add Shift-Delete keyboard shortcut 2019-09-06 04:38:04 +02:00
f44fe59054 Polishing of some lt locale translations (#67) 2019-09-05 10:35:30 +02:00
22 changed files with 4423 additions and 201 deletions

View File

@ -81,3 +81,9 @@ The javascript library accessing it is licensed under the MIT license.
## whatwg-mimetype
Licensed under MIT
## CDHeaderParser
Licensed under MPL-2
(c) 2017 Rob Wu <rob@robwu.nl> (https://robwu.nl)

View File

@ -5,12 +5,14 @@
"en": "English (US) [en]",
"es": "Español (España) [es]",
"et": "Eesti Keel [et]",
"fr": "Français (FR) [fr]",
"fr": "Français [fr]",
"hu": "Magyar (HU) [hu]",
"id": "Bahasa Indonesia [id]",
"ja": "日本語 (JP) [ja]",
"ko": "한국어 [ko]",
"lt": "Lietuvių [lt]",
"nl": "Nederlands [nl]",
"pl": "Polski (PL) [pl]",
"pl": "Polski [pl]",
"pt": "Português (Brasil) [pt]",
"ru": "Русский [ru]",
"zh_CN": "简体中文 [zh_CN]",

1170
_locales/hu/messages.json Normal file

File diff suppressed because it is too large Load Diff

View File

@ -7,6 +7,18 @@
"message": "id",
"description": "Language code the locale will use, e.g. de or en-GB or pt-BR"
},
"renamer_tags": {
"message": "Penanda Mask Penamaan",
"description": "Mask text; see mask button"
},
"renmask": {
"message": "Mask penamaan",
"description": "Renaming mask (long)"
},
"set_mask": {
"message": "Set Mask Penamaan",
"description": "Menu text; select window"
},
"addpaused": {
"message": "Tambahkan dalam kondisi terpause",
"description": "Action: Add paused"
@ -280,15 +292,15 @@
"description": "Message box title"
},
"filter_expression": {
"message": "Ekspres-Filter",
"message": "Ekspresi",
"description": "Message box label"
},
"filter_label": {
"message": "Label-Filter",
"message": "Label",
"description": "Message box label"
},
"filter_types": {
"message": "Tipe-Filter",
"message": "Tipe",
"description": "Message box label"
},
"filter_type_link": {
@ -320,7 +332,7 @@
"description": "Menu text"
},
"limited_to": {
"message": "Terbatas ke",
"message": "Batasi ke",
"description": "Label text; used in prefs/network"
},
"links": {
@ -362,11 +374,11 @@
"description": "Status text; Used in the mask column, select window"
},
"missing": {
"message": "Tidak Ada",
"message": "Hilang",
"description": "Status text in manager"
},
"move_bottom": {
"message": "Bawah",
"message": "Ke Bawah",
"description": "Action for moving a download to the bottom"
},
"move_down": {
@ -374,7 +386,7 @@
"description": "Action for moving a download down"
},
"move_top": {
"message": "Atas",
"message": "Ke Atas",
"description": "Action for moving a download to the top"
},
"move_up": {
@ -560,7 +572,7 @@
"description": "Menu text"
},
"remove_batch_downloads_question": {
"message": "Hapus semua unduhan dari kumpulan yang sama dengan unduhan terpilih?",
"message": "Hapus semua unduhan dari batch yang sama dengan unduhan terpilih?",
"description": "Messagebox text"
},
"remove_complete_downloads": {
@ -600,7 +612,7 @@
}
},
"remove_domain_downloads": {
"message": "Hapus Domain Ini",
"message": "Hapus Unduhan Dari Domain Ini",
"description": "Menu text"
},
"remove_domain_downloads_question": {
@ -630,7 +642,7 @@
"description": "Messagebox text"
},
"remove_failed_downloads": {
"message": "Gagal Menghapus",
"message": "Hapus Unduhan Gagal",
"description": "Menu text"
},
"remove_failed_downloads_question": {
@ -648,11 +660,11 @@
}
},
"remove_missing": {
"message": "Hapus Unduhan Yang Tidak Ada",
"message": "Hapus Unduhan Yang Hilang",
"description": "Menu text"
},
"remove_missing_downloads_question": {
"message": "Hapus semua unduhan yang tidak ada?",
"message": "Hapus semua unduhan yang hilang?",
"description": "Messagebox text"
},
"remove_paused_downloads": {
@ -664,7 +676,7 @@
"description": "Messagebox text"
},
"remove_selected_complete_downloads": {
"message": "Hapus Yang Selesai Di Pilihan",
"message": "Hapus Yang Selesai Dari Unduhan Terpilih",
"description": "Menu text"
},
"remove_selected_complete_downloads_question": {

1170
_locales/it/messages.json Normal file

File diff suppressed because it is too large Load Diff

1170
_locales/ja/messages.json Normal file

File diff suppressed because it is too large Load Diff

View File

@ -108,7 +108,7 @@
"description": "Media label (short)"
},
"missing": {
"message": "Trūksta",
"message": "Nėra",
"description": "Status text in manager"
},
"NETWORK_FAILED": {
@ -692,15 +692,15 @@
"description": "Preferences/General"
},
"pref_open_manager_on_queue": {
"message": "Atidaryti Menedžerio kortelę, po kai kurių parsisiuntimų atsiradimo eilėje",
"message": "Atidaryti Menedžerio kortelę po parsisiuntimo pridėjimo",
"description": "Preferences/General"
},
"pref_queue_notification": {
"message": "Parodyti pranešimą, kai eilėje atsiranda nauji parsisiuntimai",
"message": "Rodyti pranešimą po naujų parsisiuntimų pridėjimo",
"description": "Preferences/General"
},
"pref_remove_missing_on_init": {
"message": "Pašalinti trūkstamus parsisiuntimus po restarto",
"message": "Šalinti nepavykusius parsisiuntimus po restarto",
"description": "Preferences/General"
},
"pref_show_urls": {
@ -740,7 +740,7 @@
"description": "Window/tab title; Preferences"
},
"queue_finished": {
"message": "Parsisiuntimų eilė baigta",
"message": "Visi parsisiuntimai baigti",
"description": "Notification text"
},
"queued_download": {
@ -862,11 +862,11 @@
}
},
"remove_missing": {
"message": "Išvalyti trūkstamus parsisiuntimus",
"message": "Išvalyti nepavykusius parsisiuntimus",
"description": "Menu text"
},
"remove_missing_downloads_question": {
"message": "Norite išvalyti visus trūkstamus parsisiuntimus?",
"message": "Norite išvalyti visus nepavykusius parsisiuntimus?",
"description": "Messagebox text"
},
"remove_paused_downloads": {

View File

@ -19,6 +19,9 @@ import {
// eslint-disable-next-line no-unused-vars
MenuClickInfo,
CHROME,
runtime,
history,
sessions,
} from "./browser";
import { Bus } from "./bus";
import { filterInSitu } from "./util";
@ -566,6 +569,43 @@ locale.then(() => {
}
(async function init() {
const urlBase = runtime.getURL("");
history.onVisited.addListener(({url}: {url: string}) => {
if (!url || !url.startsWith(urlBase)) {
return;
}
history.deleteUrl({url});
});
const results: {url?: string}[] = await history.search({text: urlBase});
for (const {url} of results) {
if (!url) {
continue;
}
history.deleteUrl({url});
}
if (!CHROME) {
const sessionRemover = async () => {
for (const s of await sessions.getRecentlyClosed()) {
if (s.tab) {
if (s.tab.url.startsWith(urlBase)) {
await sessions.forgetClosedTab(s.tab.windowId, s.tab.sessionId);
}
continue;
}
if (!s.window || !s.window.tabs || s.window.tabs.length > 1) {
continue;
}
const [tab] = s.window.tabs;
if (tab.url.startsWith(urlBase)) {
await sessions.forgetClosedWindow(s.window.sessionId);
}
}
};
sessions.onChanged.addListener(sessionRemover);
await sessionRemover();
}
await Prefs.set("last-run", new Date());
Prefs.get("global-turbo", false).then(v => adjustAction(v));
Prefs.on("global-turbo", (prefs, key, value) => {

View File

@ -39,13 +39,15 @@ export interface RawPort {
postMessage: (message: any) => void;
}
export const {extension} = polyfill;
export const {notifications} = polyfill;
export const {browserAction} = polyfill;
export const {contextMenus} = polyfill;
export const {downloads} = polyfill;
export const {extension} = polyfill;
export const {history} = polyfill;
export const {menus} = polyfill;
export const {notifications} = polyfill;
export const {runtime} = polyfill;
export const {sessions} = polyfill;
export const {storage} = polyfill;
export const {tabs} = polyfill;
export const {webNavigation} = polyfill;

230
lib/cdheaderparser.ts Normal file
View File

@ -0,0 +1,230 @@
/**
* (c) 2017 Rob Wu <rob@robwu.nl> (https://robwu.nl)
* 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";
/* eslint-disable max-len,no-magic-numbers */
// License: MPL-2
/**
* This typescript port was done by Nils Maier based on
* https://github.com/Rob--W/open-in-browser/blob/83248155b633ed41bc9cdb1205042653e644abd2/extension/content-disposition.js
* Special thanks goes to Rob doing all the heavy lifting and putting
* it together in a reuseable, open source'd library.
*/
const R_RFC6266 = /(?:^|;)\s*filename\*\s*=\s*([^";\s][^;\s]*|"(?:[^"\\]|\\"?)+"?)/i;
const R_RFC5987 = /(?:^|;)\s*filename\s*=\s*([^";\s][^;\s]*|"(?:[^"\\]|\\"?)+"?)/i;
function unquoteRFC2616(value: string) {
if (!value.startsWith("\"")) {
return value;
}
const parts = value.slice(1).split("\\\"");
// Find the first unescaped " and terminate there.
for (let i = 0; i < parts.length; ++i) {
const quotindex = parts[i].indexOf("\"");
if (quotindex !== -1) {
parts[i] = parts[i].slice(0, quotindex);
// Truncate and stop the iteration.
parts.length = i + 1;
}
parts[i] = parts[i].replace(/\\(.)/g, "$1");
}
value = parts.join("\"");
return value;
}
export class CDHeaderParser {
private needsFixup: boolean;
// We need to keep this per instance, because of the global flag.
// Hence we need to reset it after a use.
private R_MULTI = /(?:^|;)\s*filename\*((?!0\d)\d+)(\*?)\s*=\s*([^";\s][^;\s]*|"(?:[^"\\]|\\"?)+"?)/gi;
/**
* Parse a content-disposition header, with relaxed spec tolerance
*
* @param {string} header Header to parse
* @returns {string} Parsed header
*/
parse(header: string) {
this.needsFixup = true;
// filename*=ext-value ("ext-value" from RFC 5987, referenced by RFC 6266).
{
const match = R_RFC6266.exec(header);
if (match) {
const [, tmp] = match;
let filename = unquoteRFC2616(tmp);
filename = unescape(filename);
filename = this.decodeRFC5897(filename);
filename = this.decodeRFC2047(filename);
return this.maybeFixupEncoding(filename);
}
}
// Continuations (RFC 2231 section 3, referenced by RFC 5987 section 3.1).
// filename*n*=part
// filename*n=part
{
const tmp = this.getParamRFC2231(header);
if (tmp) {
// RFC 2047, section
const filename = this.decodeRFC2047(tmp);
return this.maybeFixupEncoding(filename);
}
}
// filename=value (RFC 5987, section 4.1).
{
const match = R_RFC5987.exec(header);
if (match) {
const [, tmp] = match;
let filename = unquoteRFC2616(tmp);
filename = this.decodeRFC2047(filename);
return this.maybeFixupEncoding(filename);
}
}
return "";
}
private maybeDecode(encoding: string, value: string) {
if (!encoding) {
return value;
}
const bytes = Array.from(value, c => c.charCodeAt(0));
if (!bytes.every(code => code <= 0xff)) {
return value;
}
try {
value = new TextDecoder(encoding, {fatal: true}).
decode(new Uint8Array(bytes));
this.needsFixup = false;
}
catch {
// TextDecoder constructor threw - unrecognized encoding.
}
return value;
}
private maybeFixupEncoding(value: string) {
if (!this.needsFixup && /[\x80-\xff]/.test(value)) {
return value;
}
// Maybe multi-byte UTF-8.
value = this.maybeDecode("utf-8", value);
if (!this.needsFixup) {
return value;
}
// Try iso-8859-1 encoding.
return this.maybeDecode("iso-8859-1", value);
}
private getParamRFC2231(value: string) {
const matches: string[][] = [];
// Iterate over all filename*n= and filename*n*= with n being an integer
// of at least zero. Any non-zero number must not start with '0'.
let match;
this.R_MULTI.lastIndex = 0;
while ((match = this.R_MULTI.exec(value)) !== null) {
const [, num, quot, part] = match;
const n = parseInt(num, 10);
if (n in matches) {
// Ignore anything after the invalid second filename*0.
if (n === 0) {
break;
}
continue;
}
matches[n] = [quot, part];
}
const parts: string[] = [];
for (let n = 0; n < matches.length; ++n) {
if (!(n in matches)) {
// Numbers must be consecutive. Truncate when there is a hole.
break;
}
const [quot, rawPart] = matches[n];
let part = unquoteRFC2616(rawPart);
if (quot) {
part = unescape(part);
if (n === 0) {
part = this.decodeRFC5897(part);
}
}
parts.push(part);
}
return parts.join("");
}
private decodeRFC2047(value: string) {
// RFC 2047-decode the result. Firefox tried to drop support for it, but
// backed out because some servers use it - https://bugzil.la/875615
// Firefox's condition for decoding is here:
// eslint-disable-next-line max-len
// https://searchfox.org/mozilla-central/rev/4a590a5a15e35d88a3b23dd6ac3c471cf85b04a8/netwerk/mime/nsMIMEHeaderParamImpl.cpp#742-748
// We are more strict and only recognize RFC 2047-encoding if the value
// starts with "=?", since then it is likely that the full value is
// RFC 2047-encoded.
// Firefox also decodes words even where RFC 2047 section 5 states:
// "An 'encoded-word' MUST NOT appear within a 'quoted-string'."
// eslint-disable-next-line no-control-regex
if (!value.startsWith("=?") || /[\x00-\x19\x80-\xff]/.test(value)) {
return value;
}
// RFC 2047, section 2.4
// encoded-word = "=?" charset "?" encoding "?" encoded-text "?="
// charset = token (but let's restrict to characters that denote a
// possibly valid encoding).
// encoding = q or b
// encoded-text = any printable ASCII character other than ? or space.
// ... but Firefox permits ? and space.
return value.replace(
/=\?([\w-]*)\?([QqBb])\?((?:[^?]|\?(?!=))*)\?=/g,
(_, charset, encoding, text) => {
if (encoding === "q" || encoding === "Q") {
// RFC 2047 section 4.2.
text = text.replace(/_/g, " ");
text = text.replace(/=([0-9a-fA-F]{2})/g,
(_: string, hex: string) => String.fromCharCode(parseInt(hex, 16)));
return this.maybeDecode(charset, text);
}
// else encoding is b or B - base64 (RFC 2047 section 4.1)
try {
text = atob(text);
}
catch {
// ignored
}
return this.maybeDecode(charset, text);
});
}
private decodeRFC5897(extValue: string) {
// Decodes "ext-value" from RFC 5987.
const extEnd = extValue.indexOf("'");
if (extEnd < 0) {
// Some servers send "filename*=" without encoding'language' prefix,
// e.g. in https://github.com/Rob--W/open-in-browser/issues/26
// Let's accept the value like Firefox (57) (Chrome 62 rejects it).
return extValue;
}
const encoding = extValue.slice(0, extEnd);
const langvalue = extValue.slice(extEnd + 1);
// Ignore language (RFC 5987 section 3.2.1, and RFC 6266 section 4.1 ).
return this.maybeDecode(encoding, langvalue.replace(/^[^']*'/, ""));
}
}

View File

@ -1,11 +1,10 @@
"use strict";
// License: MIT
import MimeType from "whatwg-mimetype";
import { CHROME, downloads, webRequest } from "../browser";
import { CHROME, downloads } from "../browser";
import { Prefs } from "../prefs";
import { PromiseSerializer } from "../pserializer";
import { filterInSitu, parsePath, sanitizePath } from "../util";
import { filterInSitu, parsePath } from "../util";
import { BaseDownload } from "./basedownload";
// eslint-disable-next-line no-unused-vars
import { Manager } from "./man";
@ -21,50 +20,7 @@ import {
QUEUED,
RUNNING
} from "./state";
const PREROLL_HEURISTICS = /dl|attach|download|name|file|get|retr|^n$|\.(php|asp|py|pl|action|htm|shtm)/i;
const PREROLL_HOSTS = /4cdn|chan/;
const PREROLL_TIMEOUT = 10000;
const PREROLL_NOPE = new Set<string>();
function parseDisposition(disp: MimeType) {
if (!disp) {
return "";
}
let encoding = (disp.parameters.get("charset") || "utf-8").trim();
let file = (disp.parameters.get("filename") || "").trim().replace(/^(["'])(.*)\1$/, "$2");
if (!file) {
const encoded = disp.parameters.get("filename*");
if (!encoded) {
return "";
}
const pieces = encoded.split("'", 3);
if (pieces.length !== 3) {
return "";
}
encoding = pieces[0].trim() || encoding;
file = (pieces[3] || "").trim().replace(/^(["'])(.*)\1$/, "$2");
}
file = file.trim();
if (!file) {
return "";
}
try {
// And now for the tricky part...
// First unescape the string, to get the raw bytes
// not utf-8-interpreted bytes
// Then convert the string into an uint8[]
// Then decode
return new TextDecoder(encoding).decode(
new Uint8Array(unescape(file).split("").map(e => e.charCodeAt(0)))
);
}
catch (ex) {
console.error("Cannot decode", encoding, file, ex);
}
return "";
}
import { Preroller } from "./preroller";
type Header = {name: string; value: string};
interface Options {
@ -210,39 +166,30 @@ export class Download extends BaseDownload {
}
}
private get shouldPreroll() {
const {pathname, search, host} = this.uURL;
if (PREROLL_NOPE.has(host)) {
return false;
}
if (!this.renamer.p_ext) {
return true;
}
if (search.length) {
return true;
}
if (this.uURL.pathname.endsWith("/")) {
return true;
}
if (PREROLL_HEURISTICS.test(pathname)) {
return true;
}
if (PREROLL_HOSTS.test(host)) {
return true;
}
return false;
}
private async maybePreroll() {
try {
if (this.prerolled) {
// Check again, just in case, async and all
return;
}
if (!this.shouldPreroll) {
const roller = new Preroller(this);
if (!roller.shouldPreroll) {
return;
}
await (CHROME ? this.prerollChrome() : this.prerollFirefox());
const res = await roller.roll();
if (!res) {
return;
}
if (res.mime) {
this.mime = res.mime;
}
if (res.name) {
this.serverName = res.name;
}
if (res.error) {
this.cancel();
this.error = res.error;
}
}
catch (ex) {
console.error("Failed to preroll", this, ex.toString(), ex.stack, ex);
@ -255,101 +202,6 @@ export class Download extends BaseDownload {
}
}
private async prerollFirefox() {
const controller = new AbortController();
const {signal} = controller;
const res = await fetch(this.uURL.toString(), {
method: "HEAD",
mode: "same-origin",
signal,
});
controller.abort();
const {headers} = res;
this.prerollFinialize(headers, res);
}
async prerollChrome() {
let rid = "";
const rurl = this.uURL.toString();
let listener: any;
const wr = new Promise<any[]>(resolve => {
listener = (details: any) => {
const {url, requestId, statusCode} = details;
if (rid !== requestId && url !== rurl) {
return;
}
// eslint-disable-next-line no-magic-numbers
if (statusCode >= 300 && statusCode < 400) {
// Redirect, continue tracking;
rid = requestId;
return;
}
resolve(details.responseHeaders);
};
webRequest.onHeadersReceived.addListener(
listener, {urls: ["<all_urls>"]}, ["responseHeaders"]);
});
const p = Promise.race([
wr,
new Promise<any[]>((_, reject) =>
setTimeout(() => reject(new Error("timeout")), PREROLL_TIMEOUT))
]);
p.finally(() => {
webRequest.onHeadersReceived.removeListener(listener);
});
const controller = new AbortController();
const {signal} = controller;
const res = await fetch(rurl, {
method: "HEAD",
signal,
});
controller.abort();
const headers = await p;
this.prerollFinialize(
new Headers(headers.map(i => [i.name, i.value])), res);
}
private prerollFinialize(headers: Headers, res: Response) {
const type = MimeType.parse(headers.get("content-type") || "");
const dispHeader = headers.get("content-disposition");
let file = "";
if (dispHeader) {
const disp = new MimeType(`${type && type.toString() || "application/octet-stream"}; ${dispHeader}`);
file = parseDisposition(disp);
// Sanitize
file = sanitizePath(file.replace(/[/\\]+/g, "-"));
}
if (type) {
this.mime = type.essence;
}
this.serverName = file;
this.markDirty();
const {status} = res;
/* eslint-disable no-magic-numbers */
if (status === 404) {
this.cancel();
this.error = "SERVER_BAD_CONTENT";
}
else if (status === 403) {
this.cancel();
this.error = "SERVER_FORBIDDEN";
}
else if (status === 402 || status === 407) {
this.cancel();
this.error = "SERVER_UNAUTHORIZED";
}
else if (status === 400 || status === 405) {
PREROLL_NOPE.add(this.uURL.host);
}
else if (status > 400 && status < 500) {
this.cancel();
this.error = "SERVER_FAILED";
}
/* eslint-enable no-magic-numbers */
}
resume(forced = false) {
if (!(FORCABLE & this.state)) {
return;

220
lib/manager/preroller.ts Normal file
View File

@ -0,0 +1,220 @@
"use strict";
// License: MIT
import MimeType from "whatwg-mimetype";
// eslint-disable-next-line no-unused-vars
import { Download } from "./download";
import { CHROME, webRequest } from "../browser";
import { CDHeaderParser } from "../cdheaderparser";
import { sanitizePath, parsePath } from "../util";
import { MimeDB } from "../mime";
const PREROLL_HEURISTICS = /dl|attach|download|name|file|get|retr|^n$|\.(php|asp|py|pl|action|htm|shtm)/i;
const PREROLL_HOSTS = /4cdn|chan/;
const PREROLL_TIMEOUT = 10000;
const PREROLL_NOPE = new Set<string>();
const PREROLL_SEARCHEXTS = Object.freeze(new Set<string>([
"php",
"asp",
"aspx",
"inc",
"py",
"pl",
"action",
"htm",
"html",
"shtml"
]));
const NAME_TESTER = /\.[a-z0-9]{1,5}$/i;
const CDPARSER = new CDHeaderParser();
export interface PrerollResults {
error?: string;
name?: string;
mime?: string;
finalURL?: string;
}
export class Preroller {
private readonly download: Download
constructor(download: Download) {
this.download = download;
}
get shouldPreroll() {
const {uURL, renamer} = this.download;
const {pathname, search, host} = uURL;
if (PREROLL_NOPE.has(host)) {
return false;
}
if (!renamer.p_ext) {
return true;
}
if (search.length) {
return true;
}
if (uURL.pathname.endsWith("/")) {
return true;
}
if (PREROLL_HEURISTICS.test(pathname)) {
return true;
}
if (PREROLL_HOSTS.test(host)) {
return true;
}
return false;
}
async roll() {
try {
return await (CHROME ? this.prerollChrome() : this.prerollFirefox());
}
catch (ex) {
console.error("Failed to preroll", this, ex.toString(), ex.stack, ex);
}
return null;
}
private async prerollFirefox() {
const controller = new AbortController();
const {signal} = controller;
const {uURL} = this.download;
const res = await fetch(uURL.toString(), {
method: "GET",
headers: new Headers({
Range: "bytes=0-1",
}),
mode: "same-origin",
signal,
});
if (res.body) {
res.body.cancel();
}
controller.abort();
const {headers} = res;
return this.finalize(headers, res);
}
private async prerollChrome() {
let rid = "";
const {uURL} = this.download;
const rurl = uURL.toString();
let listener: any;
const wr = new Promise<any[]>(resolve => {
listener = (details: any) => {
const {url, requestId, statusCode} = details;
if (rid !== requestId && url !== rurl) {
return;
}
// eslint-disable-next-line no-magic-numbers
if (statusCode >= 300 && statusCode < 400) {
// Redirect, continue tracking;
rid = requestId;
return;
}
resolve(details.responseHeaders);
};
webRequest.onHeadersReceived.addListener(
listener, {urls: ["<all_urls>"]}, ["responseHeaders"]);
});
const p = Promise.race([
wr,
new Promise<any[]>((_, reject) =>
setTimeout(() => reject(new Error("timeout")), PREROLL_TIMEOUT))
]);
p.finally(() => {
webRequest.onHeadersReceived.removeListener(listener);
});
const controller = new AbortController();
const {signal} = controller;
const res = await fetch(rurl, {
method: "GET",
headers: new Headers({
Range: "bytes=0-1",
}),
signal,
});
if (res.body) {
res.body.cancel();
}
controller.abort();
const headers = await p;
return this.finalize(
new Headers(headers.map(i => [i.name, i.value])), res);
}
private finalize(headers: Headers, res: Response): PrerollResults {
const rv: PrerollResults = {};
const type = MimeType.parse(headers.get("content-type") || "");
if (type) {
rv.mime = type.essence;
}
const {p_ext: ext} = this.download.renamer;
const dispHeader = headers.get("content-disposition");
if (dispHeader) {
const file = CDPARSER.parse(dispHeader);
// Sanitize
rv.name = sanitizePath(file.replace(/[/\\]+/g, "-"));
}
else if (!ext || PREROLL_SEARCHEXTS.has(ext.toLocaleLowerCase())) {
const {searchParams} = this.download.uURL;
let detected = "";
for (const [, value] of searchParams) {
if (!NAME_TESTER.test(value)) {
continue;
}
const p = parsePath(value);
if (!p.base || !p.ext) {
continue;
}
if (!MimeDB.hasExtension(p.ext)) {
continue;
}
if (rv.mime) {
const mime = MimeDB.getMime(rv.mime);
if (mime && !mime.extensions.has(p.ext.toLowerCase())) {
continue;
}
}
const sanitized = sanitizePath(p.name);
if (sanitized.length <= detected.length) {
continue;
}
detected = sanitized;
}
if (detected) {
rv.name = detected;
}
}
rv.finalURL = res.url;
/* eslint-disable no-magic-numbers */
const {status} = res;
if (status === 404) {
rv.error = "SERVER_BAD_CONTENT";
}
else if (status === 403) {
rv.error = "SERVER_FORBIDDEN";
}
else if (status === 402 || status === 407) {
rv.error = "SERVER_UNAUTHORIZED";
}
else if (status === 400 || status === 405 || status === 416) {
PREROLL_NOPE.add(this.download.uURL.host);
if (PREROLL_NOPE.size > 1000) {
PREROLL_NOPE.delete(PREROLL_NOPE.keys().next().value);
}
}
else if (status > 400 && status < 500) {
rv.error = "SERVER_FAILED";
}
/* eslint-enable no-magic-numbers */
return rv;
}
}

View File

@ -25,9 +25,11 @@ export class MimeInfo {
}
}
export const MimeDB = new class {
export const MimeDB = new class MimeDB {
private readonly mimeToExts: Map<string, MimeInfo>;
private readonly registeredExtensions: Set<string>;
constructor() {
const exts = new Map<string, string[]>();
for (const [prim, more] of Object.entries(mime.e)) {
@ -42,6 +44,10 @@ export const MimeDB = new class {
Object.entries(mime.m),
([mime, prim]) => [mime, new MimeInfo(mime, exts.get(prim) || [prim])]
));
const all = Array.from(
this.mimeToExts.values(),
m => Array.from(m.extensions, e => e.toLowerCase()));
this.registeredExtensions = new Set(all.flat());
}
getPrimary(mime: string) {
@ -52,4 +58,8 @@ export const MimeDB = new class {
getMime(mime: string) {
return this.mimeToExts.get(mime.trim().toLocaleLowerCase());
}
hasExtension(ext: string) {
return this.registeredExtensions.has(ext.toLowerCase());
}
}();

View File

@ -98,6 +98,7 @@ export async function select(links: BaseItem[], media: BaseItem[]) {
type: "popup",
});
const window = await windows.create(windowOptions);
tracker.track(window.id, null);
try {
const port = await Promise.race<Port>([
new Promise<Port>(resolve => Bus.oncePort("select", resolve)),

View File

@ -21,6 +21,7 @@ export async function single(item: BaseItem | null) {
type: "popup",
});
const window = await windows.create(windowOptions);
tracker.track(window.id, null);
try {
const port: Port = await Promise.race<Port>([
new Promise<Port>(resolve => Bus.oncePort("single", resolve)),

View File

@ -55,13 +55,15 @@ export class WindowStateTracker {
getOptions(options: any) {
const result = Object.assign(options, {
width: this.width,
height: this.height,
state: this.state,
});
if (this.top >= 0) {
result.top = this.top;
result.left = this.left;
if (result.state !== "maximized") {
result.width = this.width;
result.height = this.height;
if (this.top >= 0) {
result.top = this.top;
result.left = this.left;
}
}
return result;
}

View File

@ -1,7 +1,7 @@
{
"manifest_version": 2,
"name": "DownThemAll!",
"version": "4.0.9",
"version": "4.0.11",
"description": "__MSG_extensionDescription__",
"homepage_url": "https://downthemall.org/",
@ -24,11 +24,13 @@
"permissions": [
"<all_urls>",
"contextMenus",
"menus",
"downloads",
"downloads.open",
"downloads.shelf",
"history",
"menus",
"notifications",
"sessions",
"storage",
"tabs",
"webNavigation",

View File

@ -25,6 +25,10 @@ html[data-platform="mac"] {
--folder-color: rgb(4, 102, 214);
}
html, body {
font-size: 10pt !important;
}
@font-face {
font-family: 'downthemall';
src: url('downthemall.woff2?75791791') format('woff2');

View File

@ -0,0 +1,289 @@
/* eslint-disable max-len */
/* eslint-env node */
"use strict";
// License: MPL-2
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { CDHeaderParser } = require("../lib/cdheaderparser");
const parser = new CDHeaderParser();
function check(header, expected) {
expect(parser.parse(header)).to.equal(expected);
}
function nocheck(header, expected) {
expect(parser.parse(header)).not.to.equal(expected);
}
describe("CDHeaderParser", function() {
it("parse wget", function() {
// From wget, test_parse_content_disposition
// http://git.savannah.gnu.org/cgit/wget.git/tree/src/http.c?id=8551ceccfedb4390fbfa82c12f0ff714dab1ac76#n5325
check("filename=\"file.ext\"", "file.ext");
check("attachment; filename=\"file.ext\"", "file.ext");
check("attachment; filename=\"file.ext\"; dummy", "file.ext");
check("attachment", ""); // wget uses NULL, we use "".
check("attachement; filename*=UTF-8'en-US'hello.txt", "hello.txt");
check("attachement; filename*0=\"hello\"; filename*1=\"world.txt\"",
"helloworld.txt");
check("attachment; filename=\"A.ext\"; filename*=\"B.ext\"", "B.ext");
check("attachment; filename*=\"A.ext\"; filename*0=\"B\"; filename*1=\"B.ext\"",
"A.ext");
// This test is faulty - https://savannah.gnu.org/bugs/index.php?52531
//check("filename**0=\"A\"; filename**1=\"A.ext\"; filename*0=\"B\";filename*1=\"B\"", "AA.ext");
});
it("parse Firefox", function() {
// From Firefox
// https://searchfox.org/mozilla-central/rev/45a3df4e6b8f653b0103d18d97c34dd666706358/netwerk/test/unit/test_MIME_params.js
// Changed as follows:
// - Replace error codes with empty string (we never throw).
const BS = "\\";
const DQUOTE = "\"";
// No filename parameter: return nothing
check("attachment;", "");
// basic
check("attachment; filename=basic", "basic");
// extended
check("attachment; filename*=UTF-8''extended", "extended");
// prefer extended to basic (bug 588781)
check("attachment; filename=basic; filename*=UTF-8''extended", "extended");
// prefer extended to basic (bug 588781)
check("attachment; filename*=UTF-8''extended; filename=basic", "extended");
// use first basic value (invalid; error recovery)
check("attachment; filename=first; filename=wrong", "first");
// old school bad HTTP servers: missing 'attachment' or 'inline'
// (invalid; error recovery)
check("filename=old", "old");
check("attachment; filename*=UTF-8''extended", "extended");
// continuations not part of RFC 5987 (bug 610054)
check("attachment; filename*0=foo; filename*1=bar", "foobar");
// Return first continuation (invalid; error recovery)
check("attachment; filename*0=first; filename*0=wrong; filename=basic", "first");
// Only use correctly ordered continuations (invalid; error recovery)
check("attachment; filename*0=first; filename*1=second; filename*0=wrong", "firstsecond");
// prefer continuation to basic (unless RFC 5987)
check("attachment; filename=basic; filename*0=foo; filename*1=bar", "foobar");
// Prefer extended to basic and/or (broken or not) continuation
// (invalid; error recovery)
check("attachment; filename=basic; filename*0=first; filename*0=wrong; filename*=UTF-8''extended", "extended");
// RFC 2231 not clear on correct outcome: we prefer non-continued extended
// (invalid; error recovery)
check("attachment; filename=basic; filename*=UTF-8''extended; filename*0=foo; filename*1=bar", "extended");
// Gaps should result in returning only value until gap hit
// (invalid; error recovery)
check("attachment; filename*0=foo; filename*2=bar", "foo");
// Don't allow leading 0's (*01) (invalid; error recovery)
check("attachment; filename*0=foo; filename*01=bar", "foo");
// continuations should prevail over non-extended (unless RFC 5987)
check("attachment; filename=basic; filename*0*=UTF-8''multi;\r\n" +
" filename*1=line;\r\n" +
" filename*2*=%20extended",
"multiline extended");
// Gaps should result in returning only value until gap hit
// (invalid; error recovery)
check("attachment; filename=basic; filename*0*=UTF-8''multi;\r\n" +
" filename*1=line;\r\n" +
" filename*3*=%20extended",
"multiline");
// First series, only please, and don't slurp up higher elements (*2 in this
// case) from later series into earlier one (invalid; error recovery)
check("attachment; filename=basic; filename*0*=UTF-8''multi;\r\n" +
" filename*1=line;\r\n" +
" filename*0*=UTF-8''wrong;\r\n" +
" filename*1=bad;\r\n" +
" filename*2=evil",
"multiline");
// RFC 2231 not clear on correct outcome: we prefer non-continued extended
// (invalid; error recovery)
check("attachment; filename=basic; filename*0=UTF-8''multi\r\n;" +
" filename*=UTF-8''extended;\r\n" +
" filename*1=line;\r\n" +
" filename*2*=%20extended",
"extended");
// sneaky: if unescaped, make sure we leave UTF-8'' in value
check("attachment; filename*0=UTF-8''unescaped;\r\n" +
" filename*1*=%20so%20includes%20UTF-8''%20in%20value",
"UTF-8''unescaped so includes UTF-8'' in value");
// sneaky: if unescaped, make sure we leave UTF-8'' in value
check("attachment; filename=basic; filename*0=UTF-8''unescaped;\r\n" +
" filename*1*=%20so%20includes%20UTF-8''%20in%20value",
"UTF-8''unescaped so includes UTF-8'' in value");
// Prefer basic over invalid continuation
// (invalid; error recovery)
check("attachment; filename=basic; filename*1=multi;\r\n" +
" filename*2=line;\r\n" +
" filename*3*=%20extended",
"basic");
// support digits over 10
check("attachment; filename=basic; filename*0*=UTF-8''0;\r\n" +
" filename*1=1; filename*2=2;filename*3=3;filename*4=4;filename*5=5;\r\n" +
" filename*6=6; filename*7=7;filename*8=8;filename*9=9;filename*10=a;\r\n" +
" filename*11=b; filename*12=c;filename*13=d;filename*14=e;filename*15=f\r\n",
"0123456789abcdef");
// support digits over 10 (detect gaps)
check("attachment; filename=basic; filename*0*=UTF-8''0;\r\n" +
" filename*1=1; filename*2=2;filename*3=3;filename*4=4;filename*5=5;\r\n" +
" filename*6=6; filename*7=7;filename*8=8;filename*9=9;filename*10=a;\r\n" +
" filename*11=b; filename*12=c;filename*14=e\r\n",
"0123456789abc");
// return nothing: invalid
// (invalid; error recovery)
check("attachment; filename*1=multi;\r\n" +
" filename*2=line;\r\n" +
" filename*3*=%20extended",
"");
// Bug 272541: Empty disposition type treated as "attachment"
// sanity check
check("attachment; filename=foo.html", "foo.html");
// the actual bug
check("; filename=foo.html", "foo.html");
// regression check, but see bug 671204
check("filename=foo.html", "foo.html");
// Bug 384571: RFC 2231 parameters not decoded when appearing in reversed order
// check ordering
check("attachment; filename=basic; filename*0*=UTF-8''0;\r\n" +
" filename*1=1; filename*2=2;filename*3=3;filename*4=4;filename*5=5;\r\n" +
" filename*6=6; filename*7=7;filename*8=8;filename*9=9;filename*10=a;\r\n" +
" filename*11=b; filename*12=c;filename*13=d;filename*15=f;filename*14=e;\r\n",
"0123456789abcdef");
// check non-digits in sequence numbers
check("attachment; filename=basic; filename*0*=UTF-8''0;\r\n" +
" filename*1a=1\r\n",
"0");
// check duplicate sequence numbers
check("attachment; filename=basic; filename*0*=UTF-8''0;\r\n" +
" filename*0=bad; filename*1=1;\r\n",
"0");
// check overflow
check("attachment; filename=basic; filename*0*=UTF-8''0;\r\n" +
" filename*11111111111111111111111111111111111111111111111111111111111=1",
"0");
// check underflow
check("attachment; filename=basic; filename*0*=UTF-8''0;\r\n" +
" filename*-1=1",
"0");
// check mixed token/quoted-string
check("attachment; filename=basic; filename*0=\"0\";\r\n" +
" filename*1=1;\r\n" +
" filename*2*=%32",
"012");
// check empty sequence number
check("attachment; filename=basic; filename**=UTF-8''0\r\n", "basic");
// Bug 419157: ensure that a MIME parameter with no charset information
// fallbacks to Latin-1
check("attachment;filename=IT839\x04\xB5(m8)2.pdf;", "IT839\u0004\u00b5(m8)2.pdf");
// Bug 588389: unescaping backslashes in quoted string parameters
// '\"', should be parsed as '"'
check(`attachment; filename=${DQUOTE}${BS + DQUOTE}${DQUOTE}`, DQUOTE);
// 'a\"b', should be parsed as 'a"b'
check(`attachment; filename=${DQUOTE}a${BS + DQUOTE}b${DQUOTE}`, `a${DQUOTE}b`);
// '\x', should be parsed as 'x'
check(`attachment; filename=${DQUOTE}${BS}x${DQUOTE}`, "x");
// test empty param (quoted-string)
check(`attachment; filename=${DQUOTE}${DQUOTE}`, "");
// test empty param
check("attachment; filename=", "");
// Bug 601933: RFC 2047 does not apply to parameters (at least in HTTP)
check("attachment; filename==?ISO-8859-1?Q?foo-=E4.html?=", "foo-\u00e4.html");
check("attachment; filename=\"=?ISO-8859-1?Q?foo-=E4.html?=\"", "foo-\u00e4.html");
// format sent by GMail as of 2012-07-23 (5987 overrides 2047)
check("attachment; filename=\"=?ISO-8859-1?Q?foo-=E4.html?=\"; filename*=UTF-8''5987", "5987");
// Bug 651185: double quotes around 2231/5987 encoded param
// Change reverted to backwards compat issues with various web services,
// such as OWA (Bug 703015), plus similar problems in Thunderbird. If this
// is tried again in the future, email probably needs to be special-cased.
// sanity check
check("attachment; filename*=utf-8''%41", "A");
// the actual bug
check(`attachment; filename*=${DQUOTE}utf-8''%41${DQUOTE}`, "A");
// Bug 670333: Content-Disposition parser does not require presence of "="
// in params
// sanity check
check("attachment; filename*=UTF-8''foo-%41.html", "foo-A.html");
// the actual bug
check("attachment; filename *=UTF-8''foo-%41.html", "");
// the actual bug, without 2231/5987 encoding
check("attachment; filename X", "");
// sanity check with WS on both sides
check("attachment; filename = foo-A.html", "foo-A.html");
// Bug 685192: in RFC2231/5987 encoding, a missing charset field should be
// treated as error
// the actual bug
check("attachment; filename*=''foo", "foo");
// sanity check
check("attachment; filename*=a''foo", "foo");
// Bug 692574: RFC2231/5987 decoding should not tolerate missing single
// quotes
// one missing
check("attachment; filename*=UTF-8'foo-%41.html", "foo-A.html");
// both missing
check("attachment; filename*=foo-%41.html", "foo-A.html");
// make sure fallback works
check("attachment; filename*=UTF-8'foo-%41.html; filename=bar.html", "foo-A.html");
// Bug 693806: RFC2231/5987 encoding: charset information should be treated
// as authoritative
// UTF-8 labeled ISO-8859-1
check("attachment; filename*=ISO-8859-1''%c3%a4", "\u00c3\u00a4");
// UTF-8 labeled ISO-8859-1, but with octets not allowed in ISO-8859-1
// accepts x82, understands it as Win1252, maps it to Unicode \u20a1
check("attachment; filename*=ISO-8859-1''%e2%82%ac", "\u00e2\u201a\u00ac");
// defective UTF-8
nocheck("attachment; filename*=UTF-8''A%e4B", "");
// defective UTF-8, with fallback
nocheck("attachment; filename*=UTF-8''A%e4B; filename=fallback", "fallback");
// defective UTF-8 (continuations), with fallback
nocheck("attachment; filename*0*=UTF-8''A%e4B; filename=fallback", "fallback");
// check that charsets aren't mixed up
check("attachment; filename*0*=ISO-8859-15''euro-sign%3d%a4; filename*=ISO-8859-1''currency-sign%3d%a4", "currency-sign=\u00a4");
// same as above, except reversed
check("attachment; filename*=ISO-8859-1''currency-sign%3d%a4; filename*0*=ISO-8859-15''euro-sign%3d%a4", "currency-sign=\u00a4");
// Bug 704989: add workaround for broken Outlook Web App (OWA)
// attachment handling
check("attachment; filename*=\"a%20b\"", "a b");
// Bug 717121: crash nsMIMEHeaderParamImpl::DoParameterInternal
check("attachment; filename=\"", "");
// We used to read past string if last param w/o = and ;
// Note: was only detected on windows PGO builds
check("attachment; filename=foo; trouble", "foo");
// Same, followed by space, hits another case
check("attachment; filename=foo; trouble ", "foo");
check("attachment", "");
// Bug 730574: quoted-string in RFC2231-continuations not handled
check("attachment; filename=basic; filename*0=\"foo\"; filename*1=\"\\b\\a\\r.html\"", "foobar.html");
// unmatched escape char
check("attachment; filename=basic; filename*0=\"foo\"; filename*1=\"\\b\\a\\", "fooba\\");
// Bug 732369: Content-Disposition parser does not require presence of ";" between params
// optimally, this would not even return the disposition type "attachment"
check("attachment; extension=bla filename=foo", "");
check("attachment; filename=foo extension=bla", "foo");
check("attachment filename=foo", "");
// Bug 777687: handling of broken %escapes
nocheck("attachment; filename*=UTF-8''f%oo; filename=bar", "bar");
nocheck("attachment; filename*=UTF-8''foo%; filename=bar", "bar");
// Bug 783502 - xpcshell test netwerk/test/unit/test_MIME_params.js fails on AddressSanitizer
check("attachment; filename=\"\\b\\a\\", "ba\\");
});
it("parse extra", function() {
// Extra tests, not covered by above tests.
check("inline; FILENAME=file.txt", "file.txt");
check("INLINE; FILENAME= \"an example.html\"", "an example.html"); // RFC 6266, section 5.
check("inline; filename= \"tl;dr.txt\"", "tl;dr.txt");
check("INLINE; FILENAME*= \"an example.html\"", "an example.html");
check("inline; filename*= \"tl;dr.txt\"", "tl;dr.txt");
check("inline; filename*0=\"tl;dr and \"; filename*1=more.txt", "tl;dr and more.txt");
});
it("parse issue 26", function() {
// https://github.com/Rob--W/open-in-browser/issues/26
check("attachment; filename=\xe5\x9c\x8b.pdf", "\u570b.pdf");
});
it("parse issue 35", function() {
// https://github.com/Rob--W/open-in-browser/issues/35
check("attachment; filename=okre\x9clenia.rtf", "okreœlenia.rtf");
});
});

30
tests/test_mime.js Normal file
View File

@ -0,0 +1,30 @@
"use strict";
// License: CC0 1.0
// eslint-disable-next-line @typescript-eslint/no-var-requires
const {MimeDB} = require("../lib/mime");
describe("MIME", function() {
it("general", function() {
expect(MimeDB.getMime("image/jpeg").major).to.equal("image");
expect(MimeDB.getMime("image/jpeg").minor).to.equal("jpeg");
expect(MimeDB.getMime("iMage/jPeg").major).to.equal("image");
expect(MimeDB.getMime("imAge/jpEg").minor).to.equal("jpeg");
});
it("exts", function() {
expect(MimeDB.getMime("image/jpeg").primary).to.equal("jpg");
expect(MimeDB.getMime("image/jpeg").primary).to.equal(
MimeDB.getPrimary("image/jpeg"));
expect(MimeDB.getMime("iMage/jPeg").primary).to.equal("jpg");
expect(MimeDB.getMime("imAge/jpEg").primary).to.equal(
MimeDB.getPrimary("image/jpeg"));
expect(Array.from(MimeDB.getMime("imAge/jpEg").extensions)).to.deep.equal(
["jpg", "jpeg", "jpe", "jfif"]);
});
it("application/octet-stream should not yield results", function() {
expect(MimeDB.getPrimary("application/octet-stream")).to.equal("");
expect(MimeDB.getMime("application/octet-Stream")).to.be.undefined;
});
});

View File

@ -27,7 +27,7 @@ LICENSED = set((".css", ".html", ".js", "*.ts"))
IGNORED = set((".DS_Store", "Thumbs.db"))
PERM_IGNORED_FX = set(("downloads.shelf", "webRequest"))
PERM_IGNORED_CHROME = set(("menus",))
PERM_IGNORED_CHROME = set(("menus", "sessions"))
SCRIPTS = [
"yarn build:regexps",

View File

@ -524,8 +524,16 @@ export class DownloadTable extends VirtualTable {
return true;
});
Keys.on("SHIFT-Delete", (event: Event) => {
const target = event.target as HTMLElement;
if (target.localName === "input") {
return false;
}
this.removeCompleteDownloads(false);
return true;
});
ctx.on("ctx-remove-all", () => this.removeAllDownloads());
ctx.on("ctx-remove-complete", () => this.removeCompleteDownloads(false));
ctx.on("ctx-remove-complete-all",
() => this.removeCompleteDownloads(false));
ctx.on("ctx-remove-complete-selected",
@ -743,6 +751,7 @@ export class DownloadTable extends VirtualTable {
}
selectionChanged() {
this.dismissTooltip();
const {empty} = this.selection;
if (empty) {
for (const d of this.disableSet) {