diff --git a/scripts/manifests/dev.json b/scripts/manifests/dev.json index e52ce48..5be16e2 100644 --- a/scripts/manifests/dev.json +++ b/scripts/manifests/dev.json @@ -2,7 +2,7 @@ "manifest_version": 2, "name": "__MSG_extensionName__", "short_name": "__MSG_extensionNameShort__", - "version": "0.7", + "version": "0.8", "author": "rugk", "description": "__MSG_extensionDescription__", diff --git a/scripts/manifests/firefox.json b/scripts/manifests/firefox.json index 71706bb..72c82c4 100644 --- a/scripts/manifests/firefox.json +++ b/scripts/manifests/firefox.json @@ -2,7 +2,7 @@ "manifest_version": 2, "name": "__MSG_extensionName__", "short_name": "__MSG_extensionNameShort__", - "version": "0.7", + "version": "0.8", "author": "rugk", "description": "__MSG_extensionDescription__", diff --git a/src/background/background.html b/src/background/background.html index d867bb4..f1aaa48 100644 --- a/src/background/background.html +++ b/src/background/background.html @@ -6,7 +6,8 @@ See also https://discourse.mozilla.org/t/using-es6-modules-in-background-scripts - + + diff --git a/src/background/background.js b/src/background/background.js index 22114ca..93e8579 100644 --- a/src/background/background.js +++ b/src/background/background.js @@ -1 +1,2 @@ +import "./modules/InstallUpgrade.js"; import "./modules/AutoRemoteFollow.js"; diff --git a/src/background/modules/Detect/Mastodon.js b/src/background/modules/Detect/Mastodon.js index 8f72d1a..7ed982e 100644 --- a/src/background/modules/Detect/Mastodon.js +++ b/src/background/modules/Detect/Mastodon.js @@ -57,9 +57,8 @@ export function getTootUrl(url) { // if this is your local server, you can obviously directly redirect and // use the local toot ID/URL - const fromStaticOwnServer = browser.storage.sync.get("insertHandle").then((handleObject) => { - const ownMastodon = Mastodon.splitUserHandle(handleObject.insertHandle); - if (mastodonServer !== ownMastodon.server) { + const fromStaticOwnServer = browser.storage.sync.get("mastodonServer").then((handleObject) => { + if (mastodonServer !== handleObject.mastodonServer.server) { return Promise.reject(new Error("is not own server URL")); } diff --git a/src/background/modules/InstallUpgrade.js b/src/background/modules/InstallUpgrade.js new file mode 100644 index 0000000..8263461 --- /dev/null +++ b/src/background/modules/InstallUpgrade.js @@ -0,0 +1,60 @@ +/** + * Upgrades user data on installation of new updates. + * + * Attention: Currently you must not include this script asyncronously. See + * https://bugzilla.mozilla.org/show_bug.cgi?id=1506464 for details. + * + * @module InstallUpgrade + */ + +import * as Mastodon from "/common/modules/Mastodon.js"; + +/** + * Checks whether an upgrade is needed. + * + * @see {@link https://developer.mozilla.org/docs/Mozilla/Add-ons/WebExtensions/API/runtime/onInstalled} + * @private + * @param {Object} details + * @returns {void} + */ +async function handleInstalled(details) { + // only trigger for usual addon updates + if (details.reason !== "update") { + return; + } + + switch (details.previousVersion) { + case "0.7": { + console.log(`Doing upgrade from ${details.previousVersion}.`, details); + + const ownMastodonSplit = await browser.storage.sync.get("insertHandle").then((handleObject) => { + return Mastodon.splitUserHandle(handleObject.insertHandle); + }); + + await browser.storage.sync.set({ + mastodonUsername: ownMastodonSplit.username, + mastodonServer: ownMastodonSplit.server, + }); + + await browser.storage.sync.remove("insertHandle"); + + console.log("Data upgrade successful.", await browser.storage.sync.get()); + + break; + } + default: + console.log(`Addon upgrade from ${details.previousVersion}. No data upgrade needed.`, details); + } +} + +/** + * Inits module. + * + * @private + * @returns {void} + */ +function init() { + browser.runtime.onInstalled.addListener(handleInstalled); +} + +init() diff --git a/src/background/modules/MastodonRedirect.js b/src/background/modules/MastodonRedirect.js index 526359b..cb88d69 100644 --- a/src/background/modules/MastodonRedirect.js +++ b/src/background/modules/MastodonRedirect.js @@ -16,9 +16,12 @@ import * as NetworkTools from "./NetworkTools.js"; * @returns {Promise} */ async function triggerRemoteAction(uri) { - const handleObject = await browser.storage.sync.get("insertHandle"); - - const ownMastodon = Mastodon.splitUserHandle(handleObject.insertHandle); + // get and assemble Mastodon object + const handleObject = await browser.storage.sync.get(["mastodonUsername", "mastodonServer"]); + const ownMastodon = { + username: handleObject.mastodonUsername, + server: handleObject.mastodonServer, + }; // skip the subscribe/interact API if it is not needed, because it is your // own server diff --git a/src/background/modules/NetworkTools.js b/src/background/modules/NetworkTools.js index b42ede9..143f26a 100644 --- a/src/background/modules/NetworkTools.js +++ b/src/background/modules/NetworkTools.js @@ -1,3 +1,9 @@ +/** + * Provides small wrapper functions around browser APIs for listening to web requests. + * + * @module NetworkTools + */ + /** * Listen to a web request of this URL. * diff --git a/src/common/modules/Mastodon.js b/src/common/modules/Mastodon.js index 6c83653..2d5820b 100644 --- a/src/common/modules/Mastodon.js +++ b/src/common/modules/Mastodon.js @@ -4,8 +4,8 @@ * @module common/modules/Mastodon */ -// https://regex101.com/r/dlnnSq/1 -const MASTODON_HANDLE_SPLIT = /^@?(.+)@(.+)$/; +// https://regex101.com/r/dlnnSq/2 +const MASTODON_HANDLE_SPLIT = /^@?([^@ ]+)@([^@ ]+)$/; /** * Do a webfinger request for Mastodon account at server. @@ -26,7 +26,7 @@ function getWebfinger(mastodonServer, mastodonHandle) { /** * Splits a Mastodon handle to return the username and server URL. * - * @function + * @public * @param {string} mastodonHandle * @throws {TypeError} * @returns {{username: string, server: string}} username/server @@ -44,6 +44,42 @@ export function splitUserHandle(mastodonHandle) { }; } +/** + * Concatenates a Mastodon username and server to return the full user handle. + * + * In this definition a Mastodon handle is not prefixed with '@', i.e. not + * '@user@example.com', but just 'user@example.com'. The function will remove + * the 'at' char if it is prepended to the username. + * + * @public + * @param {string} username + * @param {string} server + * @throws {TypeError} if the format is invalid + * @returns {string} + */ +export function concatUserHandle(username, server) { + // remove prefixed @ if needed + if (username && username.startsWith("@")) { + username = username.substring(1); + } + + // sanity checks + if (!server) { + throw new TypeError("Server must not be empty."); + } + if (!username) { + throw new TypeError("Username must not be empty."); + } + + const mastodonHandle = `${username}@${server}`; + + if (!MASTODON_HANDLE_SPLIT.test(mastodonHandle)) { + throw new TypeError("Username or server has invalid format."); + } + + return mastodonHandle; +} + /** * Return the API endpoint (path) that handles these remote follows/interactions. * diff --git a/src/manifest.json b/src/manifest.json index 1ff1f89..85148b6 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 2, "name": "__MSG_extensionName__", "short_name": "__MSG_extensionNameShort__", - "version": "0.7", + "version": "0.8", "author": "rugk", "description": "__MSG_extensionDescription__", diff --git a/src/options/options.js b/src/options/options.js index 5839797..9d6bb63 100644 --- a/src/options/options.js +++ b/src/options/options.js @@ -1,13 +1,17 @@ -"use strict"; +import * as Mastodon from "/common/modules/Mastodon.js"; const insertHandle = document.getElementById("insertHandle"); insertHandle.addEventListener("input", () => { - browser.storage.sync.set({ - "insertHandle": insertHandle.value + const ownMastodonSplit = Mastodon.splitUserHandle(insertHandle.value); + + return browser.storage.sync.set({ + mastodonUsername: ownMastodonSplit.username, + mastodonServer: ownMastodonSplit.server, }); }); -browser.storage.sync.get("insertHandle").then((handleObject) => { - insertHandle.value = handleObject.insertHandle; +browser.storage.sync.get(["mastodonUsername", "mastodonServer"]).then((handleObject) => { + const mastodonHandle = Mastodon.concatUserHandle(handleObject.mastodonUsername, handleObject.mastodonServer); + insertHandle.value = mastodonHandle; }); diff --git a/src/tests/CommonMastodon.test.js b/src/tests/CommonMastodon.test.js index 3e18069..bef841b 100644 --- a/src/tests/CommonMastodon.test.js +++ b/src/tests/CommonMastodon.test.js @@ -36,6 +36,26 @@ describe("common module: Mastodon", function () { }, 'does not handle correctly: "FakeUser123Name@fake.example"'); }); + it("correctly splits servers without TLD (LAN adresses, IPs, etc.)", function () { + chai.assert.deepEqual(Mastodon.splitUserHandle("username@tldFreeServer"), { + username: "username", + server: "tldFreeServer" + }, 'does not handle correctly: "username@tldFreeServer"'); + chai.assert.deepEqual(Mastodon.splitUserHandle("@username@tldFreeServer"), { + username: "username", + server: "tldFreeServer" + }, 'does not handle correctly: "@username@tldFreeServer"'); + + chai.assert.deepEqual(Mastodon.splitUserHandle("username@127.0.0.1"), { + username: "username", + server: "127.0.0.1" + }, 'does not handle correctly: "username@127.0.0.1"'); + chai.assert.deepEqual(Mastodon.splitUserHandle("@username@127.0.0.1"), { + username: "username", + server: "127.0.0.1" + }, 'does not handle correctly: "@username@127.0.0.1"'); + }); + it("correctly splits handles with Emoji", function () { chai.assert.deepEqual(Mastodon.splitUserHandle("thinking🤔user@whatis🤔.great.app"), { username: "thinking🤔user", @@ -69,6 +89,248 @@ describe("common module: Mastodon", function () { }); }); + describe("concatUserHandle()", function () { + it('correctly concatenates user handle', function () { + chai.assert.strictEqual( + Mastodon.concatUserHandle("rugk_testing", "mastodon.social"), + "rugk_testing@mastodon.social", + 'does not handle correctly: "rugk_testing@mastodon.social"' + ); + chai.assert.strictEqual( + Mastodon.concatUserHandle("rugk", "social.wiuwiu.de"), + "rugk@social.wiuwiu.de", + 'does not handle correctly: "rugk@social.wiuwiu.de"' + ); + chai.assert.strictEqual( + Mastodon.concatUserHandle("FakeUser123Name", "fake.example"), + "FakeUser123Name@fake.example", + 'does not handle correctly: "FakeUser123Name@fake.example"' + ); + }); + + it("correctly concatenates handles with @-prefix", function () { + chai.assert.strictEqual( + Mastodon.concatUserHandle("@rugk_testing", "mastodon.social"), + "rugk_testing@mastodon.social", + 'does not handle correctly: "rugk_testing@mastodon.social"' + ); + chai.assert.strictEqual( + Mastodon.concatUserHandle("@rugk", "social.wiuwiu.de"), + "rugk@social.wiuwiu.de", + 'does not handle correctly: "rugk@social.wiuwiu.de"' + ); + chai.assert.strictEqual( + Mastodon.concatUserHandle("@FakeUser123Name", "fake.example"), + "FakeUser123Name@fake.example", + 'does not handle correctly: "FakeUser123Name@fake.example"' + ); + }); + + it("correctly concatenates servers without TLD (LAN adresses, IPs, etc.)", function () { + chai.assert.strictEqual( + Mastodon.concatUserHandle("username", "tldFreeServer"), + "username@tldFreeServer", + 'does not handle correctly: "username@tldFreeServer"' + ); + chai.assert.strictEqual( + Mastodon.concatUserHandle("@username", "tldFreeServer"), + "username@tldFreeServer", + 'does not handle correctly: "@username@tldFreeServer"' + ); + + chai.assert.strictEqual( + Mastodon.concatUserHandle("username", "127.0.0.1"), + "username@127.0.0.1", + 'does not handle correctly: "username@127.0.0.1"' + ); + chai.assert.strictEqual( + Mastodon.concatUserHandle("@username", "127.0.0.1"), + "username@127.0.0.1", + 'does not handle correctly: "@username@127.0.0.1"' + ); + }); + + it("correctly concatenates handles with Emoji", function () { + chai.assert.strictEqual( + Mastodon.concatUserHandle("thinking🤔user", "whatis🤔.great.app"), + "thinking🤔user@whatis🤔.great.app", + 'does not handle correctly: "thinking🤔user@whatis🤔.great.app"' + ); + chai.assert.strictEqual( + Mastodon.concatUserHandle("@thinking🤔user", "whatis🤔.great.app"), + "thinking🤔user@whatis🤔.great.app", + 'does not handle correctly: "@thinking🤔user@whatis🤔.great.app"' + ); + }); + + it("correctly rejects empty variables", function () { + // empty string + chai.assert.throws( + Mastodon.concatUserHandle.bind(null, "rugk_testing@mastodon.social", ""), + TypeError, + "Server must not be empty.", + 'does not handle correctly: concatUserHandle("rugk_testing@mastodon.social", "")' + ); + chai.assert.throws( + Mastodon.concatUserHandle.bind(null, "", "rugk_testing@mastodon.social"), + TypeError, + "Username must not be empty.", + 'does not handle correctly: concatUserHandle("", "rugk_testing@mastodon.social")' + ); + + chai.assert.throws( + Mastodon.concatUserHandle.bind(null, "@rugk_testing@mastodon.social", ""), + TypeError, + "Server must not be empty.", + 'does not handle correctly: concatUserHandle("@rugk_testing@mastodon.social", "")' + ); + chai.assert.throws( + Mastodon.concatUserHandle.bind(null, "", "@rugk_testing@mastodon.social"), + TypeError, + "Username must not be empty.", + 'does not handle correctly: concatUserHandle("", "@rugk_testing@mastodon.social")' + ); + + // null + chai.assert.throws( + Mastodon.concatUserHandle.bind(null, "rugk_testing@mastodon.social", null), + TypeError, + "Server must not be empty.", + 'does not handle correctly: concatUserHandle("rugk_testing@mastodon.social", null)' + ); + chai.assert.throws( + Mastodon.concatUserHandle.bind(null, null, "rugk_testing@mastodon.social"), + TypeError, + "Username must not be empty.", + 'does not handle correctly: concatUserHandle(null, "rugk_testing@mastodon.social")' + ); + + // undefined + chai.assert.throws( + Mastodon.concatUserHandle.bind(null, "rugk_testing@mastodon.social", undefined), + TypeError, + "Server must not be empty.", + 'does not handle correctly: concatUserHandle("rugk_testing@mastodon.social", undefined)' + ); + chai.assert.throws( + Mastodon.concatUserHandle.bind(null, undefined, "rugk_testing@mastodon.social"), + TypeError, + "Username must not be empty.", + 'does not handle correctly: concatUserHandle(undefined, "rugk_testing@mastodon.social")' + ); + }); + + it("correctly rejects @ (ats) in usernames and servers", function () { + chai.assert.throws( + Mastodon.concatUserHandle.bind(null, "usernameWith@InIt", "mastodon.example"), + TypeError, + "Username or server has invalid format.", + 'does not handle correctly: concatUserHandle("usernameWith@InIt", "mastodon.example")' + ); + chai.assert.throws( + Mastodon.concatUserHandle.bind(null, "@usernameWith@InIt", "mastodon.example"), + TypeError, + "Username or server has invalid format.", + 'does not handle correctly: concatUserHandle("@usernameWith@InIt", "mastodon.example")' + ); + + chai.assert.throws( + Mastodon.concatUserHandle.bind(null, "usernameWithAtAtEnd@", "mastodon.example"), + TypeError, + "Username or server has invalid format.", + 'does not handle correctly: concatUserHandle("usernameWithAtAtEnd@", "mastodon.example")' + ); + chai.assert.throws( + Mastodon.concatUserHandle.bind(null, "@usernameWithAtAtEnd@", "mastodon.example"), + TypeError, + "Username or server has invalid format.", + 'does not handle correctly: concatUserHandle("@usernameWithAtAtEnd@", "mastodon.example")' + ); + + chai.assert.throws( + Mastodon.concatUserHandle.bind(null, "username", "@mastodon.example"), + TypeError, + "Username or server has invalid format.", + 'does not handle correctly: concatUserHandle("username", "@mastodon.example")' + ); + chai.assert.throws( + Mastodon.concatUserHandle.bind(null, "@username", "@mastodon.example"), + TypeError, + "Username or server has invalid format.", + 'does not handle correctly: concatUserHandle("@username", "@mastodon.example")' + ); + + chai.assert.throws( + Mastodon.concatUserHandle.bind(null, "username", "mastodon@example"), + TypeError, + "Username or server has invalid format.", + 'does not handle correctly: concatUserHandle("username@", "mastodon@example")' + ); + chai.assert.throws( + Mastodon.concatUserHandle.bind(null, "@username", "mastodon@example"), + TypeError, + "Username or server has invalid format.", + 'does not handle correctly: concatUserHandle("@username", "mastodon@example")' + ); + + chai.assert.throws( + Mastodon.concatUserHandle.bind(null, "@ats@Everywhere@", "@mastodon@example@"), + TypeError, + "Username or server has invalid format.", + 'does not handle correctly: concatUserHandle("@ats@Everywhere@", "@mastodon@example@")' + ); + }); + + it("correctly rejects spaces in usernames and servers", function () { + chai.assert.throws( + Mastodon.concatUserHandle.bind(null, "usernameWith InIt", "mastodon.example"), + TypeError, + "Username or server has invalid format.", + 'does not handle correctly: concatUserHandle("usernameWith InIt", "mastodon.example")' + ); + chai.assert.throws( + Mastodon.concatUserHandle.bind(null, "@usernameWith InIt", "mastodon.example"), + TypeError, + "Username or server has invalid format.", + 'does not handle correctly: concatUserHandle("@usernameWith InIt", "mastodon.example")' + ); + + chai.assert.throws( + Mastodon.concatUserHandle.bind(null, "usernameWithSpaceAtEnd ", "mastodon.example"), + TypeError, + "Username or server has invalid format.", + 'does not handle correctly: concatUserHandle("usernameWithSpaceAtEnd ", "mastodon.example")' + ); + + chai.assert.throws( + Mastodon.concatUserHandle.bind(null, "username", " mastodon.example"), + TypeError, + "Username or server has invalid format.", + 'does not handle correctly: concatUserHandle("username", " mastodon.example")' + ); + chai.assert.throws( + Mastodon.concatUserHandle.bind(null, "@username", " mastodon.example"), + TypeError, + "Username or server has invalid format.", + 'does not handle correctly: concatUserHandle("@username", " mastodon.example")' + ); + + chai.assert.throws( + Mastodon.concatUserHandle.bind(null, "username", "mastodon example"), + TypeError, + "Username or server has invalid format.", + 'does not handle correctly: concatUserHandle("username ", "mastodon example")' + ); + + chai.assert.throws( + Mastodon.concatUserHandle.bind(null, " ats Everywhere ", " mastodon example "), + TypeError, + "Username or server has invalid format.", + 'does not handle correctly: concatUserHandle(" ats Everywhere ", " mastodon example ")' + ); + }); + }); + describe("getSubscribeApiUrl()", function () { beforeEach(function() { // TODO: mock settings API to prevent savings