diff options
Diffstat (limited to 'src/ui')
-rw-r--r-- | src/ui/Prompts.js | 101 | ||||
-rw-r--r-- | src/ui/options.css | 187 | ||||
-rw-r--r-- | src/ui/options.html | 125 | ||||
-rw-r--r-- | src/ui/options.js | 220 | ||||
-rw-r--r-- | src/ui/popup.css | 235 | ||||
-rw-r--r-- | src/ui/popup.html | 41 | ||||
-rw-r--r-- | src/ui/popup.js | 249 | ||||
-rw-r--r-- | src/ui/prompt.css | 91 | ||||
-rw-r--r-- | src/ui/prompt.html | 32 | ||||
-rw-r--r-- | src/ui/prompt.js | 91 | ||||
-rw-r--r-- | src/ui/resize_hack.js | 15 | ||||
-rw-r--r-- | src/ui/siteInfo.html | 5 | ||||
-rw-r--r-- | src/ui/siteInfo.js | 20 | ||||
-rw-r--r-- | src/ui/toolbar.js | 117 | ||||
-rw-r--r-- | src/ui/ui-hc.css | 63 | ||||
-rw-r--r-- | src/ui/ui.css | 391 | ||||
-rw-r--r-- | src/ui/ui.js | 661 | ||||
-rw-r--r-- | src/ui/whirlpool.css | 45 |
18 files changed, 2689 insertions, 0 deletions
diff --git a/src/ui/Prompts.js b/src/ui/Prompts.js new file mode 100644 index 0000000..03ea9ee --- /dev/null +++ b/src/ui/Prompts.js @@ -0,0 +1,101 @@ +var Prompts = (() => { + + + var promptData; + var backlog = []; + class WindowManager { + async open(data) { + promptData = data; + this.close(); + this.currentWindow = await browser.windows.create({ + url: browser.extension.getURL("ui/prompt.html"), + type: "panel", + allowScriptsToClose: true, + // titlePreface: "NoScript ", + width: data.features.width, + height: data.features.height, + }); + } + async close() { + if (this.currentWindow) { + try { + await browser.windows.remove(this.currentWindow.id); + } catch (e) { + debug(e); + } + this.currentWindow = null; + } + } + + async focus() { + if (this.currentWindow) { + try { + await browser.windows.update(this.currentWindow.id, + { + focused: true, + } + ); + } catch (e) { + error(e, "Focusing popup window"); + } + } + } + } + + var winMan = new WindowManager(); + var Prompts = { + DEFAULTS: { + title: "", + message: "Proceed?", + options: [], + checks: [], + buttons: [_("Ok"), _("Cancel")], + multiple: "close", // or "queue", or "focus" + width: 400, + height: 300, + }, + async prompt(features) { + features = Object.assign({}, this.DEFAULTS, features || {}); + return new Promise((resolve, reject) => { + let data = { + features, + result: { + button: -1, + checks: [], + option: null, + }, + done() { + this.done = () => {}; + winMan.close(); + resolve(this.result); + if (backlog.length) { + winMan.open(backlog.shift()); + } else { + promptData = null; + } + } + }; + if (promptData) { + backlog.push(data); + switch(promptData.features.multiple) { + case "focus": + winMan.focus(); + case "queue": + break; + default: + promptData.done(); + } + } else { + winMan.open(data); + } + }); + }, + + get promptData() { + return promptData; + } + } + + return Prompts; + +})(); diff --git a/src/ui/options.css b/src/ui/options.css new file mode 100644 index 0000000..f7db24b --- /dev/null +++ b/src/ui/options.css @@ -0,0 +1,187 @@ + +/* @import url("chrome://browser/content/extension.css"); */ +body { + background: #eee url("/img/noscript-options.png") no-repeat fixed top right; + background-size: 8em; + padding: 0 2em 0 0; + margin: 0.5em 0.5em 0.5em 0.5em; +} +.mobile body { + background-size: 4em; + padding-right: 0; +} + + +#header { + display: flex; + flex-flow: column; + padding: 0; + margin: 0 6em 0 0; + text-align: right; +} +#header h1 { + color: #048; + text-shadow: 0.06em 0.06em 0.06em rgba(0,0,0,.5); + font-size: 2em; + padding: 0; + margin: 0; + text-align: right; +} +#version { + color: #048; + font-size: 0.75em; + padding: 0; + margin: 0 0 0.5em; + display: block; + text-align: right; +} + +.buttons { + display: flex; + flex-flow: row wrap; + justify-content: flex-end; + width: 100%; + text-align: right; +} + +#sect-general { + display: flex; + flex-direction: column; + justify-content: space-around; + font-size: 1em; +} + +#sect-general label, #sect-general button, #sect-general span { + white-space: nowrap; +} + +.opt-group { + display: flex; + flex-flow: row wrap; + justify-content: flex-start; + border-bottom: 1px solid rgba(255, 255, 255, .5); + padding: .5em 0; +} + +.opt-group:last-child { + border-bottom: none; + margin-bottom: .5em; +} + +section form, section fieldset { + margin: .5em 0; +} + +fieldset:disabled { + opacity: .5; +} + +.opt-group > span { + margin: 0 .5em; +} + +.sect-sites form { + display: flex; + align-items: baseline; + flex-wrap: wrap; + justify-content: space-between; +} + +.sect-sites form > label { + white-space: nowrap; +} +#newsite { + flex: 2 2; +} + +#policy { + display: block; + margin-top: .5em; + min-height: 20em; + width: 90%; +} +.hide, div.debug { + display: none; +} + +body.debug div.debug { + display: initial; +} + +.error { + background: #ff8; + color: red; +} + +#policy-error { + background: red; + color: #ff8; + padding: 0; + margin: 0; + font-weight: bold; +} + +input, button { + font-size: 1em; +} + +button.add { + font-weight: bold; +} + +input[type="file"] { + display: none; +} + +.opt-group { + padding: 0.5em 0; +} +#xssFaq { + padding: 0.5em 1em; +} +#clearclick-options { + display: none; +} + + +.flextabs__tab { + /* shift all tabs to appear before content */ + order: -1; + /* let tabs scale to fit multiple on each row */ + width: auto; + margin: 0; +} +.flextabs__content--active { + /* ignore states activated for multi (accordion) toggle view */ + display: none; +} +.flextabs__content--active--last { + /* show the last activated item */ + display: block; +} + +.flextabs__content, .flextabs__toggle[aria-expanded="true"] { + background-color: rgba(200, 200, 200, .5) !important; + border: 0 solid #888; +} + +.flextabs__toggle { + -moz-appearance: none; + border-width: 0 1px 0 0 !important; + margin: 0 4px 0 0; + background: #ccc; + outline-width: 1px 0 0 0 !important; +} + + + +.flextabs__content { + border-width: 0 1px 1px 0; + border-radius: 0 .5em 0 0; + padding: .5em; +} + +.flextabs__toggle { + border-radius: .2em .2em 0 0; + padding: .2em .4em; +} diff --git a/src/ui/options.html b/src/ui/options.html new file mode 100644 index 0000000..433a1bd --- /dev/null +++ b/src/ui/options.html @@ -0,0 +1,125 @@ +<!DOCTYPE html> +<html> +<head> +<meta name="viewport" content="width=device-width,initial-scale=1"> +<title>NoScript Settings</title> +<meta charset="utf-8"> +<link rel="icon" href="/img/noscript-options.png"> +<link rel="stylesheet" href="/lib/flextabs.css" /> +<link rel="stylesheet" href="options.css" /> +<link rel="stylesheet" href="whirlpool.css" /> +<script src="/lib/include.js"></script> +<script src="/lib/log.js"></script> +<script src="/lib/flextabs.js"></script> +<script src="/common/locale.js"></script> +<script src="/ui/ui.js"></script> +</head> +<body> +<div id="header"> +<h1 > +NoScript Options +</h1> +<div> +<span id="version"></span> +</div> + +<div class="buttons"> +<span><input id="file-import" type="file"/></span> +<button id="btn-import" accesskey="__MSG_Import_accesskey__">__MSG_Import__</button> +<button id="btn-export" accesskey="__MSG_Export_accesskey__">__MSG_Export__</button> +<button id="btn-reset" accesskey="__MSG_Reset_accesskey__">__MSG_Reset__</button> +</div> +</div> +<section id="sect-io"> + +</section> + +<div id="main-tabs" class="flextabs"> + +<h3 class="flextabs__tab"><button class="flextabs__toggle">__MSG_SectionGeneral__</button></h3> +<div class="flextabs__content flextabs__content--active--last"> + <section id="sect-general"> + <div class="opt-group"> + <span id="global-opt"> + <input type="checkbox" id="opt-global"><label for="opt-global" id="lbl-global">__MSG_NoEnforcement__</label> + </span> + <span id="auto-opt"> + <input type="checkbox" class="enforcement_required" id="opt-auto"><label for="opt-auto" id="lbl-auto">__MSG_AutoAllowTopLevel__</label> + </span> + + </div> + + <fieldset class="enforcement_required"> + <legend accesskey="__MSG_CustomizePresets_accesskey__">__MSG_CustomizePresets__</legend> + <div id="presets"></div> + </fieldset> + </section> +</div> + +<h3 class="flextabs__tab"><button class="flextabs__toggle enforcement_required">__MSG_SectionSitePermissions__</button></h3> +<div class="flextabs__content"> + <section class="sect-sites"> + <form id="form-newsite" class="browser-style" > + <label id="newsite-label" for="newsite" accesskey="__MSG_WebAddress_accesskey__">__MSG_WebAddress__</label><input name="newsite" id="newsite" type="text" placeholder="[https://]noscript.net" + ><button class="add">+</button> + </form> + <div id="sites"> + <div class="cssload-container"> + <div class="cssload-whirlpool"></div> + </div> + </div> + </section> +</div> + +<h3 class="flextabs__tab appearance_tab"><button class="flextabs__toggle">__MSG_SectionAppearance__</button></h3> +<div class="flextabs__content appearance_tab"> + <div class="opt-group desktop"> + <span id="showCtxMenuItem-opt"> + <input type="checkbox" id="opt-showCtxMenuItem"> + <label for="opt-showCtxMenuItem" id="lbl-showCtxMenuItem">__MSG_ShowCtxMenuItem__</label> + </span> + </div> + <div class="opt-group desktop"> + <span id="showCountBadge-opt"> + <input type="checkbox" id="opt-showCountBadge"> + <label for="opt-showCountBadge" id="lbl-showCountBadge">__MSG_ShowCountBadge__</label> + </span> + </div> + <div class="opt-group"> + <span id="showFullAddresses-opt"> + <input type="checkbox" id="opt-showFullAddresses"> + <label for="opt-showFullAddresses" id="lbl-showFullAddresses">__MSG_ShowFullAddresses__</label> + </span> + </div> +</div> + +<h3 class="flextabs__tab"><button class="flextabs__toggle">__MSG_SectionAdvanced__</button></h3> +<div class="flextabs__content"> + <div class="opt-group"> + <span id="xss-opts"> + <input type="checkbox" id="opt-xss"><label for="opt-xss" id="lbl-xss">__MSG_OptFilterXGet__</label> + <span id="xssFaq">(<a href="https://noscript.net/faq#xss" title="https://noscript.net/faq#xss">__MSG_XssFaq__</a>)</span> + </span> + <button id="btn-delete-xss-choices" disabled>__MSG_XSS_clearUserChoices__</button> + </div> + <div id="clearclick-options" class="opt-group"> + <input type="checkbox" id="opt-clearclick"><label for="opt-clearclick" id="lbl-clearclick">ClearClick</label> + </div> + + <section id="debug" class="browser-style"> + <div class="opt-group"> + <span><input type="checkbox" id="opt-debug"><label id="label-debug" for="opt-debug">Debug</label></span> + </div> + <div id="debug-tools" class="debug browser-style"> + <label for="policy">Policy:</label> + <div id="policy-error"></div> + <textarea id="policy" class="browser-style"> + </textarea> + </div> + </section> +</div> +</div> +<script src="/lib/persistent-tabs.js"></script> +<script src="options.js"></script> +</body> +</html> diff --git a/src/ui/options.js b/src/ui/options.js new file mode 100644 index 0000000..79e6cb7 --- /dev/null +++ b/src/ui/options.js @@ -0,0 +1,220 @@ +'use strict'; +(async () => { + + await UI.init(); + + let policy = UI.policy; + + let version = browser.runtime.getManifest().version; + document.querySelector("#version").textContent = _("Version", version); + // simple general options + opt("global", o => { + if (o) { + policy.enforced = !o.checked; + UI.updateSettings({policy}); + } + let {enforced} = policy; + let disabled = !enforced; + for (let e of document.querySelectorAll(".enforcement_required")) { + e.disabled = disabled; + } + return disabled; + }); + + opt("auto", o => { + if (o) { + policy.autoAllowTop = o.checked; + UI.updateSettings({policy}); + } + return policy.autoAllowTop; + }); + + opt("xss"); + + { + let button = document.querySelector("#btn-reset"); + button.onclick = async () => { + if (confirm(_("reset_warning"))) { + policy = new Policy(); + await UI.updateSettings({policy, local: null, sync: null, xssUserChoices: {}}); + window.location.reload(); + } + } + + let fileInput = document.querySelector("#file-import"); + fileInput.onchange = () => { + let fr = new FileReader(); + fr.onload = async () => { + try { + await UI.importSettings(fr.result); + } catch (e) { + error(e, "Importing settings %s", fr.result); + } + location.reload(); + } + fr.readAsText(fileInput.files[0]); + } + + button = document.querySelector("#btn-import"); + button.onclick = () => fileInput.click(); + + document.querySelector("#btn-export").addEventListener("click", async e => { + let button = e.target; + button.disabled = true; + let settings = await UI.exportSettings(); + let f = document.createElement("iframe"); + f.srcdoc = `<a download="noscript_data.txt" target="_blank">NoScript Export</a>`; + f.style.position = "fixed"; + f.style.top = "-999px"; + f.style.height = "1px"; + f.onload = () => { + let w = f.contentWindow; + let a = w.document.querySelector("a"); + a.href = w.URL.createObjectURL(new w.Blob([settings], { + type: "text/plain" + })); + a.click(); + setTimeout(() => { + f.remove(); + button.disabled = false; + }, 1000); + + }; + document.body.appendChild(f); + }); + } + + { + let a = document.querySelector("#xssFaq a"); + a.onclick = e => { + e.preventDefault(); + browser.tabs.create({ + url: a.href + }); + } + let button = document.querySelector("#btn-delete-xss-choices"); + let choices = UI.xssUserChoices; + button.disabled = Object.keys(choices).length === 0; + button.onclick = () => { + UI.updateSettings({ + xssUserChoices: {} + }); + button.disabled = true + }; + + } + + opt("clearclick"); + opt("debug", "local", b => { + document.body.classList.toggle("debug", b); + if (b) updateRawPolicyEditor(); + }); + + // Appearance + + opt("showCountBadge", "local"); + opt("showCtxMenuItem", "local"); + opt("showFullAddresses", "local"); + + // PRESET CUSTOMIZER + { + let parent = document.getElementById("presets"); + let presetsUI = new UI.Sites(parent, + {"DEFAULT": true, "TRUSTED": true, "UNTRUSTED": true}); + + presetsUI.render([""]); + window.setTimeout(() => { + let def = parent.querySelector('input.preset[value="DEFAULT"]'); + def.checked = true; + def.click(); + }, 10); + } + + // SITES UI + let sitesUI = new UI.Sites(document.getElementById("sites")); + { + sitesUI.onChange = () => { + if (UI.local.debug) { + updateRawPolicyEditor(); + } + }; + let sites = policy.sites; + sitesUI.render(sites); + + let newSiteForm = document.querySelector("#form-newsite"); + let newSiteInput = newSiteForm.newsite; + let button = newSiteForm.querySelector("button"); + let canAdd = s => policy.get(s).siteMatch === null; + + let validate = () => { + let site = newSiteInput.value.trim(); + button.disabled = !(Sites.isValid(site) && canAdd(site)); + sitesUI.filterSites(site); + } + validate(); + newSiteInput.addEventListener("input", validate); + + newSiteForm.addEventListener("submit", e => { + e.preventDefault(); + e.stopPropagation(); + let site = newSiteInput.value.trim(); + let valid = Sites.isValid(site); + if (valid && canAdd(site)) { + policy.set(site, policy.TRUSTED); + UI.updateSettings({policy}); + newSiteInput.value = ""; + sitesUI.render(policy.sites); + sitesUI.highlight(site); + sitesUI.onChange(); + } + }, true); + } + + + // UTILITY FUNCTIONS + + async function opt(name, storage = "sync", onchange) { + let input = document.querySelector(`#opt-${name}`); + if (!input) { + debug("Checkbox not found %s", name); + return; + } + if (typeof storage === "function") { + input.onchange = e => storage(input); + input.checked = storage(null); + } else { + let obj = UI[storage]; + if (!obj) log(storage); + input.checked = obj[name]; + if (onchange) onchange(input.checked); + input.onchange = async () => { + obj[name] = input.checked; + await UI.updateSettings({[storage]: obj}); + if (onchange) onchange(obj[name]); + } + } + } + + + function updateRawPolicyEditor() { + if (!UI.local.debug) return; + + // RAW POLICY EDITING (debug only) + let policyEditor = document.getElementById("policy"); + policyEditor.value = JSON.stringify(policy.dry(true), null, 2); + if (!policyEditor.onchange) policyEditor.onchange = (e) => { + let ed = e.currentTarget + try { + policy = new Policy(JSON.parse(ed.value)); + UI.updateSettings({policy}); + sitesUI.render(policy.sites); + ed.className = ""; + document.getElementById("policy-error").textContent = ""; + } catch (e) { + error(e); + ed.className = "error"; + document.getElementById("policy-error").textContent = e.message; + } + } + } +})(); diff --git a/src/ui/popup.css b/src/ui/popup.css new file mode 100644 index 0000000..f8b31e2 --- /dev/null +++ b/src/ui/popup.css @@ -0,0 +1,235 @@ +body { + background: white; +} + +#top { + font-size: 1em; + position: relative; + margin: 0; + height: 2.4em; + min-width: 18.75em; + border-bottom: 0.06em solid #eee; + display: flex; + -moz-user-select: none; +} + + +#top a { + appearance: none !important; + -moz-appearance: none !important; + width: 2em; + height: 2em; + margin: 0.25em; + cursor: pointer; + font-size: 1em; + font-family: sans-serif; + font-weight: bold; + color: black; + background: transparent no-repeat center; + background-size: 100%; + transform: unset; + transition: all 0.3s; + border: none; + display: block; + + top: 0; + padding: 0; + text-align: left; + vertical-align: middle; + line-height: 1em; + +} + +#top > .spacer { + flex-grow: 1; + display: block; + cursor: pointer; +} + + + +#top > .hider.open ~ .spacer { + display: none; +} + +.hider { + background: #ccc; + box-shadow: inset 0 1px 3px #444; + border-radius: 1em 1em 0 0; + display: none; + position: relative; + margin: .25em 1.5em; + padding: 0; + + height: 2em; + overflow: hidden; + opacity: .5; +} + + + +.hider.open { + display: flex; + flex-grow: 1; + opacity: 1; + padding-left: 2em; +} +.hider:hover { + opacity: 1; +} +.hider:not(.open):not(.empty) { + display: block; + text-align: right; + line-height: 1em; + overflow: hidden; + width: 2em; +} + + +.reveal { + display: block; + padding: .3em; + margin: 0; +} + +.hider.open > .reveal { + display: none !important; +} + +.hider:not(.open) > :not(.reveal) { + display: none !important; +} + +.hider-label { + position: absolute; + z-index: 100; + top: .5em; + right: .5em; + color: #222; + text-align: right; + vertical-align: middle; + line-height: 100%; + font-size: 1em; + font-weight: bold; + pointer-events: none; + text-shadow: -2px 0 2px white, 2px 0 2px white; +} + +.hider-close { + -moz-appearance: none; + appearance: none; + color: black; + background: transparent; + padding: 0; + border-radius: .2em; + border: none; + position: absolute; + left: .2em; + top: 0; + font-size: 1em; + z-index: 100; + vertical-align: middle; + padding: .2em; +} + +.hider-close:hover, .reveal:hover { + color: white !important; + text-shadow: -2px 0 2px red, 2px 0 2px red; +} + +.hider > .icon { + opacity: .7; + margin: 0 .25em; + padding: 0; +} + +#top > a:hover { + transform: scale(1.2); +} + +#top a.icon { + text-indent: -500em; + color: transparent; +} + + +#top #revoke-temp { + background-image: url(/img/ui-revoke-temp64.png); +} +#top #temp-trust-page { + background-image: url(/img/ui-temp-all64.png); +} + +#top #enforce-tab { + background-image: url(/img/ui-tab-no64.png); +} +#top #enforce-tab[aria-pressed="true"] { + background-image: url(/img/ui-tab64.png); +} + +#top #enforce { + background-image: url(/img/ui-global-no64.png); +} +#top #enforce[aria-pressed="true"] { + background-image: url(/img/ui-global64.png); +} + +#top #options { + background-image: url(/img/noscript-options.png); +} +#top #close { + background-image: url(/img/ui-close64.png); +} + +#top #reload { + background-image: url(/img/ui-reload64.png); +} + +#sites { + margin: 0.5em 0.25em; +} + +#content { + text-align: center; +} +#buttons { + text-align: center; + margin: 0.5em; + display: flex; + justify-content: space-around; + +} +#buttons button { + flex-grow: 1; + margin: .5em 2em; +} + +.disabled .toggle.icon, .toggle.icon:disabled { + opacity: .2; + pointer-events: none; +} + +#message { + height: auto; + margin: .5em; + padding: .8em 0 0 2.5em; + background-size: 2em; + background-position: left top; + background-repeat: no-repeat; + min-height: 3em; + transition: height .5s; + font-size: 1.2em; + vertical-align: middle; +} +#message.hidden { + display: none; + height: 0; + min-height: 0; + overflow: hidden; +} +.warning { + background-image: url("/img/warning64.png"); +} +.error { + background-image: url("/img/error64.png"); +} diff --git a/src/ui/popup.html b/src/ui/popup.html new file mode 100644 index 0000000..d4bab21 --- /dev/null +++ b/src/ui/popup.html @@ -0,0 +1,41 @@ +<!DOCTYPE html> +<html> +<head> +<meta name="viewport" content="width=device-width,initial-scale=1"> +<meta charset="utf-8"> +<title>NoScript Settings</title> +<meta charset="utf-8"> +<link rel="stylesheet" type="text/css" href="popup.css" /> +<script src="/lib/include.js"></script> +<script src="/lib/log.js"></script> +<script src="/common/locale.js"></script> +<script src="/ui/ui.js"></script> + +</head> +<body> +<div id="main"> +<div id="top"> + <a aria-role="button" id="close" class="close icon">__MSG_Close__</a> + <a aria-role="button" id="reload" class="reload icon">__MSG_Reload__</a> + <a aria-role="button" id="options" class="options icon">__MSG_Options__</a> + <div class="hider"> + <a aria-role="button" class="reveal" title="__MSG_Reveal__">🡆</a> + <div class="hider-label">__MSG_Hider__</div> + <button class="hider-close">🗙</button> + </div> + <div class="spacer"></div> + <a aria-role="button" id="enforce" class="toggle icon"></a> + <a aria-role="button" id="enforce-tab" class="toggle icon"></a> + <a aria-role="button" id="temp-trust-page" class="toggle icon">__MSG_TempTrustPage__</a> + <a aria-role="button" id="revoke-temp" class="toggle icon">__MSG_RevokeTemp__</a> +</div> +<div id="message" class="hidden"></div> +<div id="content"></div> +<div id="sites"></div> +<div id="buttons"> + +</div> +</div> +<script src="popup.js"></script> +</body> +</html> diff --git a/src/ui/popup.js b/src/ui/popup.js new file mode 100644 index 0000000..3f84549 --- /dev/null +++ b/src/ui/popup.js @@ -0,0 +1,249 @@ +'use strict'; + +var sitesUI; + +addEventListener("unload", e => { + if (!UI.initialized) { + browser.runtime.sendMessage({ + type: "openStandalonePopup" + }); + } +}); + +(async () => { + + function showMessage(className, message) { + let el = document.getElementById("message"); + el.textContent = message; + el.className = className; + } + + try { + let tabId; + let pendingReload = false; + let isBrowserAction = true; + let optionsClosed = false; + let tab = (await browser.tabs.query({ + windowId: browser.windows ? + (await browser.windows.getLastFocused({windowTypes: ["normal"]})).id + : null, + active: true + }))[0]; + + if (!tab || tab.id === -1) { + log("No tab found to open the UI for"); + close(); + } + if (tab.url === document.URL) { + isBrowserAction = false; + try { + tabId = parseInt(document.URL.match(/#.*\btab(\d+)/)[1]); + } catch (e) { + close(); + } + addEventListener("blur", close); + } else { + tabId = tab.id; + } + + await UI.init(tabId); + + if (isBrowserAction) { + browser.tabs.onActivated.addListener(e => { + if (e.tabId !== tabId) close(); + }); + } + + await include("/ui/toolbar.js"); + { + let clickHandlers = { + "options": e => { + browser.runtime.openOptionsPage(); + close(); + }, + "close": close, + "reload": reload, + "temp-trust-page": e => sitesUI.tempTrustAll(), + "revoke-temp": e => { + UI.revokeTemp(); + close(); + } + }; + for (let [id, handler] of Object.entries(clickHandlers)) { + document.getElementById(id).onclick = handler; + } + } + { + let policy = UI.policy; + let pressed = policy.enforced; + let button = document.getElementById("enforce"); + button.setAttribute("aria-pressed", pressed); + button.textContent = button.title = _(pressed ? "NoEnforcement" : "Enforce"); + button.onclick = () => { + policy.enforced = !pressed; + UI.updateSettings({policy, reloadAffected: true}); + close(); + } + } + { + let pressed = !UI.unrestrictedTab; + let button = document.getElementById("enforce-tab"); + button.setAttribute("aria-pressed", pressed); + button.textContent = button.title = _(pressed ? "NoEnforcementForTab" : "EnforceForTab"); + if (UI.policy.enforced) { + button.onclick = () => { + UI.updateSettings({ + unrestrictedTab: pressed, + reloadAffected: true, + }); + close(); + } + } else { + button.disabled = true; + } + } + + + let mainFrame = UI.seen && UI.seen.find(thing => thing.request.type === "main_frame"); + debug("Seen: %o", UI.seen); + if (!mainFrame) { + + if (/^https?:/.test(tab.url) && !tab.url.startsWith("https://addons.mozilla.org/")) { + document.body.classList.add("disabled"); + showMessage("warning", _("freshInstallReload")); + let buttons = document.querySelector("#buttons"); + let b = document.createElement("button"); + b.textContent = _("OK"); + b.onclick = document.getElementById("reload").onclick = () => { + reload(); + close(); + } + buttons.appendChild(b); + b = document.createElement("button"); + b.textContent = _("Cancel"); + b.onclick = () => close(); + buttons.appendChild(b); + return; + } + showMessage("warning", _("privilegedPage")); + document.getElementById("temp-trust-page").disabled = true; + if (!UI.seen) return; + } + + let justDomains = !UI.local.showFullAddresses; + + sitesUI = new UI.Sites(document.getElementById("sites")); + + sitesUI.onChange = (row) => { + pendingReload = !row.temp2perm; + if (optionsClosed) return; + browser.tabs.query({url: browser.runtime.getManifest().options_ui.page }) + .then(tabs => { + browser.tabs.remove(tabs.map(t => t.id)); + }); + optionsClosed = true; + }; + initSitesUI(); + UI.onSettings = initSitesUI; + + + + function initSitesUI() { + pendingReload = false; + let { + typesMap + } = sitesUI; + typesMap.clear(); + let policySites = UI.policy.sites; + let domains = new Map(); + + function urlToLabel(url) { + let { + origin + } = url; + let match = policySites.match(url); + if (match) return match; + if (domains.has(origin)) { + if (justDomains) return domains.get(origin); + } else { + let domain = tld.getDomain(url.hostname); + domain = url.protocol === "https:" ? Sites.secureDomainKey(domain) : domain; + domains.set(origin, domain); + if (justDomains) return domain; + } + return origin; + } + let seen = UI.seen; + let parsedSeen = seen.map(thing => Object.assign({ + type: thing.policyType + }, Sites.parse(thing.request.url))) + .filter(parsed => parsed.url && parsed.url.origin !== "null"); + + let sitesSet = new Set( + parsedSeen.map(parsed => parsed.label = urlToLabel(parsed.url)) + ); + if (!justDomains) { + for (let domain of domains.values()) sitesSet.add(domain); + } + let sites = [...sitesSet]; + for (let parsed of parsedSeen) { + sites.filter(s => parsed.label === s || domains.get(parsed.url.origin) === s).forEach(m => { + let siteTypes = typesMap.get(m); + if (!siteTypes) typesMap.set(m, siteTypes = new Set()); + siteTypes.add(parsed.type); + }); + } + + sitesUI.mainUrl = new URL(mainFrame.request.url) + sitesUI.mainSite = urlToLabel(sitesUI.mainUrl); + sitesUI.mainDomain = tld.getDomain(sitesUI.mainUrl.hostname); + + sitesUI.render(sites); + } + + function reload() { + if (sitesUI) sitesUI.clear(); + browser.tabs.reload(tabId); + pendingReload = false; + } + + function close() { + if (isBrowserAction) { + window.close(); + } else { + //browser.windows.remove(tab.windowId); + browser.tabs.remove(tab.id); + } + } + + let { + onCompleted + } = browser.webNavigation; + + let loadSnapshot = sitesUI.snapshot; + let onCompletedListener = navigated => { + if (navigated.tabId === tabId) { + UI.pullSettings(); + } + }; + onCompleted.addListener(onCompletedListener, { + url: [{ + hostContains: sitesUI.mainDomain + }] + }); + addEventListener("unload", e => { + onCompleted.removeListener(onCompletedListener); + debug("pendingReload", pendingReload); + if (pendingReload) { + UI.updateSettings({ + policy: UI.policy, + reloadAffected: true, + }); + } + }, true); + } catch (e) { + error(e, "Can't open popup"); + close(); + } + +})(); diff --git a/src/ui/prompt.css b/src/ui/prompt.css new file mode 100644 index 0000000..9406f01 --- /dev/null +++ b/src/ui/prompt.css @@ -0,0 +1,91 @@ + +body { + bottom: 8px; + font-family: sans-serif; + font-size: 12px; + color: #222; +} + +#header { + text-align: left; + margin: 0; + line-height: 24px; + color: #048; + position: relative; + font-size: 24px; + z-index: 500; + padding: 8px; + display: block; + background: url(/img/icon96.png) no-repeat top right; + height: 96px; +} + +#title { + margin-right: 96px; + font-size: 24px; + position: absolute; + bottom: 0; + top: 0; +} + +#main { + background: linear-gradient(to bottom, #e4f5fc 0%,#bfe8f9 41%,#9fd8ef 90%,#2ab0ed 100%) no-repeat; + display: flex; + flex-direction: column; + align-items: center; + padding: 120px 16px 16px 16px; + top: 0; + left: 0; + right:0; + bottom: 0; + position: fixed; + justify-content: center; +} +#message { + flex-grow: 1; + width: 100%; + max-height: 300px; + padding: 8px; + text-align: center; + word-break: break-all; +} +#message.multiline { + overflow: auto; + font-size: 12px; + text-align: justify; + margin-bottom: 16px; + background: rgba(255,255,255,.5); +} +#message.multiline p { + margin: 1px; + padding: 0; +} +#options { + display: flex; + flex-grow: 2; + flex-direction: column; + text-align: left; + align-items:baseline; + justify-content: center; +} + + +#checks { + display: flex; + flex-direction: column; + flex-grow: 1; + text-align: left; +} + +#buttons { + width: 100%; + display: flex; + flex-grow: 0; + flex-direction: row; + align-items: center; + margin: 8px; + justify-content: space-around; +} +#buttons button { + min-width: 100px; +} diff --git a/src/ui/prompt.html b/src/ui/prompt.html new file mode 100644 index 0000000..902b375 --- /dev/null +++ b/src/ui/prompt.html @@ -0,0 +1,32 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8"> +<title></title> +<meta charset="utf-8"> +<link rel="stylesheet" type="text/css" href="prompt.css" /> +<script src="/lib/include.js"></script> +<script src="/lib/log.js"></script> +<script src="/common/locale.js"></script> +<script src="/ui/resize_hack.js"></script> +</head> +<body> +<div id="header"> +<h1 id="title"></h1> +</div> +<div id="main"> +<div id="message"> +</div> +<div id="options"> +<input type="radio"> +</div> +<div id="checks"> + <input type="checkbox"> +</div> +<div id="buttons"> + <button id="button0" type="submit">OK</button><button id="button1">Cancel</button> +</div> +</div> +<script src="prompt.js"></script> +</body> +</html> diff --git a/src/ui/prompt.js b/src/ui/prompt.js new file mode 100644 index 0000000..f19aac9 --- /dev/null +++ b/src/ui/prompt.js @@ -0,0 +1,91 @@ +(async () => { + window.bg = await browser.runtime.getBackgroundPage(); + ["Prompts"] + .forEach(p => window[p] = bg[p]); + let data = Prompts.promptData; + debug(data); + let {title, message, options, checks, buttons} = data.features; + + function labelFor(el, text) { + let label = document.createElement("label"); + label.setAttribute("for", el.id); + label.textContent = text; + return label; + } + + function createInput(container, {label, type, name, checked}, count) { + let input = document.createElement("input"); + input.type = type; + input.value = count; + input.name = name; + input.checked = checked; + input.id = `${name}-${count}`; + let sub = document.createElement("div"); + sub.appendChild(input); + sub.appendChild(labelFor(input, label)); + container.appendChild(sub); + } + + function createButton(container, label, count) { + let button = document.createElement("button"); + if (count === 0) button.type = "submit"; + button.id = `${button}-${count}`; + button.value = count; + button.textContent = label; + container.appendChild(button); + } + + function renderInputs(container, dataset, type, name) { + if (typeof container === "string") { + container = document.querySelector(container); + } + if (typeof dataset === "string") { + container.innerHTML = dataset; + return; + } + container.innerHTML = ""; + let count = 0; + if (dataset && dataset[Symbol.iterator]) { + let create = type === "button" ? createButton : createInput; + for (let data of dataset) { + data.type = type; + data.name = name; + create(container, data, count++); + } + } + } + if (title) { + document.title = title; + document.querySelector("#title").textContent = title; + } + if (message) { + let lines = message.split(/\n/); + let container = document.querySelector("#message"); + container.classList.toggle("multiline", lines.length > 1); + message.innerHTML = ""; + for (let l of lines) { + let p = document.createElement("p"); + p.textContent = l; + container.appendChild(p); + } + } + renderInputs("#options", options, "radio", "opt"); + renderInputs("#checks", checks, "checkbox", "flag"); + renderInputs("#buttons", buttons, "button", "button"); + addEventListener("unload", e => { + data.done(); + }); + + let buttonClicked = e => { + let {result} = data; + result.button = parseInt(e.currentTarget.value); + let option = document.querySelector('#options [type="radio"]:checked'); + result.option = option && parseInt(option.value); + result.checks = [...document.querySelectorAll('#checks [type="checkbox"]:checked')] + .map(c => parseInt(c.value)); + data.done(); + }; + for (let b of document.querySelectorAll("#buttons button")) { + b.addEventListener("click", buttonClicked); + } +})(); diff --git a/src/ui/resize_hack.js b/src/ui/resize_hack.js new file mode 100644 index 0000000..c981e28 --- /dev/null +++ b/src/ui/resize_hack.js @@ -0,0 +1,15 @@ +document.addEventListener("DOMContentLoaded", async e => { + // Fix for Fx57 bug where bundled page loaded using + // browser.windows.create won't show contents unless resized. + // See https://bugzilla.mozilla.org/show_bug.cgi?id=1402110 + let win = await browser.windows.getCurrent({populate: true}); + if (win.tabs[0].url === document.URL) { + debug("Resize hack"); + await browser.windows.update(win.id, { + width: win.width + 1 + }); + await browser.windows.update(win.id, { + width: win.width + }); + } +}); diff --git a/src/ui/siteInfo.html b/src/ui/siteInfo.html new file mode 100644 index 0000000..0cb24ec --- /dev/null +++ b/src/ui/siteInfo.html @@ -0,0 +1,5 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<script src="/lib/log.js"></script> +<script src="/lib/include.js"></script> +<script src="siteInfo.js"></script> diff --git a/src/ui/siteInfo.js b/src/ui/siteInfo.js new file mode 100644 index 0000000..708c983 --- /dev/null +++ b/src/ui/siteInfo.js @@ -0,0 +1,20 @@ +(async () => { + let [domain, tabId] = decodeURIComponent(location.hash.replace("#", "")).split(";"); + const BASE = "https://noscript.net"; + await include(['/lib/punycode.js', '/common/Storage.js']); + let {siteInfoConsent} = await Storage.get("sync", "siteInfoConsent"); + if (!siteInfoConsent) { + await include('/common/locale.js'); + siteInfoConsent = confirm(_("siteInfo_confirm", [domain, BASE])); + if (siteInfoConsent) { + await Storage.set("sync", {siteInfoConsent}); + } else { + let current = await browser.tabs.getCurrent(); + await browser.tabs.update(parseInt(tabId), {active: true}); + await browser.tabs.remove(current.id); + return; + } + } + let ace = punycode.toASCII(domain); + location.href = `${BASE}/about/${domain};${ace}`; +})(); diff --git a/src/ui/toolbar.js b/src/ui/toolbar.js new file mode 100644 index 0000000..d2a2f6e --- /dev/null +++ b/src/ui/toolbar.js @@ -0,0 +1,117 @@ +{ + let toolbar = document.getElementById("top"); + let spacer = toolbar.querySelector(".spacer"); + let hider = toolbar.querySelector(".hider"); + + if (UI.local.toolbarLayout) { + debug(uneval(UI.local.toolbarLayout)); + let {left, right, hidden} = UI.local.toolbarLayout; + for (let id of left) { + toolbar.insertBefore(document.getElementById(id), hider); + } + for (let id of right) { + toolbar.appendChild(document.getElementById(id)); + } + for (let id of hidden) { + hider.appendChild(document.getElementById(id)); + } + } + + for (let i of toolbar.querySelectorAll(".icon")) { + if (!i.title) i.title = i.textContent; + } + + function toggleHider(b) { + let cl = hider.classList; + cl.toggle("open", b); + cl.toggle("empty", !hider.querySelector(".icon")); + } + hider.querySelector(".hider-close").onclick = e => { + toggleHider(false); + }; + + toggleHider(false); + + let dnd = { + dragstart(ev) { + let d = ev.target; + if (hider.querySelectorAll(".icon").length) { + toggleHider(true); + } + + if (!d.classList.contains("icon")) { + ev.preventDefault(); + return; + } + d.style.opacity = ".5"; + let dt = ev.dataTransfer; + dt.setData("text/plain", d.id); + dt.dropEffect = "move"; + dt.setDragImage(d, 0, 0); + toggleHider(true); + }, + dragend(ev) { + ev.target.style.opacity = ""; + }, + dragover(ev) { + ev.preventDefault(); + }, + dragenter(ev) { + let t = ev.target; + }, + dragleave(ev) { + let t = ev.target; + }, + drop(ev) { + let t = ev.target; + let d = document.getElementById(ev.dataTransfer.getData("text/plain")); + switch(t) { + case hider: + t.appendChild(d); + break; + case toolbar: + t.insertBefore(d, ev.clientX < hider.offsetLeft ? hider : spacer.nextElementSibling); + break; + default: + t.parentNode.insertBefore(d, ev.clientX < (t.offsetLeft + t.offsetWidth) ? t : t.nextElementSibling); + } + + let left = [], right = []; + let side = left; + for (let el of document.querySelectorAll("#top > .icon, #top > .spacer")) { + if (el === spacer) { + side = right; + } else { + side.push(el.id); + } + } + UI.local.toolbarLayout = { + left, right, + hidden: Array.map(document.querySelectorAll("#top > .hider > .icon"), el => el.id), + }; + + debug("%o", UI.local); + UI.updateSettings({local: UI.local}); + }, + + click(ev) { + let el = ev.target; + if (el.parentNode === hider && el.classList.contains("icon")) { + ev.preventDefault(); + ev.stopPropagation(); + } else if (el === spacer || el.classList.contains("reveal")) { + toggleHider(true); + } + } + + }; + + + for (let [action, handler] of Object.entries(dnd)) { + toolbar.addEventListener(action, handler, true); + } + + for (let draggable of document.querySelectorAll("#top .icon")) { + draggable.setAttribute("draggable", "true"); + } +} diff --git a/src/ui/ui-hc.css b/src/ui/ui-hc.css new file mode 100644 index 0000000..fd70e2a --- /dev/null +++ b/src/ui/ui-hc.css @@ -0,0 +1,63 @@ +input { + transform: none !important; + width: auto !important; + position: static !important; +} + +input[type="radio"] { + -moz-appearance: radio !important; + padding-right: .2em !important; +} +input[type="checkbox"] { + -moz-appearance: checkbox !important; +} + + +button { + text-indent: 0 !important; +} + +label { + display: initial !important; + position: static !important; + transform: none !important; + opacity: 1 !important; + text-indent: 0 !Important; + position: static; + width: auto !important; + padding: 4px !important; +} + +span.preset { + display: block; + width: auto !important; + white-space: nowrap !important; +} + +input.temp { + position: static !important; + opacity: 1 !important; +} + +.full-address { + font-size: 130%; +} + +tr.site { + border-top: 1px solid #888; +} + +#top { + display:flex; + flex-flow: row; + justify-content: space-around; + +} +#top button { + position: static; + width: auto; +} +#top button.icon { + font-size: 12px !important; + font-family: arial sans-serif !important; +} diff --git a/src/ui/ui.css b/src/ui/ui.css new file mode 100644 index 0000000..55f9247 --- /dev/null +++ b/src/ui/ui.css @@ -0,0 +1,391 @@ + +body { + font-family: sans-serif; + font: -moz-use-system-font; + font-size: 12px; +} + +.mobile > body { + font-size: 4mm; + min-width: auto; +} + +.mobile .desktop { + display: none !important; +} + + @media (max-width: 100mm) { + body { + background-size: 4em !important; + padding-right: 0 !important; + } + + .presets { + width: 0; + } + + .presets input.preset { + min-width: 0 !important; + background-color: none !important; + margin-bottom: 0; + margin-top: 1mm; + font-weight: bold; + } + .presets input.temp { + position: static; + } + .presets label.preset { + font-size: 50%; + top: -1mm; + left: 0; + margin: 0; + padding: 0; + text-align: center; + text-shadow: 0 0 4px #ff8; + position: absolute; + overflow: visible; + } + + td.presets { + white-space: nowrap !important; + vertical-align: bottom; + } + .url { + white-space: wrap; + word-break: break-all; + font-size: 75%; + letter-spacing: -0.2mm; + + } + + } + +input[type="text"] { + border: 1px solid; +} +input[type="checkbox"] { + width: 1em; + height: 1em; +} + +.presets { + -moz-user-select: none; +} +.sites { + border: 0; + background: white; + border-collapse: collapse; + border-spacing: 0; + width: 100%; + overflow-y: auto; + +} +.sites tr, .sites td { + margin: 0; + padding: 0; + border: none; + font-size: 1em; +} +.sites > tr.site:hover, .sites > tr.sites:active { + background: #abf; +} +.sites > tr:nth-child(even) {background: #fff} +.sites > tr:nth-child(odd) {background: #eee} + +.site .url { + padding: 0 0 0 0.5em; + color: #ccc; + vertical-align: middle; +} +.site .url .protocol { display: none } + +.site .url .domain { cursor: help } + +[data-key="domain"] .full-address .host, +[data-key="domain"] .full-address .sub, +[data-key="domain"] .full-address .protocol, +[data-key="host"] .full-address span .protocol, +[data-key="host"] .full-address span .protocol, { + background-color: #afe; +} +[data-key="host"] .full-address span .protocol, +[data-key="domain"] .full-address span .host, +[data-key="domain"] .full-address span .protocol { + border: none; +} + + +.site .url[data-key="domain"] .domain, +.site .url[data-key="host"] .domain, +.site .url[data-key="host"] .sub, +.site .url[data-key="unsafe"] span { + color: #a00; +} + +.site .url[data-key="secure"] .domain, +.site .url[data-key="secure"] .sub, +.site .url[data-key="full"] span { + color: black; +} + +.site .url[data-key="full"] span, +.site .url[data-key="unsafe"] span { + display: initial; +} + +.site .url .domain { + font-weight: bold; +} + +input.https-only { + font-size: 1em; + -moz-appearance: none; + background: url(/img/ui-http64.png) no-repeat center; + background-size: 1.5em; + width: 1.5em; + height: 1.5em; + margin: 0 0 -0.13em 0.13em; + padding:0; + cursor: pointer; +} +input.https-only:checked { + background-image: url(/img/ui-https64.png); +} +label.https-only { + display: none; +} + +[data-preset="UNTRUSTED"] .https-only, [data-preset="DEFAULT"] .https-only { + visibility: hidden; +} + + +td.presets { + font-size: 1em; + white-space: nowrap; +} + +.mobile td.presets { + white-space: normal; +} + +span.preset { + position: relative; + display: inline-block; + top: 0.13em; + font-size: 1em; +} + +.preset label, .preset input, .preset button { + cursor: pointer; +} + +.presets input.preset { + font-size: 1em; + -moz-appearance: none; + background: url(/img/ui-no64.png) no-repeat center left; + background-size: 1.5em; + width: 1.5em; + height: 1.5em; + outline: 0; + opacity: .5; + margin: 0 .5em 0.13em .5em; +} + +input.preset:active, input.preset:focus, input.preset:hover { + background-color: #ff8; + border-radius: .5em; +} + +.presets input.preset:checked, #presets input.preset { + opacity: 1; + transform: none; + min-width: 9.38em; + background-color: #ddd; + border-radius: 0.5em; +} + +.presets input.preset:focus { + transform: none; +} +.sites input + label { + font-size: 1em; + line-height: 1.5em; + vertical-align: top; +} +.presets label.preset { + padding: 0; + letter-spacing: -0.06em; + width: 0em; + overflow: hidden; + display: none; + text-transform: uppercase; + color: #000; + opacity: .6; + position: absolute; + left: 0em; + padding-left: 2.5em; + + transition: 0.2s all; +} + +.presets input.preset[value^="T"] + label { + text-transform: none; +} + +.presets input.preset:checked + label, #presets .presets label { + opacity: 1; + width: 100%; + display: inline-block; +} + +button.options { + -moz-appearance: none; + border: none; + background: none transparent; + font-family: sans-serif; + font-weight: bold; + color: #048; + text-shadow: -0.06em -0.06em 0.06em #fff, 0.13em 0.13em 0.13em #000; + padding: 0; + margin: 0; +} + +.preset .options { + -moz-appearance: none; + + border: 0; + background: none; + font-size: 1em; + width: 1em; + height: 1em; + + opacity: 0; + position: absolute; + bottom: 0.88em; + left: 1.13em; + + pointer-events: none; + +} + +.preset:hover input.preset:checked ~ .options { + display: block; + opacity: 1; + bottom: 0.38em; + +} +input.preset[value="T_TRUSTED"] { + background-image: url(/img/ui-temp64.png); +} + +input.preset[value="TRUSTED"] { + background-image: url(/img/ui-yes64.png) +} +input.preset[value="UNTRUSTED"] { + background-image: url(/img/ui-black64.png) +} +input.preset[value="CUSTOM"] { + background-image: url(/img/ui-custom64.png) +} + +input.temp { + font-size: 1em; + -moz-appearance: none; + margin: 0; + padding: 0; + border: 0; + opacity: 0; + background: url(/img/ui-clock64.png) no-repeat center; + background-size: 60%; + width: 1.5em; + height: 1.5em; + transition: 0.2s all; + right: 0; + top: 0; + pointer-events: none; + position: absolute; +} + +input.temp + label { + display: none; +} + +input.preset:checked ~ input.temp { + opacity: .5; + right: .5em; + pointer-events: all; +} +.presets input.preset:checked ~ input.temp:checked { + opacity: 1 !important; + background-size: 100%; + +} + +.customizing input.preset:checked, #presets input.preset:checked, .customizer fieldset { + background-color: #ffb !important; + border-radius: 0.5em 0.5em 0 0; + margin: 0 0.06em 0.06em 0.06em; +} +.customizing input.preset:checked, #presets input.preset, #presets input.preset:checked { + margin: 0 1em -0.2em 1em; + border-radius: 0.5em 0.5em 0 0; +} + +.customizing input.preset:checked + label.preset { + padding-left: 3em; +} + +.customizing, .customizer { + background-color: #cca !important; +} + +.customizer div { + transition: 0.2s height; + padding: 0; + margin: 0; +} + +span.cap { + white-space: nowrap; + display: inline-block; +} + +.customizer.closed .customizer-controls { + height: 0; + overflow: hidden; +} + +span.cap { + padding: 0.5em; + font-weight: normal; +} + +span.cap.needed { + font-weight: bold; + background-color: #c88; +} + +fieldset { + border: 0; + padding: 1.5em 0.5em 0.5em 0.5em; + margin: 0; + position: relative; +} + +legend { + font-weight: bold; + display: inline; + position: absolute; + top: 0.25em; + left: 1em; + white-space: nowrap; +} +.customizer legend { + font-weight: bold; + font-size: 0.75em; +} + +#presets .https-only { + display: none; +} diff --git a/src/ui/ui.js b/src/ui/ui.js new file mode 100644 index 0000000..99943d7 --- /dev/null +++ b/src/ui/ui.js @@ -0,0 +1,661 @@ +'use strict'; +var UI = (() => { + + var UI = { + initialized: false, + + presets: { + "DEFAULT": "Default", + "T_TRUSTED": "Trusted_temporary", + "TRUSTED": "Trusted_permanent", + "UNTRUSTED": "Untrusted", + "CUSTOM": "Custom", + }, + + async init(tabId = -1) { + UI.tabId = tabId; + let scripts = [ + "/ui/ui.css", + "/lib/punycode.js", + "/lib/tld.js", + "/common/Policy.js", + ]; + this.mobile = !("windows" in browser); + if (this.mobile) { + document.documentElement.classList.toggle("mobile", true); + scripts.push("/lib/fastclick.js"); + } + await include(scripts); + + detectHighContrast(); + + let inited = new Promise(resolve => { + let listener = m => { + if (m.type === "settings") { + UI.policy = new Policy(m.policy); + UI.snapshot = UI.policy.snapshot; + UI.seen = m.seen; + UI.unrestrictedTab = m.unrestrictedTab; + UI.xssUserChoices = m.xssUserChoices; + UI.local = m.local; + UI.sync = m.sync; + if (UI.local && !UI.local.debug) { + debug = () => {}; // be quiet! + } + resolve(); + if (UI.onSettings) UI.onSettings(); + } + }; + browser.runtime.onMessage.addListener(listener); + + if (this.mobile) FastClick.attach(document.body); + UI.pullSettings(); + }); + + await inited; + + this.initialized = true; + debug("Imported", Policy); + }, + async pullSettings() { + browser.runtime.sendMessage({type: "NoScript.broadcastSettings", tabId: UI.tabId}); + }, + async updateSettings({policy, xssUserChoices, unrestrictedTab, local, sync, reloadAffected}) { + if (policy) policy = policy.dry(true); + return await browser.runtime.sendMessage({type: "NoScript.updateSettings", + policy, + xssUserChoices, + unrestrictedTab, + local, + sync, + reloadAffected, + tabId: UI.tabId, + }); + }, + + async exportSettings() { + return await browser.runtime.sendMessage({type: "NoScript.exportSettings"}); + }, + async importSettings(data) { + return await browser.runtime.sendMessage({type: "NoScript.importSettings", data}); + }, + + async revokeTemp() { + let policy = this.policy; + Policy.hydrate(policy.dry(), policy); + if (this.isDirty(true)) { + await this.updateSettings({policy, reloadAffected: true}); + } + }, + + isDirty(reset = false) { + let currentSnapshot = this.policy.snapshot; + let dirty = currentSnapshot != this.snapshot; + if (reset) this.snapshot = currentSnapshot; + return dirty; + }, + + async openSiteInfo(domain) { + let url = `/ui/siteInfo.html#${encodeURIComponent(domain)};${UI.tabId}`; + browser.tabs.create({url}); + } + }; + + function detectHighContrast() { + // detect high contrast + let canary = document.createElement("input"); + canary.className="https-only"; + canary.style.display = "none"; + document.body.appendChild(canary); + if (UI.highContrast = window.getComputedStyle(canary).backgroundImage === "none") { + include("/ui/ui-hc.css"); + document.documentElement.classList.toggle("hc"); + } + canary.parentNode.removeChild(canary); + } + + function fireOnChange(sitesUI, data) { + if (UI.isDirty(true)) { + UI.updateSettings({policy: UI.policy}); + if (sitesUI.onChange) sitesUI.onChange(data, this); + } + } + + function compareBy(prop, a, b) { + let x = a[prop], y = b[prop]; + return x > y ? 1 : x < y ? -1 : 0; + } + + const TEMPLATE = ` + <table class="sites"> + <tr class="site"> + + <td class="presets"> + <span class="preset"> + <input id="preset" class="preset" type="radio" name="preset"><label for="preset" class="preset">PRESET</label> + <button class="options tiny">⚙</button> + <input id="temp" class="temp" type="checkbox"><label for="temp">Temporary</input> + </span> + </td> + + <td class="url" data-key="secure"> + <input class="https-only" id="https-only" type="checkbox"><label for="https-only" class="https-only"></label> + <span class="full-address"> + <span class="protocol">https://</span><span class="sub">www.</span><span class="domain">noscript.net</span><span class="path"></span> + </span> + </td> + + + + </tr> + <tr class="customizer"> + <td colspan="2"> + <div class="customizer-controls"> + <fieldset><legend></legend> + <span class="cap"> + <input class="cap" type="checkbox" value="script" /> + <label class="cap">script</label> + </span> + </fieldset> + </div> + </td> + </tr> + </table> + `; + + const TEMP_PRESETS = ["CUSTOM"]; + const DEF_PRESETS = { + // name: customizable, + "DEFAULT": false, + "T_TRUSTED": false, + "TRUSTED": false, + "UNTRUSTED": false, + "CUSTOM": true, + }; + + UI.Sites = class { + constructor(parentNode, presets = DEF_PRESETS) { + this.parentNode = parentNode; + let policy = UI.policy; + this.uiCount = UI.Sites.count = (UI.Sites.count || 0) + 1; + this.sites = policy.sites; + this.presets = presets; + this.customizing = null; + this.typesMap = new Map(); + this.clear(); + } + + initRow(table = this.table) { + let row = table.querySelector("tr.site"); + + // PRESETS + { + let presets = row.querySelector(".presets"); + let [span, input, label, options] = presets.querySelectorAll("span.preset, input.preset, label.preset, .options"); + span.remove(); + options.title = _("Options"); + for (let [preset, customizable] of Object.entries(this.presets)) { + let messageKey = UI.presets[preset]; + input.value = preset; + label.textContent = label.title = input.title = _(messageKey); + let clone = span.cloneNode(true); + clone.classList.add(preset); + let temp = clone.querySelector(".temp"); + if (TEMP_PRESETS.includes(preset)) { + temp.title = _("allowTemp", `(${label.title.toUpperCase()})`); + temp.nextElementSibling.textContent = _("allowTemp", ""); // label; + } else { + temp.nextElementSibling.remove(); + temp.remove(); + } + if (customizable) { + clone.querySelector(".options").remove(); + } + presets.appendChild(clone); + } + } + + // URL + { + let [input, label] = row.querySelectorAll("input.https-only, label.https-only"); + input.title = label.title = label.textContent = _("httpsOnly"); + } + + // CUSTOMIZER ROW + { + let [customizer, legend, cap, capInput, capLabel] = table.querySelectorAll(".customizer, legend, span.cap, input.cap, label.cap"); + row._customizer = customizer; + customizer.remove(); + let capParent = cap.parentNode; + capParent.removeChild(cap); + legend.textContent = _("allow"); + let idSuffix = UI.Sites.count; + for (let capability of Permissions.ALL) { + capInput.id = `capability-${capability}-${idSuffix}` + capLabel.setAttribute("for", capInput.id); + capInput.value = capability; + capInput.title = capLabel.textContent = _(`cap_${capability}`); + let clone = capParent.appendChild(cap.cloneNode(true)); + clone.classList.add(capability); + } + } + + // debug(table.outerHTML); + return row; + } + + allSiteRows() { + return this.table.querySelectorAll("tr.site"); + } + clear() { + debug("Clearing list", this.table); + + this.template = document.createElement("template"); + this.template.innerHTML = TEMPLATE; + this.fragment = this.template.content; + this.table = this.fragment.querySelector("table.sites"); + this.rowTemplate = this.initRow(); + + for (let r of this.allSiteRows()) { + r.parentNode.removeChild(r); + } + this.customize(null); + this.sitesCount = 0; + } + + siteNeeds(site, type) { + let siteTypes = this.typesMap && this.typesMap.get(site); + return !!siteTypes && siteTypes.has(type); + } + + handleEvent(ev) { + let target = ev.target; + let customizer = target.closest(".customizer"); + let row = customizer ? customizer.parentNode.querySelector("tr.customizing") : target.closest("tr.site"); + if (!row) return; + row.temp2perm = false; + let isTemp = target.matches("input.temp"); + let preset = target.matches("input.preset") ? target + : customizer || isTemp ? row.querySelector("input.preset:checked") + : target.closest("input.preset"); + debug("%s target %o\n\trow %s, perms %o\npreset %s %s", + ev.type, + target, row && row.siteMatch, row && row.perms, + preset && preset.value, preset && preset.checked); + + if (!preset) { + if (target.matches("input.https-only") && ev.type === "change") { + this.toggleSecure(row, target.checked); + fireOnChange(this, row); + } else if (target.matches(".domain")) { + UI.openSiteInfo(row.domain); + } + return; + } + + let policy = UI.policy; + let {siteMatch, contextMatch, perms} = row; + let presetValue = preset.value; + let policyPreset = presetValue.startsWith("T_") ? policy[presetValue.substring(2)].tempTwin : policy[presetValue]; + + if (policyPreset) { + if (row.perms !== policyPreset) { + row.temp2perm = row.perms && policyPreset.tempTwin === row.perms; + row.perms = policyPreset; + } + } + + + let isCap = customizer && target.matches(".cap"); + let tempToggle = preset.parentNode.querySelector("input.temp"); + + if (ev.type === "change") { + if (preset.checked) { + row.dataset.preset = preset.value; + } + if (isCap) { + perms.set(target.value, target.checked); + } else if (policyPreset) { + if (tempToggle && tempToggle.checked) { + policyPreset = policyPreset.tempTwin; + } + row.contextMatch = null; + row.perms = policyPreset; + delete row._customPerms; + debug("Site match", siteMatch); + if (siteMatch) { + policy.set(siteMatch, policyPreset); + } else { + this.customize(policyPreset, preset, row); + } + + } else if (preset.value === "CUSTOM") { + if (isTemp) { + row.perms.temp = target.checked; + } else { + let temp = preset.parentNode.querySelector("input.temp").checked; + let perms = row._customPerms || + (row._customPerms = new Permissions(new Set(row.perms.capabilities), temp)); + row.perms = perms; + policy.set(siteMatch, perms); + this.customize(perms, preset, row); + } + } + fireOnChange(this, row); + } else if (!(isCap || isTemp) && ev.type === "click") { + this.customize(row.perms, preset, row); + } + } + + customize(perms, preset, row) { + debug("Customize preset %s (%o) - Dirty: %s", preset && preset.value, perms, this.dirty); + for(let r of this.table.querySelectorAll("tr.customizing")) { + r.classList.toggle("customizing", false); + } + let customizer = this.rowTemplate._customizer; + customizer.classList.toggle("closed", true); + + if (!(perms && row && preset && + row.dataset.preset === preset.value && + this.presets[preset.value] && + preset !== customizer._preset)) { + delete customizer._preset; + return; + } + + customizer._preset = preset; + row.classList.toggle("customizing", true); + let immutable = Permissions.IMMUTABLE[preset.value] || {}; + for (let input of customizer.querySelectorAll("input")) { + let type = input.value; + if (type in immutable) { + input.disabled = true; + input.checked = immutable[type]; + } else { + input.checked = perms.allowing(type); + input.disabled = false; + } + input.parentNode.classList.toggle("needed", this.siteNeeds(row._site, type)); + row.parentNode.insertBefore(customizer, row.nextElementSibling); + customizer.classList.toggle("closed", false); + customizer.onkeydown = e => { + switch(e.keyCode) { + case 38: + case 8: + e.preventDefault(); + this.onkeydown = null; + this.customize(null); + preset.focus(); + return false; + } + } + window.setTimeout(() => customizer.querySelector("input").focus(), 50); + } + } + + render(sites = this.sites, sorter = this.sorter) { + let parentNode = this.parentNode; + debug("Rendering %o inside %o", sites, parentNode); + if (sites) this._populate(sites, sorter); + parentNode.innerHTML = ""; + parentNode.appendChild(this.fragment); + let root = parentNode.querySelector("table.sites"); + debug("Wiring", root); + if (!root.wiredBy) { + root.addEventListener("click", this, true); + root.addEventListener("change", this, true); + root.wiredBy = this; + } + return root; + } + + _populate(sites, sorter) { + this.clear(); + if (sites instanceof Sites) { + for (let [site, perms] of sites) { + this.append(site, site, perms); + } + } else { + for (let site of sites) { + let context = null; + if (site.site) { + site = site.site; + context = site.context; + } + let {siteMatch, perms, contextMatch} = UI.policy.get(site, context); + this.append(site, siteMatch, perms, contextMatch); + } + this.sites = sites; + } + this.sort(sorter); + window.setTimeout(() => this.focus(), 50); + } + + focus() { + let firstPreset = this.table.querySelector("input.preset:checked"); + if (firstPreset) firstPreset.focus(); + } + + sort(sorter = this.sorter) { + if (this.mainDomain) { + let md = this.mainDomain; + let wrappedCompare = sorter; + sorter = (a, b) => { + let x = a.domain, y = b.domain; + if (x === md) { + if (y !== md) { + return -1; + } + } else if (y === md) { + return 1; + } + return wrappedCompare(a, b); + } + } + let rows = [...this.allSiteRows()].sort(sorter); + if (this.mainSite) { + let mainLabel = "." + this.mainDomain; + let topIdx = rows.findIndex(r => r._label === mainLabel); + if (topIdx === -1) rows.findIndex(r => r._site === this.mainSite); + if (topIdx !== -1) { + // move the row to the top + let topRow = rows.splice(topIdx, 1)[0]; + rows.unshift(topRow); + topRow.classList.toggle("main", true); + } + } + this.clear(); + for (let row of rows) this.table.appendChild(row); + this.table.appendChild(this.rowTemplate._customizer); + } + + sorter(a, b) { + return compareBy("domain", a, b) || compareBy("_label", a, b); + } + + async tempTrustAll() { + let {policy} = UI; + let changed = 0; + for (let row of this.allSiteRows()) { + if (row._preset === "DEFAULT") { + policy.set(row._site, policy.TRUSTED.tempTwin); + changed++; + } + } + if (changed && UI.isDirty(true)) { + await UI.updateSettings({policy, reloadAffected: true}); + } + return changed; + } + + createSiteRow(site, siteMatch, perms, contextMatch = null, sitesCount = this.sitesCount++) { + debug("Creating row for site: %s, matching %s / %s, %o", site, siteMatch, contextMatch, perms); + + let row = this.rowTemplate.cloneNode(true); + row.sitesCount = sitesCount; + let url; + try { + url = new URL(site); + } catch (e) { + let protocol = Sites.isSecureDomainKey(site) ? "https:" : "http:"; + let hostname = Sites.toggleSecureDomainKey(site, false); + url = {protocol, hostname, origin: `${protocol}://${site}`, pathname: "/"}; + } + + let hostname = Sites.toExternal(url.hostname); + let domain = tld.getDomain(hostname); + + if (!siteMatch) { + // siteMatch = url.protocol === "https:" ? Sites.secureDomainKey(domain) : site; + siteMatch = site; + } + let secure = Sites.isSecureDomainKey(siteMatch); + let keyStyle = secure ? "secure" + : !domain || /^\w+:/.test(siteMatch) ? + (url.protocol === "https:" ? "full" : "unsafe") + : domain === hostname ? "domain" : "host"; + + let urlContainer = row.querySelector(".url"); + urlContainer.dataset.key = keyStyle; + row._site = site; + + row.siteMatch = siteMatch; + row.contextMatch = contextMatch; + row.perms = perms; + row.domain = domain || siteMatch; + if (domain) { // "normal" URL + let justDomain = hostname === domain; + let domainEntry = secure || domain === site; + row._label = domainEntry ? "." + domain : site; + row.querySelector(".protocol").textContent = `${url.protocol}//`; + row.querySelector(".sub").textContent = justDomain ? + (keyStyle === "full" || keyStyle == "unsafe" + ? "" : "…") + : hostname.substring(0, hostname.length - domain.length); + + row.querySelector(".domain").textContent = domain; + row.querySelector(".path").textContent = siteMatch.length > url.origin.length ? url.pathname : ""; + let httpsOnly = row.querySelector("input.https-only"); + httpsOnly.checked = keyStyle === "full" || keyStyle === "secure"; + } else { + row._label = siteMatch; + urlContainer.querySelector(".full-address").textContent = siteMatch; + } + + let presets = row.querySelectorAll("input.preset"); + let idSuffix = `-${this.uiCount}-${sitesCount}`; + for (let p of presets) { + p.id = `${p.value}${idSuffix}`; + p.name = `preset${idSuffix}`; + let label = p.nextElementSibling; + label.setAttribute("for", p.id); + let temp = p.parentNode.querySelector("input.temp"); + if (temp) { + temp.id = `temp-${p.id}`; + label = temp.nextElementSibling; + label.setAttribute("for", temp.id); + } + } + let policy = UI.policy; + + let presetName = "CUSTOM"; + for (let p of ["TRUSTED", "UNTRUSTED", "DEFAULT"]) { + let preset = policy[p]; + switch (perms) { + case preset: + presetName = p; + break; + case preset.tempTwin: + presetName = `T_${p}`; + if (!presetName in UI.presets) { + presetName = p; + } + break; + } + } + let tempFirst = true; // TODO: make it a preference + let unsafeMatch = keyStyle !== "secure" && keyStyle !== "full"; + if (presetName === "DEFAULT" && (tempFirst || unsafeMatch)) { + // prioritize temporary privileges over permanent + for (let p of TEMP_PRESETS) { + if (p in this.presets && (unsafeMatch || tempFirst && p === "TRUSTED")) { + row.querySelector(`.presets input[value="${p}"]`).parentNode.querySelector("input.temp").checked = true; + perms = policy.TRUSTED.tempTwin; + } + } + } + let preset = row.querySelector(`.presets input[value="${presetName}"]`); + if (!preset) { + debug(`Preset %s not found in %s!`, presetName, row.innerHTML); + } else { + preset.checked = true; + row.dataset.preset = row._preset = presetName; + if (TEMP_PRESETS.includes(presetName)) { + let temp = preset.parentNode.querySelector("input.temp"); + if (temp) { + temp.checked = perms.temp; + } + } + } + return row; + } + + append(site, siteMatch, perms, contextMatch) { + this.table.appendChild(this.createSiteRow(...arguments)); + } + + toggleSecure(row, secure = !!row.querySelector("https-only:checked")) { + this.customize(null); + let site = row.siteMatch; + site = site.replace(/^https?:/, secure ? "https:" : "http:"); + if (site === row.siteMatch) { + site = Sites.toggleSecureDomainKey(site, secure); + } + if (site !== row.siteMatch) { + let {policy} = UI; + policy.set(row.siteMatch, policy.DEFAULT); + policy.set(site, row.perms); + for(let r of this.allSiteRows()) { + if (r !== row && r.siteMatch === site && r.contextMatch === row.contextMatch) { + r.parentNode.removeChild(r); + } + } + let newRow = this.createSiteRow(site, site, row.perms, row.contextMatch, row.sitesCount); + row.parentNode.replaceChild(newRow, row); + } + } + + highlight(key) { + key = Sites.toExternal(key); + for (let r of this.allSiteRows()) { + if (r.querySelector(".full-address").textContent.trim().includes(key)) { + let url = r.lastElementChild; + url.style.transition = r.style.transition = "none"; + r.style.backgroundColor = "#850"; + url.style.transform = "scale(2)"; + r.querySelector("input.preset:checked").focus(); + window.setTimeout(() => { + r.style.transition = "1s background-color"; + url.style.transition = "1s transform"; + r.style.backgroundColor = ""; + url.style.transform = "none"; + r.scrollIntoView(); + }, 50); + } + } + } + + filterSites(key) { + key = Sites.toExternal(key); + for (let r of this.allSiteRows()) { + if (r.querySelector(".full-address").textContent.trim().includes(key)) { + r.style.display = ""; + } else { + r.style.display = "none"; + } + } + } + } + + return UI; +})(); diff --git a/src/ui/whirlpool.css b/src/ui/whirlpool.css new file mode 100644 index 0000000..0e2147a --- /dev/null +++ b/src/ui/whirlpool.css @@ -0,0 +1,45 @@ + +.cssload-container{ + position:relative; +} + +.cssload-whirlpool, +.cssload-whirlpool::before, +.cssload-whirlpool::after { + position: absolute; + top: 50%; + left: 50%; + border: 1px solid rgb(204,204,204); + border-left-color: rgb(0,0,0); + border-radius: 974px; +} + +.cssload-whirlpool { + margin: -24px 0 0 -24px; + height: 49px; + width: 49px; + animation: cssload-rotate 1150ms linear infinite; +} + +.cssload-whirlpool::before { + content: ""; + margin: -22px 0 0 -22px; + height: 43px; + width: 43px; + animation: cssload-rotate 1150ms linear infinite; +} + +.cssload-whirlpool::after { + content: ""; + margin: -28px 0 0 -28px; + height: 55px; + width: 55px; + animation: cssload-rotate 2300ms linear infinite; +} + + +@keyframes cssload-rotate { + 100% { + transform: rotate(360deg); + } +} |