From 4acf282ae6d5ae24a956908a87478d944f8519b9 Mon Sep 17 00:00:00 2001 From: hackademix Date: Thu, 13 Sep 2018 15:56:29 +0200 Subject: Brand new general settings page for white/black list management and other preferences. --- html/display_panel/content/display-panel.html | 5 +- html/display_panel/content/main_panel.js | 27 ++- html/preferences_panel/pref.js | 316 ++++++++++++++++++++++---- html/preferences_panel/preferences_panel.html | 89 +++++--- html/preferences_panel/prefs.css | 87 +++++++ manifest.json | 3 +- 6 files changed, 435 insertions(+), 92 deletions(-) create mode 100644 html/preferences_panel/prefs.css diff --git a/html/display_panel/content/display-panel.html b/html/display_panel/content/display-panel.html index 2ed0c9c..df153b3 100644 --- a/html/display_panel/content/display-panel.html +++ b/html/display_panel/content/display-panel.html @@ -36,8 +36,8 @@
+ href= href="https://www.gnu.org/software/librejs/" + title="LibreJS Page Settings">

LibreJS

@@ -52,6 +52,7 @@ +
diff --git a/html/display_panel/content/main_panel.js b/html/display_panel/content/main_panel.js index 930f7f2..c55b167 100644 --- a/html/display_panel/content/main_panel.js +++ b/html/display_panel/content/main_panel.js @@ -64,6 +64,11 @@ document.querySelector("#complain").onclick = e => { close(); } +document.querySelector("#open-options").onclick = e => { + browser.runtime.openOptionsPage(); + close(); +} + document.querySelector("#reload").onclick = async e => { let {tabId} = currentReport; if (tabId) { @@ -72,9 +77,9 @@ document.querySelector("#reload").onclick = async e => { } }; -/* +/* * Takes in the [[file_id, reason],...] array and the group name for one group -* of scripts found in this tab, rendering it as a list with management buttons. +* of scripts found in this tab, rendering it as a list with management buttons. * Groups are "unknown", "blacklisted", "whitelisted", "accepted", and "blocked". */ function createList(data, group){ @@ -98,7 +103,7 @@ function createList(data, group){ let [scriptId, reason] = entry; let li = liTemplate.cloneNode(true); let a = li.querySelector("a"); - a.href = scriptId.split("(")[0]; + a.href = scriptId.split("(")[0]; a.textContent = scriptId; li.querySelector(".reason").textContent = reason; let bySite = !!reason.match(/https?:\/\/[^/]+\/\*/); @@ -116,7 +121,7 @@ function createList(data, group){ /** * Updates scripts lists and buttons to act on them. * If return_HTML is true, it returns the HTML of the popup window without updating it. -* example report argument: +* example report argument: * { * "accepted": [["FILENAME 1","REASON 1"],["FILENAME 2","REASON 2"]], * "blocked": [["FILENAME 1","REASON 1"],["FILENAME 2","REASON 2"]], @@ -131,29 +136,29 @@ function refreshUI(report) { currentReport = report; document.querySelector("#site").className = report.siteStatus || ""; - document.querySelector("#site h2").textContent = + document.querySelector("#site h2").textContent = `This site ${report.site}`; - + for (let toBeErased of document.querySelectorAll("#info h2:not(.site) > *, #info ul > *")) { toBeErased.remove(); } - + let scriptsCount = 0; for (let group of ["unknown", "accepted", "whitelisted", "blocked", "blacklisted"]) { if (group in report) createList(report, group); scriptsCount += report[group].length; } - + for (let b of document.querySelectorAll(`.forget, .whitelist, .blacklist`)) { b.disabled = false; } for (let b of document.querySelectorAll( - `.unknown .forget, .accepted .forget, .blocked .forget, + `.unknown .forget, .accepted .forget, .blocked .forget, .whitelisted .whitelist, .blacklisted .blacklist` )) { b.disabled = true; - } - + } + let noscript = scriptsCount === 0; document.body.classList.toggle("empty", noscript); } diff --git a/html/preferences_panel/pref.js b/html/preferences_panel/pref.js index 4642d32..9cecbb6 100644 --- a/html/preferences_panel/pref.js +++ b/html/preferences_panel/pref.js @@ -1,7 +1,8 @@ /** * GNU LibreJS - A browser add-on to block nonfree nontrivial JavaScript. -* * +* * Copyright (C) 2017 Nathan Nichols +* Copyright (C) 2018 Giorgio maone * * This file is part of GNU LibreJS. * @@ -19,59 +20,288 @@ * along with GNU LibreJS. If not, see . */ -var store; +(() => { + "use strict"; -function storage_got(items){ - var inputs = document.getElementsByTagName("input"); + const LIST_NAMES = ["white", "black"]; - if(items["pref_whitelist"] == "undefined"){ - items["pref_whitelist"] = ""; - } + var Model = { + lists: {}, + prefs: null, - if(items["pref_subject"] == "undefined" || items["pref_subject"] == ""){ - items["pref_subject"] = "Issues with Javascript on your website"; - } + malformedUrl(url) { + let error = null; + try { + let objUrl = new URL(url); + url = objUrl.href; + if (!objUrl.protocol.startsWith("http")) { + error = "Please enter http:// or https:// URLs only"; + } else if (!/^[^*]+\*?$/.test(url)) { + error = "Only one single trailing path wildcard (/*) allowed"; + } + } catch (e) { + error = "Invalid URL"; + if (url && !url.includes("://")) error += ": missing protocol, either http:// or https://"; + else if (url.endsWith("://")) error += ": missing domain name"; + } + return error; + }, - if(items["pref_body"] == "undefined" || items["pref_body"] == ""){ - items["pref_body"] = "Please consider using a free license for the Javascript on your website. [Message generated by LibreJS. See https://www.gnu.org/software/librejs/ for more information]"; - } + async save(prefs = this.prefs) { + if (prefs !== this.prefs) { + this.prefs = Object.assign(this.prefs, prefs); + } + this.saving = true; + try { + return await browser.storage.local.set(prefs); + } finally { + this.saving = false; + } + }, - for(var i = 0; i < inputs.length; i++){ - if(inputs[i].id.indexOf("pref_") != -1){ - if(inputs[i].type == "checkbox" && items[inputs[i].id]){ - inputs[i].checked = true; + async addToList(list, ...items) { + let other = list === Model.lists.black ? Model.lists.white : Model.lists.black; + this.saving = true; + try { + await Promise.all([ + other.remove(...items), + list.store(...items) + ]); + } finally { + this.saving = false; } - if(inputs[i].type == "text" && items[inputs[i].id] != undefined){ - inputs[i].value = items[inputs[i].id]; - } } - } + }; + Model.loading = (async () => { + let prefsNames = [ + "whitelist", + "blacklist", + "subject", + "body" + ]; + Model.prefs = await browser.storage.local.get(prefsNames.map(name => `pref_${name}`)); + + for (let listName of LIST_NAMES) { + let prefName = `pref_${listName}list`; + await (Model.lists[listName] = new ListStore(prefName, Storage.CSV)) + .load(Model.prefs[prefName]); + } + })(); + + var Controller = { + init() { + let widgetsRoot = this.root = document.getElementById("widgets"); + for (let widget of widgetsRoot.querySelectorAll('[id^="pref_"]')) { + if (widget.id in Model.lists) { + populateListUI(widget); + } else if (widget.id in Model.prefs) { + widget.value = Model.prefs[widget.id]; + } + } + + this.populateListUI(); + this.syncAll(); + + for (let ev in Listeners) { + widgetsRoot.addEventListener(ev, Listeners[ev]); + } + document.getElementById("site").onfocus = e => { + if (!e.target.value.trim()) { + e.target.value = "https://"; + } + }; + + browser.storage.onChanged.addListener(changes => { + if (!Model.saving && + ("pref_whitelist" in changes || "pref_blacklist" in changes)) { + setTimeout(() => { + this.populateListUI(); + this.syncAll(); + }, 10); + } + }); + }, + + async addSite(list) { + let url = document.getElementById("site").value.trim(); + + if (url && !Model.malformedUrl(url)) { + await this.addToList(list, url); + } + }, + async addToList(list, ...items) { + await Model.addToList(list, ...items); + this.populateListUI(); + this.syncAll(); + }, + async swapSelection(list) { + let origin = list === Model.lists.black ? "white" : "black"; + await this.addToList(list, ...Array.map( + document.querySelectorAll(`select#${origin} option:checked`), + option => option.value) + ); + }, + + syncAll() { + this.syncListsUI(); + this.syncSiteUI(); + }, + + syncSiteUI() { + let widget = document.getElementById("site"); + let list2button = listName => document.getElementById(`cmd-${listName}list-site`); + + for (let bi of LIST_NAMES.map(list2button)) { + bi.disabled = true; + } + + let url = widget.value.trim(); + let malformedUrl = url && Model.malformedUrl(url); + widget.classList.toggle("error", !!malformedUrl); + document.getElementById("site-error").textContent = malformedUrl || ""; + if (!url) return; + if (url !== widget.value) { + widget.value = url; + } + + for (let listName of LIST_NAMES) { + let list = Model.lists[listName]; + if (!list.contains(url)) { + list2button(listName).disabled = false; + } + } + }, + + syncListsUI() { + let total = 0; + for (let id of ["black", "white"]) { + let selected = document.querySelectorAll(`select#${id} option:checked`).length; + let other = id === "black" ? "white" : "black"; + document.getElementById(`cmd-${other}list`).disabled = selected === 0; + total += selected; + } + document.getElementById("cmd-delete").disabled = total === 0; + }, + async deleteSelection() { + for (let id of ["black", "white"]) { + let selection = document.querySelectorAll(`select#${id} option:checked`); + await Model.lists[id].remove(...Array.map(selection, option => option.value)); + } + this.populateListUI(); + this.syncAll(); + }, -} -browser.storage.local.get(storage_got); - -document.getElementById("save_changes").addEventListener("click", function(){ - var inputs = document.getElementsByTagName("input"); - // TODO: validate/sanitize the user inputs - var data = {}; - for(var i = 0; i < inputs.length; i++){ - if(inputs[i].id.indexOf("pref_") != -1){ - var input_val = ""; - if(inputs[i].type == "checkbox"){ - input_val = inputs[i].checked; - } else{ - if(inputs[i.value] != "undefined"){ - input_val = inputs[i].value; - } else{ - input_val = ""; + populateListUI(widget) { + if (!widget) { + for(let id of ["white", "black"]) { + this.populateListUI(document.getElementById(id)); } + return; + } + widget.innerHTML = ""; + let items = [...Model.lists[widget.id].items].sort(); + let options = new DocumentFragment(); + for (let item of items) { + let option = document.createElement("option"); + option.value = option.textContent = option.title = item; + options.appendChild(option); + } + widget.appendChild(options); + } + }; + + var KeyEvents = { + Delete(e) { + if (e.target.matches("#lists select")) { + Controller.deleteSelection(); + } + }, + Enter(e) { + if (e.target.id === "site") { + e.target.parentElement.querySelector("button[default]").click(); + } + }, + KeyA(e) { + if (e.target.matches("select") && e.ctrlKey) { + for (let o of e.target.options) { + o.selected = true; + } + Controller.syncListsUI(); } - var input_id = inputs[i].id; - data[input_id] = input_val; } } - console.log(data); - browser.storage.local.set(data); -}); + var Listeners = { + async change(e) { + let {target} = e; + let {id} = target; + + if (id in Model.lists) { + Controller.syncListsUI(); + let selection = target.querySelectorAll("option:checked"); + if (selection.length === 1) { + document.getElementById("site").value = selection[0].value; + } + return; + } + }, + + click(e) { + let {target} = e; + + if (!/^cmd-(white|black|delete)(list-site)?/.test(target.id)) return; + e.preventDefault(); + let cmd = RegExp.$1; + if (cmd === "delete") { + Controller.deleteSelection(); + return; + } + let list = Model.lists[cmd]; + if (list) { + Controller[RegExp.$2 ? "addSite" : "swapSelection"](list); + return; + } + }, + + keypress(e) { + let {code} = e; + if (code && typeof KeyEvents[code] === "function") { + if (KeyEvents[code](e) === false) { + e.preventDefault(); + } + return; + } + }, + + async input(e) { + let {target} = e; + let {id} = target; + if (!id) return; + + if (id === "site") { + Controller.syncSiteUI(); + let url = target.value; + if (url) { + let o = document.querySelector(`#lists select option[value="${url}"]`); + if (o) { + o.scrollIntoView(); + o.selected = true; + } + } + return; + } + + if (id.startsWith("pref_")) { + await Model.save({[id]: target.value}); + return; + } + } + }; + + window.addEventListener("DOMContentLoaded", async e => { + await Model.loading; + Controller.init(); + }); + +})(); diff --git a/html/preferences_panel/preferences_panel.html b/html/preferences_panel/preferences_panel.html index 2d01f94..fff6f9c 100644 --- a/html/preferences_panel/preferences_panel.html +++ b/html/preferences_panel/preferences_panel.html @@ -1,10 +1,13 @@ + - + + - -

Default complaint email subject

- - - -

Default complaint email body

- - - - - - - - +[Message generated by LibreJS. See https://www.gnu.org/software/librejs/ for more information] + + +
diff --git a/html/preferences_panel/prefs.css b/html/preferences_panel/prefs.css new file mode 100644 index 0000000..3ac498e --- /dev/null +++ b/html/preferences_panel/prefs.css @@ -0,0 +1,87 @@ +@import url("chrome://browser/content/extension.css"); +@import url("/html/common.css"); +h3 { + position: absolute; + bottom: 0px; + left: 240px; + font-size: 18px; +} +textarea { + width: 100%; +} +fieldset { + border: none; + padding: 0; + margin-top: 1em; + border-top: 1px solid #ccc; +} +legend { + font-weight: bold; + margin: 0; + padding: 0; +} +label, legend { + display: block; + font-size: 1.2em; +} + +#lists { + display: flex; + flex-direction: row; +} +.list-container { + flex: 3; + flex-direction: row; +} +.list-container select { +width: 100% +} + +.black { + color: #600; +} +.white { + color: #060; +} + +#commands { + display: flex; + justify-content: center; + flex: none; + flex-flow: column nowrap; +} + +#commands button { + font-weight: bold; +} +input[type="text"] { + width: 100%; +} + +#lists label { + font-weight: bold; +} +#lists select { + color: black; +} +#black { + background-color: #fcc; +} +#white { + background-color: #cfc; +} + +#new-site { + display: flex; + flex 2; +} +.error-msg { + color: red; +} +.error-msg::after { + content: "\00A0"; +} +.error { + background: #ffe; + color: #800; +} diff --git a/manifest.json b/manifest.json index aecf9a8..e952f22 100644 --- a/manifest.json +++ b/manifest.json @@ -34,7 +34,8 @@ "default_popup": "html/display_panel/content/display-panel.html" }, "options_ui": { - "page": "html/preferences_panel/preferences_panel.html" + "page": "html/preferences_panel/preferences_panel.html", + "open_in_tab": true }, "web_accessible_resources": [ "html/report_page/report.html" -- cgit v1.2.3