diff --git a/ext/webextension/src/browser_action/main_popup.js b/ext/webextension/src/browser_action/main_popup.js index c53856c..a4a129a 100644 --- a/ext/webextension/src/browser_action/main_popup.js +++ b/ext/webextension/src/browser_action/main_popup.js @@ -19,7 +19,7 @@ import {SiteStore} from "../lib/sitestore.js"; import {Site} from "../lib/sites.js"; -import {defer, copy_to_clipboard} from "../lib/utils.js"; +import {defer, copy_to_clipboard, regexpEscape} from "../lib/utils.js"; import {parseUri} from "../lib/uritools.js"; import config from "../lib/config.js"; import {ui} from "./ui.js"; @@ -241,21 +241,54 @@ function updateUIForDomainSettings(combined) } function extractDomainFromUrl(url) { - if (!url || url.startsWith('about:') - || url.startsWith('resource:') - || url.startsWith('moz-extension:') - || url.startsWith('chrome-extension:') - || url.startsWith('chrome:')) - url = ''; - let domain_parts = parseUri(url).domain.split("."); - let significant_parts = 2; - const common_slds = ['co','com','gov','govt','net','org','edu','priv','ac']; - let second_level_domain = domain_parts[domain_parts.length-2].toLowerCase(); - if (domain_parts.length > 2 && common_slds.includes(second_level_domain)) - significant_parts = 3; - - let significant_domain = domain_parts.slice(-significant_parts) - return [domain_parts, significant_domain]; + if (!url) + throw "extractDomainFromUrl: No url provided!"; + + var urlParsed = parseUri(url); + { + let protocolsIgnored = ["about", "resource", "moz-extension", "chrome-extension", "chrome", "edge", "brave"]; + if (protocolsIgnored.includes(urlParsed.protocol)) + throw "extractDomainFromUrl: Invalid url protocol provided"; + } + + let rxDomainMatchers = [ + /([^\.]+\.[^\.]+)$/i, // Default matcher matches two-part domain names (.* pattern) i.e. github.com + ]; + + config.treat_as_same_site.split('\n').forEach(function(pattern) { + pattern = pattern.trim(); + if (pattern.length == 0) return; + if (!pattern.startsWith('.')) return; + if (pattern.endsWith('.')) return; + //FIXME: More strict domain character parsing + // (should actually be config done at entry time) + pattern = '*' + pattern; + pattern = regexpEscape(pattern); + pattern = pattern.replace(/\\\*/g, '[^\.]+'); + rxDomainMatchers.push(new RegExp(pattern, 'i')); + }); + + let domainMatched = null; + + rxDomainMatchers.forEach(function(rx) { + let rxResult = rx.exec(urlParsed.domain); + if (!rxResult) return; + + if (domainMatched == null) + return domainMatched = rxResult[0]; + + // Select domain with most parts that match + let domainMatchedParts = domainMatched.split('.'); + let domainThisParts = rxResult[0].split('.'); + + if (domainThisParts.length > domainMatchedParts.length) + domainMatched = rxResult[0]; + }); + + if (domainMatched == null) + throw 'extractDomainFromUrl: Could not match any sites! Check your configuration settings!'; + + return [urlParsed.domain.split('.'), domainMatched.split('.')]; } function showSessionSetup() { @@ -301,7 +334,17 @@ function popup(masterkey) { } window.addEventListener('load', function () { - config.get(['username', 'key_id', 'defaulttype', 'pass_to_clipboard', 'pass_store', 'passwdtimeout', 'use_sync', 'defaultname']) + config.get([ + 'username', + 'key_id', + 'defaulttype', + 'pass_to_clipboard', + 'pass_store', + 'passwdtimeout', + 'use_sync', + 'defaultname', + 'treat_as_same_site', + ]) .then(v=>{ return runtimeSendMessage({action: 'masterkey_get', use_pass_store: !!v.pass_store}); }) diff --git a/ext/webextension/src/css/mpwd.css b/ext/webextension/src/css/mpwd.css index 291fed9..9fbf6b3 100644 --- a/ext/webextension/src/css/mpwd.css +++ b/ext/webextension/src/css/mpwd.css @@ -87,7 +87,11 @@ button:focus, input:focus, mp-combobox:focus-within { outline-style: auto; } -input, select, mp-combobox { +input, +select, +mp-combobox, +textarea, +no-op { background: #1b1d23; color: #d7dae0; height: 2em; @@ -198,7 +202,8 @@ button#siteconfig_show { } #siteconfig > input, #siteconfig > select, -#siteconfig > option { +#siteconfig > option, +no-op { font-size: 1em; font-weight: normal; width: 6em; @@ -302,7 +307,9 @@ button#siteconfig_show { .configitem > input, .configitem > select, -.configitem > option { +.configitem > option, +.configitem > textarea, +no-op { margin-top:1.5em; margin-left:-8em; font-size: 1em; @@ -316,6 +323,25 @@ button#siteconfig_show { margin-top:0.2em; } +.configitem > textarea#treat_as_same_site { + width: 11.5em; + height: 13em; + font-size: 0.7em; + margin-left: -11.5em; + margin-top: 2em; + white-space: nowrap; +} + +/* FIXME: I don't understand how the left alignment of inputs works here, + using negative margins and wrapping - so I just manually margin-left + by eye to line up - however it would be better to align this p + the same way as the input it is annotating */ +.configitem > textarea#treat_as_same_site + p { + font-size: 0.6em; + margin-left: 6.7em; + line-height: normal; +} + #stored_sites { width:100%; text-align: center; diff --git a/ext/webextension/src/lib/config.js b/ext/webextension/src/lib/config.js index feabf60..18e13fa 100644 --- a/ext/webextension/src/lib/config.js +++ b/ext/webextension/src/lib/config.js @@ -53,6 +53,9 @@ class Config { get passwdtimeout() { if (typeof this._cache.passwdtimeout === 'undefined') throw new Error("need get(['passwdtimeout'])"); else return this._cache.passwdtimeout; } + get treat_as_same_site() { if (typeof this._cache.treat_as_same_site === 'undefined') + throw new Error("need get(['treat_as_same_site'])"); + else return this._cache.treat_as_same_site; } get use_sync() { if (typeof this._cache.use_sync === 'undefined') throw new Error("need get(['use_sync'])"); else return this._cache.use_sync; } @@ -137,7 +140,7 @@ class Config { if (lst.includes('username')) result.username = result.username || ''; if (lst.includes('pass_store')) result.pass_store = !!result.pass_store; if (lst.includes('passwdtimeout')) result.passwdtimeout = isNaN(result.passwdtimeout) ? -1 : result.passwdtimeout; - + if (lst.includes('treat_as_same_site')) result.treat_as_same_site = result.treat_as_same_site || [ '.ac.*', '.co.*', '.com.*', '.edu.*', '.geek.*', '.gov.*', '.govt.*', '.net.*', '.org.*', '.school.*' ].join('\n'); Object.assign(this._cache, result); return singlekey ? result[lst[0]] : result; diff --git a/ext/webextension/src/lib/utils.js b/ext/webextension/src/lib/utils.js index 5d8727d..cae4407 100644 --- a/ext/webextension/src/lib/utils.js +++ b/ext/webextension/src/lib/utils.js @@ -46,3 +46,8 @@ export function copy_to_clipboard(mimetype, data) { document.execCommand("Copy", false, null); document.oncopy=null; } + +export function regexpEscape(string) { + // https://stackoverflow.com/a/6969486 + return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string +} diff --git a/ext/webextension/src/options/globaloptions.js b/ext/webextension/src/options/globaloptions.js index 1ad9dfd..5ae22b1 100644 --- a/ext/webextension/src/options/globaloptions.js +++ b/ext/webextension/src/options/globaloptions.js @@ -58,6 +58,9 @@ document.querySelector('#auto_submit_username').addEventListener('change', funct document.querySelector('#pass_store').addEventListener('change', function() { config.set({pass_store: this.checked}); }); +document.querySelector('#treat_as_same_site').addEventListener('change', function() { + config.set({treat_as_same_site: this.value}); +}); document.querySelector('#use_sync').addEventListener('change', async function() { let oldstore = (this.checked?chrome.storage.local:chrome.storage.sync); let newstore = (!this.checked?chrome.storage.local:chrome.storage.sync); @@ -150,14 +153,17 @@ document.querySelector('#use_sync').addEventListener('change', async function() }); window.addEventListener('load', function() { - config.get(['defaulttype', - 'defaultname', - 'passwdtimeout', - 'pass_to_clipboard', - 'auto_submit_pass', - 'auto_submit_username', - 'pass_store', - 'use_sync']) + config.get([ + 'defaulttype', + 'defaultname', + 'passwdtimeout', + 'pass_to_clipboard', + 'auto_submit_pass', + 'auto_submit_username', + 'pass_store', + 'treat_as_same_site', + 'use_sync', + ]) .then(data => { data = Object.assign({defaulttype: 'l', passwdtimeout: 0, pass_to_clipboard: true, defaultname: '', @@ -170,6 +176,7 @@ window.addEventListener('load', function() { document.querySelector('#auto_submit_pass').checked = data.auto_submit_pass; document.querySelector('#auto_submit_username').checked = data.auto_submit_username; document.querySelector('#pass_store').checked = data.pass_store; + document.querySelector('#treat_as_same_site').value = data.treat_as_same_site; document.querySelector('#use_sync').checked = data.use_sync; }); }); diff --git a/ext/webextension/src/options/index.html b/ext/webextension/src/options/index.html index 78985e4..cd9d8d0 100644 --- a/ext/webextension/src/options/index.html +++ b/ext/webextension/src/options/index.html @@ -34,59 +34,73 @@

Global settings

-
- - -
-
- - -
-
- - -
+
+ + +
+
+ + +
+
+ + +
+
+ + +

+ - Enter overrides above, one per line
+ - lines must start with a . (period)
+ - lines that cannot be parsed are ignored
+ - * is wildcard, will match anything except . (period)
+ - Example:
+      .net.* will treat x.net.nz as a site,
+      instead of ignoring x and using net.nz as the site +

+
+
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
diff --git a/ext/webextension/src/options/options.html b/ext/webextension/src/options/options.html index f6a652a..4346173 100644 --- a/ext/webextension/src/options/options.html +++ b/ext/webextension/src/options/options.html @@ -8,7 +8,6 @@ div.item { break-inside: avoid; width:100%; - height:3em; display:flex; justify-content: space-between; align-items: center; @@ -26,7 +25,9 @@ input, select, -option { +option, +textarea, +no-op { display:block; border: 0; width: 14em; @@ -36,6 +37,11 @@ input[type=checkbox] { width:1em; } +#treat_as_same_site { + height: 15em; + white-space: nowrap; + overflow: auto; +} @@ -71,6 +77,11 @@ +
+ + + +