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 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 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 zcmchWc~nz(7RO)McS0Z#Fi4;Xq7XvDl0-#9SV9{x>~Po;vhgt6dwD$(c(oW}T{Vzi}g0!}PpGwZ7w6Yb2+x*=|HjNPe4 zV%F;7(y!#<&dc{U7=`aP+yEe;1XATwc#2xAw7;*fn z+i6m0Y4xr*cU}*zPO7o9^_r+HT02S{;E>---$8-BoAvM~r;Rtu9{%(F=T%UUmz^;) zQ}$9wn0a966!874;GX$mQ|#MD9bKmf;*2H1QGe_1nz0$ZWSO3R$zNmr`2&nm6LR}F z(JSPB(>?vC(qE!e`BU~Ysp_JG3)8<~IPuB1w@po930}OF-kW`W6Gbmqxt#+pIxXD6 zaV}A!93{CDKnwW7R!kKs5OUh9a^J5a2S5&Vh*&8d^oQ(I=MD19_sV|MOr# z+y5Lf7tGDh&0?v1&>Q~Qj=rbJV)v;${a_m(wYdzG|K?>YDgF>X2qH!)7+D9?B9wsv zkRzHVnh{IoeLx<1K*6R%&VDc!weM4Tpje8GhfefU6xhlawxmIL6b@7RMJx8RRAltH z8#biEIDg0?03x7KG7hHZfH~mni)9>4k$@5@C>7;qZ*FdGD>q3*|Fr^%i$VM4zXrz8 z+`_=hi_S^S5xvpeHFSS^(ABzUqiDF`TSsK^`QKT3esis+n2?nk0_82K1NQdp_Y141 ztuSti$$k&d`iG-Ic9Jg^o~GHyHc!4-f7jBSet3AkhotYBM9$Gv2BYyX*>6RL6-0=h zkD&)s6(>~<0OU8e7Uu2t{rRZT7>SAwH~!I-XkbsUoz3t^HP;FgQjj=70Z9cbC7?`9 zLNqEd>2;seYue+}juBlg)_<~NXqP$}G1mTZ3nfPg7RwXkoHHr8Eo5Pz%;D#Pi$9N1 z0=N{51EUw(Moj8G+M<1hY*t`|0i*3!6_Ec?pLs~f;m?{JOWnaFDs{@6bQS+n)uk%4y#5t%7bJPOjJ)>^THvpv!dMA}`$9gSZ=ffziS>g>BYCB8_|ZD5{~T4o@V&CfX#9qY&I31=HvdgKnp!uCdsxlFgo;C?=B z`Ps%ggFx5UD>E}2TyLGik`+dcqn|ETKEgHgK#`rV3r*jeR)TP4>4#ETq1m_~8cm;u zHF5u-?R>jb1e<6Z1}%(I#~ELQl7az;eDcT*Sa$}^&JhVuLD)*UT&zFo zczM|}P)G_>$_1dJ=Y(Gxin;xciNtCl45|=NsQ|W;BW34-GQH(s{W5PN@`PzR2CuR{y literal 0 HcmV?d00001 diff --git a/images/24.png b/images/24.png new file mode 100644 index 0000000000000000000000000000000000000000..bf130c76b87459ce6729b7a4e6ef4d47ec29bc11 GIT binary patch literal 2439 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 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 zcmZ{mcTm&I7RP@HC4lr^A_PQ2HwHuqRaz3HD@|$$#c+dx5D=t=UZe#CK@dbK0!q1n zqM>R;P^393RU;r5ktzrXs4w0(_szUF^JeGl&Ybi4&e@&s{&6zwZP9!@(mVhF@L@6L zj;s^$JGeMmy9@Yx8UTP|2xeyX1Uw!9lxeXveaur6Xn&6p10p7*pH3ODzp0*n0@}`F zaaS-X#rdeUX6%`QRK;h{96~g7_gwFxI&{A*GgY|~1q9J>xP^quW<0jiA`9~T$KG0Z zE`1${?;1LCgyM;eDc%jVUDyP23o|bb6X6YZ{=t| zS*G=NrLdhaTFmKAOnkq8%9eOwyNCa)=&ur~rAM9GU_Q)kCk{dBc<4G(#V0et94FFtG0sv|< z0PM3;)Fl82XBoEa0|2OE0FVeNd+cDqDscLqMVkY^ey>|ywFRsUcL>HU3;;Bb{SHuM zwQ&S1387$ZEFg>APH)|G<5IA>`E9uEwg3vsjmT zazrmV8Z0N@{3YXNa@PH9%W!3zLrU6y^+`YCmtUFsX2OYTpl%UNd|=)Pge}2|_s)d6 zsYCLl2tiKZNzb7VkPF}RGwa(>@_BbnSjgxI((F_EwTN%HWaO{=)~Q`$#e1tmTf_$8 zc9e)y;ATd`&Qey6EO%XuU#7-1+eA9A7=@95o{^baG2DNYrj!%L$! z+awdONnzx*bU*FfrX!{@m{mG#DP}$%Upif_^juOfGu3gmbi@F}K8Ld~;b#*7n!thR<)1=^qof1s3ATs1 z%8{d&wR%w-X3QTBod~k%a#9JXo5R4sawKpX-u(0{srNFquv2jOE^zhgRjA>Ww%wEc zIUniNrYf&Tp}pee`%=H4Q*@7b%asadq1Apc&M-QarS_H9~ersLTwzc>$=JvMe*4={BBEw$Bw1`EYQ_0|0e9+Z*&g z5@PHIg>*dDL+DW1pwO+JAGbNDUc<{1fq+*A>mx=dZus?|ItLN8GSS4RRj447DJ4J> zSvHOPMo_S?cg&YIp>Ao+mody?xY?n5o*gyPgDM#b+2eecdAo^?-YmbRMVQ8|%#3r7 zT~`_!T7AgaMU~U{zq`(*aQ#}{o&-O8vZTU1=+)p@+Z2r8)8m3-Emn^eFbHFmLOXW@ z5zK0$yq8s4UaIe@P_|Mds0zRYqZ8;Y976^VGF#`yC{B=(x>0*IFNHp-ybpjJ&Gx6l zpa=L+m`-3}Wc=EQ(W@Xr{ZVt~) z(31_X73Og40TzRaeQmZ%R3gAD(zAANBjm5>$(q@T!2`($5wqUid+9avAM3!XA?W)0s=weuL6* z%05Pj_Zw|WI%K=s`mBB(m^oxN5=%Mnv?qruc!CMz4qoGh53uLwB+q1UO}aR|&YuWf zSD#l~kH4`YIv_J)lW0?*GkuT*@J1xKg7hjDWxk}kri@4E*9U;#C>0HiT};`=XYu1l z*)opV8z(e8!CY&abmxj&l!vfVt~zu-a~h$3P3fXTBy)9V2h+w0FW74r@4Vlv0gFziDfd4pnb4%`!Cye8n7HT<+=@ z>gMf3Zh3=$rrYp_@!c%X7S%~zINS9mShy5E%Ra+BBe3Yj)JdTSjt|*bm(B+D4~<9{ z2iK`~M^-3Nya6wUtdqs0%ys@|4)c*KkPqi3TAQbvzdbpLxGY`l((#(V;Iw30#b~TZ zwDvbU&IP@wd!wpt9M*5uj`aT3TsD^nqFA_6%t*n({+B3# z8QC94@%MvWCQvTI&{!LLmjr$Z7IgeKuLgOW^@HJSCj$PyQRXF3)?x0{H&}ko^3Kz;CI$5BB$c O0IY?rd84UM;=cfwyj+F= literal 0 HcmV?d00001 diff --git a/images/64.png b/images/64.png new file mode 100644 index 0000000000000000000000000000000000000000..45b0375d2d530ff15a9d5df7126d872dc709e010 GIT binary patch literal 3821 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 a2=w+L0l%fLUS?Qk1TZtUHfk{RO86Il|EzNW 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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 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