Compare commits
53 Commits
Author | SHA1 | Date | |
---|---|---|---|
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 | |||
e4b0629dee | |||
5c2700ca36 | |||
639a582804 | |||
2d1f185fcd | |||
38735ed0ae | |||
216bc590da | |||
1c10d8005a | |||
1fcfbe5360 | |||
8d3dda1cec | |||
be18f667d9 | |||
027b2c4fb1 | |||
4ed92878be | |||
a6930f309e | |||
fdcdae0412 | |||
2c18ddaaa8 | |||
994e7ad0a6 | |||
95536b36be | |||
9c159d5d24 | |||
42ccfd5dc5 | |||
dabf7f8a28 | |||
9cac48f439 | |||
ef6bc840d8 | |||
1c38ec1357 | |||
a4436bd6c8 | |||
5a4b8143b2 | |||
00a5712427 |
10
LICENSE.md
10
LICENSE.md
@ -77,3 +77,13 @@ Licensed under the Mozilla Public License 2.0.
|
|||||||
|
|
||||||
The list itself is licensed under the Mozilla Public License 2.0.
|
The list itself is licensed under the Mozilla Public License 2.0.
|
||||||
The javascript library accessing it is licensed under the MIT license.
|
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)
|
||||||
|
@ -5,13 +5,16 @@
|
|||||||
"en": "English (US) [en]",
|
"en": "English (US) [en]",
|
||||||
"es": "Español (España) [es]",
|
"es": "Español (España) [es]",
|
||||||
"et": "Eesti Keel [et]",
|
"et": "Eesti Keel [et]",
|
||||||
"fr": "Français (FR) [fr]",
|
"fr": "Français [fr]",
|
||||||
|
"hu": "Magyar (HU) [hu]",
|
||||||
"id": "Bahasa Indonesia [id]",
|
"id": "Bahasa Indonesia [id]",
|
||||||
|
"ja": "日本語 (JP) [ja]",
|
||||||
"ko": "한국어 [ko]",
|
"ko": "한국어 [ko]",
|
||||||
"lt": "Lietuvių [lt]",
|
"lt": "Lietuvių [lt]",
|
||||||
"nl": "Nederlands [nl]",
|
"nl": "Nederlands [nl]",
|
||||||
"pl": "Polski (PL) [pl]",
|
"pl": "Polski [pl]",
|
||||||
"pt": "Português (Brasil) [pt]",
|
"pt": "Português (Brasil) [pt]",
|
||||||
"ru": "Русский [ru]",
|
"ru": "Русский [ru]",
|
||||||
"zh_CN": "简体中文 [zh_CN]"
|
"zh_CN": "简体中文 [zh_CN]",
|
||||||
|
"zh_TW": "正體中文 [zh_TW]"
|
||||||
}
|
}
|
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",
|
"message": "id",
|
||||||
"description": "Language code the locale will use, e.g. de or en-GB or pt-BR"
|
"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": {
|
"addpaused": {
|
||||||
"message": "Tambahkan dalam kondisi terpause",
|
"message": "Tambahkan dalam kondisi terpause",
|
||||||
"description": "Action: Add paused"
|
"description": "Action: Add paused"
|
||||||
@ -280,15 +292,15 @@
|
|||||||
"description": "Message box title"
|
"description": "Message box title"
|
||||||
},
|
},
|
||||||
"filter_expression": {
|
"filter_expression": {
|
||||||
"message": "Ekspres-Filter",
|
"message": "Ekspresi",
|
||||||
"description": "Message box label"
|
"description": "Message box label"
|
||||||
},
|
},
|
||||||
"filter_label": {
|
"filter_label": {
|
||||||
"message": "Label-Filter",
|
"message": "Label",
|
||||||
"description": "Message box label"
|
"description": "Message box label"
|
||||||
},
|
},
|
||||||
"filter_types": {
|
"filter_types": {
|
||||||
"message": "Tipe-Filter",
|
"message": "Tipe",
|
||||||
"description": "Message box label"
|
"description": "Message box label"
|
||||||
},
|
},
|
||||||
"filter_type_link": {
|
"filter_type_link": {
|
||||||
@ -320,7 +332,7 @@
|
|||||||
"description": "Menu text"
|
"description": "Menu text"
|
||||||
},
|
},
|
||||||
"limited_to": {
|
"limited_to": {
|
||||||
"message": "Terbatas ke",
|
"message": "Batasi ke",
|
||||||
"description": "Label text; used in prefs/network"
|
"description": "Label text; used in prefs/network"
|
||||||
},
|
},
|
||||||
"links": {
|
"links": {
|
||||||
@ -362,11 +374,11 @@
|
|||||||
"description": "Status text; Used in the mask column, select window"
|
"description": "Status text; Used in the mask column, select window"
|
||||||
},
|
},
|
||||||
"missing": {
|
"missing": {
|
||||||
"message": "Tidak Ada",
|
"message": "Hilang",
|
||||||
"description": "Status text in manager"
|
"description": "Status text in manager"
|
||||||
},
|
},
|
||||||
"move_bottom": {
|
"move_bottom": {
|
||||||
"message": "Bawah",
|
"message": "Ke Bawah",
|
||||||
"description": "Action for moving a download to the bottom"
|
"description": "Action for moving a download to the bottom"
|
||||||
},
|
},
|
||||||
"move_down": {
|
"move_down": {
|
||||||
@ -374,7 +386,7 @@
|
|||||||
"description": "Action for moving a download down"
|
"description": "Action for moving a download down"
|
||||||
},
|
},
|
||||||
"move_top": {
|
"move_top": {
|
||||||
"message": "Atas",
|
"message": "Ke Atas",
|
||||||
"description": "Action for moving a download to the top"
|
"description": "Action for moving a download to the top"
|
||||||
},
|
},
|
||||||
"move_up": {
|
"move_up": {
|
||||||
@ -560,7 +572,7 @@
|
|||||||
"description": "Menu text"
|
"description": "Menu text"
|
||||||
},
|
},
|
||||||
"remove_batch_downloads_question": {
|
"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"
|
"description": "Messagebox text"
|
||||||
},
|
},
|
||||||
"remove_complete_downloads": {
|
"remove_complete_downloads": {
|
||||||
@ -600,7 +612,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"remove_domain_downloads": {
|
"remove_domain_downloads": {
|
||||||
"message": "Hapus Domain Ini",
|
"message": "Hapus Unduhan Dari Domain Ini",
|
||||||
"description": "Menu text"
|
"description": "Menu text"
|
||||||
},
|
},
|
||||||
"remove_domain_downloads_question": {
|
"remove_domain_downloads_question": {
|
||||||
@ -630,7 +642,7 @@
|
|||||||
"description": "Messagebox text"
|
"description": "Messagebox text"
|
||||||
},
|
},
|
||||||
"remove_failed_downloads": {
|
"remove_failed_downloads": {
|
||||||
"message": "Gagal Menghapus",
|
"message": "Hapus Unduhan Gagal",
|
||||||
"description": "Menu text"
|
"description": "Menu text"
|
||||||
},
|
},
|
||||||
"remove_failed_downloads_question": {
|
"remove_failed_downloads_question": {
|
||||||
@ -648,11 +660,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"remove_missing": {
|
"remove_missing": {
|
||||||
"message": "Hapus Unduhan Yang Tidak Ada",
|
"message": "Hapus Unduhan Yang Hilang",
|
||||||
"description": "Menu text"
|
"description": "Menu text"
|
||||||
},
|
},
|
||||||
"remove_missing_downloads_question": {
|
"remove_missing_downloads_question": {
|
||||||
"message": "Hapus semua unduhan yang tidak ada?",
|
"message": "Hapus semua unduhan yang hilang?",
|
||||||
"description": "Messagebox text"
|
"description": "Messagebox text"
|
||||||
},
|
},
|
||||||
"remove_paused_downloads": {
|
"remove_paused_downloads": {
|
||||||
@ -664,7 +676,7 @@
|
|||||||
"description": "Messagebox text"
|
"description": "Messagebox text"
|
||||||
},
|
},
|
||||||
"remove_selected_complete_downloads": {
|
"remove_selected_complete_downloads": {
|
||||||
"message": "Hapus Yang Selesai Di Pilihan",
|
"message": "Hapus Yang Selesai Dari Unduhan Terpilih",
|
||||||
"description": "Menu text"
|
"description": "Menu text"
|
||||||
},
|
},
|
||||||
"remove_selected_complete_downloads_question": {
|
"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)"
|
"description": "Media label (short)"
|
||||||
},
|
},
|
||||||
"missing": {
|
"missing": {
|
||||||
"message": "Trūksta",
|
"message": "Nėra",
|
||||||
"description": "Status text in manager"
|
"description": "Status text in manager"
|
||||||
},
|
},
|
||||||
"NETWORK_FAILED": {
|
"NETWORK_FAILED": {
|
||||||
@ -684,7 +684,7 @@
|
|||||||
"description": "Preferences/General"
|
"description": "Preferences/General"
|
||||||
},
|
},
|
||||||
"pref_hide_context": {
|
"pref_hide_context": {
|
||||||
"message": "Nerodyti bendrųjų kontekstinio meniu elementų",
|
"message": "Kontekstiniame meniu nerodyti bendrųjų elementų",
|
||||||
"description": "Preferences/General"
|
"description": "Preferences/General"
|
||||||
},
|
},
|
||||||
"pref_manager_tooltip": {
|
"pref_manager_tooltip": {
|
||||||
@ -692,15 +692,15 @@
|
|||||||
"description": "Preferences/General"
|
"description": "Preferences/General"
|
||||||
},
|
},
|
||||||
"pref_open_manager_on_queue": {
|
"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"
|
"description": "Preferences/General"
|
||||||
},
|
},
|
||||||
"pref_queue_notification": {
|
"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"
|
"description": "Preferences/General"
|
||||||
},
|
},
|
||||||
"pref_remove_missing_on_init": {
|
"pref_remove_missing_on_init": {
|
||||||
"message": "Pašalinti trūkstamus parsisiuntimus po restarto",
|
"message": "Šalinti nepavykusius parsisiuntimus po restarto",
|
||||||
"description": "Preferences/General"
|
"description": "Preferences/General"
|
||||||
},
|
},
|
||||||
"pref_show_urls": {
|
"pref_show_urls": {
|
||||||
@ -740,7 +740,7 @@
|
|||||||
"description": "Window/tab title; Preferences"
|
"description": "Window/tab title; Preferences"
|
||||||
},
|
},
|
||||||
"queue_finished": {
|
"queue_finished": {
|
||||||
"message": "Parsisiuntimų eilė baigta",
|
"message": "Visi parsisiuntimai baigti",
|
||||||
"description": "Notification text"
|
"description": "Notification text"
|
||||||
},
|
},
|
||||||
"queued_download": {
|
"queued_download": {
|
||||||
@ -862,11 +862,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"remove_missing": {
|
"remove_missing": {
|
||||||
"message": "Išvalyti trūkstamus parsisiuntimus",
|
"message": "Išvalyti nepavykusius parsisiuntimus",
|
||||||
"description": "Menu text"
|
"description": "Menu text"
|
||||||
},
|
},
|
||||||
"remove_missing_downloads_question": {
|
"remove_missing_downloads_question": {
|
||||||
"message": "Norite išvalyti visus trūkstamus parsisiuntimus?",
|
"message": "Norite išvalyti visus nepavykusius parsisiuntimus?",
|
||||||
"description": "Messagebox text"
|
"description": "Messagebox text"
|
||||||
},
|
},
|
||||||
"remove_paused_downloads": {
|
"remove_paused_downloads": {
|
||||||
|
@ -12,7 +12,7 @@
|
|||||||
"description": "Action: Add paused"
|
"description": "Action: Add paused"
|
||||||
},
|
},
|
||||||
"add_download": {
|
"add_download": {
|
||||||
"message": "добавить закачку",
|
"message": "Добавить закачку",
|
||||||
"description": "Action for adding a download"
|
"description": "Action for adding a download"
|
||||||
},
|
},
|
||||||
"add_new": {
|
"add_new": {
|
||||||
@ -244,7 +244,7 @@
|
|||||||
"description": "OneClick! action; Menu text"
|
"description": "OneClick! action; Menu text"
|
||||||
},
|
},
|
||||||
"dta_turbo_all": {
|
"dta_turbo_all": {
|
||||||
"message": "ОднимКликом! - Все вкладки",
|
"message": "OneClick! - Все вкладки",
|
||||||
"description": "Menu text"
|
"description": "Menu text"
|
||||||
},
|
},
|
||||||
"dta_turbo_image": {
|
"dta_turbo_image": {
|
||||||
|
File diff suppressed because it is too large
Load Diff
1170
_locales/zh_TW/messages.json
Normal file
1170
_locales/zh_TW/messages.json
Normal file
File diff suppressed because it is too large
Load Diff
396
data/mime.json
Normal file
396
data/mime.json
Normal file
@ -0,0 +1,396 @@
|
|||||||
|
{
|
||||||
|
"e": {
|
||||||
|
"3gpp": "3gp",
|
||||||
|
"asx": "asf",
|
||||||
|
"gz": "gzip",
|
||||||
|
"heic": "heif",
|
||||||
|
"html": [
|
||||||
|
"htm",
|
||||||
|
"shtml",
|
||||||
|
"php"
|
||||||
|
],
|
||||||
|
"jar": [
|
||||||
|
"war",
|
||||||
|
"ear"
|
||||||
|
],
|
||||||
|
"jpg": [
|
||||||
|
"jpeg",
|
||||||
|
"jpe",
|
||||||
|
"jfif"
|
||||||
|
],
|
||||||
|
"js": "jsx",
|
||||||
|
"mid": [
|
||||||
|
"midi",
|
||||||
|
"kar"
|
||||||
|
],
|
||||||
|
"mkv": [
|
||||||
|
"mk3d",
|
||||||
|
"mks"
|
||||||
|
],
|
||||||
|
"mov": [
|
||||||
|
"qt",
|
||||||
|
"moov"
|
||||||
|
],
|
||||||
|
"mpg": [
|
||||||
|
"mpe",
|
||||||
|
"mpeg"
|
||||||
|
],
|
||||||
|
"pem": [
|
||||||
|
"crt",
|
||||||
|
"der"
|
||||||
|
],
|
||||||
|
"pl": "pm",
|
||||||
|
"prc": "pdb",
|
||||||
|
"ps": [
|
||||||
|
"eps",
|
||||||
|
"ai"
|
||||||
|
],
|
||||||
|
"svg": "svgz",
|
||||||
|
"tcl": "tk",
|
||||||
|
"tif": "tiff"
|
||||||
|
},
|
||||||
|
"m": {
|
||||||
|
"application/7z": "7z",
|
||||||
|
"application/7z-compressed": "7z",
|
||||||
|
"application/ai": "ps",
|
||||||
|
"application/atom": "atom",
|
||||||
|
"application/atom+xml": "atom",
|
||||||
|
"application/bz2": "bz2",
|
||||||
|
"application/bzip2": "bz2",
|
||||||
|
"application/cco": "cco",
|
||||||
|
"application/cocoa": "cco",
|
||||||
|
"application/compressed": "gz",
|
||||||
|
"application/crt": "pem",
|
||||||
|
"application/der": "pem",
|
||||||
|
"application/doc": "doc",
|
||||||
|
"application/ear": "jar",
|
||||||
|
"application/eot": "eot",
|
||||||
|
"application/eps": "ps",
|
||||||
|
"application/gz": "gz",
|
||||||
|
"application/gzip": "gz",
|
||||||
|
"application/hqx": "hqx",
|
||||||
|
"application/jar": "jar",
|
||||||
|
"application/jardiff": "jardiff",
|
||||||
|
"application/java-archive": "jar",
|
||||||
|
"application/java-archive-diff": "jardiff",
|
||||||
|
"application/java-jnlp-file": "jnlp",
|
||||||
|
"application/javascript": "js",
|
||||||
|
"application/jnlp": "jnlp",
|
||||||
|
"application/js": "js",
|
||||||
|
"application/json": "json",
|
||||||
|
"application/jsx": "js",
|
||||||
|
"application/kml": "kml",
|
||||||
|
"application/kmz": "kmz",
|
||||||
|
"application/m3u8": "m3u8",
|
||||||
|
"application/mac-binhex40": "hqx",
|
||||||
|
"application/makeself": "run",
|
||||||
|
"application/msword": "doc",
|
||||||
|
"application/odg": "odg",
|
||||||
|
"application/odp": "odp",
|
||||||
|
"application/ods": "ods",
|
||||||
|
"application/odt": "odt",
|
||||||
|
"application/pdb": "prc",
|
||||||
|
"application/pdf": "pdf",
|
||||||
|
"application/pem": "pem",
|
||||||
|
"application/perl": "pl",
|
||||||
|
"application/pilot": "prc",
|
||||||
|
"application/pl": "pl",
|
||||||
|
"application/pm": "pl",
|
||||||
|
"application/postscript": "ps",
|
||||||
|
"application/ppt": "ppt",
|
||||||
|
"application/prc": "prc",
|
||||||
|
"application/ps": "ps",
|
||||||
|
"application/rar": "rar",
|
||||||
|
"application/rar-compressed": "rar",
|
||||||
|
"application/redhat-package-manager": "rpm",
|
||||||
|
"application/rpm": "rpm",
|
||||||
|
"application/rss": "rss",
|
||||||
|
"application/rss+xml": "rss",
|
||||||
|
"application/rtf": "rtf",
|
||||||
|
"application/run": "run",
|
||||||
|
"application/sea": "sea",
|
||||||
|
"application/shockwave-flash": "swf",
|
||||||
|
"application/sit": "sit",
|
||||||
|
"application/stuffit": "sit",
|
||||||
|
"application/swf": "swf",
|
||||||
|
"application/tar": "tar",
|
||||||
|
"application/tcl": "tcl",
|
||||||
|
"application/tk": "tcl",
|
||||||
|
"application/vnd.apple.mpegurl": "m3u8",
|
||||||
|
"application/vnd.google-earth.kml+xml": "kml",
|
||||||
|
"application/vnd.google-earth.kmz": "kmz",
|
||||||
|
"application/vnd.ms-excel": "xls",
|
||||||
|
"application/vnd.ms-fontobject": "eot",
|
||||||
|
"application/vnd.ms-powerpoint": "ppt",
|
||||||
|
"application/vnd.oasis.opendocument.graphics": "odg",
|
||||||
|
"application/vnd.oasis.opendocument.presentation": "odp",
|
||||||
|
"application/vnd.oasis.opendocument.spreadsheet": "ods",
|
||||||
|
"application/vnd.oasis.opendocument.text": "odt",
|
||||||
|
"application/vnd.wap.wmlc": "wmlc",
|
||||||
|
"application/war": "jar",
|
||||||
|
"application/wmlc": "wmlc",
|
||||||
|
"application/x-7z": "7z",
|
||||||
|
"application/x-7z-compressed": "7z",
|
||||||
|
"application/x-ai": "ps",
|
||||||
|
"application/x-atom": "atom",
|
||||||
|
"application/x-atom+xml": "atom",
|
||||||
|
"application/x-bz2": "bz2",
|
||||||
|
"application/x-bzip2": "bz2",
|
||||||
|
"application/x-cco": "cco",
|
||||||
|
"application/x-cocoa": "cco",
|
||||||
|
"application/x-compressed": "gz",
|
||||||
|
"application/x-crt": "pem",
|
||||||
|
"application/x-der": "pem",
|
||||||
|
"application/x-doc": "doc",
|
||||||
|
"application/x-ear": "jar",
|
||||||
|
"application/x-eot": "eot",
|
||||||
|
"application/x-eps": "ps",
|
||||||
|
"application/x-gz": "gz",
|
||||||
|
"application/x-gzip": "gz",
|
||||||
|
"application/x-hqx": "hqx",
|
||||||
|
"application/x-jar": "jar",
|
||||||
|
"application/x-jardiff": "jardiff",
|
||||||
|
"application/x-java-archive": "jar",
|
||||||
|
"application/x-java-archive-diff": "jardiff",
|
||||||
|
"application/x-java-jnlp-file": "jnlp",
|
||||||
|
"application/x-javascript": "js",
|
||||||
|
"application/x-jnlp": "jnlp",
|
||||||
|
"application/x-js": "js",
|
||||||
|
"application/x-json": "json",
|
||||||
|
"application/x-jsx": "js",
|
||||||
|
"application/x-kml": "kml",
|
||||||
|
"application/x-kmz": "kmz",
|
||||||
|
"application/x-m3u8": "m3u8",
|
||||||
|
"application/x-mac-binhex40": "hqx",
|
||||||
|
"application/x-makeself": "run",
|
||||||
|
"application/x-msword": "doc",
|
||||||
|
"application/x-odg": "odg",
|
||||||
|
"application/x-odp": "odp",
|
||||||
|
"application/x-ods": "ods",
|
||||||
|
"application/x-odt": "odt",
|
||||||
|
"application/x-pdb": "prc",
|
||||||
|
"application/x-pdf": "pdf",
|
||||||
|
"application/x-pem": "pem",
|
||||||
|
"application/x-perl": "pl",
|
||||||
|
"application/x-pilot": "prc",
|
||||||
|
"application/x-pl": "pl",
|
||||||
|
"application/x-pm": "pl",
|
||||||
|
"application/x-postscript": "ps",
|
||||||
|
"application/x-ppt": "ppt",
|
||||||
|
"application/x-prc": "prc",
|
||||||
|
"application/x-ps": "ps",
|
||||||
|
"application/x-rar": "rar",
|
||||||
|
"application/x-rar-compressed": "rar",
|
||||||
|
"application/x-redhat-package-manager": "rpm",
|
||||||
|
"application/x-rpm": "rpm",
|
||||||
|
"application/x-rss": "rss",
|
||||||
|
"application/x-rss+xml": "rss",
|
||||||
|
"application/x-rtf": "rtf",
|
||||||
|
"application/x-run": "run",
|
||||||
|
"application/x-sea": "sea",
|
||||||
|
"application/x-shockwave-flash": "swf",
|
||||||
|
"application/x-sit": "sit",
|
||||||
|
"application/x-stuffit": "sit",
|
||||||
|
"application/x-swf": "swf",
|
||||||
|
"application/x-tar": "tar",
|
||||||
|
"application/x-tcl": "tcl",
|
||||||
|
"application/x-tk": "tcl",
|
||||||
|
"application/x-vnd.apple.mpegurl": "m3u8",
|
||||||
|
"application/x-vnd.google-earth.kml+xml": "kml",
|
||||||
|
"application/x-vnd.google-earth.kmz": "kmz",
|
||||||
|
"application/x-vnd.ms-excel": "xls",
|
||||||
|
"application/x-vnd.ms-fontobject": "eot",
|
||||||
|
"application/x-vnd.ms-powerpoint": "ppt",
|
||||||
|
"application/x-vnd.oasis.opendocument.graphics": "odg",
|
||||||
|
"application/x-vnd.oasis.opendocument.presentation": "odp",
|
||||||
|
"application/x-vnd.oasis.opendocument.spreadsheet": "ods",
|
||||||
|
"application/x-vnd.oasis.opendocument.text": "odt",
|
||||||
|
"application/x-vnd.wap.wmlc": "wmlc",
|
||||||
|
"application/x-war": "jar",
|
||||||
|
"application/x-wmlc": "wmlc",
|
||||||
|
"application/x-x509-ca-cert": "pem",
|
||||||
|
"application/x-xhtml": "xhtml",
|
||||||
|
"application/x-xhtml+xml": "xhtml",
|
||||||
|
"application/x-xls": "xls",
|
||||||
|
"application/x-xpi": "xpi",
|
||||||
|
"application/x-xpinstall": "xpi",
|
||||||
|
"application/x-xspf": "xspf",
|
||||||
|
"application/x-xspf+xml": "xspf",
|
||||||
|
"application/x-xz": "xz",
|
||||||
|
"application/x-zip": "zip",
|
||||||
|
"application/x509-ca-cert": "pem",
|
||||||
|
"application/xhtml": "xhtml",
|
||||||
|
"application/xhtml+xml": "xhtml",
|
||||||
|
"application/xls": "xls",
|
||||||
|
"application/xpi": "xpi",
|
||||||
|
"application/xpinstall": "xpi",
|
||||||
|
"application/xspf": "xspf",
|
||||||
|
"application/xspf+xml": "xspf",
|
||||||
|
"application/xz": "xz",
|
||||||
|
"application/zip": "zip",
|
||||||
|
"audio/kar": "mid",
|
||||||
|
"audio/m4a": "m4a",
|
||||||
|
"audio/matroska": "mka",
|
||||||
|
"audio/mid": "mid",
|
||||||
|
"audio/midi": "mid",
|
||||||
|
"audio/mka": "mka",
|
||||||
|
"audio/mp3": "mp3",
|
||||||
|
"audio/mpeg": "mp3",
|
||||||
|
"audio/ogg": "ogg",
|
||||||
|
"audio/ra": "ra",
|
||||||
|
"audio/realaudio": "ra",
|
||||||
|
"audio/x-kar": "mid",
|
||||||
|
"audio/x-m4a": "m4a",
|
||||||
|
"audio/x-matroska": "mka",
|
||||||
|
"audio/x-mid": "mid",
|
||||||
|
"audio/x-midi": "mid",
|
||||||
|
"audio/x-mka": "mka",
|
||||||
|
"audio/x-mp3": "mp3",
|
||||||
|
"audio/x-mpeg": "mp3",
|
||||||
|
"audio/x-ogg": "ogg",
|
||||||
|
"audio/x-ra": "ra",
|
||||||
|
"audio/x-realaudio": "ra",
|
||||||
|
"font/woff": "woff",
|
||||||
|
"font/woff2": "woff2",
|
||||||
|
"font/x-woff": "woff",
|
||||||
|
"font/x-woff2": "woff2",
|
||||||
|
"image/bmp": "bmp",
|
||||||
|
"image/gif": "gif",
|
||||||
|
"image/heic": "heic",
|
||||||
|
"image/heif": "heic",
|
||||||
|
"image/heif-sequence": "heic",
|
||||||
|
"image/ico": "ico",
|
||||||
|
"image/icon": "ico",
|
||||||
|
"image/jfif": "jpg",
|
||||||
|
"image/jng": "jng",
|
||||||
|
"image/jpe": "jpg",
|
||||||
|
"image/jpeg": "jpg",
|
||||||
|
"image/jpg": "jpg",
|
||||||
|
"image/ms-bmp": "bmp",
|
||||||
|
"image/png": "png",
|
||||||
|
"image/svg": "svg",
|
||||||
|
"image/svg+xml": "svg",
|
||||||
|
"image/svgz": "svg",
|
||||||
|
"image/tif": "tif",
|
||||||
|
"image/tiff": "tif",
|
||||||
|
"image/vnd.wap.wbmp": "wbmp",
|
||||||
|
"image/wbmp": "wbmp",
|
||||||
|
"image/webp": "webp",
|
||||||
|
"image/x-bmp": "bmp",
|
||||||
|
"image/x-gif": "gif",
|
||||||
|
"image/x-heic": "heic",
|
||||||
|
"image/x-heif": "heic",
|
||||||
|
"image/x-heif-sequence": "heic",
|
||||||
|
"image/x-ico": "ico",
|
||||||
|
"image/x-icon": "ico",
|
||||||
|
"image/x-jfif": "jpg",
|
||||||
|
"image/x-jng": "jng",
|
||||||
|
"image/x-jpe": "jpg",
|
||||||
|
"image/x-jpeg": "jpg",
|
||||||
|
"image/x-jpg": "jpg",
|
||||||
|
"image/x-ms-bmp": "bmp",
|
||||||
|
"image/x-png": "png",
|
||||||
|
"image/x-svg": "svg",
|
||||||
|
"image/x-svg+xml": "svg",
|
||||||
|
"image/x-svgz": "svg",
|
||||||
|
"image/x-tif": "tif",
|
||||||
|
"image/x-tiff": "tif",
|
||||||
|
"image/x-vnd.wap.wbmp": "wbmp",
|
||||||
|
"image/x-wbmp": "wbmp",
|
||||||
|
"image/x-webp": "webp",
|
||||||
|
"text/component": "htc",
|
||||||
|
"text/css": "css",
|
||||||
|
"text/htc": "htc",
|
||||||
|
"text/htm": "html",
|
||||||
|
"text/html": "html",
|
||||||
|
"text/jad": "jad",
|
||||||
|
"text/javascript": "js",
|
||||||
|
"text/js": "js",
|
||||||
|
"text/jsx": "js",
|
||||||
|
"text/mathml": "mml",
|
||||||
|
"text/mml": "mml",
|
||||||
|
"text/php": "html",
|
||||||
|
"text/plain": "txt",
|
||||||
|
"text/shtml": "html",
|
||||||
|
"text/txt": "txt",
|
||||||
|
"text/vnd.sun.j2me.app-descriptor": "jad",
|
||||||
|
"text/vnd.wap.wml": "wml",
|
||||||
|
"text/wml": "wml",
|
||||||
|
"text/x-component": "htc",
|
||||||
|
"text/x-css": "css",
|
||||||
|
"text/x-htc": "htc",
|
||||||
|
"text/x-htm": "html",
|
||||||
|
"text/x-html": "html",
|
||||||
|
"text/x-jad": "jad",
|
||||||
|
"text/x-javascript": "js",
|
||||||
|
"text/x-js": "js",
|
||||||
|
"text/x-jsx": "js",
|
||||||
|
"text/x-mathml": "mml",
|
||||||
|
"text/x-mml": "mml",
|
||||||
|
"text/x-php": "html",
|
||||||
|
"text/x-plain": "txt",
|
||||||
|
"text/x-shtml": "html",
|
||||||
|
"text/x-txt": "txt",
|
||||||
|
"text/x-vnd.sun.j2me.app-descriptor": "jad",
|
||||||
|
"text/x-vnd.wap.wml": "wml",
|
||||||
|
"text/x-wml": "wml",
|
||||||
|
"text/x-xml": "xml",
|
||||||
|
"text/xml": "xml",
|
||||||
|
"video/3gp": "3gpp",
|
||||||
|
"video/3gpp": "3gpp",
|
||||||
|
"video/asf": "asx",
|
||||||
|
"video/asx": "asx",
|
||||||
|
"video/avi": "avi",
|
||||||
|
"video/flv": "flv",
|
||||||
|
"video/m4v": "m4v",
|
||||||
|
"video/matroska": "mkv",
|
||||||
|
"video/mk3d": "mkv",
|
||||||
|
"video/mks": "mkv",
|
||||||
|
"video/mkv": "mkv",
|
||||||
|
"video/mng": "mng",
|
||||||
|
"video/moov": "mov",
|
||||||
|
"video/mov": "mov",
|
||||||
|
"video/mp2t": "ts",
|
||||||
|
"video/mp4": "mp4",
|
||||||
|
"video/mpe": "mpg",
|
||||||
|
"video/mpeg": "mpg",
|
||||||
|
"video/mpg": "mpg",
|
||||||
|
"video/ms-asf": "asx",
|
||||||
|
"video/ms-wmv": "wmv",
|
||||||
|
"video/msvideo": "avi",
|
||||||
|
"video/opus": "opus",
|
||||||
|
"video/qt": "mov",
|
||||||
|
"video/quicktime": "mov",
|
||||||
|
"video/ts": "ts",
|
||||||
|
"video/webm": "webm",
|
||||||
|
"video/wmv": "wmv",
|
||||||
|
"video/x-3gp": "3gpp",
|
||||||
|
"video/x-3gpp": "3gpp",
|
||||||
|
"video/x-asf": "asx",
|
||||||
|
"video/x-asx": "asx",
|
||||||
|
"video/x-avi": "avi",
|
||||||
|
"video/x-flv": "flv",
|
||||||
|
"video/x-m4v": "m4v",
|
||||||
|
"video/x-matroska": "mkv",
|
||||||
|
"video/x-mk3d": "mkv",
|
||||||
|
"video/x-mks": "mkv",
|
||||||
|
"video/x-mkv": "mkv",
|
||||||
|
"video/x-mng": "mng",
|
||||||
|
"video/x-moov": "mov",
|
||||||
|
"video/x-mov": "mov",
|
||||||
|
"video/x-mp2t": "ts",
|
||||||
|
"video/x-mp4": "mp4",
|
||||||
|
"video/x-mpe": "mpg",
|
||||||
|
"video/x-mpeg": "mpg",
|
||||||
|
"video/x-mpg": "mpg",
|
||||||
|
"video/x-ms-asf": "asx",
|
||||||
|
"video/x-ms-wmv": "wmv",
|
||||||
|
"video/x-msvideo": "avi",
|
||||||
|
"video/x-opus": "opus",
|
||||||
|
"video/x-qt": "mov",
|
||||||
|
"video/x-quicktime": "mov",
|
||||||
|
"video/x-ts": "ts",
|
||||||
|
"video/x-webm": "webm",
|
||||||
|
"video/x-wmv": "wmv"
|
||||||
|
}
|
||||||
|
}
|
@ -19,6 +19,9 @@ import {
|
|||||||
// eslint-disable-next-line no-unused-vars
|
// eslint-disable-next-line no-unused-vars
|
||||||
MenuClickInfo,
|
MenuClickInfo,
|
||||||
CHROME,
|
CHROME,
|
||||||
|
runtime,
|
||||||
|
history,
|
||||||
|
sessions,
|
||||||
} from "./browser";
|
} from "./browser";
|
||||||
import { Bus } from "./bus";
|
import { Bus } from "./bus";
|
||||||
import { filterInSitu } from "./util";
|
import { filterInSitu } from "./util";
|
||||||
@ -103,7 +106,7 @@ class Handler {
|
|||||||
discarded: false,
|
discarded: false,
|
||||||
};
|
};
|
||||||
if (!CHROME) {
|
if (!CHROME) {
|
||||||
toptions.hidden = true;
|
toptions.hidden = false;
|
||||||
}
|
}
|
||||||
const selectedTabs = options.allTabs ?
|
const selectedTabs = options.allTabs ?
|
||||||
await tabs.query(toptions) as any[] :
|
await tabs.query(toptions) as any[] :
|
||||||
@ -566,6 +569,43 @@ locale.then(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
(async function init() {
|
(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());
|
await Prefs.set("last-run", new Date());
|
||||||
Prefs.get("global-turbo", false).then(v => adjustAction(v));
|
Prefs.get("global-turbo", false).then(v => adjustAction(v));
|
||||||
Prefs.on("global-turbo", (prefs, key, value) => {
|
Prefs.on("global-turbo", (prefs, key, value) => {
|
||||||
|
@ -39,16 +39,19 @@ export interface RawPort {
|
|||||||
postMessage: (message: any) => void;
|
postMessage: (message: any) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const {extension} = polyfill;
|
|
||||||
export const {notifications} = polyfill;
|
|
||||||
export const {browserAction} = polyfill;
|
export const {browserAction} = polyfill;
|
||||||
export const {contextMenus} = polyfill;
|
export const {contextMenus} = polyfill;
|
||||||
export const {downloads} = polyfill;
|
export const {downloads} = polyfill;
|
||||||
|
export const {extension} = polyfill;
|
||||||
|
export const {history} = polyfill;
|
||||||
export const {menus} = polyfill;
|
export const {menus} = polyfill;
|
||||||
|
export const {notifications} = polyfill;
|
||||||
export const {runtime} = polyfill;
|
export const {runtime} = polyfill;
|
||||||
|
export const {sessions} = polyfill;
|
||||||
export const {storage} = polyfill;
|
export const {storage} = polyfill;
|
||||||
export const {tabs} = polyfill;
|
export const {tabs} = polyfill;
|
||||||
export const {webNavigation} = polyfill;
|
export const {webNavigation} = polyfill;
|
||||||
|
export const {webRequest} = polyfill;
|
||||||
export const {windows} = polyfill;
|
export const {windows} = polyfill;
|
||||||
|
|
||||||
export const CHROME = navigator.appVersion.includes("Chrome/");
|
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(/^[^']*'/, ""));
|
||||||
|
}
|
||||||
|
}
|
@ -9,7 +9,7 @@ import { EventEmitter } from "./events";
|
|||||||
import { TYPE_LINK, TYPE_MEDIA, TYPE_ALL } from "./constants";
|
import { TYPE_LINK, TYPE_MEDIA, TYPE_ALL } from "./constants";
|
||||||
// eslint-disable-next-line no-unused-vars
|
// eslint-disable-next-line no-unused-vars
|
||||||
import { Overlayable } from "./objectoverlay";
|
import { Overlayable } from "./objectoverlay";
|
||||||
import * as DEFAULT_FILTERS from "../data/filters.json";
|
import DEFAULT_FILTERS from "../data/filters.json";
|
||||||
import { FASTFILTER } from "./recentlist";
|
import { FASTFILTER } from "./recentlist";
|
||||||
import { _, locale } from "./i18n";
|
import { _, locale } from "./i18n";
|
||||||
// eslint-disable-next-line no-unused-vars
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
// License: MIT
|
// License: MIT
|
||||||
|
|
||||||
import {memoize} from "./memoize";
|
import {memoize} from "./memoize";
|
||||||
import * as langs from "../_locales/all.json";
|
import langs from "../_locales/all.json";
|
||||||
import { sorted, naturalCaseCompare } from "./sorting";
|
import { sorted, naturalCaseCompare } from "./sorting";
|
||||||
|
|
||||||
|
|
||||||
|
@ -27,6 +27,9 @@ const SAVEDPROPS = [
|
|||||||
"written",
|
"written",
|
||||||
// server stuff
|
// server stuff
|
||||||
"serverName",
|
"serverName",
|
||||||
|
"browserName",
|
||||||
|
"mime",
|
||||||
|
"prerolled",
|
||||||
// other options
|
// other options
|
||||||
"private",
|
"private",
|
||||||
// db
|
// db
|
||||||
@ -39,10 +42,13 @@ const DEFAULTS = {
|
|||||||
state: QUEUED,
|
state: QUEUED,
|
||||||
error: "",
|
error: "",
|
||||||
serverName: "",
|
serverName: "",
|
||||||
|
browserName: "",
|
||||||
fileName: "",
|
fileName: "",
|
||||||
totalSize: 0,
|
totalSize: 0,
|
||||||
written: 0,
|
written: 0,
|
||||||
manId: 0,
|
manId: 0,
|
||||||
|
mime: "",
|
||||||
|
prerolled: false
|
||||||
};
|
};
|
||||||
|
|
||||||
let sessionId = 0;
|
let sessionId = 0;
|
||||||
@ -59,14 +65,26 @@ export class BaseDownload {
|
|||||||
|
|
||||||
public url: string;
|
public url: string;
|
||||||
|
|
||||||
|
public usable: string;
|
||||||
|
|
||||||
public uReferrer: URLd;
|
public uReferrer: URLd;
|
||||||
|
|
||||||
public referrer: string;
|
public referrer: string;
|
||||||
|
|
||||||
|
public usableReferrer: string;
|
||||||
|
|
||||||
public startDate: Date;
|
public startDate: Date;
|
||||||
|
|
||||||
public fileName: string;
|
public fileName: string;
|
||||||
|
|
||||||
|
public description?: string;
|
||||||
|
|
||||||
|
public title?: string;
|
||||||
|
|
||||||
|
public batch: number;
|
||||||
|
|
||||||
|
public idx: number;
|
||||||
|
|
||||||
public error: string;
|
public error: string;
|
||||||
|
|
||||||
public postData: any;
|
public postData: any;
|
||||||
@ -79,8 +97,13 @@ export class BaseDownload {
|
|||||||
|
|
||||||
public serverName: string;
|
public serverName: string;
|
||||||
|
|
||||||
|
public browserName: string;
|
||||||
|
|
||||||
|
public mime: string;
|
||||||
|
|
||||||
public mask: string;
|
public mask: string;
|
||||||
|
|
||||||
|
public prerolled: boolean;
|
||||||
|
|
||||||
constructor(options: any) {
|
constructor(options: any) {
|
||||||
Object.assign(this, DEFAULTS);
|
Object.assign(this, DEFAULTS);
|
||||||
@ -115,6 +138,10 @@ export class BaseDownload {
|
|||||||
return this.serverName || this.fileName || this.urlName || "index.html";
|
return this.serverName || this.fileName || this.urlName || "index.html";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get currentName() {
|
||||||
|
return this.browserName || this.dest.name || this.finalName;
|
||||||
|
}
|
||||||
|
|
||||||
get urlName() {
|
get urlName() {
|
||||||
const path = parsePath(this.uURL);
|
const path = parsePath(this.uURL);
|
||||||
if (path.name) {
|
if (path.name) {
|
||||||
@ -152,6 +179,7 @@ export class BaseDownload {
|
|||||||
rv.destName = dest.name;
|
rv.destName = dest.name;
|
||||||
rv.destPath = dest.path;
|
rv.destPath = dest.path;
|
||||||
rv.destFull = dest.full;
|
rv.destFull = dest.full;
|
||||||
|
rv.currentName = this.browserName || rv.destName || rv.finalName;
|
||||||
rv.error = this.error;
|
rv.error = this.error;
|
||||||
rv.ext = this.renamer.p_ext;
|
rv.ext = this.renamer.p_ext;
|
||||||
return rv;
|
return rv;
|
||||||
|
@ -1,25 +1,26 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
// License: MIT
|
// License: MIT
|
||||||
|
|
||||||
|
import { CHROME, downloads } from "../browser";
|
||||||
import { Prefs } from "../prefs";
|
import { Prefs } from "../prefs";
|
||||||
import { parsePath, filterInSitu } from "../util";
|
|
||||||
import {
|
|
||||||
QUEUED, RUNNING, CANCELED, PAUSED, MISSING, DONE,
|
|
||||||
FORCABLE, PAUSABLE, CANCELABLE,
|
|
||||||
} from "./state";
|
|
||||||
import { BaseDownload } from "./basedownload";
|
|
||||||
import { PromiseSerializer } from "../pserializer";
|
import { PromiseSerializer } from "../pserializer";
|
||||||
|
import { filterInSitu, parsePath } from "../util";
|
||||||
|
import { BaseDownload } from "./basedownload";
|
||||||
// eslint-disable-next-line no-unused-vars
|
// eslint-disable-next-line no-unused-vars
|
||||||
import { Manager } from "./man";
|
import { Manager } from "./man";
|
||||||
import { downloads, CHROME } from "../browser";
|
import Renamer from "./renamer";
|
||||||
import { debounce } from "../../uikit/lib/util";
|
import {
|
||||||
|
CANCELABLE,
|
||||||
|
CANCELED,
|
||||||
const setShelfEnabled = downloads.setShelfEnabled || function() {
|
DONE,
|
||||||
// ignored
|
FORCABLE,
|
||||||
};
|
MISSING,
|
||||||
|
PAUSABLE,
|
||||||
const reenableShelf = debounce(() => setShelfEnabled(true), 1000, true);
|
PAUSED,
|
||||||
|
QUEUED,
|
||||||
|
RUNNING
|
||||||
|
} from "./state";
|
||||||
|
import { Preroller } from "./preroller";
|
||||||
|
|
||||||
type Header = {name: string; value: string};
|
type Header = {name: string; value: string};
|
||||||
interface Options {
|
interface Options {
|
||||||
@ -53,6 +54,7 @@ export class Download extends BaseDownload {
|
|||||||
}
|
}
|
||||||
|
|
||||||
markDirty() {
|
markDirty() {
|
||||||
|
this.renamer = new Renamer(this);
|
||||||
this.manager.setDirty(this);
|
this.manager.setDirty(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -80,6 +82,11 @@ export class Download extends BaseDownload {
|
|||||||
this.updateStateFromBrowser();
|
this.updateStateFromBrowser();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (state[0].state === "complete") {
|
||||||
|
this.changeState(DONE);
|
||||||
|
this.updateStateFromBrowser();
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!state[0].canResume) {
|
if (!state[0].canResume) {
|
||||||
throw new Error("Cannot resume");
|
throw new Error("Cannot resume");
|
||||||
}
|
}
|
||||||
@ -97,9 +104,22 @@ export class Download extends BaseDownload {
|
|||||||
if (this.state !== QUEUED) {
|
if (this.state !== QUEUED) {
|
||||||
throw new Error("invalid state");
|
throw new Error("invalid state");
|
||||||
}
|
}
|
||||||
console.trace("starting", this.toString(), this.toMsg());
|
console.log("starting", this.toString(), this.toMsg());
|
||||||
this.changeState(RUNNING);
|
this.changeState(RUNNING);
|
||||||
|
|
||||||
|
// Do NOT await
|
||||||
|
this.reallyStart();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async reallyStart() {
|
||||||
try {
|
try {
|
||||||
|
if (!this.prerolled) {
|
||||||
|
await this.maybePreroll();
|
||||||
|
if (this.state !== RUNNING) {
|
||||||
|
// Aborted by preroll
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
const options: Options = {
|
const options: Options = {
|
||||||
conflictAction: await Prefs.get("conflict-action"),
|
conflictAction: await Prefs.get("conflict-action"),
|
||||||
filename: this.dest.full,
|
filename: this.dest.full,
|
||||||
@ -124,8 +144,6 @@ export class Download extends BaseDownload {
|
|||||||
this.manager.removeManId(this.manId);
|
this.manager.removeManId(this.manId);
|
||||||
}
|
}
|
||||||
|
|
||||||
setShelfEnabled(false);
|
|
||||||
try {
|
|
||||||
try {
|
try {
|
||||||
this.manager.addManId(
|
this.manager.addManId(
|
||||||
this.manId = await downloads.download(options), this);
|
this.manId = await downloads.download(options), this);
|
||||||
@ -139,10 +157,6 @@ export class Download extends BaseDownload {
|
|||||||
this.manager.addManId(
|
this.manager.addManId(
|
||||||
this.manId = await downloads.download(options), this);
|
this.manId = await downloads.download(options), this);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
finally {
|
|
||||||
reenableShelf();
|
|
||||||
}
|
|
||||||
this.markDirty();
|
this.markDirty();
|
||||||
}
|
}
|
||||||
catch (ex) {
|
catch (ex) {
|
||||||
@ -152,6 +166,42 @@ export class Download extends BaseDownload {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async maybePreroll() {
|
||||||
|
try {
|
||||||
|
if (this.prerolled) {
|
||||||
|
// Check again, just in case, async and all
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const roller = new Preroller(this);
|
||||||
|
if (!roller.shouldPreroll) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
if (this.state === RUNNING) {
|
||||||
|
this.prerolled = true;
|
||||||
|
this.markDirty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
resume(forced = false) {
|
resume(forced = false) {
|
||||||
if (!(FORCABLE & this.state)) {
|
if (!(FORCABLE & this.state)) {
|
||||||
return;
|
return;
|
||||||
@ -181,9 +231,10 @@ export class Download extends BaseDownload {
|
|||||||
}
|
}
|
||||||
|
|
||||||
reset() {
|
reset() {
|
||||||
|
this.prerolled = false;
|
||||||
this.manId = 0;
|
this.manId = 0;
|
||||||
this.written = this.totalSize = 0;
|
this.written = this.totalSize = 0;
|
||||||
this.serverName = "";
|
this.mime = this.serverName = this.browserName = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
async removeFromBrowser() {
|
async removeFromBrowser() {
|
||||||
@ -260,8 +311,11 @@ export class Download extends BaseDownload {
|
|||||||
const state = (await downloads.search({id: this.manId})).pop();
|
const state = (await downloads.search({id: this.manId})).pop();
|
||||||
const {filename, error} = state;
|
const {filename, error} = state;
|
||||||
const path = parsePath(filename);
|
const path = parsePath(filename);
|
||||||
this.serverName = path.name;
|
this.browserName = path.name;
|
||||||
this.adoptSize(state);
|
this.adoptSize(state);
|
||||||
|
if (!this.mime && state.mime) {
|
||||||
|
this.mime = state.mime;
|
||||||
|
}
|
||||||
this.markDirty();
|
this.markDirty();
|
||||||
switch (state.state) {
|
switch (state.state) {
|
||||||
case "in_progress":
|
case "in_progress":
|
||||||
|
@ -25,11 +25,14 @@ const DIRTY_TIMEOUT = 100;
|
|||||||
const MISSING_TIMEOUT = 12 * 1000;
|
const MISSING_TIMEOUT = 12 * 1000;
|
||||||
const RELOAD_TIMEOUT = 10 * 1000;
|
const RELOAD_TIMEOUT = 10 * 1000;
|
||||||
|
|
||||||
|
const setShelfEnabled = downloads.setShelfEnabled || function() {
|
||||||
|
// ignored
|
||||||
|
};
|
||||||
|
|
||||||
export class Manager extends EventEmitter {
|
export class Manager extends EventEmitter {
|
||||||
private items: Download[];
|
private items: Download[];
|
||||||
|
|
||||||
private active: boolean;
|
public active: boolean;
|
||||||
|
|
||||||
private notifiedFinished: boolean;
|
private notifiedFinished: boolean;
|
||||||
|
|
||||||
@ -93,7 +96,10 @@ export class Manager extends EventEmitter {
|
|||||||
}
|
}
|
||||||
this.items.push(rv);
|
this.items.push(rv);
|
||||||
});
|
});
|
||||||
await this.resetScheduler();
|
|
||||||
|
// Do not wait for the scheduler
|
||||||
|
this.resetScheduler();
|
||||||
|
|
||||||
this.emit("inited");
|
this.emit("inited");
|
||||||
setTimeout(() => this.checkMissing(), MISSING_TIMEOUT);
|
setTimeout(() => this.checkMissing(), MISSING_TIMEOUT);
|
||||||
runtime.onUpdateAvailable.addListener(() => {
|
runtime.onUpdateAvailable.addListener(() => {
|
||||||
@ -148,7 +154,7 @@ export class Manager extends EventEmitter {
|
|||||||
}
|
}
|
||||||
const next = await this.scheduler.next(this.running);
|
const next = await this.scheduler.next(this.running);
|
||||||
if (!next) {
|
if (!next) {
|
||||||
this.maybeNotifyFinished();
|
this.maybeRunFinishActions();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (this.running.has(next) || next.state !== QUEUED) {
|
if (this.running.has(next) || next.state !== QUEUED) {
|
||||||
@ -168,10 +174,31 @@ export class Manager extends EventEmitter {
|
|||||||
async startDownload(download: Download) {
|
async startDownload(download: Download) {
|
||||||
// Add to running first, so we don't confuse the scheduler and other parts
|
// Add to running first, so we don't confuse the scheduler and other parts
|
||||||
this.running.add(download);
|
this.running.add(download);
|
||||||
|
setShelfEnabled(false);
|
||||||
await download.start();
|
await download.start();
|
||||||
this.notifiedFinished = false;
|
this.notifiedFinished = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async maybeRunFinishActions() {
|
||||||
|
if (this.running.size) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await this.maybeNotifyFinished();
|
||||||
|
if (this.running.size) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.shouldReload) {
|
||||||
|
this.saveQueue.trigger();
|
||||||
|
setTimeout(() => {
|
||||||
|
if (this.running.size) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
runtime.reload();
|
||||||
|
}, RELOAD_TIMEOUT);
|
||||||
|
}
|
||||||
|
setShelfEnabled(true);
|
||||||
|
}
|
||||||
|
|
||||||
async maybeNotifyFinished() {
|
async maybeNotifyFinished() {
|
||||||
if (!(await Prefs.get("finish-notification"))) {
|
if (!(await Prefs.get("finish-notification"))) {
|
||||||
return;
|
return;
|
||||||
@ -181,14 +208,6 @@ export class Manager extends EventEmitter {
|
|||||||
}
|
}
|
||||||
this.notifiedFinished = true;
|
this.notifiedFinished = true;
|
||||||
new Notification(null, _("queue-finished"));
|
new Notification(null, _("queue-finished"));
|
||||||
if (this.shouldReload) {
|
|
||||||
setTimeout(() => {
|
|
||||||
if (this.running.size) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
runtime.reload();
|
|
||||||
}, RELOAD_TIMEOUT);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
addManId(id: number, download: Download) {
|
addManId(id: number, download: Download) {
|
||||||
@ -236,7 +255,7 @@ export class Manager extends EventEmitter {
|
|||||||
this.emit("dirty", items);
|
this.emit("dirty", items);
|
||||||
}
|
}
|
||||||
|
|
||||||
save(items: Download[]) {
|
private save(items: Download[]) {
|
||||||
DB.saveItems(items.filter(i => !i.removed)).
|
DB.saveItems(items.filter(i => !i.removed)).
|
||||||
catch(console.error);
|
catch(console.error);
|
||||||
}
|
}
|
||||||
@ -361,6 +380,10 @@ export class Manager extends EventEmitter {
|
|||||||
}
|
}
|
||||||
this.emit("active", this.active);
|
this.emit("active", this.active);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getMsgItems() {
|
||||||
|
return this.items.map(e => e.toMsg());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let inited: Promise<Manager>;
|
let inited: Promise<Manager>;
|
||||||
|
@ -5,6 +5,10 @@ import { donate, openPrefs } from "../windowutils";
|
|||||||
import { API } from "../api";
|
import { API } from "../api";
|
||||||
// eslint-disable-next-line no-unused-vars
|
// eslint-disable-next-line no-unused-vars
|
||||||
import { BaseDownload } from "./basedownload";
|
import { BaseDownload } from "./basedownload";
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
import { Manager } from "./man";
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
import { Port } from "../bus";
|
||||||
|
|
||||||
type SID = {sid: number};
|
type SID = {sid: number};
|
||||||
type SIDS = {
|
type SIDS = {
|
||||||
@ -13,9 +17,9 @@ type SIDS = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export class ManagerPort {
|
export class ManagerPort {
|
||||||
private manager: any;
|
private manager: Manager;
|
||||||
|
|
||||||
private port: any;
|
private port: Port;
|
||||||
|
|
||||||
constructor(manager: any, port: any) {
|
constructor(manager: any, port: any) {
|
||||||
this.manager = manager;
|
this.manager = manager;
|
||||||
@ -79,7 +83,6 @@ export class ManagerPort {
|
|||||||
}
|
}
|
||||||
|
|
||||||
sendAll() {
|
sendAll() {
|
||||||
this.port.post(
|
this.port.post("all", this.manager.getMsgItems());
|
||||||
"all", this.manager.items.map((e: BaseDownload) => e.toMsg()));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
220
lib/manager/preroller.ts
Normal file
220
lib/manager/preroller.ts
Normal file
@ -0,0 +1,220 @@
|
|||||||
|
"use strict";
|
||||||
|
// License: MIT
|
||||||
|
|
||||||
|
import MimeType from "whatwg-mimetype";
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
import { Download } from "./download";
|
||||||
|
import { CHROME, webRequest } from "../browser";
|
||||||
|
import { CDHeaderParser } from "../cdheaderparser";
|
||||||
|
import { sanitizePath, parsePath } from "../util";
|
||||||
|
import { MimeDB } from "../mime";
|
||||||
|
|
||||||
|
const PREROLL_HEURISTICS = /dl|attach|download|name|file|get|retr|^n$|\.(php|asp|py|pl|action|htm|shtm)/i;
|
||||||
|
const PREROLL_HOSTS = /4cdn|chan/;
|
||||||
|
const PREROLL_TIMEOUT = 10000;
|
||||||
|
const PREROLL_NOPE = new Set<string>();
|
||||||
|
const PREROLL_SEARCHEXTS = Object.freeze(new Set<string>([
|
||||||
|
"php",
|
||||||
|
"asp",
|
||||||
|
"aspx",
|
||||||
|
"inc",
|
||||||
|
"py",
|
||||||
|
"pl",
|
||||||
|
"action",
|
||||||
|
"htm",
|
||||||
|
"html",
|
||||||
|
"shtml"
|
||||||
|
]));
|
||||||
|
const NAME_TESTER = /\.[a-z0-9]{1,5}$/i;
|
||||||
|
const CDPARSER = new CDHeaderParser();
|
||||||
|
|
||||||
|
export interface PrerollResults {
|
||||||
|
error?: string;
|
||||||
|
name?: string;
|
||||||
|
mime?: string;
|
||||||
|
finalURL?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Preroller {
|
||||||
|
private readonly download: Download
|
||||||
|
|
||||||
|
constructor(download: Download) {
|
||||||
|
this.download = download;
|
||||||
|
}
|
||||||
|
|
||||||
|
get shouldPreroll() {
|
||||||
|
const {uURL, renamer} = this.download;
|
||||||
|
const {pathname, search, host} = uURL;
|
||||||
|
if (PREROLL_NOPE.has(host)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!renamer.p_ext) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (search.length) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (uURL.pathname.endsWith("/")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (PREROLL_HEURISTICS.test(pathname)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (PREROLL_HOSTS.test(host)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async roll() {
|
||||||
|
try {
|
||||||
|
return await (CHROME ? this.prerollChrome() : this.prerollFirefox());
|
||||||
|
}
|
||||||
|
catch (ex) {
|
||||||
|
console.error("Failed to preroll", this, ex.toString(), ex.stack, ex);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async prerollFirefox() {
|
||||||
|
const controller = new AbortController();
|
||||||
|
const {signal} = controller;
|
||||||
|
const {uURL} = this.download;
|
||||||
|
const res = await fetch(uURL.toString(), {
|
||||||
|
method: "GET",
|
||||||
|
headers: new Headers({
|
||||||
|
Range: "bytes=0-1",
|
||||||
|
}),
|
||||||
|
mode: "same-origin",
|
||||||
|
signal,
|
||||||
|
});
|
||||||
|
if (res.body) {
|
||||||
|
res.body.cancel();
|
||||||
|
}
|
||||||
|
controller.abort();
|
||||||
|
const {headers} = res;
|
||||||
|
return this.finalize(headers, res);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async prerollChrome() {
|
||||||
|
let rid = "";
|
||||||
|
const {uURL} = this.download;
|
||||||
|
const rurl = uURL.toString();
|
||||||
|
let listener: any;
|
||||||
|
const wr = new Promise<any[]>(resolve => {
|
||||||
|
listener = (details: any) => {
|
||||||
|
const {url, requestId, statusCode} = details;
|
||||||
|
if (rid !== requestId && url !== rurl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line no-magic-numbers
|
||||||
|
if (statusCode >= 300 && statusCode < 400) {
|
||||||
|
// Redirect, continue tracking;
|
||||||
|
rid = requestId;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resolve(details.responseHeaders);
|
||||||
|
};
|
||||||
|
webRequest.onHeadersReceived.addListener(
|
||||||
|
listener, {urls: ["<all_urls>"]}, ["responseHeaders"]);
|
||||||
|
});
|
||||||
|
const p = Promise.race([
|
||||||
|
wr,
|
||||||
|
new Promise<any[]>((_, reject) =>
|
||||||
|
setTimeout(() => reject(new Error("timeout")), PREROLL_TIMEOUT))
|
||||||
|
]);
|
||||||
|
|
||||||
|
p.finally(() => {
|
||||||
|
webRequest.onHeadersReceived.removeListener(listener);
|
||||||
|
});
|
||||||
|
const controller = new AbortController();
|
||||||
|
const {signal} = controller;
|
||||||
|
const res = await fetch(rurl, {
|
||||||
|
method: "GET",
|
||||||
|
headers: new Headers({
|
||||||
|
Range: "bytes=0-1",
|
||||||
|
}),
|
||||||
|
signal,
|
||||||
|
});
|
||||||
|
if (res.body) {
|
||||||
|
res.body.cancel();
|
||||||
|
}
|
||||||
|
controller.abort();
|
||||||
|
const headers = await p;
|
||||||
|
return this.finalize(
|
||||||
|
new Headers(headers.map(i => [i.name, i.value])), res);
|
||||||
|
}
|
||||||
|
|
||||||
|
private finalize(headers: Headers, res: Response): PrerollResults {
|
||||||
|
const rv: PrerollResults = {};
|
||||||
|
|
||||||
|
const type = MimeType.parse(headers.get("content-type") || "");
|
||||||
|
if (type) {
|
||||||
|
rv.mime = type.essence;
|
||||||
|
}
|
||||||
|
|
||||||
|
const {p_ext: ext} = this.download.renamer;
|
||||||
|
const dispHeader = headers.get("content-disposition");
|
||||||
|
if (dispHeader) {
|
||||||
|
const file = CDPARSER.parse(dispHeader);
|
||||||
|
// Sanitize
|
||||||
|
rv.name = sanitizePath(file.replace(/[/\\]+/g, "-"));
|
||||||
|
}
|
||||||
|
else if (!ext || PREROLL_SEARCHEXTS.has(ext.toLocaleLowerCase())) {
|
||||||
|
const {searchParams} = this.download.uURL;
|
||||||
|
let detected = "";
|
||||||
|
for (const [, value] of searchParams) {
|
||||||
|
if (!NAME_TESTER.test(value)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const p = parsePath(value);
|
||||||
|
if (!p.base || !p.ext) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!MimeDB.hasExtension(p.ext)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (rv.mime) {
|
||||||
|
const mime = MimeDB.getMime(rv.mime);
|
||||||
|
if (mime && !mime.extensions.has(p.ext.toLowerCase())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const sanitized = sanitizePath(p.name);
|
||||||
|
if (sanitized.length <= detected.length) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
detected = sanitized;
|
||||||
|
}
|
||||||
|
if (detected) {
|
||||||
|
rv.name = detected;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rv.finalURL = res.url;
|
||||||
|
|
||||||
|
/* eslint-disable no-magic-numbers */
|
||||||
|
const {status} = res;
|
||||||
|
if (status === 404) {
|
||||||
|
rv.error = "SERVER_BAD_CONTENT";
|
||||||
|
}
|
||||||
|
else if (status === 403) {
|
||||||
|
rv.error = "SERVER_FORBIDDEN";
|
||||||
|
}
|
||||||
|
else if (status === 402 || status === 407) {
|
||||||
|
rv.error = "SERVER_UNAUTHORIZED";
|
||||||
|
}
|
||||||
|
else if (status === 400 || status === 405 || status === 416) {
|
||||||
|
PREROLL_NOPE.add(this.download.uURL.host);
|
||||||
|
if (PREROLL_NOPE.size > 1000) {
|
||||||
|
PREROLL_NOPE.delete(PREROLL_NOPE.keys().next().value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (status > 400 && status < 500) {
|
||||||
|
rv.error = "SERVER_FAILED";
|
||||||
|
}
|
||||||
|
/* eslint-enable no-magic-numbers */
|
||||||
|
|
||||||
|
return rv;
|
||||||
|
}
|
||||||
|
}
|
@ -2,8 +2,12 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
// License: MIT
|
// License: MIT
|
||||||
|
|
||||||
import { parsePath, sanitizePath } from "../util";
|
|
||||||
import { _ } from "../i18n";
|
import { _ } from "../i18n";
|
||||||
|
import { MimeDB } from "../mime";
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
import { parsePath, PathInfo, sanitizePath } from "../util";
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
import { BaseDownload } from "./basedownload";
|
||||||
|
|
||||||
const REPLACE_EXPR = /\*\w+\*/gi;
|
const REPLACE_EXPR = /\*\w+\*/gi;
|
||||||
|
|
||||||
@ -22,21 +26,41 @@ const DATE_FORMATTER = new Intl.NumberFormat(undefined, {
|
|||||||
});
|
});
|
||||||
|
|
||||||
export default class Renamer {
|
export default class Renamer {
|
||||||
private readonly d: any;
|
private readonly d: BaseDownload;
|
||||||
|
|
||||||
constructor(download: any) {
|
private readonly nameinfo: PathInfo;
|
||||||
|
|
||||||
|
constructor(download: BaseDownload) {
|
||||||
this.d = download;
|
this.d = download;
|
||||||
|
const info = parsePath(this.d.finalName);
|
||||||
|
this.nameinfo = this.fixupExtension(info);
|
||||||
}
|
}
|
||||||
|
|
||||||
get nameinfo() {
|
private fixupExtension(info: PathInfo): PathInfo {
|
||||||
return parsePath(this.d.finalName);
|
if (!this.d.mime) {
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
const mime = MimeDB.getMime(this.d.mime);
|
||||||
|
if (!mime) {
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
const {ext} = info;
|
||||||
|
if (mime.major === "image" || mime.major === "video") {
|
||||||
|
if (ext && mime.extensions.has(ext.toLowerCase())) {
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
return new PathInfo(info.base, mime.primary, info.path);
|
||||||
|
}
|
||||||
|
if (ext) {
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
return new PathInfo(info.base, mime.primary, info.path);
|
||||||
}
|
}
|
||||||
|
|
||||||
get ref() {
|
get ref() {
|
||||||
return this.d.uReferrer;
|
return this.d.uReferrer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
get p_name() {
|
get p_name() {
|
||||||
return this.nameinfo.base;
|
return this.nameinfo.base;
|
||||||
}
|
}
|
||||||
@ -184,7 +208,7 @@ export default class Renamer {
|
|||||||
(self[prop] || "").trim() :
|
(self[prop] || "").trim() :
|
||||||
type;
|
type;
|
||||||
if (flat) {
|
if (flat) {
|
||||||
return rv.replace(/\/+/g, "-");
|
return rv.replace(/[/\\]+/g, "-");
|
||||||
}
|
}
|
||||||
return rv;
|
return rv;
|
||||||
}));
|
}));
|
||||||
|
65
lib/mime.ts
Normal file
65
lib/mime.ts
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
"use strict";
|
||||||
|
// License: MIT
|
||||||
|
|
||||||
|
import mime from "../data/mime.json";
|
||||||
|
|
||||||
|
export class MimeInfo {
|
||||||
|
public readonly type: string;
|
||||||
|
|
||||||
|
public readonly extensions: Set<string>;
|
||||||
|
|
||||||
|
public readonly major: string;
|
||||||
|
|
||||||
|
public readonly minor: string;
|
||||||
|
|
||||||
|
public readonly primary: string;
|
||||||
|
|
||||||
|
constructor(type: string, extensions: string[]) {
|
||||||
|
this.type = type;
|
||||||
|
const [major, minor] = type.split("/", 2);
|
||||||
|
this.major = major;
|
||||||
|
this.minor = minor;
|
||||||
|
[this.primary] = extensions;
|
||||||
|
this.extensions = new Set(extensions);
|
||||||
|
Object.freeze(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)) {
|
||||||
|
let toadd = more;
|
||||||
|
if (!Array.isArray(toadd)) {
|
||||||
|
toadd = [toadd];
|
||||||
|
}
|
||||||
|
toadd.unshift(prim);
|
||||||
|
exts.set(prim, toadd);
|
||||||
|
}
|
||||||
|
this.mimeToExts = new Map(Array.from(
|
||||||
|
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) {
|
||||||
|
const info = this.mimeToExts.get(mime.trim().toLocaleLowerCase());
|
||||||
|
return info ? info.primary : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
getMime(mime: string) {
|
||||||
|
return this.mimeToExts.get(mime.trim().toLocaleLowerCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
hasExtension(ext: string) {
|
||||||
|
return this.registeredExtensions.has(ext.toLowerCase());
|
||||||
|
}
|
||||||
|
}();
|
@ -1,9 +1,9 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
// License: MIT
|
// License: MIT
|
||||||
|
|
||||||
import * as DEFAULT_PREFS from "../data/prefs.json";
|
import DEFAULT_PREFS from "../data/prefs.json";
|
||||||
import { EventEmitter } from "./events";
|
import { EventEmitter } from "./events";
|
||||||
import {loadOverlay} from "./objectoverlay";
|
import { loadOverlay } from "./objectoverlay";
|
||||||
import { storage } from "./browser";
|
import { storage } from "./browser";
|
||||||
|
|
||||||
const PREFS = Symbol("PREFS");
|
const PREFS = Symbol("PREFS");
|
||||||
|
@ -98,6 +98,7 @@ export async function select(links: BaseItem[], media: BaseItem[]) {
|
|||||||
type: "popup",
|
type: "popup",
|
||||||
});
|
});
|
||||||
const window = await windows.create(windowOptions);
|
const window = await windows.create(windowOptions);
|
||||||
|
tracker.track(window.id, null);
|
||||||
try {
|
try {
|
||||||
const port = await Promise.race<Port>([
|
const port = await Promise.race<Port>([
|
||||||
new Promise<Port>(resolve => Bus.oncePort("select", resolve)),
|
new Promise<Port>(resolve => Bus.oncePort("select", resolve)),
|
||||||
|
@ -21,6 +21,7 @@ export async function single(item: BaseItem | null) {
|
|||||||
type: "popup",
|
type: "popup",
|
||||||
});
|
});
|
||||||
const window = await windows.create(windowOptions);
|
const window = await windows.create(windowOptions);
|
||||||
|
tracker.track(window.id, null);
|
||||||
try {
|
try {
|
||||||
const port: Port = await Promise.race<Port>([
|
const port: Port = await Promise.race<Port>([
|
||||||
new Promise<Port>(resolve => Bus.oncePort("single", resolve)),
|
new Promise<Port>(resolve => Bus.oncePort("single", resolve)),
|
||||||
|
78
lib/util.ts
78
lib/util.ts
@ -2,8 +2,8 @@
|
|||||||
// License: MIT
|
// License: MIT
|
||||||
|
|
||||||
import * as psl from "psl";
|
import * as psl from "psl";
|
||||||
import {memoize, identity} from "./memoize";
|
import { identity, memoize } from "./memoize";
|
||||||
export {debounce} from "../uikit/lib/util";
|
export { debounce } from "../uikit/lib/util";
|
||||||
|
|
||||||
export class Promised {
|
export class Promised {
|
||||||
private promise: Promise<any>;
|
private promise: Promise<any>;
|
||||||
@ -96,8 +96,72 @@ export const IS_WIN = typeof navigator !== "undefined" &&
|
|||||||
export const sanitizePath = identity(
|
export const sanitizePath = identity(
|
||||||
IS_WIN ? sanitizePathWindows : sanitizePathGeneric);
|
IS_WIN ? sanitizePathWindows : sanitizePathGeneric);
|
||||||
|
|
||||||
|
export class PathInfo {
|
||||||
|
private baseField: string;
|
||||||
|
|
||||||
|
private extField: string;
|
||||||
|
|
||||||
|
private pathField: string;
|
||||||
|
|
||||||
|
private nameField: string;
|
||||||
|
|
||||||
|
private fullField: string;
|
||||||
|
|
||||||
|
constructor(base: string, ext: string, path: string) {
|
||||||
|
this.baseField = base;
|
||||||
|
this.extField = ext;
|
||||||
|
this.pathField = path;
|
||||||
|
this.update();
|
||||||
|
}
|
||||||
|
|
||||||
|
get base() {
|
||||||
|
return this.baseField;
|
||||||
|
}
|
||||||
|
|
||||||
|
set base(nv) {
|
||||||
|
this.baseField = sanitizePath(nv);
|
||||||
|
this.update();
|
||||||
|
}
|
||||||
|
|
||||||
|
get ext() {
|
||||||
|
return this.extField;
|
||||||
|
}
|
||||||
|
|
||||||
|
set ext(nv) {
|
||||||
|
this.extField = sanitizePath(nv);
|
||||||
|
this.update();
|
||||||
|
}
|
||||||
|
|
||||||
|
get name() {
|
||||||
|
return this.nameField;
|
||||||
|
}
|
||||||
|
|
||||||
|
get path() {
|
||||||
|
return this.pathField;
|
||||||
|
}
|
||||||
|
|
||||||
|
set path(nv) {
|
||||||
|
this.pathField = sanitizePath(nv);
|
||||||
|
this.update();
|
||||||
|
}
|
||||||
|
|
||||||
|
get full() {
|
||||||
|
return this.fullField;
|
||||||
|
}
|
||||||
|
|
||||||
|
private update() {
|
||||||
|
this.nameField = this.extField ? `${this.baseField}.${this.extField}` : this.baseField;
|
||||||
|
this.fullField = this.pathField ? `${this.pathField}/${this.nameField}` : this.nameField;
|
||||||
|
}
|
||||||
|
|
||||||
|
clone() {
|
||||||
|
return new PathInfo(this.baseField, this.extField, this.pathField);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// XXX cleanup + test
|
// XXX cleanup + test
|
||||||
export const parsePath = memoize(function parsePath(path: string | URL) {
|
export const parsePath = memoize(function parsePath(
|
||||||
|
path: string | URL): PathInfo {
|
||||||
if (path instanceof URL) {
|
if (path instanceof URL) {
|
||||||
path = decodeURIComponent(path.pathname);
|
path = decodeURIComponent(path.pathname);
|
||||||
}
|
}
|
||||||
@ -127,13 +191,7 @@ export const parsePath = memoize(function parsePath(path: string | URL) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
path = pieces.join("/");
|
path = pieces.join("/");
|
||||||
return {
|
return new PathInfo(base, ext, path);
|
||||||
path,
|
|
||||||
name,
|
|
||||||
base,
|
|
||||||
ext,
|
|
||||||
full: path ? `${path}/${name}` : name
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export class CoalescedUpdate<T> extends Set<T> {
|
export class CoalescedUpdate<T> extends Set<T> {
|
||||||
|
@ -55,14 +55,16 @@ export class WindowStateTracker {
|
|||||||
|
|
||||||
getOptions(options: any) {
|
getOptions(options: any) {
|
||||||
const result = Object.assign(options, {
|
const result = Object.assign(options, {
|
||||||
width: this.width,
|
|
||||||
height: this.height,
|
|
||||||
state: this.state,
|
state: this.state,
|
||||||
});
|
});
|
||||||
|
if (result.state !== "maximized") {
|
||||||
|
result.width = this.width;
|
||||||
|
result.height = this.height;
|
||||||
if (this.top >= 0) {
|
if (this.top >= 0) {
|
||||||
result.top = this.top;
|
result.top = this.top;
|
||||||
result.left = this.left;
|
result.left = this.left;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
import { windows, tabs, runtime } from "../lib/browser";
|
import { windows, tabs, runtime } from "../lib/browser";
|
||||||
import {getManager} from "./manager/man";
|
import {getManager} from "./manager/man";
|
||||||
import * as DEFAULT_ICONS from "../data/icons.json";
|
import DEFAULT_ICONS from "../data/icons.json";
|
||||||
|
|
||||||
const DONATE_URL = "https://www.downthemall.org/howto/donate/";
|
const DONATE_URL = "https://www.downthemall.org/howto/donate/";
|
||||||
const MANAGER_URL = "/windows/manager.html";
|
const MANAGER_URL = "/windows/manager.html";
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"manifest_version": 2,
|
"manifest_version": 2,
|
||||||
"name": "DownThemAll!",
|
"name": "DownThemAll!",
|
||||||
"version": "4.0.8",
|
"version": "4.0.11",
|
||||||
|
|
||||||
"description": "__MSG_extensionDescription__",
|
"description": "__MSG_extensionDescription__",
|
||||||
"homepage_url": "https://downthemall.org/",
|
"homepage_url": "https://downthemall.org/",
|
||||||
@ -9,7 +9,7 @@
|
|||||||
|
|
||||||
"default_locale": "en",
|
"default_locale": "en",
|
||||||
|
|
||||||
"content_security_policy": "script-src 'self'; style-src 'self' 'unsafe-inline'; img-src data: blob: 'self'; connect-src data: blob: 'self'; default-src 'self'",
|
"content_security_policy": "script-src 'self'; style-src 'self' 'unsafe-inline'; img-src data: blob: 'self'; connect-src data: blob: http: https: 'self'; default-src 'self'",
|
||||||
|
|
||||||
"icons": {
|
"icons": {
|
||||||
"16": "style/icon16.png",
|
"16": "style/icon16.png",
|
||||||
@ -24,14 +24,17 @@
|
|||||||
"permissions": [
|
"permissions": [
|
||||||
"<all_urls>",
|
"<all_urls>",
|
||||||
"contextMenus",
|
"contextMenus",
|
||||||
"menus",
|
|
||||||
"downloads",
|
"downloads",
|
||||||
"downloads.open",
|
"downloads.open",
|
||||||
"downloads.shelf",
|
"downloads.shelf",
|
||||||
|
"history",
|
||||||
|
"menus",
|
||||||
"notifications",
|
"notifications",
|
||||||
|
"sessions",
|
||||||
"storage",
|
"storage",
|
||||||
"tabs",
|
"tabs",
|
||||||
"webNavigation"
|
"webNavigation",
|
||||||
|
"webRequest"
|
||||||
],
|
],
|
||||||
|
|
||||||
"background": {
|
"background": {
|
||||||
|
@ -33,7 +33,9 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/psl": "^1.1.0",
|
"@types/psl": "^1.1.0",
|
||||||
|
"@types/whatwg-mimetype": "^2.1.0",
|
||||||
"psl": "^1.3.0",
|
"psl": "^1.3.0",
|
||||||
"webextension-polyfill": "^0.4.0"
|
"webextension-polyfill": "^0.4.0",
|
||||||
|
"whatwg-mimetype": "^2.3.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -295,7 +295,7 @@ class Gatherer {
|
|||||||
function gather(msg: any, sender: any, callback: Function) {
|
function gather(msg: any, sender: any, callback: Function) {
|
||||||
try {
|
try {
|
||||||
if (!msg || msg.type !== "DTA:gather" || !callback) {
|
if (!msg || msg.type !== "DTA:gather" || !callback) {
|
||||||
return;
|
return Promise.resolve(null);
|
||||||
}
|
}
|
||||||
const gatherer = new Gatherer(msg);
|
const gatherer = new Gatherer(msg);
|
||||||
const result = {
|
const result = {
|
||||||
@ -313,10 +313,11 @@ function gather(msg: any, sender: any, callback: Function) {
|
|||||||
),
|
),
|
||||||
};
|
};
|
||||||
urlToUsable(result, result.baseURL);
|
urlToUsable(result, result.baseURL);
|
||||||
callback(result);
|
return Promise.resolve(result);
|
||||||
}
|
}
|
||||||
catch (ex) {
|
catch (ex) {
|
||||||
console.error(ex.toString(), ex.stack, ex);
|
console.error(ex.toString(), ex.stack, ex);
|
||||||
|
return Promise.resolve(null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -18,12 +18,17 @@
|
|||||||
--folder-color: rgb(214, 165, 4);
|
--folder-color: rgb(214, 165, 4);
|
||||||
--maskbutton-color: rgb(236, 185, 16);
|
--maskbutton-color: rgb(236, 185, 16);
|
||||||
--missing-color: rgb(0, 82, 204);
|
--missing-color: rgb(0, 82, 204);
|
||||||
|
--open-color: rgba(236, 185, 16, 0.8);
|
||||||
}
|
}
|
||||||
|
|
||||||
html[data-platform="mac"] {
|
html[data-platform="mac"] {
|
||||||
--folder-color: rgb(4, 102, 214);
|
--folder-color: rgb(4, 102, 214);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
html, body {
|
||||||
|
font-size: 10pt !important;
|
||||||
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'downthemall';
|
font-family: 'downthemall';
|
||||||
src: url('downthemall.woff2?75791791') format('woff2');
|
src: url('downthemall.woff2?75791791') format('woff2');
|
||||||
|
@ -108,11 +108,11 @@ body > * {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#colURL {
|
#colURL {
|
||||||
width: 38%;
|
width: 42%;
|
||||||
}
|
}
|
||||||
|
|
||||||
#colPercent {
|
#colPercent {
|
||||||
width: 3em;
|
width: 4em;
|
||||||
min-width: 3em;
|
min-width: 3em;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -121,11 +121,11 @@ body > * {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#colSize {
|
#colSize {
|
||||||
width: 15em;
|
width: 14em;
|
||||||
}
|
}
|
||||||
|
|
||||||
#colSpeed {
|
#colSpeed {
|
||||||
width: 6em;
|
width: 7em;
|
||||||
}
|
}
|
||||||
|
|
||||||
#colDomain,
|
#colDomain,
|
||||||
@ -154,6 +154,14 @@ body > * {
|
|||||||
height: 26px;
|
height: 26px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.virtualtable-row.opening {
|
||||||
|
background: var(--open-color) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.virtualtable-progress-container {
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
.virtualtable-progress-bar {
|
.virtualtable-progress-bar {
|
||||||
height: 14px;
|
height: 14px;
|
||||||
}
|
}
|
||||||
@ -262,6 +270,7 @@ body > * {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.virtualtable-column-6,
|
.virtualtable-column-6,
|
||||||
|
.virtualtable-column-4,
|
||||||
.virtualtable-column-3 {
|
.virtualtable-column-3 {
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
@ -430,6 +439,8 @@ body > * {
|
|||||||
justify-items: stretch;
|
justify-items: stretch;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
box-shadow: 2px 2px 6px black;
|
box-shadow: 2px 2px 6px black;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
#tooltip-infos {
|
#tooltip-infos {
|
||||||
|
@ -232,3 +232,7 @@ body > * {
|
|||||||
#maskButton {
|
#maskButton {
|
||||||
justify-self: flex-start;
|
justify-self: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#btnDownload {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
@ -81,3 +81,7 @@ h3 {
|
|||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#btnDownload {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
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;
|
||||||
|
});
|
||||||
|
});
|
@ -19,7 +19,7 @@ const OPTS = {
|
|||||||
state: DownloadState.QUEUED,
|
state: DownloadState.QUEUED,
|
||||||
batch: 42,
|
batch: 42,
|
||||||
idx: 23,
|
idx: 23,
|
||||||
mask: "*name*.*ext",
|
mask: "*name*.*ext*",
|
||||||
description: "desc / ript.ion .",
|
description: "desc / ript.ion .",
|
||||||
title: " *** TITLE *** ",
|
title: " *** TITLE *** ",
|
||||||
};
|
};
|
||||||
@ -57,6 +57,49 @@ describe("Renamer", function() {
|
|||||||
expect(dest.path).to.equal("");
|
expect(dest.path).to.equal("");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("*name*.*ext* (mime override)", function() {
|
||||||
|
const {dest} = new BaseDownload(
|
||||||
|
Object.assign({}, OPTS, {
|
||||||
|
mask: "*name* *batch*.*ext*",
|
||||||
|
mime: "image/jpeg"
|
||||||
|
}));
|
||||||
|
expect(dest.full).to.equal("filenäme 042.jpg");
|
||||||
|
expect(dest.name).to.equal("filenäme 042.jpg");
|
||||||
|
expect(dest.base).to.equal("filenäme 042");
|
||||||
|
expect(dest.ext).to.equal("jpg");
|
||||||
|
expect(dest.path).to.equal("");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("*name*.*ext* (mime no override)", function() {
|
||||||
|
const {dest} = new BaseDownload(
|
||||||
|
Object.assign({}, OPTS, {
|
||||||
|
mask: "*name* *batch*.*ext*",
|
||||||
|
mime: "image/jpeg",
|
||||||
|
url: "https://www.example.co.uk/filen%C3%A4me.JPe",
|
||||||
|
usable: "https://www.example.co.uk/filenäme.JPe",
|
||||||
|
}));
|
||||||
|
expect(dest.full).to.equal("filenäme 042.JPe");
|
||||||
|
expect(dest.name).to.equal("filenäme 042.JPe");
|
||||||
|
expect(dest.base).to.equal("filenäme 042");
|
||||||
|
expect(dest.ext).to.equal("JPe");
|
||||||
|
expect(dest.path).to.equal("");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("*name*.*ext* (mime override; missing ext)", function() {
|
||||||
|
const {dest} = new BaseDownload(
|
||||||
|
Object.assign({}, OPTS, {
|
||||||
|
mask: "*name* *batch*.*ext*",
|
||||||
|
mime: "application/json",
|
||||||
|
url: "https://www.example.co.uk/filen%C3%A4me",
|
||||||
|
usable: "https://www.example.co.uk/filenäme",
|
||||||
|
}));
|
||||||
|
expect(dest.full).to.equal("filenäme 042.json");
|
||||||
|
expect(dest.name).to.equal("filenäme 042.json");
|
||||||
|
expect(dest.base).to.equal("filenäme 042");
|
||||||
|
expect(dest.ext).to.equal("json");
|
||||||
|
expect(dest.path).to.equal("");
|
||||||
|
});
|
||||||
|
|
||||||
it("*text*", function() {
|
it("*text*", function() {
|
||||||
const dest = makeOne("*text*");
|
const dest = makeOne("*text*");
|
||||||
expect(dest.full).to.equal("desc/ript.ion");
|
expect(dest.full).to.equal("desc/ript.ion");
|
||||||
|
@ -12,6 +12,8 @@
|
|||||||
"noImplicitReturns": true,
|
"noImplicitReturns": true,
|
||||||
"noUnusedLocals": true,
|
"noUnusedLocals": true,
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"importHelpers": true,
|
||||||
"sourceMap": true
|
"sourceMap": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
23
util/additional.types
Normal file
23
util/additional.types
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
types {
|
||||||
|
application/x-x509-ca-cert pem crt der;
|
||||||
|
application/javascript js jsx;
|
||||||
|
audio/x-matroska mka;
|
||||||
|
image/bmp bmp;
|
||||||
|
image/heic heic heif;
|
||||||
|
image/heic heic heif;
|
||||||
|
image/heif-sequence heic heif;
|
||||||
|
image/heif-sequence heic heif;
|
||||||
|
image/jpeg jpg jpeg jpe jfif;
|
||||||
|
image/webp webp;
|
||||||
|
text/html html htm shtml php;
|
||||||
|
text/javascript js jsx;
|
||||||
|
video/mpeg mpg mpe mpeg mpg;
|
||||||
|
video/opus opus;
|
||||||
|
video/x-matroska mkv mk3d mks;
|
||||||
|
video/quicktime mov qt moov;
|
||||||
|
application/x-compressed gz;
|
||||||
|
application/x-gzip gz gzip;
|
||||||
|
application/x-bzip2 bz2;
|
||||||
|
application/x-tar tar;
|
||||||
|
application/x-xz xz;
|
||||||
|
}
|
@ -26,8 +26,8 @@ UNCOMPRESSABLE = set((".png", ".jpg", ".zip", ".woff2"))
|
|||||||
LICENSED = set((".css", ".html", ".js", "*.ts"))
|
LICENSED = set((".css", ".html", ".js", "*.ts"))
|
||||||
IGNORED = set((".DS_Store", "Thumbs.db"))
|
IGNORED = set((".DS_Store", "Thumbs.db"))
|
||||||
|
|
||||||
PERM_IGNORED_FX = set(("downloads.shelf",))
|
PERM_IGNORED_FX = set(("downloads.shelf", "webRequest"))
|
||||||
PERM_IGNORED_CHROME = set(("menus",))
|
PERM_IGNORED_CHROME = set(("menus", "sessions"))
|
||||||
|
|
||||||
SCRIPTS = [
|
SCRIPTS = [
|
||||||
"yarn build:regexps",
|
"yarn build:regexps",
|
||||||
|
98
util/mime.types
Normal file
98
util/mime.types
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
https://github.com/nginx/nginx/raw/master/conf/mime.types
|
||||||
|
|
||||||
|
types {
|
||||||
|
text/html html htm shtml;
|
||||||
|
text/css css;
|
||||||
|
text/xml xml;
|
||||||
|
image/gif gif;
|
||||||
|
image/jpeg jpeg jpg;
|
||||||
|
application/javascript js;
|
||||||
|
application/atom+xml atom;
|
||||||
|
application/rss+xml rss;
|
||||||
|
|
||||||
|
text/mathml mml;
|
||||||
|
text/plain txt;
|
||||||
|
text/vnd.sun.j2me.app-descriptor jad;
|
||||||
|
text/vnd.wap.wml wml;
|
||||||
|
text/x-component htc;
|
||||||
|
|
||||||
|
image/png png;
|
||||||
|
image/svg+xml svg svgz;
|
||||||
|
image/tiff tif tiff;
|
||||||
|
image/vnd.wap.wbmp wbmp;
|
||||||
|
image/webp webp;
|
||||||
|
image/x-icon ico;
|
||||||
|
image/x-jng jng;
|
||||||
|
image/x-ms-bmp bmp;
|
||||||
|
|
||||||
|
font/woff woff;
|
||||||
|
font/woff2 woff2;
|
||||||
|
|
||||||
|
application/java-archive jar war ear;
|
||||||
|
application/json json;
|
||||||
|
application/mac-binhex40 hqx;
|
||||||
|
application/msword doc;
|
||||||
|
application/pdf pdf;
|
||||||
|
application/postscript ps eps ai;
|
||||||
|
application/rtf rtf;
|
||||||
|
application/vnd.apple.mpegurl m3u8;
|
||||||
|
application/vnd.google-earth.kml+xml kml;
|
||||||
|
application/vnd.google-earth.kmz kmz;
|
||||||
|
application/vnd.ms-excel xls;
|
||||||
|
application/vnd.ms-fontobject eot;
|
||||||
|
application/vnd.ms-powerpoint ppt;
|
||||||
|
application/vnd.oasis.opendocument.graphics odg;
|
||||||
|
application/vnd.oasis.opendocument.presentation odp;
|
||||||
|
application/vnd.oasis.opendocument.spreadsheet ods;
|
||||||
|
application/vnd.oasis.opendocument.text odt;
|
||||||
|
application/vnd.openxmlformats-officedocument.presentationml.presentation
|
||||||
|
pptx;
|
||||||
|
application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
|
||||||
|
xlsx;
|
||||||
|
application/vnd.openxmlformats-officedocument.wordprocessingml.document
|
||||||
|
docx;
|
||||||
|
application/vnd.wap.wmlc wmlc;
|
||||||
|
application/x-7z-compressed 7z;
|
||||||
|
application/x-cocoa cco;
|
||||||
|
application/x-java-archive-diff jardiff;
|
||||||
|
application/x-java-jnlp-file jnlp;
|
||||||
|
application/x-makeself run;
|
||||||
|
application/x-perl pl pm;
|
||||||
|
application/x-pilot prc pdb;
|
||||||
|
application/x-rar-compressed rar;
|
||||||
|
application/x-redhat-package-manager rpm;
|
||||||
|
application/x-sea sea;
|
||||||
|
application/x-shockwave-flash swf;
|
||||||
|
application/x-stuffit sit;
|
||||||
|
application/x-tcl tcl tk;
|
||||||
|
application/x-x509-ca-cert der pem crt;
|
||||||
|
application/x-xpinstall xpi;
|
||||||
|
application/xhtml+xml xhtml;
|
||||||
|
application/xspf+xml xspf;
|
||||||
|
application/zip zip;
|
||||||
|
|
||||||
|
application/octet-stream bin exe dll;
|
||||||
|
application/octet-stream deb;
|
||||||
|
application/octet-stream dmg;
|
||||||
|
application/octet-stream iso img;
|
||||||
|
application/octet-stream msi msp msm;
|
||||||
|
|
||||||
|
audio/midi mid midi kar;
|
||||||
|
audio/mpeg mp3;
|
||||||
|
audio/ogg ogg;
|
||||||
|
audio/x-m4a m4a;
|
||||||
|
audio/x-realaudio ra;
|
||||||
|
|
||||||
|
video/3gpp 3gpp 3gp;
|
||||||
|
video/mp2t ts;
|
||||||
|
video/mp4 mp4;
|
||||||
|
video/mpeg mpeg mpg;
|
||||||
|
video/quicktime mov;
|
||||||
|
video/webm webm;
|
||||||
|
video/x-flv flv;
|
||||||
|
video/x-m4v m4v;
|
||||||
|
video/x-mng mng;
|
||||||
|
video/x-ms-asf asx asf;
|
||||||
|
video/x-ms-wmv wmv;
|
||||||
|
video/x-msvideo avi;
|
||||||
|
}
|
76
util/seed_mime.py
Executable file
76
util/seed_mime.py
Executable file
@ -0,0 +1,76 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from collections import OrderedDict
|
||||||
|
|
||||||
|
def unique(seq):
|
||||||
|
return list(OrderedDict([i, None] for i in seq if i))
|
||||||
|
|
||||||
|
def generate(major, minor, exts):
|
||||||
|
exts = exts[:]
|
||||||
|
yield f"{major}/{minor}", exts
|
||||||
|
if (minor.startswith("x-")):
|
||||||
|
yield f"{major}/{minor[2:]}", exts
|
||||||
|
else:
|
||||||
|
yield f"{major}/x-{minor}", exts
|
||||||
|
for ext in exts:
|
||||||
|
yield f"{major}/{ext}", exts
|
||||||
|
yield f"{major}/x-{ext}", exts
|
||||||
|
|
||||||
|
|
||||||
|
def make(text, final):
|
||||||
|
lines = "".join([
|
||||||
|
line.strip()
|
||||||
|
for line in re.search(r"\{(.*)\}", text, re.S).group(1).split("\n")
|
||||||
|
if line.strip() and not line.strip().startswith("#")
|
||||||
|
]).split(";")
|
||||||
|
|
||||||
|
additional = []
|
||||||
|
for line in lines:
|
||||||
|
if not line:
|
||||||
|
continue
|
||||||
|
m = re.match(r"([a-z1-9]+)/([^\s]+)\s+(.+?)$", line)
|
||||||
|
if not m:
|
||||||
|
continue
|
||||||
|
[major, minor, exts] = m.groups()
|
||||||
|
exts = unique(e.lower().strip() for e in exts.split(" ") if e.strip())
|
||||||
|
mime = f"{major}/{minor}"
|
||||||
|
if mime == "application/octet-stream":
|
||||||
|
continue
|
||||||
|
if mime in final:
|
||||||
|
final[mime] += exts
|
||||||
|
continue
|
||||||
|
final[mime] = exts
|
||||||
|
additional += (major, minor, exts),
|
||||||
|
|
||||||
|
for [major, minor, exts] in additional:
|
||||||
|
for [mime, exts] in generate(major, minor, exts):
|
||||||
|
if mime in final:
|
||||||
|
continue
|
||||||
|
final[mime] = exts
|
||||||
|
|
||||||
|
final = OrderedDict()
|
||||||
|
for file in sys.argv[1:]:
|
||||||
|
with open(file, "r") as fp:
|
||||||
|
make(fp.read(), final)
|
||||||
|
|
||||||
|
multi = dict()
|
||||||
|
for [mime, exts] in list(final.items()):
|
||||||
|
exts = unique(exts)
|
||||||
|
prim = exts[0]
|
||||||
|
final[mime] = prim
|
||||||
|
if len(exts) == 1:
|
||||||
|
continue
|
||||||
|
exts = exts[1:]
|
||||||
|
if len(exts) == 1:
|
||||||
|
multi[prim] = exts[0]
|
||||||
|
else:
|
||||||
|
multi[prim] = exts
|
||||||
|
|
||||||
|
final = OrderedDict(sorted(final.items()))
|
||||||
|
multi = OrderedDict(sorted(multi.items()))
|
||||||
|
|
||||||
|
print(json.dumps(dict(e=multi, m=final), indent=2))
|
||||||
|
print("generated", len(final), "mimes", "with", len(multi), "multis", file=sys.stderr)
|
@ -94,8 +94,10 @@ export class TextFilter extends ItemFilter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
allow(item: DownloadItem) {
|
allow(item: DownloadItem) {
|
||||||
return this.expr.test(
|
const {expr} = this;
|
||||||
[item.usable, item.description, item.finalName].join(" "));
|
return expr.test(item.currentName) ||
|
||||||
|
expr.test(item.usable) ||
|
||||||
|
expr.test(item.description);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -116,7 +116,7 @@ export class DownloadItem extends EventEmitter {
|
|||||||
|
|
||||||
public error: string;
|
public error: string;
|
||||||
|
|
||||||
public finalName: string;
|
public currentName: string;
|
||||||
|
|
||||||
public ext?: string;
|
public ext?: string;
|
||||||
|
|
||||||
@ -144,6 +144,8 @@ export class DownloadItem extends EventEmitter {
|
|||||||
|
|
||||||
private largeIconField?: string;
|
private largeIconField?: string;
|
||||||
|
|
||||||
|
public opening: boolean;
|
||||||
|
|
||||||
constructor(owner: DownloadTable, raw: any, stats?: Stats) {
|
constructor(owner: DownloadTable, raw: any, stats?: Stats) {
|
||||||
super();
|
super();
|
||||||
Object.assign(this, raw);
|
Object.assign(this, raw);
|
||||||
@ -159,7 +161,7 @@ export class DownloadItem extends EventEmitter {
|
|||||||
return this.iconField;
|
return this.iconField;
|
||||||
}
|
}
|
||||||
this.iconField = this.owner.icons.get(
|
this.iconField = this.owner.icons.get(
|
||||||
iconForPath(this.finalName, ICON_BASE_SIZE));
|
iconForPath(this.currentName, ICON_BASE_SIZE));
|
||||||
if (this.ext) {
|
if (this.ext) {
|
||||||
IconCache.get(this.ext, ICON_REAL_SIZE).then(icon => {
|
IconCache.get(this.ext, ICON_REAL_SIZE).then(icon => {
|
||||||
if (icon) {
|
if (icon) {
|
||||||
@ -178,7 +180,7 @@ export class DownloadItem extends EventEmitter {
|
|||||||
return this.largeIconField;
|
return this.largeIconField;
|
||||||
}
|
}
|
||||||
this.largeIconField = this.owner.icons.get(
|
this.largeIconField = this.owner.icons.get(
|
||||||
iconForPath(this.finalName, LARGE_ICON_BASE_SIZE));
|
iconForPath(this.currentName, LARGE_ICON_BASE_SIZE));
|
||||||
if (this.ext) {
|
if (this.ext) {
|
||||||
IconCache.get(this.ext, LARGE_ICON_REAL_SIZE).then(icon => {
|
IconCache.get(this.ext, LARGE_ICON_REAL_SIZE).then(icon => {
|
||||||
if (icon) {
|
if (icon) {
|
||||||
@ -217,7 +219,7 @@ export class DownloadItem extends EventEmitter {
|
|||||||
if (this.owner.showUrls.value) {
|
if (this.owner.showUrls.value) {
|
||||||
return this.usable;
|
return this.usable;
|
||||||
}
|
}
|
||||||
return this.finalName;
|
return this.currentName;
|
||||||
}
|
}
|
||||||
|
|
||||||
get fmtSize() {
|
get fmtSize() {
|
||||||
@ -522,8 +524,16 @@ export class DownloadTable extends VirtualTable {
|
|||||||
return true;
|
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-all", () => this.removeAllDownloads());
|
||||||
ctx.on("ctx-remove-complete", () => this.removeCompleteDownloads(false));
|
|
||||||
ctx.on("ctx-remove-complete-all",
|
ctx.on("ctx-remove-complete-all",
|
||||||
() => this.removeCompleteDownloads(false));
|
() => this.removeCompleteDownloads(false));
|
||||||
ctx.on("ctx-remove-complete-selected",
|
ctx.on("ctx-remove-complete-selected",
|
||||||
@ -607,8 +617,12 @@ export class DownloadTable extends VirtualTable {
|
|||||||
this.selection.clear();
|
this.selection.clear();
|
||||||
|
|
||||||
this.tooltip = null;
|
this.tooltip = null;
|
||||||
this.on("hover", async info => {
|
const tooltipWatcher = new PrefWatcher("tooltip", true);
|
||||||
if (!(await Prefs.get("tooltip"))) {
|
this.on("hover", info => {
|
||||||
|
if (!document.hasFocus()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!tooltipWatcher.value) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const item = this.downloads.filtered[info.rowid];
|
const item = this.downloads.filtered[info.rowid];
|
||||||
@ -737,6 +751,7 @@ export class DownloadTable extends VirtualTable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
selectionChanged() {
|
selectionChanged() {
|
||||||
|
this.dismissTooltip();
|
||||||
const {empty} = this.selection;
|
const {empty} = this.selection;
|
||||||
if (empty) {
|
if (empty) {
|
||||||
for (const d of this.disableSet) {
|
for (const d of this.disableSet) {
|
||||||
@ -777,7 +792,8 @@ export class DownloadTable extends VirtualTable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
resumeDownloads(forced = false) {
|
resumeDownloads(forced = false) {
|
||||||
const sids = this.getSelectedSids(DownloadState.RESUMABLE);
|
const sids = this.getSelectedSids(
|
||||||
|
forced ? DownloadState.FORCABLE : DownloadState.RESUMABLE);
|
||||||
if (!sids.length) {
|
if (!sids.length) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -801,20 +817,30 @@ export class DownloadTable extends VirtualTable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async openFile() {
|
async openFile() {
|
||||||
if (this.focusRow < 0) {
|
this.dismissTooltip();
|
||||||
|
const {focusRow} = this;
|
||||||
|
if (focusRow < 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const item = this.downloads.filtered[this.focusRow];
|
const item = this.downloads.filtered[focusRow];
|
||||||
if (!item || !item.manId || item.state !== DownloadState.DONE) {
|
if (!item || !item.manId || item.state !== DownloadState.DONE) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
item.opening = true;
|
||||||
try {
|
try {
|
||||||
|
this.invalidateRow(focusRow);
|
||||||
await downloads.open(item.manId);
|
await downloads.open(item.manId);
|
||||||
}
|
}
|
||||||
catch (ex) {
|
catch (ex) {
|
||||||
console.error(ex, ex.toString(), ex);
|
console.error(ex, ex.toString(), ex);
|
||||||
PORT.post("missing", {sid: item.sessionId});
|
PORT.post("missing", {sid: item.sessionId});
|
||||||
}
|
}
|
||||||
|
finally {
|
||||||
|
setTimeout(() => {
|
||||||
|
item.opening = false;
|
||||||
|
this.invalidateRow(focusRow);
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async openDirectory() {
|
async openDirectory() {
|
||||||
@ -1111,7 +1137,16 @@ export class DownloadTable extends VirtualTable {
|
|||||||
if (!item) {
|
if (!item) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
if (item.opening) {
|
||||||
|
return ["opening"];
|
||||||
|
}
|
||||||
const cls = StateClasses.get(item.state);
|
const cls = StateClasses.get(item.state);
|
||||||
|
if (cls && item.opening) {
|
||||||
|
return [cls, "opening"];
|
||||||
|
}
|
||||||
|
if (item.opening) {
|
||||||
|
return ["opening"];
|
||||||
|
}
|
||||||
return cls && [cls] || null;
|
return cls && [cls] || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
10
yarn.lock
10
yarn.lock
@ -46,6 +46,11 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@types/psl/-/psl-1.1.0.tgz#390c5df1613b166ce3c3eb9fda4d93dc3eeec7b5"
|
resolved "https://registry.yarnpkg.com/@types/psl/-/psl-1.1.0.tgz#390c5df1613b166ce3c3eb9fda4d93dc3eeec7b5"
|
||||||
integrity sha512-HhZnoLAvI2koev3czVPzBNRYvdrzJGLjQbWZhqFmS9Q6a0yumc5qtfSahBGb5g+6qWvA8iiQktqGkwoIXa/BNQ==
|
integrity sha512-HhZnoLAvI2koev3czVPzBNRYvdrzJGLjQbWZhqFmS9Q6a0yumc5qtfSahBGb5g+6qWvA8iiQktqGkwoIXa/BNQ==
|
||||||
|
|
||||||
|
"@types/whatwg-mimetype@^2.1.0":
|
||||||
|
version "2.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/whatwg-mimetype/-/whatwg-mimetype-2.1.0.tgz#f981bbdf1813a75820a6ec3a7fdfa0d452552cc7"
|
||||||
|
integrity sha512-bJ/bZ+pA69lm+Ll8JJRoAD9saH7unIMfxPQQpl7bxa00qNqvUXSyk3xvoRMea1uCpAOxweI7CzjWx48ysX6yug==
|
||||||
|
|
||||||
"@typescript-eslint/eslint-plugin@^2.0.0":
|
"@typescript-eslint/eslint-plugin@^2.0.0":
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.0.0.tgz#609a5d7b00ce21a6f94d7ef282eba9da57ca1e42"
|
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.0.0.tgz#609a5d7b00ce21a6f94d7ef282eba9da57ca1e42"
|
||||||
@ -3718,6 +3723,11 @@ webpack@^4.39.3:
|
|||||||
watchpack "^1.6.0"
|
watchpack "^1.6.0"
|
||||||
webpack-sources "^1.4.1"
|
webpack-sources "^1.4.1"
|
||||||
|
|
||||||
|
whatwg-mimetype@^2.3.0:
|
||||||
|
version "2.3.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz#3d4b1e0312d2079879f826aff18dbeeca5960fbf"
|
||||||
|
integrity sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==
|
||||||
|
|
||||||
which-module@^2.0.0:
|
which-module@^2.0.0:
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a"
|
resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a"
|
||||||
|
Reference in New Issue
Block a user