Compare commits
39 Commits
Author | SHA1 | Date | |
---|---|---|---|
c2b9664b4b | |||
e760d2b022 | |||
1a836d914b | |||
1b0e6eb6c4 | |||
39827ad485 | |||
79c4d4e98f | |||
427bd2f348 | |||
4fefd0e128 | |||
d79060237d | |||
2df7a1c592 | |||
8c4ceb3e4b | |||
bf725ece72 | |||
76992bd4f4 | |||
dccd530475 | |||
f1fa01a0eb | |||
7949142ef6 | |||
af1da8fc0a | |||
39f4237cde | |||
b676ed74cd | |||
bf474877ca | |||
7ee13af238 | |||
d488e5874a | |||
b1a7c22452 | |||
e928d202ee | |||
c39961d253 | |||
c6d11fcd7f | |||
eb96103478 | |||
583ccfc7b1 | |||
e0437718a0 | |||
2126ae022b | |||
2ef39dcb19 | |||
047c865e76 | |||
c586cd00cc | |||
ee7f470269 | |||
f04dda308b | |||
071458e262 | |||
9ffc96de4d | |||
26e9a5404a | |||
f44fe59054 |
@ -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)
|
||||
|
1
TODO.md
1
TODO.md
@ -13,7 +13,6 @@ Planned for later.
|
||||
* Delete files (well, as far as the browser allows)
|
||||
* Inter-addon API (basic)
|
||||
* Add downloads
|
||||
* Chrome support
|
||||
* vtable perf: cache column widths
|
||||
* Download options
|
||||
* This is a bit more limited, as we cannot modify options of downloads that have been started (and paused) or that are done.
|
||||
|
@ -1,16 +1,20 @@
|
||||
{
|
||||
"ar": "العربية [ar]",
|
||||
"cs": "Čeština (CZ) [cs]",
|
||||
"de": "Deutsch [de]",
|
||||
"el": "Ελληνικά [el]",
|
||||
"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]",
|
||||
"it": "Italiano [it]",
|
||||
"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/ar/messages.json
Normal file
1170
_locales/ar/messages.json
Normal file
File diff suppressed because it is too large
Load Diff
@ -11,272 +11,6 @@
|
||||
"message": "Pausiert hinzufügen",
|
||||
"description": "Action: Add paused"
|
||||
},
|
||||
"cancel": {
|
||||
"message": "Abbrechen",
|
||||
"description": "Button text: Cancel"
|
||||
},
|
||||
"canceled": {
|
||||
"message": "Abgebrochen",
|
||||
"description": "Download statu text"
|
||||
},
|
||||
"colConnections": {
|
||||
"message": "Gleichzeitige Verbindungen",
|
||||
"description": "Table column in prefs/network"
|
||||
},
|
||||
"colDomain": {
|
||||
"message": "Domain",
|
||||
"description": "Table column in manager"
|
||||
},
|
||||
"colETA": {
|
||||
"message": "Verbl. Zeit",
|
||||
"description": "Table column in manager"
|
||||
},
|
||||
"colNameURL": {
|
||||
"message": "Name/URL",
|
||||
"description": "Table column in manager"
|
||||
},
|
||||
"colPercent": {
|
||||
"message": "%",
|
||||
"description": "Table column in manager"
|
||||
},
|
||||
"colProgress": {
|
||||
"message": "Fortschritt",
|
||||
"description": "Table column in manager"
|
||||
},
|
||||
"colSegments": {
|
||||
"message": "Segmente",
|
||||
"description": "Table column in manager"
|
||||
},
|
||||
"colSize": {
|
||||
"message": "Größe",
|
||||
"description": "Table column in manager"
|
||||
},
|
||||
"colSpeed": {
|
||||
"message": "Geschwindigkeit",
|
||||
"description": "Table column in manager"
|
||||
},
|
||||
"CRASH": {
|
||||
"message": "Interner Browser Fehler",
|
||||
"description": "Error Message"
|
||||
},
|
||||
"delete": {
|
||||
"message": "Entfernen",
|
||||
"description": "button text"
|
||||
},
|
||||
"description": {
|
||||
"message": "Beschreibung",
|
||||
"description": "Description (keep it short); e.g. the description column in select"
|
||||
},
|
||||
"donate": {
|
||||
"message": "Spenden!",
|
||||
"description": "Donate button"
|
||||
},
|
||||
"done": {
|
||||
"message": "Fertig",
|
||||
"description": "Status text"
|
||||
},
|
||||
"download": {
|
||||
"message": "Download",
|
||||
"description": "Download (noun); e.g. Download column in select"
|
||||
},
|
||||
"extensionDescription": {
|
||||
"message": "Der Massen-Downloader für Deinen Browser",
|
||||
"description": "DownThemAll! tagline, displayed in about:addons; Please do NOT refer to a specific browser such as firefox, as we will probably support more than one"
|
||||
},
|
||||
"fastfiltering": {
|
||||
"message": "Schnelles Filtern",
|
||||
"description": "Label for Fast Filtering input"
|
||||
},
|
||||
"FILE_FAILED": {
|
||||
"message": "Dateizugriffsfehler",
|
||||
"description": "Error Message"
|
||||
},
|
||||
"finishing": {
|
||||
"message": "Beenden",
|
||||
"description": "Status text"
|
||||
},
|
||||
"links": {
|
||||
"message": "Links",
|
||||
"description": "Links tab label (short); select window"
|
||||
},
|
||||
"mask": {
|
||||
"message": "Maske",
|
||||
"description": "Renaming mask (short); used in e.g. select"
|
||||
},
|
||||
"media": {
|
||||
"message": "Medien",
|
||||
"description": "Media label (short)"
|
||||
},
|
||||
"missing": {
|
||||
"message": "Fehlt",
|
||||
"description": "Status text in manager"
|
||||
},
|
||||
"NETWORK_FAILED": {
|
||||
"message": "Netzwerkfehler",
|
||||
"description": "Error Message"
|
||||
},
|
||||
"ok": {
|
||||
"message": "OK",
|
||||
"description": "Button text; Used in message boxes"
|
||||
},
|
||||
"paused": {
|
||||
"message": "Pausiert",
|
||||
"description": "Status text; manager"
|
||||
},
|
||||
"queued": {
|
||||
"message": "Wartend",
|
||||
"description": "Status text"
|
||||
},
|
||||
"referrer": {
|
||||
"message": "Referrer",
|
||||
"description": "Label for \"Referrer\""
|
||||
},
|
||||
"remember": {
|
||||
"message": "Diese Entscheidung merken",
|
||||
"description": "Checkbox text for confirmation, e.g. when removing a download in manager"
|
||||
},
|
||||
"rename": {
|
||||
"message": "Umbenennen",
|
||||
"description": "UI for renaming; currently unused"
|
||||
},
|
||||
"renmask": {
|
||||
"message": "Umbennenungsmaske",
|
||||
"description": "Renaming mask (long)"
|
||||
},
|
||||
"reset": {
|
||||
"message": "Zurücksetzen",
|
||||
"description": "Button text; pref window"
|
||||
},
|
||||
"running": {
|
||||
"message": "Laufend",
|
||||
"description": "Status text"
|
||||
},
|
||||
"save": {
|
||||
"message": "Speichern",
|
||||
"description": "Button text; e.g. prefs/Network"
|
||||
},
|
||||
"search": {
|
||||
"message": "Suchen…",
|
||||
"description": "Placeholder text; manager status search field"
|
||||
},
|
||||
"SERVER_BAD_CONTENT": {
|
||||
"message": "Nicht gefunden",
|
||||
"description": "Error message"
|
||||
},
|
||||
"SERVER_FAILED": {
|
||||
"message": "Server-Fehler",
|
||||
"description": "Error message"
|
||||
},
|
||||
"SERVER_FORBIDDEN": {
|
||||
"message": "Nicht erlaubt",
|
||||
"description": "Error message"
|
||||
},
|
||||
"SERVER_UNAUTHORIZED": {
|
||||
"message": "Keine Berechtigung",
|
||||
"description": "Error message"
|
||||
},
|
||||
"sizeB": {
|
||||
"message": "$S$B",
|
||||
"description": "Size formatting; bytes",
|
||||
"placeholders": {
|
||||
"s": {
|
||||
"content": "$1",
|
||||
"example": "100b"
|
||||
}
|
||||
}
|
||||
},
|
||||
"sizeGB": {
|
||||
"message": "$S$GB",
|
||||
"description": "Size formatting; giga bytes",
|
||||
"placeholders": {
|
||||
"s": {
|
||||
"content": "$1",
|
||||
"example": "100.200GB"
|
||||
}
|
||||
}
|
||||
},
|
||||
"sizeKB": {
|
||||
"message": "$S$KB",
|
||||
"description": "Size formatting; kilo bytes",
|
||||
"placeholders": {
|
||||
"s": {
|
||||
"content": "$1",
|
||||
"example": "100.2KB"
|
||||
}
|
||||
}
|
||||
},
|
||||
"sizeMB": {
|
||||
"message": "$S$MB",
|
||||
"description": "Size formatting; mega bytes",
|
||||
"placeholders": {
|
||||
"s": {
|
||||
"content": "$1",
|
||||
"example": "100.22MB"
|
||||
}
|
||||
}
|
||||
},
|
||||
"sizePB": {
|
||||
"message": "$S$PB",
|
||||
"description": "Size formatting; peta bytes (you never know)",
|
||||
"placeholders": {
|
||||
"s": {
|
||||
"content": "$1",
|
||||
"example": "100.212PB"
|
||||
}
|
||||
}
|
||||
},
|
||||
"sizeTB": {
|
||||
"message": "$S$TB",
|
||||
"description": "Size formatting; tera bytes (you never know)",
|
||||
"placeholders": {
|
||||
"s": {
|
||||
"content": "$1",
|
||||
"example": "100.002TB"
|
||||
}
|
||||
}
|
||||
},
|
||||
"speedB": {
|
||||
"message": "$SPEED$b/s",
|
||||
"description": "Speed formatting; bytes",
|
||||
"placeholders": {
|
||||
"speed": {
|
||||
"content": "$1",
|
||||
"example": "100b/s"
|
||||
}
|
||||
}
|
||||
},
|
||||
"speedKB": {
|
||||
"message": "$SPEED$KB/s",
|
||||
"description": "Speed formatting; kilo bytes",
|
||||
"placeholders": {
|
||||
"speed": {
|
||||
"content": "$1",
|
||||
"example": "100.1KB/s"
|
||||
}
|
||||
}
|
||||
},
|
||||
"speedMB": {
|
||||
"message": "$SPEED$MB/s",
|
||||
"description": "Speed formatting; mega bytes",
|
||||
"placeholders": {
|
||||
"speed": {
|
||||
"content": "$1",
|
||||
"example": "100.20MB/s"
|
||||
}
|
||||
}
|
||||
},
|
||||
"title": {
|
||||
"message": "Titel",
|
||||
"description": "Column text; Title label (short)"
|
||||
},
|
||||
"unlimited": {
|
||||
"message": "Unbegrenzt",
|
||||
"description": "Option text; Prefs/Network"
|
||||
},
|
||||
"useonlyonce": {
|
||||
"message": "Einmalig",
|
||||
"description": "Label for Use-Once checkboxes"
|
||||
},
|
||||
"add_download": {
|
||||
"message": "Download hinzufügen",
|
||||
"description": "Action for adding a download"
|
||||
@ -329,6 +63,14 @@
|
||||
"message": "Batch Download",
|
||||
"description": "Messagebox title for batch confirmations"
|
||||
},
|
||||
"cancel": {
|
||||
"message": "Abbrechen",
|
||||
"description": "Button text: Cancel"
|
||||
},
|
||||
"canceled": {
|
||||
"message": "Abgebrochen",
|
||||
"description": "Download status text"
|
||||
},
|
||||
"cancel_download": {
|
||||
"message": "Abbrechen",
|
||||
"description": "Action to cancel downloads, e.g. from the context menu"
|
||||
@ -342,9 +84,45 @@
|
||||
"description": "Checkbox label text for decision confirmations"
|
||||
},
|
||||
"check_selected_items": {
|
||||
"message": "Ausgewählte Einträge makieren",
|
||||
"message": "Ausgewählte Einträge markieren",
|
||||
"description": "Menu text"
|
||||
},
|
||||
"colConnections": {
|
||||
"message": "Gleichzeitige Verbindungen",
|
||||
"description": "Table column in prefs/network"
|
||||
},
|
||||
"colDomain": {
|
||||
"message": "Domain",
|
||||
"description": "Table column in manager"
|
||||
},
|
||||
"colETA": {
|
||||
"message": "Verbl. Zeit",
|
||||
"description": "Table column in manager"
|
||||
},
|
||||
"colNameURL": {
|
||||
"message": "Name/URL",
|
||||
"description": "Table column in manager"
|
||||
},
|
||||
"colPercent": {
|
||||
"message": "%",
|
||||
"description": "Table column in manager"
|
||||
},
|
||||
"colProgress": {
|
||||
"message": "Fortschritt",
|
||||
"description": "Table column in manager"
|
||||
},
|
||||
"colSegments": {
|
||||
"message": "Segmente",
|
||||
"description": "Table column in manager"
|
||||
},
|
||||
"colSize": {
|
||||
"message": "Größe",
|
||||
"description": "Table column in manager"
|
||||
},
|
||||
"colSpeed": {
|
||||
"message": "Geschwindigkeit",
|
||||
"description": "Table column in manager"
|
||||
},
|
||||
"conflict_overwrite": {
|
||||
"message": "Überschreiben",
|
||||
"description": "Option text; prefs/general"
|
||||
@ -357,6 +135,10 @@
|
||||
"message": "Umbenennen",
|
||||
"description": "Option text; prefs/general"
|
||||
},
|
||||
"CRASH": {
|
||||
"message": "Interner Browser Fehler",
|
||||
"description": "Error Message"
|
||||
},
|
||||
"create_filter": {
|
||||
"message": "Filter erstellen",
|
||||
"description": "Button text; Create filter dialog; prefs/filters"
|
||||
@ -405,26 +187,42 @@
|
||||
"message": "Videos (mp4, webm, mkv, …)",
|
||||
"description": "Filter label for the Videos filter"
|
||||
},
|
||||
"delete": {
|
||||
"message": "Entfernen",
|
||||
"description": "button text"
|
||||
},
|
||||
"description": {
|
||||
"message": "Beschreibung",
|
||||
"description": "Description (keep it short); e.g. the description column in select"
|
||||
},
|
||||
"disable_other_filters": {
|
||||
"message": "Andere deaktivieren",
|
||||
"description": "Checkbox label. Keep it short"
|
||||
},
|
||||
"donate": {
|
||||
"message": "Spenden!",
|
||||
"description": "Donate button"
|
||||
},
|
||||
"done": {
|
||||
"message": "Fertig",
|
||||
"description": "Status text"
|
||||
},
|
||||
"download": {
|
||||
"message": "Download",
|
||||
"description": "Download (noun); e.g. Download column in select"
|
||||
},
|
||||
"download_verb": {
|
||||
"message": "Download",
|
||||
"description": "Download (verb/action); e.g. in single and select buttons"
|
||||
},
|
||||
"dta_regular_all": {
|
||||
"message": "DownThemAll! - Alle Tabs",
|
||||
"description": "Menu text"
|
||||
},
|
||||
"dta_turbo_all": {
|
||||
"message": "OneClick! - Alle Tabs",
|
||||
"description": "Menu text"
|
||||
},
|
||||
"dta_regular": {
|
||||
"message": "DownThemAll!",
|
||||
"description": "Regular dta action; Menu text"
|
||||
},
|
||||
"dta_regular_all": {
|
||||
"message": "DownThemAll! - Alle Tabs",
|
||||
"description": "Menu text"
|
||||
},
|
||||
"dta_regular_image": {
|
||||
"message": "Bild mit DownThemAll! speichern",
|
||||
"description": "Menu text"
|
||||
@ -445,6 +243,10 @@
|
||||
"message": "OneClick!",
|
||||
"description": "OneClick! action; Menu text"
|
||||
},
|
||||
"dta_turbo_all": {
|
||||
"message": "OneClick! - Alle Tabs",
|
||||
"description": "Menu text"
|
||||
},
|
||||
"dta_turbo_image": {
|
||||
"message": "Bild mit OneClick! speichern",
|
||||
"description": "Menu text"
|
||||
@ -477,16 +279,28 @@
|
||||
"message": "Nichts ausgewählt",
|
||||
"description": "Error Message; select window"
|
||||
},
|
||||
"extensionDescription": {
|
||||
"message": "Der Massen-Downloader für Deinen Browser",
|
||||
"description": "DownThemAll! tagline, displayed in about:addons; Please do NOT refer to a specific browser such as firefox, as we will probably support more than one"
|
||||
},
|
||||
"fastfiltering": {
|
||||
"message": "Schnelles Filtern",
|
||||
"description": "Label for Fast Filtering input"
|
||||
},
|
||||
"fastfilter_placeholder": {
|
||||
"message": "Platzhalter-Ausdruck oder Regular Expression",
|
||||
"description": "Placeholder for fastfilter inputs"
|
||||
},
|
||||
"FILE_FAILED": {
|
||||
"message": "Dateizugriffsfehler",
|
||||
"description": "Error Message"
|
||||
},
|
||||
"filter_at_least_one": {
|
||||
"message": "Mindestens einen Filter-Typ auswählen!",
|
||||
"description": "Error message when no filter types are selected for a filter in the preferences UI"
|
||||
},
|
||||
"filter_create_title": {
|
||||
"message": "Neuen Filter erstelln",
|
||||
"message": "Neuen Filter erstellen",
|
||||
"description": "Message box title"
|
||||
},
|
||||
"filter_expression": {
|
||||
@ -497,6 +311,10 @@
|
||||
"message": "Filter-Titel",
|
||||
"description": "Message box label"
|
||||
},
|
||||
"filter_types": {
|
||||
"message": "Filter Typen",
|
||||
"description": "Message box label"
|
||||
},
|
||||
"filter_type_link": {
|
||||
"message": "Link Filter",
|
||||
"description": "Message box checkbox label"
|
||||
@ -505,9 +323,9 @@
|
||||
"message": "Medien Filter",
|
||||
"description": "Message box checkbox label"
|
||||
},
|
||||
"filter_types": {
|
||||
"message": "Filter Typen",
|
||||
"description": "Message box label"
|
||||
"finishing": {
|
||||
"message": "Beenden",
|
||||
"description": "Status text"
|
||||
},
|
||||
"force_start": {
|
||||
"message": "Start erzwingen",
|
||||
@ -557,6 +375,14 @@
|
||||
"message": "Begrenzt auf",
|
||||
"description": "Label text; used in prefs/network"
|
||||
},
|
||||
"links": {
|
||||
"message": "Links",
|
||||
"description": "Links tab label (short); select window"
|
||||
},
|
||||
"manager_short": {
|
||||
"message": "Manager",
|
||||
"description": "Menu text"
|
||||
},
|
||||
"manager_status_items": {
|
||||
"message": "$COMPLETE$ von $TOTAL$ Downloads beendet ($SHOWING$ angezeigt), $RUNNING$ laufend",
|
||||
"description": "Status bar text; manager",
|
||||
@ -579,18 +405,26 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"manager_short": {
|
||||
"message": "Manager",
|
||||
"description": "Menu text"
|
||||
},
|
||||
"manager_title": {
|
||||
"message": "DownThemAll! Manager",
|
||||
"description": "Window/tab title"
|
||||
},
|
||||
"mask": {
|
||||
"message": "Maske",
|
||||
"description": "Renaming mask (short); used in e.g. select"
|
||||
},
|
||||
"mask_default": {
|
||||
"message": "Standard-Maske",
|
||||
"description": "Status text; Used in the mask column, select window"
|
||||
},
|
||||
"media": {
|
||||
"message": "Medien",
|
||||
"description": "Media label (short)"
|
||||
},
|
||||
"missing": {
|
||||
"message": "Fehlt",
|
||||
"description": "Status text in manager"
|
||||
},
|
||||
"move_bottom": {
|
||||
"message": "Ende",
|
||||
"description": "Action for moving a download to the bottom"
|
||||
@ -617,18 +451,22 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"NETWORK_FAILED": {
|
||||
"message": "Netzwerkfehler",
|
||||
"description": "Error Message"
|
||||
},
|
||||
"never_ask_again": {
|
||||
"message": "Nicht wieder fragen",
|
||||
"description": "Donation button"
|
||||
},
|
||||
"no_links": {
|
||||
"message": "Keine Links gefunden!",
|
||||
"description": "Notification text"
|
||||
},
|
||||
"noitems_label": {
|
||||
"message": "Nichts ausgewählt",
|
||||
"description": "Status bar text in select"
|
||||
},
|
||||
"no_links": {
|
||||
"message": "Keine Links gefunden!",
|
||||
"description": "Notification text"
|
||||
},
|
||||
"numitems_label": {
|
||||
"message": "$ITEMS$ Downloads ausgewählt",
|
||||
"description": "Status bar text in select; Number of items selected (label)",
|
||||
@ -639,6 +477,10 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"ok": {
|
||||
"message": "OK",
|
||||
"description": "Button text; Used in message boxes"
|
||||
},
|
||||
"open_directory": {
|
||||
"message": "Verzeichnis öffnen",
|
||||
"description": "Menu text; manager context"
|
||||
@ -663,10 +505,26 @@
|
||||
"message": "Netzwerk",
|
||||
"description": "Pref tab text"
|
||||
},
|
||||
"paused": {
|
||||
"message": "Pausiert",
|
||||
"description": "Status text; manager"
|
||||
},
|
||||
"pause_download": {
|
||||
"message": "Pausieren",
|
||||
"description": "Action for pausing a download"
|
||||
},
|
||||
"prefs_conflicts": {
|
||||
"message": "Wenn eine Datei bereits existiert",
|
||||
"description": "Preferences/General; group text"
|
||||
},
|
||||
"prefs_short": {
|
||||
"message": "Einstellungen",
|
||||
"description": "Menu text; Preferences"
|
||||
},
|
||||
"prefs_title": {
|
||||
"message": "DownThemAll! Einstellungen",
|
||||
"description": "Window/tab title; Preferences"
|
||||
},
|
||||
"pref_add_paused": {
|
||||
"message": "Neue Downloads immer pausiert hinzufügen, anstatt sie direkt zu starten",
|
||||
"description": "Preferences/General"
|
||||
@ -687,14 +545,26 @@
|
||||
"message": "Keine allgemeinen Menü-Eintrage anzeigen",
|
||||
"description": "Preferences/General"
|
||||
},
|
||||
"pref_manager": {
|
||||
"message": "Manager",
|
||||
"description": "Preferences/General; group text"
|
||||
},
|
||||
"pref_manager_tooltip": {
|
||||
"message": "Keine Tooltips im Manager-Tab anzeigen",
|
||||
"description": "Preferences/General"
|
||||
},
|
||||
"pref_netglobal": {
|
||||
"message": "Allgemeine Netzwerk-Beschränkungen",
|
||||
"description": "Preferences/General; group text"
|
||||
},
|
||||
"pref_open_manager_on_queue": {
|
||||
"message": "Den Manager öffnen nachdem neue Downloads zur Warteschlange hinzugefügt wurden",
|
||||
"description": "Preferences/General"
|
||||
},
|
||||
"pref_queueing": {
|
||||
"message": "Download-Warteschlange",
|
||||
"description": "Preferences/General; group text"
|
||||
},
|
||||
"pref_queue_notification": {
|
||||
"message": "Benachrichtigung anzeigen, wenn neue Downloads hinzugefügt wurden",
|
||||
"description": "Preferences/General"
|
||||
@ -711,37 +581,13 @@
|
||||
"message": "Versuche Text-Links in Webseiten zu finden (langsamer)",
|
||||
"description": "Preferences/General"
|
||||
},
|
||||
"pref_manager": {
|
||||
"message": "Manager",
|
||||
"description": "Preferences/General; group text"
|
||||
},
|
||||
"pref_netglobal": {
|
||||
"message": "Allgemeine Netzwerk-Beschränkungen",
|
||||
"description": "Preferences/General; group text"
|
||||
},
|
||||
"pref_queueing": {
|
||||
"message": "Download-Warteschlange",
|
||||
"description": "Preferences/General; group text"
|
||||
},
|
||||
"pref_ui": {
|
||||
"message": "Benutzeroberfläche",
|
||||
"description": "Preferences/General; group text"
|
||||
},
|
||||
"prefs_conflicts": {
|
||||
"message": "Wenn eine Datei bereits existiert",
|
||||
"description": "Preferences/General; group text"
|
||||
},
|
||||
"prefs_short": {
|
||||
"message": "Einstellungen",
|
||||
"description": "Menu text; Preferences"
|
||||
},
|
||||
"prefs_title": {
|
||||
"message": "DownThemAll! Einstellungen",
|
||||
"description": "Window/tab title; Preferences"
|
||||
},
|
||||
"queue_finished": {
|
||||
"message": "Die Download-Warteschlange ist fertig",
|
||||
"description": "Notification text"
|
||||
"queued": {
|
||||
"message": "Wartend",
|
||||
"description": "Status text"
|
||||
},
|
||||
"queued_download": {
|
||||
"message": "Ein Download hinzugefügt!",
|
||||
@ -757,6 +603,18 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"queue_finished": {
|
||||
"message": "Die Download-Warteschlange ist fertig",
|
||||
"description": "Notification text"
|
||||
},
|
||||
"referrer": {
|
||||
"message": "Referrer",
|
||||
"description": "Label for \"Referrer\""
|
||||
},
|
||||
"remember": {
|
||||
"message": "Diese Entscheidung merken",
|
||||
"description": "Checkbox text for confirmation, e.g. when removing a download in manager"
|
||||
},
|
||||
"remove_all_complete_downloads": {
|
||||
"message": "Alle fertigen entfernen",
|
||||
"description": "Menu text"
|
||||
@ -831,18 +689,18 @@
|
||||
"message": "Download entfernen",
|
||||
"description": "Action for removing a download, no matter what state"
|
||||
},
|
||||
"remove_download_question": {
|
||||
"message": "Wirklich alle ausgewählten Downloads entfernen?",
|
||||
"description": "Messagebox text"
|
||||
},
|
||||
"remove_downloads": {
|
||||
"message": "Downloas entfernen",
|
||||
"message": "Downloads entfernen",
|
||||
"description": "Menu text"
|
||||
},
|
||||
"remove_downloads_title": {
|
||||
"message": "Wirklich Downloads entfernen?",
|
||||
"description": "Messagebox title; manager"
|
||||
},
|
||||
"remove_download_question": {
|
||||
"message": "Wirklich alle ausgewählten Downloads entfernen?",
|
||||
"description": "Messagebox text"
|
||||
},
|
||||
"remove_failed_downloads": {
|
||||
"message": "Fehlgeschlagene entfernen",
|
||||
"description": "Menu text"
|
||||
@ -889,6 +747,10 @@
|
||||
"message": "Ausgewählte entfernen",
|
||||
"description": "Menu text"
|
||||
},
|
||||
"rename": {
|
||||
"message": "Umbenennen",
|
||||
"description": "UI for renaming; currently unused"
|
||||
},
|
||||
"renamer_batch": {
|
||||
"message": "Batch Nummer",
|
||||
"description": "Mask text; see mask button"
|
||||
@ -1005,6 +867,14 @@
|
||||
"message": "Datum hinzugefügt - Jahr",
|
||||
"description": "Mask text; see mask button"
|
||||
},
|
||||
"renmask": {
|
||||
"message": "Umbenennungsmaske",
|
||||
"description": "Renaming mask (long)"
|
||||
},
|
||||
"reset": {
|
||||
"message": "Zurücksetzen",
|
||||
"description": "Button text; pref window"
|
||||
},
|
||||
"reset_confirmations": {
|
||||
"message": "Gemerkte Entscheidungen zurücksetzen",
|
||||
"description": "Button text; pref/General"
|
||||
@ -1025,6 +895,18 @@
|
||||
"message": "Fortsetzen",
|
||||
"description": "Action for resuming a download"
|
||||
},
|
||||
"running": {
|
||||
"message": "Laufend",
|
||||
"description": "Status text"
|
||||
},
|
||||
"save": {
|
||||
"message": "Speichern",
|
||||
"description": "Button text; e.g. prefs/Network"
|
||||
},
|
||||
"search": {
|
||||
"message": "Suchen…",
|
||||
"description": "Placeholder text; manager status search field"
|
||||
},
|
||||
"select_all": {
|
||||
"message": "Alles auswählen",
|
||||
"description": "Menu text; e.g. select context"
|
||||
@ -1041,6 +923,22 @@
|
||||
"message": "DownThemAll! - Downloads auswählen",
|
||||
"description": "Title of the select window"
|
||||
},
|
||||
"SERVER_BAD_CONTENT": {
|
||||
"message": "Nicht gefunden",
|
||||
"description": "Error message"
|
||||
},
|
||||
"SERVER_FAILED": {
|
||||
"message": "Server-Fehler",
|
||||
"description": "Error message"
|
||||
},
|
||||
"SERVER_FORBIDDEN": {
|
||||
"message": "Nicht erlaubt",
|
||||
"description": "Error message"
|
||||
},
|
||||
"SERVER_UNAUTHORIZED": {
|
||||
"message": "Keine Berechtigung",
|
||||
"description": "Error message"
|
||||
},
|
||||
"set_mask": {
|
||||
"message": "Umbenennungsmaske setzen",
|
||||
"description": "Menu text; select window"
|
||||
@ -1050,30 +948,62 @@
|
||||
"description": "Header text; single window"
|
||||
},
|
||||
"single_header": {
|
||||
"message": "Download URL (link) und andere Optionen eingeben",
|
||||
"message": "Download URL (Link) und andere Optionen eingeben",
|
||||
"description": "Header text; single window"
|
||||
},
|
||||
"single_title": {
|
||||
"message": "DownThemAll! - Link hinzufügen",
|
||||
"description": "Title of single window"
|
||||
},
|
||||
"size_progress": {
|
||||
"message": "$WRITTEN$ von $TOTAL$",
|
||||
"description": "Status text; manager size column",
|
||||
"sizeB": {
|
||||
"message": "$S$B",
|
||||
"description": "Size formatting; bytes",
|
||||
"placeholders": {
|
||||
"total": {
|
||||
"content": "$2",
|
||||
"example": ""
|
||||
},
|
||||
"written": {
|
||||
"s": {
|
||||
"content": "$1",
|
||||
"example": ""
|
||||
"example": "100b"
|
||||
}
|
||||
}
|
||||
},
|
||||
"size_unknown": {
|
||||
"message": "Unbekannt",
|
||||
"description": "Status text; manager size column"
|
||||
"sizeGB": {
|
||||
"message": "$S$GB",
|
||||
"description": "Size formatting; giga bytes",
|
||||
"placeholders": {
|
||||
"s": {
|
||||
"content": "$1",
|
||||
"example": "100.200GB"
|
||||
}
|
||||
}
|
||||
},
|
||||
"sizeKB": {
|
||||
"message": "$S$KB",
|
||||
"description": "Size formatting; kilo bytes",
|
||||
"placeholders": {
|
||||
"s": {
|
||||
"content": "$1",
|
||||
"example": "100.2KB"
|
||||
}
|
||||
}
|
||||
},
|
||||
"sizeMB": {
|
||||
"message": "$S$MB",
|
||||
"description": "Size formatting; mega bytes",
|
||||
"placeholders": {
|
||||
"s": {
|
||||
"content": "$1",
|
||||
"example": "100.22MB"
|
||||
}
|
||||
}
|
||||
},
|
||||
"sizePB": {
|
||||
"message": "$S$PB",
|
||||
"description": "Size formatting; peta bytes (you never know)",
|
||||
"placeholders": {
|
||||
"s": {
|
||||
"content": "$1",
|
||||
"example": "100.212PB"
|
||||
}
|
||||
}
|
||||
},
|
||||
"sizes_huge": {
|
||||
"message": "Riesig (> $HIGH$)",
|
||||
@ -1127,6 +1057,64 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"sizeTB": {
|
||||
"message": "$S$TB",
|
||||
"description": "Size formatting; tera bytes (you never know)",
|
||||
"placeholders": {
|
||||
"s": {
|
||||
"content": "$1",
|
||||
"example": "100.002TB"
|
||||
}
|
||||
}
|
||||
},
|
||||
"size_progress": {
|
||||
"message": "$WRITTEN$ von $TOTAL$",
|
||||
"description": "Status text; manager size column",
|
||||
"placeholders": {
|
||||
"total": {
|
||||
"content": "$2",
|
||||
"example": ""
|
||||
},
|
||||
"written": {
|
||||
"content": "$1",
|
||||
"example": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"size_unknown": {
|
||||
"message": "Unbekannt",
|
||||
"description": "Status text; manager size column"
|
||||
},
|
||||
"speedB": {
|
||||
"message": "$SPEED$b/s",
|
||||
"description": "Speed formatting; bytes",
|
||||
"placeholders": {
|
||||
"speed": {
|
||||
"content": "$1",
|
||||
"example": "100b/s"
|
||||
}
|
||||
}
|
||||
},
|
||||
"speedKB": {
|
||||
"message": "$SPEED$KB/s",
|
||||
"description": "Speed formatting; kilo bytes",
|
||||
"placeholders": {
|
||||
"speed": {
|
||||
"content": "$1",
|
||||
"example": "100.1KB/s"
|
||||
}
|
||||
}
|
||||
},
|
||||
"speedMB": {
|
||||
"message": "$SPEED$MB/s",
|
||||
"description": "Speed formatting; mega bytes",
|
||||
"placeholders": {
|
||||
"speed": {
|
||||
"content": "$1",
|
||||
"example": "100.20MB/s"
|
||||
}
|
||||
}
|
||||
},
|
||||
"statusNetwork_active_title": {
|
||||
"message": "Neue Downloads werden gestartet",
|
||||
"description": "Status bar tooltip; manager network icon"
|
||||
@ -1135,8 +1123,12 @@
|
||||
"message": "Neue Downloads werden nicht gestartet",
|
||||
"description": "Status bar tooltip; manager network icon"
|
||||
},
|
||||
"title": {
|
||||
"message": "Titel",
|
||||
"description": "Column text; Title label (short)"
|
||||
},
|
||||
"toggle_selected_items": {
|
||||
"message": "Markierungen für Auswahl umgekehren",
|
||||
"message": "Markierungen für Auswahl umkehren",
|
||||
"description": "Menu text; select"
|
||||
},
|
||||
"tooltip_date": {
|
||||
@ -1166,5 +1158,13 @@
|
||||
"uncheck_selected_items": {
|
||||
"message": "Markierung von Auswahl entfernen",
|
||||
"description": "Menu text; select"
|
||||
},
|
||||
"unlimited": {
|
||||
"message": "Unbegrenzt",
|
||||
"description": "Option text; Prefs/Network"
|
||||
},
|
||||
"useonlyonce": {
|
||||
"message": "Einmalig",
|
||||
"description": "Label for Use-Once checkboxes"
|
||||
}
|
||||
}
|
1170
_locales/hu/messages.json
Normal file
1170
_locales/hu/messages.json
Normal file
File diff suppressed because it is too large
Load Diff
@ -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
1170
_locales/it/messages.json
Normal file
File diff suppressed because it is too large
Load Diff
1170
_locales/ja/messages.json
Normal file
1170
_locales/ja/messages.json
Normal file
File diff suppressed because it is too large
Load Diff
@ -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": {
|
||||
|
@ -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";
|
||||
@ -45,6 +48,9 @@ const CHROME_CONTEXTS = Object.freeze(new Set([
|
||||
|
||||
async function runContentJob(tab: Tab, file: string, msg: any) {
|
||||
try {
|
||||
if (tab && tab.incognito && msg) {
|
||||
msg.private = tab.incognito;
|
||||
}
|
||||
const res = await tabs.executeScript(tab.id, {
|
||||
file,
|
||||
allFrames: true,
|
||||
@ -566,6 +572,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) => {
|
||||
|
@ -19,6 +19,7 @@ export interface MessageSender {
|
||||
|
||||
export interface Tab {
|
||||
id?: number;
|
||||
incognito?: boolean;
|
||||
}
|
||||
|
||||
export interface MenuClickInfo {
|
||||
@ -39,17 +40,71 @@ export interface RawPort {
|
||||
postMessage: (message: any) => void;
|
||||
}
|
||||
|
||||
export const {extension} = polyfill;
|
||||
export const {notifications} = polyfill;
|
||||
interface WebRequestFilter {
|
||||
urls?: string[];
|
||||
}
|
||||
|
||||
interface WebRequestListener {
|
||||
addListener(
|
||||
callback: Function,
|
||||
filter: WebRequestFilter,
|
||||
extraInfoSpec: string[]
|
||||
): void;
|
||||
removeListener(callback: Function): void;
|
||||
}
|
||||
|
||||
type Header = {name: string; value: string};
|
||||
|
||||
export interface DownloadOptions {
|
||||
conflictAction: string;
|
||||
filename: string;
|
||||
saveAs: boolean;
|
||||
url: string;
|
||||
method?: string;
|
||||
body?: string;
|
||||
incognito?: boolean;
|
||||
headers: Header[];
|
||||
}
|
||||
|
||||
export interface DownloadsQuery {
|
||||
id?: number;
|
||||
}
|
||||
|
||||
interface Downloads {
|
||||
download(download: DownloadOptions): Promise<number>;
|
||||
open(manId: number): Promise<void>;
|
||||
show(manId: number): Promise<void>;
|
||||
pause(manId: number): Promise<void>;
|
||||
resume(manId: number): Promise<void>;
|
||||
cancel(manId: number): Promise<void>;
|
||||
erase(query: DownloadsQuery): Promise<void>;
|
||||
search(query: DownloadsQuery): Promise<any[]>;
|
||||
getFileIcon(id: number, options?: any): Promise<string>;
|
||||
setShelfEnabled(state: boolean): void;
|
||||
onCreated: ExtensionListener;
|
||||
onChanged: ExtensionListener;
|
||||
onErased: ExtensionListener;
|
||||
}
|
||||
|
||||
interface WebRequest {
|
||||
onBeforeSendHeaders: WebRequestListener;
|
||||
onSendHeaders: WebRequestListener;
|
||||
onHeadersReceived: WebRequestListener;
|
||||
}
|
||||
|
||||
export const {browserAction} = polyfill;
|
||||
export const {contextMenus} = polyfill;
|
||||
export const {downloads} = polyfill;
|
||||
export const {downloads}: {downloads: 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;
|
||||
export const {webRequest} = polyfill;
|
||||
export const {webRequest}: {webRequest: WebRequest} = polyfill;
|
||||
export const {windows} = polyfill;
|
||||
|
||||
export const CHROME = navigator.appVersion.includes("Chrome/");
|
||||
|
230
lib/cdheaderparser.ts
Normal file
230
lib/cdheaderparser.ts
Normal 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(/^[^']*'/, ""));
|
||||
}
|
||||
}
|
@ -1,11 +1,11 @@
|
||||
"use strict";
|
||||
// License: MIT
|
||||
|
||||
import MimeType from "whatwg-mimetype";
|
||||
import { CHROME, downloads, webRequest } from "../browser";
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import { CHROME, downloads, DownloadOptions } 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,62 +21,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 "";
|
||||
}
|
||||
|
||||
type Header = {name: string; value: string};
|
||||
interface Options {
|
||||
conflictAction: string;
|
||||
filename: string;
|
||||
saveAs: boolean;
|
||||
url: string;
|
||||
method?: string;
|
||||
body?: string;
|
||||
incognito?: boolean;
|
||||
headers: Header[];
|
||||
}
|
||||
import { Preroller } from "./preroller";
|
||||
|
||||
export class Download extends BaseDownload {
|
||||
public manager: Manager;
|
||||
@ -120,23 +65,23 @@ export class Download extends BaseDownload {
|
||||
if (this.manId) {
|
||||
const {manId: id} = this;
|
||||
try {
|
||||
const state = await downloads.search({id});
|
||||
if (state[0].state === "in_progress") {
|
||||
const state = (await downloads.search({id})).pop() || {};
|
||||
if (state.state === "in_progress" && !state.error && !state.paused) {
|
||||
this.changeState(RUNNING);
|
||||
this.updateStateFromBrowser();
|
||||
return;
|
||||
}
|
||||
if (state[0].state === "complete") {
|
||||
if (state.state === "complete") {
|
||||
this.changeState(DONE);
|
||||
this.updateStateFromBrowser();
|
||||
return;
|
||||
}
|
||||
if (!state[0].canResume) {
|
||||
if (!state.canResume) {
|
||||
throw new Error("Cannot resume");
|
||||
}
|
||||
// Cannot await here
|
||||
// Firefox bug: will not return until download is finished
|
||||
downloads.resume(id).catch(() => {});
|
||||
downloads.resume(id).catch(console.error);
|
||||
this.changeState(RUNNING);
|
||||
return;
|
||||
}
|
||||
@ -164,7 +109,7 @@ export class Download extends BaseDownload {
|
||||
return;
|
||||
}
|
||||
}
|
||||
const options: Options = {
|
||||
const options: DownloadOptions = {
|
||||
conflictAction: await Prefs.get("conflict-action"),
|
||||
filename: this.dest.full,
|
||||
saveAs: false,
|
||||
@ -184,6 +129,12 @@ export class Download extends BaseDownload {
|
||||
value: this.referrer
|
||||
});
|
||||
}
|
||||
else if (CHROME) {
|
||||
options.headers.push({
|
||||
name: "X-DTA-ID",
|
||||
value: this.sessionId.toString(),
|
||||
});
|
||||
}
|
||||
if (this.manId) {
|
||||
this.manager.removeManId(this.manId);
|
||||
}
|
||||
@ -210,39 +161,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 +197,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;
|
||||
@ -467,7 +314,10 @@ export class Download extends BaseDownload {
|
||||
this.markDirty();
|
||||
switch (state.state) {
|
||||
case "in_progress":
|
||||
if (error) {
|
||||
if (state.paused) {
|
||||
this.changeState(PAUSED);
|
||||
}
|
||||
else if (error) {
|
||||
this.cancel();
|
||||
this.error = error;
|
||||
}
|
||||
|
@ -16,8 +16,9 @@ import { Download } from "./download";
|
||||
import { ManagerPort } from "./port";
|
||||
import { Scheduler } from "./scheduler";
|
||||
import { Limits } from "./limits";
|
||||
import { downloads, runtime } from "../browser";
|
||||
import { downloads, runtime, webRequest, CHROME } from "../browser";
|
||||
|
||||
const US = runtime.getURL("");
|
||||
|
||||
const AUTOSAVE_TIMEOUT = 2000;
|
||||
const DIRTY_TIMEOUT = 100;
|
||||
@ -83,6 +84,14 @@ export class Manager extends EventEmitter {
|
||||
Limits.on("changed", () => {
|
||||
this.resetScheduler();
|
||||
});
|
||||
|
||||
if (CHROME) {
|
||||
webRequest.onBeforeSendHeaders.addListener(
|
||||
this.stuffReferrer.bind(this),
|
||||
{urls: ["<all_urls>"]},
|
||||
["blocking", "requestHeaders", "extraHeaders"]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async init() {
|
||||
@ -384,6 +393,31 @@ export class Manager extends EventEmitter {
|
||||
getMsgItems() {
|
||||
return this.items.map(e => e.toMsg());
|
||||
}
|
||||
|
||||
stuffReferrer(details: any): any {
|
||||
if (details.tabId > 0 && !US.startsWith(details.initiator)) {
|
||||
return undefined;
|
||||
}
|
||||
const sidx = details.requestHeaders.findIndex(
|
||||
(e: any) => e.name.toLowerCase() === "x-dta-id");
|
||||
if (sidx < 0) {
|
||||
return undefined;
|
||||
}
|
||||
const sid = parseInt(details.requestHeaders[sidx].value, 10);
|
||||
details.requestHeaders.splice(sidx, 1);
|
||||
const item = this.sids.get(sid);
|
||||
if (!item) {
|
||||
return undefined;
|
||||
}
|
||||
details.requestHeaders.push({
|
||||
name: "Referer",
|
||||
value: (item.uReferrer || item.uURL).toString()
|
||||
});
|
||||
const rv: any = {
|
||||
requestHeaders: details.requestHeaders
|
||||
};
|
||||
return rv;
|
||||
}
|
||||
}
|
||||
|
||||
let inited: Promise<Manager>;
|
||||
|
234
lib/manager/preroller.ts
Normal file
234
lib/manager/preroller.ts
Normal file
@ -0,0 +1,234 @@
|
||||
"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>();
|
||||
|
||||
/* eslint-disable no-magic-numbers */
|
||||
const NOPE_STATUSES = Object.freeze(new Set([
|
||||
400,
|
||||
401,
|
||||
402,
|
||||
405,
|
||||
416,
|
||||
]));
|
||||
/* eslint-enable no-magic-numbers */
|
||||
|
||||
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, uReferrer} = this.download;
|
||||
const res = await fetch(uURL.toString(), {
|
||||
method: "GET",
|
||||
headers: new Headers({
|
||||
Range: "bytes=0-1",
|
||||
}),
|
||||
mode: "same-origin",
|
||||
signal,
|
||||
referrer: (uReferrer || uURL).toString(),
|
||||
});
|
||||
if (res.body) {
|
||||
res.body.cancel();
|
||||
}
|
||||
controller.abort();
|
||||
const {headers} = res;
|
||||
return this.finalize(headers, res);
|
||||
}
|
||||
|
||||
private async prerollChrome() {
|
||||
let rid = "";
|
||||
const {uURL, uReferrer} = 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",
|
||||
"X-DTA-ID": this.download.sessionId.toString(),
|
||||
}),
|
||||
signal,
|
||||
referrer: (uReferrer || uURL).toString(),
|
||||
});
|
||||
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 (NOPE_STATUSES.has(status)) {
|
||||
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;
|
||||
}
|
||||
}
|
12
lib/mime.ts
12
lib/mime.ts
@ -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());
|
||||
}
|
||||
}();
|
||||
|
@ -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)),
|
||||
@ -186,8 +187,8 @@ export async function select(links: BaseItem[], media: BaseItem[]) {
|
||||
openPrefs();
|
||||
});
|
||||
|
||||
port.on("openUrls", ({urls}) => {
|
||||
openUrls(urls);
|
||||
port.on("openUrls", ({urls, incognito}) => {
|
||||
openUrls(urls, incognito);
|
||||
});
|
||||
|
||||
try {
|
||||
|
@ -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)),
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -8,37 +8,45 @@ import DEFAULT_ICONS from "../data/icons.json";
|
||||
const DONATE_URL = "https://www.downthemall.org/howto/donate/";
|
||||
const MANAGER_URL = "/windows/manager.html";
|
||||
|
||||
export async function mostRecentBrowser(): Promise<any> {
|
||||
export async function mostRecentBrowser(incognito: boolean): Promise<any> {
|
||||
let window;
|
||||
try {
|
||||
window = await windows.getCurrent({windowTypes: ["normal"]});
|
||||
window = await windows.getCurrent();
|
||||
if (window.type !== "normal") {
|
||||
throw new Error("not a normal window");
|
||||
}
|
||||
if (incognito && !window.incognito) {
|
||||
throw new Error("Not incognito");
|
||||
}
|
||||
}
|
||||
catch {
|
||||
try {
|
||||
window = await windows.getlastFocused({windowTypes: ["normal"]});
|
||||
window = await windows.getlastFocused();
|
||||
if (window.type !== "normal") {
|
||||
throw new Error("not a normal window");
|
||||
}
|
||||
if (incognito && !window.incognito) {
|
||||
throw new Error("Not incognito");
|
||||
}
|
||||
}
|
||||
catch {
|
||||
window = Array.from(await windows.getAll({windowTypes: ["normal"]})).
|
||||
filter((w: any) => w.type === "normal").pop();
|
||||
filter(
|
||||
(w: any) => w.type === "normal" && !!w.incognito === !!incognito).
|
||||
pop();
|
||||
}
|
||||
}
|
||||
if (!window) {
|
||||
window = await windows.create({
|
||||
url: DONATE_URL,
|
||||
incognito: !!incognito,
|
||||
type: "normal",
|
||||
});
|
||||
}
|
||||
return window;
|
||||
}
|
||||
|
||||
export async function openInTab(url: string) {
|
||||
const window = await mostRecentBrowser();
|
||||
export async function openInTab(url: string, incognito: boolean) {
|
||||
const window = await mostRecentBrowser(incognito);
|
||||
await tabs.create({
|
||||
active: true,
|
||||
url,
|
||||
@ -47,7 +55,7 @@ export async function openInTab(url: string) {
|
||||
await windows.update(window.id, {focused: true});
|
||||
}
|
||||
|
||||
export async function openInTabOrFocus(url: string) {
|
||||
export async function openInTabOrFocus(url: string, incognito: boolean) {
|
||||
const etabs = await tabs.query({
|
||||
url
|
||||
});
|
||||
@ -57,21 +65,21 @@ export async function openInTabOrFocus(url: string) {
|
||||
await windows.update(tab.windowId, {focused: true});
|
||||
return;
|
||||
}
|
||||
await openInTab(url);
|
||||
await openInTab(url, incognito);
|
||||
}
|
||||
|
||||
export async function maybeOpenInTab(url: string) {
|
||||
export async function maybeOpenInTab(url: string, incognito: boolean) {
|
||||
const etabs = await tabs.query({
|
||||
url
|
||||
});
|
||||
if (etabs.length) {
|
||||
return;
|
||||
}
|
||||
await openInTab(url);
|
||||
await openInTab(url, incognito);
|
||||
}
|
||||
|
||||
export async function donate() {
|
||||
await openInTab(DONATE_URL);
|
||||
await openInTab(DONATE_URL, false);
|
||||
}
|
||||
|
||||
export async function openPrefs() {
|
||||
@ -86,15 +94,15 @@ export async function openManager(focus = true) {
|
||||
console.error(ex.toString(), ex);
|
||||
}
|
||||
if (focus) {
|
||||
await openInTabOrFocus(await runtime.getURL(MANAGER_URL));
|
||||
await openInTabOrFocus(await runtime.getURL(MANAGER_URL), false);
|
||||
}
|
||||
else {
|
||||
await maybeOpenInTab(await runtime.getURL(MANAGER_URL));
|
||||
await maybeOpenInTab(await runtime.getURL(MANAGER_URL), false);
|
||||
}
|
||||
}
|
||||
|
||||
export async function openUrls(urls: string) {
|
||||
const window = await mostRecentBrowser();
|
||||
export async function openUrls(urls: string, incognito: boolean) {
|
||||
const window = await mostRecentBrowser(incognito);
|
||||
for (const url of urls) {
|
||||
try {
|
||||
await tabs.create({
|
||||
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
"manifest_version": 2,
|
||||
"name": "DownThemAll!",
|
||||
"version": "4.0.9",
|
||||
"version": "4.0.12",
|
||||
|
||||
"description": "__MSG_extensionDescription__",
|
||||
"homepage_url": "https://downthemall.org/",
|
||||
@ -24,15 +24,18 @@
|
||||
"permissions": [
|
||||
"<all_urls>",
|
||||
"contextMenus",
|
||||
"menus",
|
||||
"downloads",
|
||||
"downloads.open",
|
||||
"downloads.shelf",
|
||||
"history",
|
||||
"menus",
|
||||
"notifications",
|
||||
"sessions",
|
||||
"storage",
|
||||
"tabs",
|
||||
"webNavigation",
|
||||
"webRequest"
|
||||
"webRequest",
|
||||
"webRequestBlocking"
|
||||
],
|
||||
|
||||
"background": {
|
||||
|
@ -77,6 +77,8 @@ function urlToUsable(e: any, u: string) {
|
||||
}
|
||||
|
||||
class Gatherer {
|
||||
private: boolean;
|
||||
|
||||
textLinks: boolean;
|
||||
|
||||
selectionOnly: boolean;
|
||||
@ -88,6 +90,7 @@ class Gatherer {
|
||||
transferable: string[];
|
||||
|
||||
constructor(options: any) {
|
||||
this.private = !!options.private;
|
||||
this.textLinks = options.textLinks;
|
||||
this.selectionOnly = options.selectionOnly;
|
||||
this.selection = options.selectionOnly ? getSelection() : null;
|
||||
@ -255,6 +258,7 @@ class Gatherer {
|
||||
return {
|
||||
url: url.href,
|
||||
title,
|
||||
private: this.private
|
||||
};
|
||||
}
|
||||
catch (ex) {
|
||||
|
@ -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');
|
||||
|
289
tests/test_cdheaderparser.js
Normal file
289
tests/test_cdheaderparser.js
Normal 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
30
tests/test_mime.js
Normal 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;
|
||||
});
|
||||
});
|
@ -70,7 +70,7 @@ def main():
|
||||
if modified:
|
||||
try:
|
||||
with open("messages.json.tmp", "w", encoding="utf-8") as outp:
|
||||
json.dump(data, outp, sort_keys=True, indent=2)
|
||||
json.dump(data, outp, sort_keys=True, indent=2, ensure_ascii=False)
|
||||
os.rename("messages.json.tmp", "_locales/en/messages.json")
|
||||
finally:
|
||||
try:
|
||||
|
@ -26,8 +26,8 @@ UNCOMPRESSABLE = set((".png", ".jpg", ".zip", ".woff2"))
|
||||
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_FX = set(("downloads.shelf", "webRequest", "webRequestBlocking"))
|
||||
PERM_IGNORED_CHROME = set(("menus", "sessions"))
|
||||
|
||||
SCRIPTS = [
|
||||
"yarn build:regexps",
|
||||
|
@ -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) {
|
||||
|
@ -428,17 +428,42 @@ class SelectionTable extends VirtualTable {
|
||||
}
|
||||
|
||||
openSelection() {
|
||||
const items = this.items.filter((i, idx) => this.selection.contains(idx));
|
||||
if (!items.length) {
|
||||
const privates: BaseMatchedItem[] = [];
|
||||
const items = this.items.filter((i, idx) => this.selection.contains(idx)).
|
||||
filter(i => {
|
||||
if (i.private) {
|
||||
privates.push(i);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
if (!items.length && !privates.length) {
|
||||
if (this.focusRow < 0) {
|
||||
return;
|
||||
}
|
||||
items.push(this.items.at(this.focusRow));
|
||||
const item = this.items.at(this.focusRow);
|
||||
if (item.private) {
|
||||
privates.push(item);
|
||||
}
|
||||
else {
|
||||
items.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
if (items.length) {
|
||||
PORT.postMessage({
|
||||
msg: "openUrls",
|
||||
urls: items.map(e => e.url),
|
||||
incognito: false,
|
||||
});
|
||||
}
|
||||
if (privates.length) {
|
||||
PORT.postMessage({
|
||||
msg: "openUrls",
|
||||
urls: privates.map(e => e.url),
|
||||
incognito: true,
|
||||
});
|
||||
}
|
||||
PORT.postMessage({
|
||||
msg: "openUrls",
|
||||
urls: items.map(e => e.url)
|
||||
});
|
||||
}
|
||||
|
||||
applyDeltaTo(delta: ItemDelta[], items: ItemCollection) {
|
||||
|
Reference in New Issue
Block a user