summaryrefslogtreecommitdiff
path: root/src/ui
diff options
context:
space:
mode:
Diffstat (limited to 'src/ui')
-rw-r--r--src/ui/Prompts.js101
-rw-r--r--src/ui/options.css187
-rw-r--r--src/ui/options.html125
-rw-r--r--src/ui/options.js220
-rw-r--r--src/ui/popup.css235
-rw-r--r--src/ui/popup.html41
-rw-r--r--src/ui/popup.js249
-rw-r--r--src/ui/prompt.css91
-rw-r--r--src/ui/prompt.html32
-rw-r--r--src/ui/prompt.js91
-rw-r--r--src/ui/resize_hack.js15
-rw-r--r--src/ui/siteInfo.html5
-rw-r--r--src/ui/siteInfo.js20
-rw-r--r--src/ui/toolbar.js117
-rw-r--r--src/ui/ui-hc.css63
-rw-r--r--src/ui/ui.css391
-rw-r--r--src/ui/ui.js661
-rw-r--r--src/ui/whirlpool.css45
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);
+ }
+}