This commit is contained in:
Wirlaburla 2023-01-09 18:43:25 -06:00
commit bd986b2fdf
25 changed files with 1173 additions and 0 deletions

6
README.md Normal file
View File

@ -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.

43
action.css Normal file
View File

@ -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;
}

37
action.html Normal file
View File

@ -0,0 +1,37 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<link rel="stylesheet" type="text/css" href="action_browser.css" />
<link rel="stylesheet" type="text/css" href="action.css" />
</head>
<body>
<div class="panel">
<div id="update" class="panel-section">
<div id="update_message"></div>
<div>
<button data-message="update_changelog_button"></button>
<button data-message="donate_button"></button>
</div>
</div>
<div id="warning" class="panel-section" data-message="update_warning"></div>
<div id="error" class="panel-section" data-message="update_error"></div>
<div class="panel-section panel-section-list">
<div id="nobrowsers" class="panel-section" data-message="popup_nobrowsers"></div>
<ul id="browsers">
<template>
<li class="panel-list-item">
<img class="icon" width="16" height="16" />
<div class="name text"></div>
</li>
</template>
</ul>
</div>
<div class="panel-section panel-section-footer">
<div class="panel-section-footer-button" data-message="popup_options_button"></div>
</div>
</div>
<script type="text/javascript" src="common.js"></script>
<script type="text/javascript" src="action.js"></script>
</body>
</html>

117
action.js Normal file
View File

@ -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);
}

75
action_browser.css Normal file
View File

@ -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);
}

33
background.js Normal file
View File

@ -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);

89
common.js Normal file
View File

@ -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;
}

BIN
images/128.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

BIN
images/16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

BIN
images/24.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

BIN
images/32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

BIN
images/48.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

BIN
images/64.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

BIN
images/96.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

41
installed.css Normal file
View File

@ -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;
}

25
installed.html Normal file
View File

@ -0,0 +1,25 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<link rel="stylesheet" type="text/css" href="action_browser.css" />
<link rel="stylesheet" type="text/css" href="installed.css" />
</head>
<body>
<div class="panel">
<div>
<img src="images/openwith.svg" width="64" height="64" />
</div>
<div>
<div id="header" data-message="popup_installed_header2"></div>
<div data-message="popup_installed_text"></div>
<div><a data-href="webextension_info_url" data-message="webextension_info_label"></a></div>
<div id="button">
<button data-message="popup_options_button"></button>
</div>
</div>
</div>
<script type="text/javascript" src="common.js"></script>
<script type="text/javascript" src="installed.js"></script>
</body>
</html>

8
installed.js Normal file
View File

@ -0,0 +1,8 @@
/* globals chrome, get_strings */
get_strings();
document.querySelector('button').onclick = function() {
chrome.runtime.openOptionsPage(function() {
window.close();
});
};

38
manifest.json Normal file
View File

@ -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"
}
}

110
native/download_with_linux.py Executable file
View File

@ -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.')

152
native/download_with_mac.py Executable file
View File

@ -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.')

View File

@ -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
}

View File

@ -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.')

12
options.css Normal file
View File

@ -0,0 +1,12 @@
body {
display: flex;
justify-content: center;
}
#group > div {
display: flex;
gap: 4px;
margin: 4px;
}
#test_results, #exeinput {
flex-grow: 1;
}

27
options.html Normal file
View File

@ -0,0 +1,27 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title data-message="options_title">Download With Options</title>
<link rel="stylesheet" type="text/css" href="options.css" />
<link rel="icon" type="image/png" href="images/16.png" />
</head>
<body>
<center id=group>
<div><img width=24 height=24 src='images/24.png'> <b>Download With</b><div style='flex-grow: 1;'></div>Options</div>
<hr>
<b>Native Test</b>
<div id=test>
<div id=test_results>Click button to test.</div>
<button id=test_button data-message="test_button">Test</button>
</div>
<b>Executable</b>
<div id=execute>
<input id=exeinput type=text></div>
</div>
<div>Use '%s' in place of the URL.</div>
</center>
<script type="text/javascript" src="common.js"></script>
<script type="text/javascript" src="options.js"></script>
</body>
</html>

35
options.js Normal file
View File

@ -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;
});