From bd986b2fdfa7e9fdd4154399cf9051f87d8b9318 Mon Sep 17 00:00:00 2001 From: Wirlaburla Date: Mon, 9 Jan 2023 18:43:25 -0600 Subject: [PATCH] release\ --- README.md | 6 ++ action.css | 43 ++++++++ action.html | 37 +++++++ action.js | 117 ++++++++++++++++++++++ action_browser.css | 75 ++++++++++++++ background.js | 33 +++++++ common.js | 89 +++++++++++++++++ images/128.png | Bin 0 -> 4611 bytes images/16.png | Bin 0 -> 2043 bytes images/24.png | Bin 0 -> 2439 bytes images/32.png | Bin 0 -> 2460 bytes images/48.png | Bin 0 -> 3086 bytes images/64.png | Bin 0 -> 3821 bytes images/96.png | Bin 0 -> 5560 bytes installed.css | 41 ++++++++ installed.html | 25 +++++ installed.js | 8 ++ manifest.json | 38 +++++++ native/download_with_linux.py | 110 +++++++++++++++++++++ native/download_with_mac.py | 152 ++++++++++++++++++++++++++++ native/download_with_windows.ps1 | 161 ++++++++++++++++++++++++++++++ native/download_with_windows.py | 164 +++++++++++++++++++++++++++++++ options.css | 12 +++ options.html | 27 +++++ options.js | 35 +++++++ 25 files changed, 1173 insertions(+) create mode 100644 README.md create mode 100644 action.css create mode 100644 action.html create mode 100644 action.js create mode 100644 action_browser.css create mode 100644 background.js create mode 100644 common.js create mode 100644 images/128.png create mode 100644 images/16.png create mode 100644 images/24.png create mode 100644 images/32.png create mode 100644 images/48.png create mode 100644 images/64.png create mode 100644 images/96.png create mode 100644 installed.css create mode 100644 installed.html create mode 100644 installed.js create mode 100644 manifest.json create mode 100755 native/download_with_linux.py create mode 100755 native/download_with_mac.py create mode 100644 native/download_with_windows.ps1 create mode 100644 native/download_with_windows.py create mode 100644 options.css create mode 100644 options.html create mode 100644 options.js diff --git a/README.md b/README.md new file mode 100644 index 0000000..6c61bc6 --- /dev/null +++ b/README.md @@ -0,0 +1,6 @@ +Download With Extension +================ + +Download With is an extension that overrides the default download operation and instead sends it to a defined external program. It's based off code from [OpenWith](https://github.com/darktrojan/openwith) and used it as the foundation. + +The purpose of this addon is to bring back support for your own external download manager, without having to use a specific program with included integrations. \ No newline at end of file diff --git a/action.css b/action.css new file mode 100644 index 0000000..de04221 --- /dev/null +++ b/action.css @@ -0,0 +1,43 @@ +body { + min-width: 180px; +} +#update, +#warning, +#error, +#nobrowsers { + display: none; + margin: 4px; + padding: 8px; + color: #fff; + line-height: 1.4; + text-align: center; +} +#update { + background-color: #0e9352; +} +#update_message { + max-width: 280px; + margin: 0 auto 8px; +} +#warning { + background-color: #fec82f; +} +#error { + background-color: #e82727; +} +#nobrowsers { + color: #222426; +} +#browsers { + margin: 0; + padding: 0; + list-style: none; +} +.name { + -webkit-margin-start: 8px; + margin-inline-start: 8px; + white-space: nowrap; +} +.panel-section-footer-button { + margin: 0; +} diff --git a/action.html b/action.html new file mode 100644 index 0000000..709dcf6 --- /dev/null +++ b/action.html @@ -0,0 +1,37 @@ + + + + + + + + +
+
+
+
+ + +
+
+
+
+
+
+
    + +
+
+ +
+ + + + diff --git a/action.js b/action.js new file mode 100644 index 0000000..ce051ee --- /dev/null +++ b/action.js @@ -0,0 +1,117 @@ +/* globals chrome, get_string, get_strings, is_same_colour, ERROR_COLOUR, WARNING_COLOUR */ +let errorMessage = document.getElementById('error'); +let warningMessage = document.getElementById('warning'); +let updateMessage = document.getElementById('update'); +let browsersList = document.getElementById('browsers'); +let browsersTemplate = browsersList.querySelector('template'); + +get_strings(); + +chrome.browserAction.getBadgeBackgroundColor({}, function(colour) { + chrome.browserAction.setBadgeText({text: ''}); + chrome.browserAction.setBadgeBackgroundColor({color: [0, 0, 0, 0]}); + + if (is_same_colour(colour, ERROR_COLOUR)) { + errorMessage.style.display = 'block'; + } else if (is_same_colour(colour, WARNING_COLOUR)) { + warningMessage.style.display = 'block'; + } else { + chrome.management.getSelf(function({version: currentVersion}) { + let now = new Date(); + + chrome.storage.local.get({ + versionLastUpdate: '1970-01-01T00:00:00.000Z', + versionLastAck: '1970-01-01T00:00:00.000Z' + }, function({versionLastUpdate, versionLastAck}) { + if (typeof versionLastUpdate == 'string') { + versionLastUpdate = new Date(versionLastUpdate); + } + if (typeof versionLastAck == 'string') { + versionLastAck = new Date(versionLastAck); + } + if (now - versionLastUpdate < 43200000 && now - versionLastAck > 604800000) { + document.getElementById('update_message').textContent = get_string('update_message', currentVersion); + updateMessage.style.display = 'block'; + } + chrome.storage.local.set({ + versionLastUpdate: versionLastUpdate.toJSON(), + versionLastAck: versionLastAck.toJSON() + }); + }); + }); + } +}); + +let userIcons = new Map(); +chrome.runtime.sendMessage({action: 'get_icons'}, function(result) { + for (let l of result) { + userIcons.set(l.id.toString(), l); + } + + chrome.runtime.sendMessage({action: 'get_browsers'}, function(browsers) { + if (browsers.length === 0) { + document.getElementById('nobrowsers').style.display = 'block'; + } + for (let b of browsers) { + if (b.hidden) { + continue; + } + add_browser(b); + } + }); +}); + +browsersList.onclick = function(event) { + let target = event.target; + while (target && target.localName != 'li') { + target = target.parentNode; + } + chrome.tabs.query({active: true, currentWindow: true}, function(tabs) { + chrome.runtime.sendMessage({ + action: 'open_browser', + id: target.dataset.id, + url: event.ctrlKey ? null : tabs[0].url + }); + window.close(); + }); +}; + +updateMessage.onclick = function({target}) { + chrome.storage.local.set({versionLastAck: new Date().toJSON()}); + switch (target.dataset.message) { + case 'update_changelog_button': + chrome.management.getSelf(function({version}) { + chrome.tabs.create({url: 'https://addons.mozilla.org/addon/open-with/versions/' + version}); + }); + return; + case 'donate_button': + chrome.tabs.create({url: 'https://darktrojan.github.io/donate.html?openwith'}); + return; + } + open_options_tab(); +}; +errorMessage.onclick = warningMessage.onclick = function() { + chrome.storage.local.set({versionLastAck: new Date().toJSON()}); + open_options_tab(); +}; +document.querySelector('.panel-section-footer-button').onclick = open_options_tab; + +function open_options_tab() { + chrome.runtime.openOptionsPage(function() { + window.close(); + }); +} + +function add_browser(b) { + let li = browsersTemplate.content.firstElementChild.cloneNode(true); + li.dataset.id = b.id; + if ('icon' in b && b.icon) { // b.icon could be undefined if we stuffed up (#170) + if (b.icon.startsWith('user_icon_')) { + li.querySelector('img').src = userIcons.get(b.icon.substring(10))['16']; + } else { + li.querySelector('img').src = 'icons/' + b.icon + '_16x16.png'; + } + } + li.querySelector('div.name').textContent = b.name; + browsersList.appendChild(li); +} diff --git a/action_browser.css b/action_browser.css new file mode 100644 index 0000000..f2003d5 --- /dev/null +++ b/action_browser.css @@ -0,0 +1,75 @@ +html, +body { + background: transparent; + box-sizing: border-box; + color: #222426; + cursor: default; + display: flex; + flex-direction: column; + margin: 0; + padding: 0; + -moz-user-select: none; + user-select: none; + font: message-box; + font-size: 13.333px; + line-height: 1.5; +} +body * { + box-sizing: border-box; +} +button { + font-size: 13.333px; +} +.panel-section { + display: flex; + flex-direction: row; +} +.panel-section-list { + flex-direction: column; + padding: 4px 0; +} +.panel-list-item { + align-items: center; + display: flex; + flex-direction: row; + height: 24px; + padding: 0 16px; +} +.panel-list-item:not(.disabled):hover { + background-color: rgba(0, 0, 0, 0.06); + border-bottom: 1px solid rgba(0, 0, 0, 0.1); + border-top: 1px solid rgba(0, 0, 0, 0.1); +} +.panel-list-item:not(.disabled):hover:active { + background-color: rgba(0, 0, 0, 0.1); +} +.panel-list-item > .icon { + flex-grow: 0; + flex-shrink: 0; +} +.panel-list-item > .text { + flex-grow: 10; +} +.panel-section-footer { + background-color: rgba(0, 0, 0, 0.06); + border-top: 1px solid rgba(0, 0, 0, 0.15); + color: #1a1a1a; + display: flex; + flex-direction: row; + height: 41px; + margin-top: -1px; + padding: 0; +} +.panel-section-footer-button { + flex: 1 1 auto; + height: 100%; + margin: 0 -1px; + padding: 12px; + text-align: center; +} +.panel-section-footer-button:hover { + background-color: rgba(0, 0, 0, 0.06); +} +.panel-section-footer-button:hover:active { + background-color: rgba(0, 0, 0, 0.1); +} diff --git a/background.js b/background.js new file mode 100644 index 0000000..9bbd26b --- /dev/null +++ b/background.js @@ -0,0 +1,33 @@ +/* globals chrome, compare_versions, get_version_warn, ERROR_COLOUR, WARNING_COLOUR */ +var exe; + +function open_external(item) { + function error_listener(error) { + console.error(error, chrome.runtime.lastError); + } + + browser.downloads.cancel(item.id).then( + function () { + let command = exe.replace('%s', item.url); + + let port = chrome.runtime.connectNative('download_with'); + port.onDisconnect.addListener(error_listener); + port.onMessage.addListener((m) => { + console.log(m); + port.onDisconnect.removeListener(error_listener); + port.disconnect(); + }); + console.log('executing: '+command); + port.postMessage(command.split(' ')); + }, + function (err) { + console.log(`download_with: Could not cancel ${item.filename} (${item.id}): Error: ${err}`); + } + ); +} + +chrome.storage.local.get({execute: null}, function({execute}) { + exe = execute; +}); + +browser.downloads.onCreated.addListener(open_external); \ No newline at end of file diff --git a/common.js b/common.js new file mode 100644 index 0000000..45763d9 --- /dev/null +++ b/common.js @@ -0,0 +1,89 @@ +/* globals chrome */ +/* exported get_version_warn, compare_versions, compare_object_versions, get_string, get_strings, + ERROR_COLOUR, WARNING_COLOUR, is_same_colour */ +var _version_warn = null; +async function get_version_warn() { + if (!!_version_warn) { + return _version_warn; + } + + if ('browser' in this && 'runtime' in this.browser && 'getBrowserInfo' in this.browser.runtime) { + browserInfo = await browser.runtime.getBrowserInfo(); + if (browserInfo.name == 'Thunderbird') { + return '7.2.3'; + } + } + + return '0.0.1'; + + // return new Promise(function(resolve) { + // chrome.runtime.getPlatformInfo(function(platformInfo) { + // _version_warn = platformInfo.os == 'win' ? '7.0.1' : '7.0b10'; + // resolve(_version_warn); + // }); + // }); +} + +function compare_versions(a, b) { + function split_apart(name) { + var parts = []; + var lastIsDigit = false; + var part = ''; + for (let c of name.toString()) { + let currentIsDigit = c >= '0' && c <= '9'; + if (c == '.' || lastIsDigit != currentIsDigit) { + if (part) { + parts.push(lastIsDigit ? parseInt(part, 10) : part); + } + part = c == '.' ? '' : c; + } else { + part += c; + } + lastIsDigit = currentIsDigit; + } + if (part) { + parts.push(lastIsDigit ? parseInt(part, 10) : part); + } + return parts; + } + function compare_parts(x, y) { + let xType = typeof x; + let yType = typeof y; + + switch (xType) { + case yType: + return x == y ? 0 : (x < y ? -1 : 1); + case 'string': + return -1; + case 'undefined': + return yType == 'number' ? (y === 0 ? 0 : -1) : 1; + case 'number': + return x === 0 && yType == 'undefined' ? 0 : 1; + } + } + let aParts = split_apart(a); + let bParts = split_apart(b); + for (let i = 0; i <= aParts.length || i <= bParts.length; i++) { + let comparison = compare_parts(aParts[i], bParts[i]); + if (comparison !== 0) { + return comparison; + } + } + return 0; +} + +function compare_object_versions(a, b) { + return compare_versions(a.name, b.name); +} + +var ERROR_COLOUR = [232, 39, 39, 255]; +var WARNING_COLOUR = [254, 200, 47, 255]; + +function is_same_colour(a, b) { + for (let i = 0; i < 4; i++) { + if (a[i] != b[i]) { + return false; + } + } + return true; +} diff --git a/images/128.png b/images/128.png new file mode 100644 index 0000000000000000000000000000000000000000..9487cda542cecc934900686e186e16e48f22d077 GIT binary patch literal 4611 zcmXww2{_Y_#pV&;kpIU+gcoU^$i!gRQzFEJ5gGeo)X$Qg5%e3K4hbL2?5 zizp_Oo5f;|ZU5){d!GOEeqQhAc|Wh`{mA|b@&vCWF8}~1tSrqO0RX`N2ZS2{fNsMZ zg8%?Z2rxCZw=y+_g@gwC1_b#4z?rO=tjm_&2&rC@VGoa};N@gYzkSKMWHqT4Uh`Uk zJBco*ZB=88a~{fn{OAyJUYBw0x!yb7wN;7=gupL=R8lfOsr(0LRbigW)5m0Nc4V|| z0l44wk3N@rhVh7Nmfxa5lb3j5^{uWEVJAO3w&XNW6_sjnGS<+Rgt<7UN3Z|JG! zXB9#yA*ATlN-VC|Cviv2uf@aXOZ4{1hoRG-G$sQqGRg3Km zMl>Tyie#x;=%JtNjENsc_ehE^|IUM~xdyglLJ@mA{r|GOU`?lR0krj#;bylj|r%a3m7=1mb>b_z~Bt$l56)6s`}w#_7&1VU&d$Xg^7008crHb^so{a?!4YjOYp zfQDGQg#m!7+<$-~iAMJU0N}w`*_reFhDw1&c?WSg5&!@a81riw)8M;zeg44!rlCGJ zFg~|o5dj!~7}Cnl-ZhTzBmnTwSeY3+M^0~e+614P6lD$3WWVQAuf6$&oUZyJY?&!$ zNdQ2P8{ILZ=EpGOGWH?@BX(jxG2gF7c=ENb*OSWqPyQ5x@KzzD?Sho@?+!#wMcLjsU_+Lzb#DLsk5k9Z98%3z4u>K-!ZzREBCEEozZ(4is{i&(g*7%fIH(PT zvnH?}3w__i8rK@Hb2tOb!X2^Hf7e>e+&Oajm*FEQa4Y3dH1=osPtyOiaqOG5R^0i) zxz~mjurl`|6zDbd;EbiOsCQRfM`qbZ>!!?k(Bn?Dc)V7Ixt+%(vt5C_1O2mlCX6P& zgPWi9oMfgckSVtrP0tZVTYhbghi7f@osVeEP@Sk6c;Q;BQ43hS7)(+X9N}aKO@zds<+5-wG5p=JH(>Xk3f+1IqpC z`REfu2pcXUPn1-Ew+cjk(|uY6?@9D@61}t#I_Be|)d0AFssr6bn|Qrl^Pt{mIU0bA zd2Sq2K2Yxh1-z%|InKM6{6)#CHbC1s(0aWps9J%2{eQZ0PKZnhJ_Npk+3<^R`Uz-H znhgp#hE0TecfM2yRYS4S`X8QAP@kwmZj^>&hU#&9yrsXjAMHH8T}dh;7d!;Sj&?ln zYu0vAP#-vQwGRd{7durD9}5%^iIsEtaZYaxj~Kgx+svimr_XZ1P0@!n9JBIx25n&m zBLofNeIiXe-Lcxgm3EC2VgAV*s%}6L-hS#n#PJUNp!>NEP;~SmObU7qRLyy$NWJ^V z3kcyPryTuJ5uU#$bd4kT6qW}O-$?>de1Kpe@)$V~*i?u5X4vrL1B*lZdW<j^vs40&i!yxRT_A6^_$AI0ecvNp%;YZ!AV3-yS|J_BSQK;~sF zfQuZKW*VpdiZkqkb3G+@X@ zLj(N4`UMFgpi2cY(BDvnwto!10Ou9V&dAg^qskfAdaZ({d&pQPkLy)sN_`LI=O~b=V)kg&cV7E7;aKrR582T~!3`&*N);>XDRLx%y}9oei*{i@^%n>L{I0xbgw65m!!4?IJzC*SVt_B0Mtm*R}Fz_q`$LS`2V?#^E0oU2haS*?pgH?7Rv9Keyo+3DYC z65>ai!ichS5_BGSN=j!zy3uCC-yj*cTCKvTij#^QU)I{ z^z73P`(QTqP3J-+t@c=%9;7bCYdU`#SYtBgY& z=)UU}{K7}_fqxFtIY>$3vH+-Tck=?{>~WSsI?L}lZ{O|#(gL_8?r>T!mTWPv&) zZjXtsZaSNUPC3G3!7&sb0RTJQKBz(Ja(GFe^IhT3TQjngl>Ww9o1?UfUin8u5KMZM z{PKxJVaF014x7RaY}78G9@Ho4L>7DbGUOQ?h8sV>!r9X342dI6bCx}rvg}GhbExw_ zfZRy8f=wkv0v}|!jYue1CXDKNS3%Tyf2trI4I5RAc7Cd30f`eaZ0WdBE)zeH?`h0>R%N`>H^gT|iOo zYo@0c3p+$Ea7^PMm!|&ZrJU-p;A5#PBTgF^ftmoyMQTO7>pzOY>?;UzTzuoWg{PLi zscEc8_=G*uZUR9i$2kSS>3fWpI%(;3Z`vemi81+ph%LtKJ}tqhj6`({AWF>M_iW>i zkp~UKvm7~u@bQv&CJ>%l4mFCa&mKMWw>Bx~Lr%1A&Os}AD|-|R5LfH2s^ep=$rFQSepRPrYGi3@U8alX|M4Fds7 z-CmuVICau%~Z*&}#@SD!*d+X_-KscjmU~*md zMKE6A^P`!$j1jXE-w}ak+lPv)^5gN2;3UyFgpcFm4%>5m73Fhw@ zM{g-njS`DjACML1I9VQl8N*yhHw^IJiuLh47n;M8POqh4&l^j-cJ!{yKej$Grun>) zgC;h%)j`xJ*b3V1Lc?_}7VW>$Lolz9Z=&TG$7AcONFQ{7Q>WRF@6Qu`K)iSBgW!Y*VPJduQB8DZ(!ph&B%@QKR z;_7*GPbO!US!`^pg_K|AP2v`GtSk2Wwiq06p8VecMnGl@i2-{FXPfwAB0+Vq2 zrIEZ$-1wkkv}2ED|D0co$3j18IKUen!CZo}M!@#+XJ7;sK~afZ6^s`Abs@xnip>8) zA4#nZ>tC5KG}TOj-n8P*et5yWYsENQ)-`beMo?znDZad15F74-%efrWXU#rPI&CWg zI<5_?M3p<5c9G2N5eYG|KSr^HMqc+QU42>ruFrU$NwK%45ND zSgJR-=f`Koc*vU6QwG&*C{_5uuo>Wa;<&lJOgNlTbVFYq+Xc#zS9dnH;V$NnxrR#FNz^5+H_a`CFH|rrp2HKkm4&@^x%+gq|Dl?|B14& z0#=r=H;|DjS!XO^Q382$zYw>cP21Gvo8V+e)@A~YFd!-ItUeE4Hqx%487?>_6g!_pJ?7BAO%8PkkY zQo-sCNzLJshdNO=zpq7v(!O3)vtb+Bu-iFSGSu`AhVuYU0a@)&mOs^3nP5!=aI1|) zY{P^EP+%}Uv#L9j5%>;^{43&kF-^m&$~{ckHE}#Lu`<5o!{Cx7t;LJ2$L4EYQ_E$g zq_X}M{oH4J^0IqP6ZBFzswlTSWPv(dd7%(te&tQ^&c+VbiwSebB9VD)gK3 z$QC1-R+1j{cbnVGmZe$wHCkrhp>b1}h|e>M-EFaf17%$2>w}fYH|uTtKJxx z>GY-Gn6oT+q`jzP`#z`LM9Zb#C|@xm9DrXQB0fVCWB>qrC+3SITa?O-+WKRGx<0ck zahXAck?|`Pjf)3v+>ux0;_G4mNPg{>BDwsuL6?Q;df1|w<@s6VvR33@S%P@fju~yj zdx`V@Q3MFvD}U{)?cCH(YydQbprX5M)Lff0&x>q_cL@pKs3WV@(nnmaUyZJxBL*`Q z66l=D3dw?SxXcA?m8GC#bXwR^O!|@96i|8?geF4N%Tcw3c5o_kx>1wy_Vt<`&Q7qR zn#iNQCBTuq^syv@3oK-2DZ3|YqBli7gX^K-dc#R~{(AM}kwi#{Nt8Vk&@`{*&avj7 zbUU#b!ga5q{Jry}ZDs6qVbRU`YW=BSu<~6Uk2%~6tN(6Oc%%B$%I?zw<0#(pRgymb{mV z`aA2#rS5k)W|hjSQz8oxZ#y+ei;a-jA>A~dgHqF;QBwCd8}h1;jma)D$WtN%#40xf zk1^YYs~uv;(dCd4AFio+t)`SCn4kUt#Wfq?%+K~k8#8mB^J0m>BL|iHEyYHED6U0- zX>N8ny(|%<#b0hI5J0@CvGOVys$Ps5fjVIPUUxmcl?TGw;OG1CgpA)Ce}?{w7buV! ik3xQY0@Q>V01c5sOCj1|>c&7yb literal 0 HcmV?d00001 diff --git a/images/16.png b/images/16.png new file mode 100644 index 0000000000000000000000000000000000000000..3341ed3e930b898ebee83a47b78cc0030d8c16e3 GIT binary patch literal 2043 zcmchYX;72b8pofo?|UF%kU$YcA%=uN5*2~4BrRarVOUH^LU=LT`{o4-h-?9&1L8s* zS`ie%wzhJ6%VG-(qKL~aO6#-~>P4>9mWm=s3oamcu+vU^@7%BVnR&jP^ZU*JIUmkB zHG=2}a}y^M05IqCLSq5IYQ2qg0bsL_WgGxuAHwAd5V06w^Woyd+j&=l$ioAHLxx1_ z?bY~o!3po`ZRBo~un(JX1i8?IH{`{(=5YtR5MJ3~%} z{H2&Qb64Mq`TeWFp2x$+R&N`0bR8RrHOva+|E;@g#(MO;d0N_ezXHq8?-C4J@EeE7 zUO{(SZtFgg{SuWToU)%u(G=~SoBoBsi%Yt(VQR{X-vU9*iTaI<` zoJ&*yKoRp|qW}s$07|t0F94u)1|XLP@K6ZAX#{XkH}%Hs03c)wBICmV06~yAMYuoe zn3aZ1-m^C~T;}0thSuWVA_M~AJ+c0wmN@Sxhp(@`f<*r~g zm4OnsV)}H<#t(I1q9g=C()XrhBiYF7!P568XCYZKL?+3}Tw7b)P-&C^07^VIIu8E3 z2>PaGdKO+RUP`v)jgwtNccuqjEqhjrhV#F5#OI&?Jy^&0QL8Z_XsM1wdrRhEbz}DX zx#eRP1UKbmzlUezz0m->tS{!Cq}s=voP4qJuDK~||M24;imqoOHM^iX5C907Z$*UW zhfAK10f4cZm!binzOmkfx7&B+0e~USj|wyV(U`1fPqLj&_X8kYE{spcV@3HCHKxi! z9`g-&*~rC;n^Ng2s(ablczJUQnSRoo|c_&NXF z&to)ZB+YF1=-IXrqehRmsBL0yuz$E7yX|@nvlhovcO;QcpYo<$BEA~z zO>(Y%YTzX5W4t`o?lP&mxh34Gl1^=OEf5Q{$rkArv5Xcg`2}*BEU*7Sz4%{0Iq84< z!*r9_ETTy7!c@}5=_Rcyy+(W|#Tds)_q8Jr%|F)Y5OtJBtPj zrh8JSe^4{OTK0Jyl}Nl8s?EEE73sLRemYxoLR)*TEi4DA=G;=NVtRZ9>TE(8|y$*I`=_!L*B0< zEq(1=WYtAtE_rb$QEkKmS$=XZZ!UApX8miLfXS?cZZ|4mPA%xJ#+71 z!H=ieC6mBxMIXhiPuf`u+pd4nU*mBD(j)Y?azsF`Z>*RA9V(`GLuzsXLjgkBAV|-D{2r%!>uYaelar74E7#s;WFO-P7^-PmfD)i*?$B&%Jj*a7uq!UfSI@Av3+{R|JM;UID zf&D_p;?va?Hi@OHQ(+4og-U{C75s1EaL`Wq`iJ;O0*)x88eF7M|EELe(EWZ3Nov&w0l`&tczG0C4*qlSnmU3{m5VN(t+Sqh#hHa@+?|QZ%sT#9Hg!?+EKaT{ZpF-Ez~V030A*V@R8x$qrx`_6nRR4$F8t+11$ zU2Q2!H+2#!LT>4%+WYLp>7tbEbdz1F#ChbY=Ukri51jQp>+{1}ulKXo`mEU zq%UH>YoWVkZGU>yc ziFd@_hSr;3e6n`$y8@~LmS01<>>l8Md2kl#|J==sEjM|rdc1lIku}|yKKRtTf5Emd z%RMDEQIp-g|0Vt%j!>$4! zlw0h)lF$`Y9J|BB*lDOFdup3yJBzp{VHrv3dO*YUrxBgmEf4Si{ILl3b~4eSuakV{ zn~mLDfTR3l#;@sRub$Bc?fSD7?U6bnzpzt{XUCS@YBoqnX!go9Ja-SJU4>jJwR8&j zt?H)c>y$q^Yq`Uiv9+q~v{xg4qF7OJS1g8yjm(@P-e0)f(lV6&`2)M5-A0|StY=x} z$N>O&J}Z&~K$ZglxoQAD0RVEx07xeR@SF<(y%GSdV&8?;%K^|JMzEuU0RRvLNmCV6 z*jyk^o{gHdMu0q3MS;!8O0*Y@T_t6R#6ma~aaZb7VKWMh^@efK|ChNd7tIfp21%0A z;sX_~-ms-7Z2WynZdxK!PNu>*PZ$kBkPmD_fwA-1<%(q#7)yoCsjxX)=9dDAAPDkT zxKQT)%8aO7NQ13{e)ZGY|W~)(kl*Qp#E>TS0@Z=HnC?OM$U8*eXEb z%#j7eOJaQ$OaC=Rg>lOj_MviWjFcUyaQj*X`|ZWjVCx{cTZEJuD+v#ldwIha6c{t# z-h9vV8~nON3kZTlV&N(oGX)aGNI9ucYMf+svY5Lr?T4ha1c8_@6bn+Jlo%5sjuik(o%#{aHFCr_2z=rnceqpmk=n}kv|DmPc%zMrh= zchYuQ_BDT$zD>yPff#Mn%C5;VzOEf|VQl(iDO!7}@Wk-cOgBjr7hZnz_4IK30z==- z=BMvx-fl2Rtm?GD5KK|0bDlf{0HBip63RSJKZoH00Q3~BwJHFJ_H&NfQ0t!o0MJ3g zIKeujdX`!k!&2JaNdQotS=q*d+Z`w~p1^(5-Z+;kfe|fz%K! zpPIfRka4tSQ9%9XGgaofOQBdYdDs=-VLEf^LY!6cTWuV##q)D@t<3}ZRp$^~0hw57 zo5|;4R>FVu47cFUlaYOV{m^W!*5SB|Bk$Gnpq(Ym7kl-N zRc6Kj!>QsNmA}EUtwWEinzn4h5OaUeZ^*usInoqA@GmUKfRO=y-jL&ZOWuWt2P;JJnoRrd=I+}Sg`iJhi2CY^P8bdaQ zo$b?HP~tctU|f3Rz#S+@FY#MMW;`t{&v?gElV?w|w_Y@@oO*eEQgR&KNH>m2nJCJ8 zKUTccTJPva!S;kK((}vrdO0pR?82VI4|isc9`ZRbC{)Q+ONVxarti(1IBS+)ZqBmx zq&uWfT5dIn|mD7OP265=2diRzdN-RMoJX1Ag-cNEk* zHKY}77g<^6;Y@d+F2*=aMCkE%tGi2Uo|H#xAAqmFC@zLb-h=fC=f&w++B%w^j#zI) zxd?ObZ!QBTZuDOfinycKuDKJuwk|rA(A-vcPHW70^w%?Qw;PNbp)FjFANv@H)}S<| zzcbi-C!ojMn@ZWzomVm0e731^J(3|s70K`4xo_X0Uk36Yw9V)lJ8~Yh$)4_XiaHYb z#h`+R+2_X}Ju#>n?cdv;YlE>Sov$b^_z~mn_9&pRt2XPe#P~K1u@PujRGjaR-PjHQ zFh_#dAPl)wDwH9Bp%n5Ep#Z-gLQ?UeVVsD#yv0}mz_K|=z(a&|{#OE!No2Ac$;XXM ziza!|$sTkMk_(AMCy|D&3U2+IKq?nNqRjs-C<|C20RRxbS3n@CP=)ZMivQG*=^m8% zA~`=4&UIxmr-Ar$O*QK%TU*CcEL!i_d3R^tcjle%JpX+@^UQai9CtUT-7*?70D#>j zqJt*@0KF5qGys5|n79G}B$vH=7Nb4N%HnWhveT~XjJgZZ5S{WDo^F=ssw!ip^ONtw^Xb}~>wz%}gq_3gl#aGM3 zf0jK16}Hwtae8#e(Latia)g^t$iBtM3HF4?c@yH+)7a7Jqe7|duhwRCzSr^-a%k01 zA8T*J^(w`aicZQtH{nd<)rm&R5#J&YT$kh%cxR9xo{G;eg4DyYk%aNJ!*i2;ezO!6t!8Al*ZwA-)x#c z_aWrZN!K-&bbG?WZAkZ<8fO)T9_|hdd}yAqr$q#juHZb&M;X|@s(2x}5V7KVDnzU@ zdy1WOW`1%7LiF-`wpT2cS2J*5ZR>AuA4}R?b7|<-&fu}?h?;4Polz)6PgejE^#I^z z0I&f7;LZXNivi$M2mrWJ092V}Z64MD0O?R?CkL>-bCsR9iU0rzCh-&-0F&=_0y$TG zJPrT=^eDA!ykl~>hWC< zQSf$09HEx{r#T?*PP?paUFw4hb?!czFBU5{LJO3?J!4Esx$ z9e(=(QB#`W1NN-vrLm9iuE`pksAxW)qGG~*!)r)-lrY&GFge9M{PUXeXIEe4PKS&* z0V_#DTc>_5r#vI?-bKgQ1Fl<~?2T&EF#6nft`%O9cM#I0L`;jwe*u$B_t|yht&zP) z#;6iad(Y$Utq^coe<=+AXi+a@q*zhGMiz^ozEB+Zg`9!izUPwFsa(4Ld0?4-SFs7F zx2}5y?z^NZ?$D{|QUKOroed^;Y|_-ySygq_TrE*0ghiGE2V! zAq}mkJ$UsE0MI~^gPpg3Px+P8ktiQzf7;9d{Q<2j&-LY@ci%Tw`$L^c=3S)R`>EZA zHV34Gy_1z3dR0>}@K9TA@5rX1D;whTjg`Dect8QK+Oy`4Ya6X_@W`{&fq9|u3wd6z zh*Kz>PbaSlMXfXO+cP4wH5mk*PB$Vww4#TGPSmCF3E5GHU_^*X2p1=YquiUZcWqAnyc@F z(-+cT#+?wkLL^^vbIYgYncZKz-+w_f6=$2#;RpAJDqrxBsMI`%IA`LR)0i?YOmu4z z7%pD-(e=%YhJ@P2N#5f2)6WIoy-qh9mJ%COB4Ofu0rslPcScfHKSjWmmScpF4WlP6 zfokRmfu0pCFzR|*h18{{CqUs3mS9@1$_ajjIR^@?;q_-a5LR%}JFMX58;p$gyL|#( zawc61Jt2YwLArEmQ%Od!WLSsHKz}S#NsccF5G2+5wS6`MsZbj|BZo5|vVJ+b)(Bn1 zEXLW-*kI(6;kD*5;qMQMIQJWC1sF_e{erQ0`S$WZer2zFkCGEl4cGErAHv;+@{i=~ zznFkePh9CkEYPw#XDo|49j~Zc@cEA1@o?mNzuYxGYZYv!Qy=R#YR@XHJZ-lM9)F{X zQfs}};C%S`0{lT4QY^t#wzyWw&|XB46UHg;Xy?x!GxjW*=3QQ^Mmz6CzAY>FnjD=~ zER*VzvLV8^X8UeFdZlX_anrYOixDE^8Sw_wZ?o~-3G9*8?vZ$gdHhK&HC)HY@x+cE&LtV#E>1J{3E20#x>d)+0q`VPOlS=m~AtG8GIMIc;!g(MQ32ijkK;R&}D-M78-yK`JpqO4`> z8fNtBJx>*N|ks+|MUY0_3g!nYhHJHHY|D0u;A;qo@y2HjMK zA>sP618ypBp}hS;@>37cURfK#hYe;mz-rx8jHb^L0PU|CpFl>W2_14c_c9|w>P1jM-?6A|NgMfsB*dEnWdMLWhv3h_GoquZF&uzrQOO)? z7&@NDi9|b*T;2WBT28%VuSQ=w3yfNlDtSQbEa}a~UVK8FN z%%T4hq8VYd@Pz*_6xbf40{{;E-N2zmQ`sDHH1mHQEY9pdB@Cy^K>)zs9fwAt;3y1c z0*e+A$pP-Mp>!Gr?ZAj8(_)$nOalM_x_@C|R5pc0V{&MW7_jpkIy9C>=NQvs&}>g9 zdzT11B>;f(UvxA%CN`W*;l#42tUqy=AM>9B0ObA#7-3X8*ii#QNO%AMAQ9Xg>g+>! F{{~P%DewRQ literal 0 HcmV?d00001 diff --git a/images/48.png b/images/48.png new file mode 100644 index 0000000000000000000000000000000000000000..ab26eaa1a8cf25ceea0d1e3f0ab0e1fd7220e32b GIT binary patch literal 3086 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 UR97!^8~^~An_MuiGxCc67k~e(a{vGU literal 0 HcmV?d00001 diff --git a/images/96.png b/images/96.png new file mode 100644 index 0000000000000000000000000000000000000000..b5e84dc0dcb49be3069035a924a0d4341d277250 GIT binary patch literal 5560 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 C>@s}- literal 0 HcmV?d00001 diff --git a/installed.css b/installed.css new file mode 100644 index 0000000..729af9d --- /dev/null +++ b/installed.css @@ -0,0 +1,41 @@ +body { + min-width: 25em; +} +div.panel { + display: flex; +} +div.panel > div { + padding: 8px; + line-height: 1.4; +} +div.panel > div:last-child { + flex-grow: 1; +} +div#header { + font-weight: 600; +} +a { + color: #0454d4; +} +div#button { + margin-top: 8px; + text-align: right; +} +button { + background-color: #fbfbfb; + border: 1px solid #b1b1b1; + box-shadow: 0 0 0 0 transparent; + height: 24px; + outline: 0 !important; + padding: 0 8px 0; + transition-duration: 250ms; + transition-property: box-shadow, border; +} +button:hover { + background-color: #ebebeb; + border: 1px solid #b1b1b1; +} +button:hover:active { + background-color: #d4d4d4; + border: 1px solid #858585; +} diff --git a/installed.html b/installed.html new file mode 100644 index 0000000..82b28a5 --- /dev/null +++ b/installed.html @@ -0,0 +1,25 @@ + + + + + + + + +
+
+ +
+
+ +
+
+
+ +
+
+
+ + + + diff --git a/installed.js b/installed.js new file mode 100644 index 0000000..e5d7767 --- /dev/null +++ b/installed.js @@ -0,0 +1,8 @@ +/* globals chrome, get_strings */ +get_strings(); + +document.querySelector('button').onclick = function() { + chrome.runtime.openOptionsPage(function() { + window.close(); + }); +}; diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..b6eb50d --- /dev/null +++ b/manifest.json @@ -0,0 +1,38 @@ +{ + "manifest_version": 2, + "name": "Download With", + "description": "Set an external manager for your downloads.", + "version": "0.0.1", + "applications": { + "gecko": { + "id": "downloadwith@wirlaburla.github.io", + "strict_min_version": "63.0" + } + }, + "icons": { + "16": "images/16.png", + "24": "images/24.png", + "32": "images/32.png", + "48": "images/48.png", + "64": "images/64.png", + "96": "images/96.png", + "128": "images/128.png" + }, + "background": { + "scripts": [ + "common.js", + "background.js" + ] + }, + "permissions": [ + "downloads", + "nativeMessaging", + "storage", + "tabs" + ], + "options_ui": { + "browser_style": false, + "open_in_tab": true, + "page": "options.html" + } +} diff --git a/native/download_with_linux.py b/native/download_with_linux.py new file mode 100755 index 0000000..01825ab --- /dev/null +++ b/native/download_with_linux.py @@ -0,0 +1,110 @@ +#!/usr/bin/env python3 +from __future__ import print_function + +import os +import sys +import json +import struct +import subprocess + +VERSION = '0.0.1' + +try: + sys.stdin.buffer + + # Python 3.x version + # Read a message from stdin and decode it. + def getMessage(): + rawLength = sys.stdin.buffer.read(4) + if len(rawLength) == 0: + sys.exit(0) + messageLength = struct.unpack('@I', rawLength)[0] + message = sys.stdin.buffer.read(messageLength).decode('utf-8') + return json.loads(message) + + # Send an encoded message to stdout + def sendMessage(messageContent): + encodedContent = json.dumps(messageContent).encode('utf-8') + encodedLength = struct.pack('@I', len(encodedContent)) + + sys.stdout.buffer.write(encodedLength) + sys.stdout.buffer.write(encodedContent) + sys.stdout.buffer.flush() + +except AttributeError: + # Python 2.x version (if sys.stdin.buffer is not defined) + print('Python 3.2 or newer is required.') + sys.exit(-1) + + +def install(): + home_path = os.getenv('HOME') + + manifest = { + 'name': 'download_with', + 'description': 'Download With native host', + 'path': os.path.realpath(__file__), + 'type': 'stdio', + } + locations = { + 'chrome': os.path.join(home_path, '.config', 'google-chrome', 'NativeMessagingHosts'), + 'chrome-beta': os.path.join(home_path, '.config', 'google-chrome-beta', 'NativeMessagingHosts'), + 'chrome-unstable': os.path.join(home_path, '.config', 'google-chrome-unstable', 'NativeMessagingHosts'), + 'chromium': os.path.join(home_path, '.config', 'chromium', 'NativeMessagingHosts'), + 'firefox': os.path.join(home_path, '.mozilla', 'native-messaging-hosts'), + 'librewolf': os.path.join(home_path, '.librewolf', 'native-messaging-hosts'), + 'waterfox': os.path.join(home_path, '.waterfox', 'native-messaging-hosts'), + 'waterfox-g4': os.path.join(home_path, '.waterfox', 'native-messaging-hosts'), + 'thunderbird': os.path.join(home_path, '.thunderbird', 'native-messaging-hosts'), + } + filename = 'download_with.json' + + for browser, location in locations.items(): + if os.path.exists(os.path.dirname(location)): + if not os.path.exists(location): + os.mkdir(location) + + browser_manifest = manifest.copy() + if browser in ['firefox', 'thunderbird', 'librewolf', 'waterfox', 'waterfox-g4']: + browser_manifest['allowed_extensions'] = ['downloadwith@wirlaburla.github.io'] + else: + browser_manifest['allowed_origins'] = [ + 'chrome-extension://cogjlncmljjnjpbgppagklanlcbchlno/', # Chrome + 'chrome-extension://fbmcaggceafhobjkhnaakhgfmdaadhhg/', # Opera + ] + + with open(os.path.join(location, filename), 'w') as file: + file.write( + json.dumps(browser_manifest, indent=2, separators=(',', ': '), sort_keys=True).replace(' ', '\t') + '\n' + ) + +def listen(): + receivedMessage = getMessage() + if receivedMessage == 'ping': + sendMessage({ + 'version': VERSION, + 'file': os.path.realpath(__file__) + }) + else: + devnull = open(os.devnull, 'w') + subprocess.Popen(receivedMessage, stdout=devnull, stderr=devnull) + sendMessage(None) + + +if __name__ == '__main__': + if len(sys.argv) == 2: + if sys.argv[1] == 'install': + install() + sys.exit(0) + allowed_extensions = [ + 'downloadwith@wirlaburla.github.io', + 'chrome-extension://cogjlncmljjnjpbgppagklanlcbchlno/', + 'chrome-extension://fbmcaggceafhobjkhnaakhgfmdaadhhg/', + ] + for ae in allowed_extensions: + if ae in sys.argv: + listen() + sys.exit(0) + + print('This is the Download With native helper, version %s.' % VERSION) + print('Run this script again with the word "install" after the file name to install.') diff --git a/native/download_with_mac.py b/native/download_with_mac.py new file mode 100755 index 0000000..1df7074 --- /dev/null +++ b/native/download_with_mac.py @@ -0,0 +1,152 @@ +#!/usr/bin/env python3 +from __future__ import print_function + +import os +import sys +import json +import struct +import subprocess + +VERSION = '7.2.6' + +try: + sys.stdin.buffer + + # Python 3.x version + # Read a message from stdin and decode it. + def getMessage(): + rawLength = sys.stdin.buffer.read(4) + if len(rawLength) == 0: + sys.exit(0) + messageLength = struct.unpack('@I', rawLength)[0] + message = sys.stdin.buffer.read(messageLength).decode('utf-8') + return json.loads(message) + + # Send an encoded message to stdout + def sendMessage(messageContent): + encodedContent = json.dumps(messageContent).encode('utf-8') + encodedLength = struct.pack('@I', len(encodedContent)) + + sys.stdout.buffer.write(encodedLength) + sys.stdout.buffer.write(encodedContent) + sys.stdout.buffer.flush() + +except AttributeError: + print('Python 3.2 or newer is required.') + sys.exit(-1) + + +def install(): + home_path = os.getenv('HOME') + + manifest = { + 'name': 'open_with', + 'description': 'Open With native host', + 'path': os.path.realpath(__file__), + 'type': 'stdio', + } + locations = { + 'chrome': os.path.join(home_path, 'Library', 'Application Support', 'Google', 'Chrome', 'NativeMessagingHosts'), + 'chromium': os.path.join(home_path, 'Library', 'Application Support', 'Chromium', 'NativeMessagingHosts'), + 'edge': os.path.join(home_path, 'Library', 'Application Support', 'Microsoft Edge', 'NativeMessagingHosts'), + 'firefox': os.path.join(home_path, 'Library', 'Application Support', 'Mozilla', 'NativeMessagingHosts'), + 'thunderbird1': os.path.join(home_path, 'Library', 'Application Support', 'Thunderbird', 'NativeMessagingHosts'), + 'thunderbird2': os.path.join(home_path, 'Library', 'Mozilla', 'NativeMessagingHosts'), + } + filename = 'open_with.json' + + for browser, location in locations.items(): + if os.path.exists(os.path.dirname(location)): + if not os.path.exists(location): + os.makedirs(location, exist_ok=True) + + browser_manifest = manifest.copy() + if browser in ['firefox', 'thunderbird1', 'thunderbird2']: + browser_manifest['allowed_extensions'] = ['openwith@darktrojan.net'] + else: + browser_manifest['allowed_origins'] = [ + 'chrome-extension://cogjlncmljjnjpbgppagklanlcbchlno/', # Chrome + 'chrome-extension://fbmcaggceafhobjkhnaakhgfmdaadhhg/', # Opera + ] + + with open(os.path.join(location, filename), 'w') as file: + file.write( + json.dumps(browser_manifest, indent=2, separators=(',', ': '), sort_keys=True).replace(' ', '\t') + '\n' + ) + + +def find_browsers(): + apps = [ + 'Chrome', + 'Chromium', + 'Firefox', + 'Google Chrome', + 'Microsoft Edge', + 'Opera', + 'Safari', + 'SeaMonkey', + ] + paths = [ + os.path.join(os.getenv('HOME'), 'Applications'), + '/Applications', + ] + + results = [] + for p in paths: + for a in apps: + fp = os.path.join(p, a) + '.app' + if os.path.exists(fp): + results.append({ + 'name': a, + 'command': '"%s.app"' % os.path.join(p, a) + }) + return results + + +def listen(): + receivedMessage = getMessage() + if receivedMessage == 'ping': + sendMessage({ + 'version': VERSION, + 'file': os.path.realpath(__file__) + }) + elif receivedMessage == 'find': + sendMessage(find_browsers()) + else: + for k, v in os.environ.items(): + if k.startswith('MOZ_'): + try: + os.unsetenv(k) + except: + os.environ[k] = '' + + devnull = open(os.devnull, 'w') + if receivedMessage[0].endswith('.app'): + command = ['/usr/bin/open', '-a'] + receivedMessage + else: + command = receivedMessage + subprocess.Popen(command, stdout=devnull, stderr=devnull) + sendMessage(None) + + +if __name__ == '__main__': + if len(sys.argv) == 2: + if sys.argv[1] == 'install': + install() + sys.exit(0) + elif sys.argv[1] == 'find_browsers': + print(find_browsers()) + sys.exit(0) + + allowed_extensions = [ + 'openwith@darktrojan.net', + 'chrome-extension://cogjlncmljjnjpbgppagklanlcbchlno/', + 'chrome-extension://fbmcaggceafhobjkhnaakhgfmdaadhhg/', + ] + for ae in allowed_extensions: + if ae in sys.argv: + listen() + sys.exit(0) + + print('This is the Open With native helper, version %s.' % VERSION) + print('Run this script again with the word "install" after the file name to install.') diff --git a/native/download_with_windows.ps1 b/native/download_with_windows.ps1 new file mode 100644 index 0000000..0a8e10b --- /dev/null +++ b/native/download_with_windows.ps1 @@ -0,0 +1,161 @@ +function GetMessage { + $reader = New-Object System.IO.BinaryReader([System.Console]::OpenStandardInput()) + $messageLength = $reader.ReadInt32() + $messageBytes = $reader.ReadBytes($messageLength) + return [System.Text.Encoding]::UTF8.GetString($messageBytes) | ConvertFrom-Json +} + +function SendReply { + param ($reply) + $replyBytes = [System.Text.Encoding]::UTF8.GetBytes(($reply | ConvertTo-Json)) + $writer = New-Object System.IO.BinaryWriter([System.Console]::OpenStandardOutput()) + $writer.Write($replyBytes.Count) + $writer.Write($replyBytes) +} + +function Install { + $registry_locations = @{ + chrome='HKCU:\Software\Google\Chrome\NativeMessagingHosts'; + firefox='HKCU:\Software\Mozilla\NativeMessagingHosts' + } + + $install_path = Split-Path $PSCommandPath -Parent + $bat_path = (Join-Path $install_path -ChildPath 'open_with.bat') + New-Item -Force -Path $bat_path -Value (@' +@echo off +call "powershell" -file " +'@ + $PSCommandPath + '"') > $null + + $manifest = @{name='open_with';type='stdio';path=$bat_path;description='Open With native host'} + + foreach ($browser in $registry_locations.Keys) { + $registry_location = $registry_locations[$browser] + if (Get-Item (Split-Path $registry_location -Parent)) { + if (!(Get-Item $registry_location -ErrorAction Ignore)) { + New-Item $registry_location > $null + } + + $registry_location = Join-Path $registry_location -ChildPath 'open_with' + $manifest_location = Join-Path $install_path -ChildPath ('open_with_' + $browser + '.json') + if (!(Get-Item $registry_location -ErrorAction Ignore)) { + New-Item $registry_location > $null + } + + Set-Item -Path $registry_location -Value $manifest_location -Force + $browser_manifest = $manifest.Clone() + if ($browser -eq 'firefox') { + $browser_manifest['allowed_extensions'] = @('openwith@darktrojan.net') + } else { + $browser_manifest['allowed_origins'] = @('chrome-extension://cogjlncmljjnjpbgppagklanlcbchlno/') + } + New-Item -Force -Path $manifest_location -Value ($browser_manifest | ConvertTo-Json) > $null + } + } +} + +function FindBrowsers { + return (Get-ChildItem -Path 'Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Clients\StartMenuInternet\' | + Select-Object -Property @{Name='name';Expression={$_.GetValue($null)}}, @{Name='command';Expression={$_.OpenSubKey('shell\open\command').GetValue($null)}}) +} + +# From https://github.com/FuzzySecurity/PowerShell-Suite +function Invoke-CreateProcess { + param ( + [Parameter(Mandatory = $True)] + [string]$Binary, + [Parameter(Mandatory = $False)] + [string]$Args=$null, + [Parameter(Mandatory = $True)] + [string]$CreationFlags, + [Parameter(Mandatory = $True)] + [string]$ShowWindow, + [Parameter(Mandatory = $True)] + [string]$StartF + ) + + # Define all the structures for CreateProcess + Add-Type -TypeDefinition @" + using System; + using System.Diagnostics; + using System.Runtime.InteropServices; + + [StructLayout(LayoutKind.Sequential)] + public struct PROCESS_INFORMATION + { + public IntPtr hProcess; public IntPtr hThread; public uint dwProcessId; public uint dwThreadId; + } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + public struct STARTUPINFO + { + public uint cb; public string lpReserved; public string lpDesktop; public string lpTitle; + public uint dwX; public uint dwY; public uint dwXSize; public uint dwYSize; public uint dwXCountChars; + public uint dwYCountChars; public uint dwFillAttribute; public uint dwFlags; public short wShowWindow; + public short cbReserved2; public IntPtr lpReserved2; public IntPtr hStdInput; public IntPtr hStdOutput; + public IntPtr hStdError; + } + + [StructLayout(LayoutKind.Sequential)] + public struct SECURITY_ATTRIBUTES + { + public int length; public IntPtr lpSecurityDescriptor; public bool bInheritHandle; + } + + public static class Kernel32 + { + [DllImport("kernel32.dll", SetLastError=true)] + public static extern bool CreateProcess( + string lpApplicationName, string lpCommandLine, ref SECURITY_ATTRIBUTES lpProcessAttributes, + ref SECURITY_ATTRIBUTES lpThreadAttributes, bool bInheritHandles, uint dwCreationFlags, + IntPtr lpEnvironment, string lpCurrentDirectory, ref STARTUPINFO lpStartupInfo, + out PROCESS_INFORMATION lpProcessInformation); + } +"@ + + # StartupInfo Struct + $StartupInfo = New-Object STARTUPINFO + $StartupInfo.dwFlags = $StartF # StartupInfo.dwFlag + $StartupInfo.wShowWindow = $ShowWindow # StartupInfo.ShowWindow + $StartupInfo.cb = [System.Runtime.InteropServices.Marshal]::SizeOf($StartupInfo) # Struct Size + + # ProcessInfo Struct + $ProcessInfo = New-Object PROCESS_INFORMATION + + # SECURITY_ATTRIBUTES Struct (Process & Thread) + $SecAttr = New-Object SECURITY_ATTRIBUTES + $SecAttr.Length = [System.Runtime.InteropServices.Marshal]::SizeOf($SecAttr) + + # CreateProcess --> lpCurrentDirectory + $GetCurrentPath = (Get-Item -Path ".\" -Verbose).FullName + + # Call CreateProcess + [Kernel32]::CreateProcess($Binary, $Args, [ref] $SecAttr, [ref] $SecAttr, $false, $CreationFlags, [IntPtr]::Zero, $GetCurrentPath, [ref] $StartupInfo, [ref] $ProcessInfo) |out-null + + echo "`nProcess Information:" + Get-Process -Id $ProcessInfo.dwProcessId |ft +} + +if ($args.Length -eq 1) { + if ($args[0] -eq 'install') { + Install + Exit(0) + } elseif ($args[0] -eq 'find_browsers') { + FindBrowsers | Format-List + Exit(0) + } +} + +$message = GetMessage +if ($message -eq 'ping') { + SendReply @{'version'='7.2.2';'file'=$PSCommandPath} +} elseif ($message -eq 'find') { + SendReply (FindBrowsers) +} else { + if ($message.Length -gt 1) { + $c = $message.Length - 1 + Invoke-CreateProcess -Binary $message[0] -Args ('"' + $message[0] + '" ' + [String]::Join(' ', $message[1..$c])) -CreationFlags 0x01000010 -ShowWindow 1 -StartF 1 + } else { + Invoke-CreateProcess -Binary $message[0] -CreationFlags 0x01000000 -ShowWindow 1 -StartF 1 + } + SendReply $null +} diff --git a/native/download_with_windows.py b/native/download_with_windows.py new file mode 100644 index 0000000..d47aa80 --- /dev/null +++ b/native/download_with_windows.py @@ -0,0 +1,164 @@ +from __future__ import print_function + +import os +import sys +import json +import struct +import subprocess + +VERSION = '7.2.6' + +try: + sys.stdin.buffer + + # Python 3.x version + # Read a message from stdin and decode it. + def getMessage(): + rawLength = sys.stdin.buffer.read(4) + if len(rawLength) == 0: + sys.exit(0) + messageLength = struct.unpack('@I', rawLength)[0] + message = sys.stdin.buffer.read(messageLength).decode('utf-8') + return json.loads(message) + + # Send an encoded message to stdout + def sendMessage(messageContent): + encodedContent = json.dumps(messageContent).encode('utf-8') + encodedLength = struct.pack('@I', len(encodedContent)) + + sys.stdout.buffer.write(encodedLength) + sys.stdout.buffer.write(encodedContent) + sys.stdout.buffer.flush() + +except AttributeError: + # Python 2.x version (if sys.stdin.buffer is not defined) + print('Python 3.2 or newer is required.') + sys.exit(-1) + + +def install(): + import sys + try: + import winreg as _winreg + except: + import _winreg + + this_file = os.path.realpath(__file__) + install_path = os.path.dirname(this_file) + + manifest = { + 'name': 'open_with', + 'description': 'Open With native host', + 'path': this_file, + 'type': 'stdio', + } + + manifest['path'] = filename = os.path.join(install_path, 'open_with.bat') + with open(filename, 'w') as file: + file.write('@echo off\r\ncall "%s" "%s" %%1 %%2\r\n' % (sys.executable, this_file)) + + registry_locations = { + 'chrome': os.path.join('Software', 'Google', 'Chrome', 'NativeMessagingHosts'), + 'chromium': os.path.join('Software', 'Chromium', 'NativeMessagingHosts'), + 'firefox': os.path.join('Software', 'Mozilla', 'NativeMessagingHosts'), + 'thunderbird': os.path.join('Software', 'Thunderbird', 'NativeMessagingHosts'), + } + + for browser, registry_location in registry_locations.items(): + browser_manifest = manifest.copy() + if browser in ['firefox', 'thunderbird']: + browser_manifest['allowed_extensions'] = ['openwith@darktrojan.net'] + else: + browser_manifest['allowed_origins'] = [ + 'chrome-extension://cogjlncmljjnjpbgppagklanlcbchlno/', # Chrome + 'chrome-extension://fbmcaggceafhobjkhnaakhgfmdaadhhg/', # Opera + ] + + filename = os.path.join(install_path, 'open_with_%s.json' % browser) + with open(filename, 'w') as file: + file.write( + json.dumps(browser_manifest, indent=2, separators=(',', ': '), sort_keys=True).replace(' ', '\t') + '\n' + ) + + key = _winreg.CreateKey(_winreg.HKEY_CURRENT_USER, registry_location) + _winreg.SetValue(key, 'open_with', _winreg.REG_SZ, filename) + + +def find_browsers(): + try: + import winreg as _winreg + except: + import _winreg + + windir = os.getenv('windir') + key = _winreg.OpenKey(_winreg.HKEY_LOCAL_MACHINE, os.path.join('Software', 'Clients', 'StartMenuInternet')) + count = _winreg.QueryInfoKey(key)[0] + + browsers = [] + found_msedge = False + while count > 0: + subkey = _winreg.EnumKey(key, count - 1) + try: + browsers.append({ + 'name': _winreg.QueryValue(key, subkey), + 'command': _winreg.QueryValue(key, os.path.join(subkey, 'shell', 'open', 'command')) + }) + if subkey == 'Microsoft Edge': + found_msedge = True + except: + pass + count -= 1 + + if not found_msedge and \ + os.path.exists(os.path.join(windir, 'SystemApps', 'Microsoft.MicrosoftEdge_8wekyb3d8bbwe', 'MicrosoftEdge.exe')): + browsers.append({ + 'name': 'Microsoft Edge', + 'command': os.path.join(windir, 'explorer.exe') + ' "microsoft-edge:%s "' + }) + + return browsers + + +def listen(): + receivedMessage = getMessage() + if receivedMessage == 'ping': + sendMessage({ + 'version': VERSION, + 'file': os.path.realpath(__file__) + }) + elif receivedMessage == 'find': + sendMessage(find_browsers()) + else: + for k, v in os.environ.items(): + if k.startswith('MOZ_'): + try: + os.unsetenv(k) + except: + os.environ[k] = '' + + CREATE_BREAKAWAY_FROM_JOB = 0x01000000 + CREATE_NEW_CONSOLE = 0x00000010 + subprocess.Popen(receivedMessage, creationflags=CREATE_BREAKAWAY_FROM_JOB | CREATE_NEW_CONSOLE) + sendMessage(None) + +if __name__ == '__main__': + if len(sys.argv) == 2: + if sys.argv[1] == 'install': + install() + sys.exit(0) + elif sys.argv[1] == 'find_browsers': + print(find_browsers()) + sys.exit(0) + + allowed_extensions = [ + 'openwith@darktrojan.net', + 'chrome-extension://cogjlncmljjnjpbgppagklanlcbchlno/', + 'chrome-extension://fbmcaggceafhobjkhnaakhgfmdaadhhg/', + ] + for ae in allowed_extensions: + if ae in sys.argv: + listen() + sys.exit(0) + + print('This is the Open With native helper, version %s.' % VERSION) + print('Run this script again with the word "install" after the file name to install.') diff --git a/options.css b/options.css new file mode 100644 index 0000000..a86e28f --- /dev/null +++ b/options.css @@ -0,0 +1,12 @@ +body { + display: flex; + justify-content: center; +} +#group > div { + display: flex; + gap: 4px; + margin: 4px; +} +#test_results, #exeinput { + flex-grow: 1; +} \ No newline at end of file diff --git a/options.html b/options.html new file mode 100644 index 0000000..f75e4d5 --- /dev/null +++ b/options.html @@ -0,0 +1,27 @@ + + + + +Download With Options + + + + +
+
Download With
Options
+
+ Native Test +
+
Click button to test.
+ +
+ Executable +
+
+ +
Use '%s' in place of the URL.
+
+ + + + diff --git a/options.js b/options.js new file mode 100644 index 0000000..7758646 --- /dev/null +++ b/options.js @@ -0,0 +1,35 @@ +/* globals chrome, compare_versions, compare_object_versions, get_version_warn, get_string, get_strings */ +let testResult = document.getElementById('test_results'); +let exeInput = document.getElementById('exeinput'); + +document.getElementById('test_button').onclick = function() { + function error_listener() { + testResult.style.color = 'red'; + testResult.innerText = "Error"; + } + + let port = chrome.runtime.connectNative('download_with'); + port.onDisconnect.addListener(error_listener); + port.onMessage.addListener(function(message) { + if (message) { + console.log(message); + testResult.style.color = 'darkgreen'; + testResult.innerText = "Success!"; + } else { + error_listener(); + } + port.onDisconnect.removeListener(error_listener); + port.disconnect(); + }); + port.postMessage('ping'); +}; + + + +exeInput.onchange = function(event) { + chrome.storage.local.set({execute: exeInput.value}); +} + +chrome.storage.local.get({execute: null}, function({execute}) { + exeInput.value = execute; +}); \ No newline at end of file