commit bd986b2fdfa7e9fdd4154399cf9051f87d8b9318 Author: Wirlaburla Date: Mon Jan 9 18:43:25 2023 -0600 release\ 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 0000000..9487cda Binary files /dev/null and b/images/128.png differ diff --git a/images/16.png b/images/16.png new file mode 100644 index 0000000..3341ed3 Binary files /dev/null and b/images/16.png differ diff --git a/images/24.png b/images/24.png new file mode 100644 index 0000000..bf130c7 Binary files /dev/null and b/images/24.png differ diff --git a/images/32.png b/images/32.png new file mode 100644 index 0000000..3b0ae57 Binary files /dev/null and b/images/32.png differ diff --git a/images/48.png b/images/48.png new file mode 100644 index 0000000..ab26eaa Binary files /dev/null and b/images/48.png differ diff --git a/images/64.png b/images/64.png new file mode 100644 index 0000000..45b0375 Binary files /dev/null and b/images/64.png differ diff --git a/images/96.png b/images/96.png new file mode 100644 index 0000000..b5e84dc Binary files /dev/null and b/images/96.png differ 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