Improve parsing of sgnl:// hrefs
This commit is contained in:
parent
90bf0f4eea
commit
de7a69dee9
5 changed files with 231 additions and 27 deletions
32
main.js
32
main.js
|
@ -5,7 +5,6 @@ const url = require('url');
|
|||
const os = require('os');
|
||||
const fs = require('fs-extra');
|
||||
const crypto = require('crypto');
|
||||
const qs = require('qs');
|
||||
const normalizePath = require('normalize-path');
|
||||
const fg = require('fast-glob');
|
||||
const PQueue = require('p-queue').default;
|
||||
|
@ -95,6 +94,7 @@ const {
|
|||
installWebHandler,
|
||||
} = require('./app/protocol_filter');
|
||||
const { installPermissionsHandler } = require('./app/permissions');
|
||||
const { isSgnlHref, parseSgnlHref } = require('./ts/util/sgnlHref');
|
||||
|
||||
let appStartInitialSpellcheckSetting = true;
|
||||
|
||||
|
@ -149,10 +149,10 @@ if (!process.mas) {
|
|||
|
||||
showWindow();
|
||||
}
|
||||
// Are they trying to open a sgnl link?
|
||||
const incomingUrl = getIncomingUrl(argv);
|
||||
if (incomingUrl) {
|
||||
handleSgnlLink(incomingUrl);
|
||||
// Are they trying to open a sgnl:// href?
|
||||
const incomingHref = getIncomingHref(argv);
|
||||
if (incomingHref) {
|
||||
handleSgnlHref(incomingHref);
|
||||
}
|
||||
// Handled
|
||||
return true;
|
||||
|
@ -470,9 +470,9 @@ async function readyForUpdates() {
|
|||
isReadyForUpdates = true;
|
||||
|
||||
// First, install requested sticker pack
|
||||
const incomingUrl = getIncomingUrl(process.argv);
|
||||
if (incomingUrl) {
|
||||
handleSgnlLink(incomingUrl);
|
||||
const incomingHref = getIncomingHref(process.argv);
|
||||
if (incomingHref) {
|
||||
handleSgnlHref(incomingHref);
|
||||
}
|
||||
|
||||
// Second, start checking for app updates
|
||||
|
@ -1138,9 +1138,9 @@ app.setAsDefaultProtocolClient('sgnl');
|
|||
app.on('will-finish-launching', () => {
|
||||
// open-url must be set from within will-finish-launching for macOS
|
||||
// https://stackoverflow.com/a/43949291
|
||||
app.on('open-url', (event, incomingUrl) => {
|
||||
app.on('open-url', (event, incomingHref) => {
|
||||
event.preventDefault();
|
||||
handleSgnlLink(incomingUrl);
|
||||
handleSgnlHref(incomingHref);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -1374,16 +1374,16 @@ function installSettingsSetter(name) {
|
|||
});
|
||||
}
|
||||
|
||||
function getIncomingUrl(argv) {
|
||||
return argv.find(arg => arg.startsWith('sgnl://'));
|
||||
function getIncomingHref(argv) {
|
||||
return argv.find(arg => isSgnlHref(arg, logger));
|
||||
}
|
||||
|
||||
function handleSgnlLink(incomingUrl) {
|
||||
const { host: command, query } = url.parse(incomingUrl);
|
||||
const args = qs.parse(query);
|
||||
function handleSgnlHref(incomingHref) {
|
||||
const { command, args } = parseSgnlHref(incomingHref, logger);
|
||||
if (command === 'addstickers' && mainWindow && mainWindow.webContents) {
|
||||
console.log('Opening sticker pack from sgnl protocol link');
|
||||
const { pack_id: packId, pack_key: packKeyHex } = args;
|
||||
const packId = args.get('pack_id');
|
||||
const packKeyHex = args.get('pack_key');
|
||||
const packKey = Buffer.from(packKeyHex, 'hex').toString('base64');
|
||||
mainWindow.webContents.send('show-sticker-pack', { packId, packKey });
|
||||
} else {
|
||||
|
|
|
@ -107,7 +107,6 @@
|
|||
"pify": "3.0.0",
|
||||
"protobufjs": "6.8.6",
|
||||
"proxy-agent": "3.1.1",
|
||||
"qs": "6.5.1",
|
||||
"react": "16.8.3",
|
||||
"react-blurhash": "0.1.2",
|
||||
"react-contextmenu": "2.11.0",
|
||||
|
@ -176,7 +175,6 @@
|
|||
"@types/node-fetch": "2.5.7",
|
||||
"@types/normalize-path": "3.0.0",
|
||||
"@types/pify": "3.0.2",
|
||||
"@types/qs": "6.5.1",
|
||||
"@types/react": "16.8.5",
|
||||
"@types/react-dom": "16.8.2",
|
||||
"@types/react-measure": "2.0.5",
|
||||
|
|
173
ts/test/util/sgnlHref_test.ts
Normal file
173
ts/test/util/sgnlHref_test.ts
Normal file
|
@ -0,0 +1,173 @@
|
|||
import { assert } from 'chai';
|
||||
import Sinon from 'sinon';
|
||||
import { LoggerType } from '../../types/Logging';
|
||||
|
||||
import { isSgnlHref, parseSgnlHref } from '../../util/sgnlHref';
|
||||
|
||||
function shouldNeverBeCalled() {
|
||||
assert.fail('This should never be called');
|
||||
}
|
||||
|
||||
const explodingLogger: LoggerType = {
|
||||
fatal: shouldNeverBeCalled,
|
||||
error: shouldNeverBeCalled,
|
||||
warn: shouldNeverBeCalled,
|
||||
info: shouldNeverBeCalled,
|
||||
debug: shouldNeverBeCalled,
|
||||
trace: shouldNeverBeCalled,
|
||||
};
|
||||
|
||||
describe('sgnlHref', () => {
|
||||
describe('isSgnlHref', () => {
|
||||
it('returns false for non-strings', () => {
|
||||
const logger = {
|
||||
...explodingLogger,
|
||||
warn: Sinon.spy(),
|
||||
};
|
||||
|
||||
const castToString = (value: unknown): string => value as string;
|
||||
|
||||
assert.isFalse(isSgnlHref(castToString(undefined), logger));
|
||||
assert.isFalse(isSgnlHref(castToString(null), logger));
|
||||
assert.isFalse(isSgnlHref(castToString(123), logger));
|
||||
|
||||
Sinon.assert.calledThrice(logger.warn);
|
||||
});
|
||||
|
||||
it('returns false for invalid URLs', () => {
|
||||
assert.isFalse(isSgnlHref('', explodingLogger));
|
||||
assert.isFalse(isSgnlHref('sgnl', explodingLogger));
|
||||
assert.isFalse(isSgnlHref('sgnl://::', explodingLogger));
|
||||
});
|
||||
|
||||
it('returns false if the protocol is not "sgnl:"', () => {
|
||||
assert.isFalse(isSgnlHref('https://example', explodingLogger));
|
||||
assert.isFalse(
|
||||
isSgnlHref(
|
||||
'https://signal.art/addstickers/?pack_id=abc',
|
||||
explodingLogger
|
||||
)
|
||||
);
|
||||
assert.isFalse(isSgnlHref('signal://example', explodingLogger));
|
||||
});
|
||||
|
||||
it('returns true if the protocol is "sgnl:"', () => {
|
||||
assert.isTrue(isSgnlHref('sgnl://', explodingLogger));
|
||||
assert.isTrue(isSgnlHref('sgnl://example', explodingLogger));
|
||||
assert.isTrue(isSgnlHref('sgnl://example.com', explodingLogger));
|
||||
assert.isTrue(isSgnlHref('SGNL://example', explodingLogger));
|
||||
assert.isTrue(isSgnlHref('sgnl://example?foo=bar', explodingLogger));
|
||||
assert.isTrue(isSgnlHref('sgnl://example/', explodingLogger));
|
||||
assert.isTrue(isSgnlHref('sgnl://example#', explodingLogger));
|
||||
|
||||
assert.isTrue(isSgnlHref('sgnl:foo', explodingLogger));
|
||||
|
||||
assert.isTrue(isSgnlHref('sgnl://user:pass@example', explodingLogger));
|
||||
assert.isTrue(isSgnlHref('sgnl://example.com:1234', explodingLogger));
|
||||
assert.isTrue(
|
||||
isSgnlHref('sgnl://example.com/extra/path/data', explodingLogger)
|
||||
);
|
||||
assert.isTrue(
|
||||
isSgnlHref('sgnl://example/?foo=bar#hash', explodingLogger)
|
||||
);
|
||||
});
|
||||
|
||||
it('accepts URL objects', () => {
|
||||
const invalid = new URL('https://example.com');
|
||||
assert.isFalse(isSgnlHref(invalid, explodingLogger));
|
||||
const valid = new URL('sgnl://example');
|
||||
assert.isTrue(isSgnlHref(valid, explodingLogger));
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseSgnlHref', () => {
|
||||
it('returns a null command for invalid URLs', () => {
|
||||
['', 'sgnl', 'https://example/?foo=bar'].forEach(href => {
|
||||
assert.deepEqual(parseSgnlHref(href, explodingLogger), {
|
||||
command: null,
|
||||
args: new Map<never, never>(),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('parses the command for URLs with no arguments', () => {
|
||||
[
|
||||
'sgnl://foo',
|
||||
'sgnl://foo/',
|
||||
'sgnl://foo?',
|
||||
'SGNL://foo?',
|
||||
'sgnl://user:pass@foo',
|
||||
'sgnl://foo/path/data#hash-data',
|
||||
].forEach(href => {
|
||||
assert.deepEqual(parseSgnlHref(href, explodingLogger), {
|
||||
command: 'foo',
|
||||
args: new Map<string, string>(),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("parses a command's arguments", () => {
|
||||
assert.deepEqual(
|
||||
parseSgnlHref(
|
||||
'sgnl://Foo?bar=baz&qux=Quux&num=123&empty=&encoded=hello%20world',
|
||||
explodingLogger
|
||||
),
|
||||
{
|
||||
command: 'Foo',
|
||||
args: new Map([
|
||||
['bar', 'baz'],
|
||||
['qux', 'Quux'],
|
||||
['num', '123'],
|
||||
['empty', ''],
|
||||
['encoded', 'hello world'],
|
||||
]),
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('treats the port as part of the command', () => {
|
||||
assert.propertyVal(
|
||||
parseSgnlHref('sgnl://foo:1234', explodingLogger),
|
||||
'command',
|
||||
'foo:1234'
|
||||
);
|
||||
});
|
||||
|
||||
it('ignores duplicate query parameters', () => {
|
||||
assert.deepPropertyVal(
|
||||
parseSgnlHref('sgnl://x?foo=bar&foo=totally-ignored', explodingLogger),
|
||||
'args',
|
||||
new Map([['foo', 'bar']])
|
||||
);
|
||||
});
|
||||
|
||||
it('ignores other parts of the URL', () => {
|
||||
[
|
||||
'sgnl://foo?bar=baz',
|
||||
'sgnl://foo/?bar=baz',
|
||||
'sgnl://foo/lots/of/path?bar=baz',
|
||||
'sgnl://foo?bar=baz#hash',
|
||||
'sgnl://user:pass@foo?bar=baz',
|
||||
].forEach(href => {
|
||||
assert.deepEqual(parseSgnlHref(href, explodingLogger), {
|
||||
command: 'foo',
|
||||
args: new Map([['bar', 'baz']]),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("doesn't do anything fancy with arrays or objects in the query string", () => {
|
||||
// The `qs` module does things like this, which we don't want.
|
||||
assert.deepPropertyVal(
|
||||
parseSgnlHref('sgnl://x?foo[]=bar&foo[]=baz', explodingLogger),
|
||||
'args',
|
||||
new Map([['foo[]', 'bar']])
|
||||
);
|
||||
assert.deepPropertyVal(
|
||||
parseSgnlHref('sgnl://x?foo[bar][baz]=foobarbaz', explodingLogger),
|
||||
'args',
|
||||
new Map([['foo[bar][baz]', 'foobarbaz']])
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
42
ts/util/sgnlHref.ts
Normal file
42
ts/util/sgnlHref.ts
Normal file
|
@ -0,0 +1,42 @@
|
|||
import { LoggerType } from '../types/Logging';
|
||||
|
||||
function parseUrl(value: unknown, logger: LoggerType): null | URL {
|
||||
if (value instanceof URL) {
|
||||
return value;
|
||||
} else if (typeof value === 'string') {
|
||||
try {
|
||||
return new URL(value);
|
||||
} catch (err) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
logger.warn('Tried to parse a sgnl:// URL but got an unexpected type');
|
||||
return null;
|
||||
}
|
||||
|
||||
export function isSgnlHref(value: string | URL, logger: LoggerType): boolean {
|
||||
const url = parseUrl(value, logger);
|
||||
return url !== null && url.protocol === 'sgnl:';
|
||||
}
|
||||
|
||||
type ParsedSgnlHref =
|
||||
| { command: null; args: Map<never, never> }
|
||||
| { command: string; args: Map<string, string> };
|
||||
export function parseSgnlHref(
|
||||
href: string,
|
||||
logger: LoggerType
|
||||
): ParsedSgnlHref {
|
||||
const url = parseUrl(href, logger);
|
||||
if (!url || !isSgnlHref(url, logger)) {
|
||||
return { command: null, args: new Map<never, never>() };
|
||||
}
|
||||
|
||||
const args = new Map<string, string>();
|
||||
url.searchParams.forEach((value, key) => {
|
||||
if (!args.has(key)) {
|
||||
args.set(key, value);
|
||||
}
|
||||
});
|
||||
|
||||
return { command: url.host, args };
|
||||
}
|
|
@ -2339,11 +2339,6 @@
|
|||
resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.2.tgz#690a1475b84f2a884fd07cd797c00f5f31356ea8"
|
||||
integrity sha512-ce5d3q03Ex0sy4R14722Rmt6MT07Ua+k4FwDfdcToYJcMKNtRVQvJ6JCAPdAmAnbRb6CsX6aYb9m96NGod9uTw==
|
||||
|
||||
"@types/qs@6.5.1":
|
||||
version "6.5.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.5.1.tgz#a38f69c62528d56ba7bd1f91335a8004988d72f7"
|
||||
integrity sha512-mNhVdZHdtKHMMxbqzNK3RzkBcN1cux3AvuCYGTvjEIQT2uheH3eCAyYsbMbh2Bq8nXkeOWs1kyDiF7geWRFQ4Q==
|
||||
|
||||
"@types/range-parser@*":
|
||||
version "1.2.3"
|
||||
resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.3.tgz#7ee330ba7caafb98090bece86a5ee44115904c2c"
|
||||
|
@ -12310,10 +12305,6 @@ qs@5.2.0:
|
|||
version "5.2.0"
|
||||
resolved "https://registry.yarnpkg.com/qs/-/qs-5.2.0.tgz#a9f31142af468cb72b25b30136ba2456834916be"
|
||||
|
||||
qs@6.5.1:
|
||||
version "6.5.1"
|
||||
resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.1.tgz#349cdf6eef89ec45c12d7d5eb3fc0c870343a6d8"
|
||||
|
||||
qs@6.7.0:
|
||||
version "6.7.0"
|
||||
resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc"
|
||||
|
|
Loading…
Reference in a new issue