diff options
author | Ruben Rodriguez <ruben@gnu.org> | 2018-09-13 20:58:40 +0000 |
---|---|---|
committer | Ruben Rodriguez <ruben@gnu.org> | 2018-09-13 20:58:40 +0000 |
commit | 3e4c252f982637b54719b427ac85ac3701d44abd (patch) | |
tree | 7b433796d4d3fcb9647e0da0cafe89e5e4d62521 | |
parent | 0e004bf1a3c154b98a96494dc97ecba58b8162ed (diff) | |
parent | 3bf972bafeac738301623db6b08bedfd476abeca (diff) |
Merge #17 `Brand new settings UI to manage whitelist, blacklist and other preferences. `
-rw-r--r-- | bg/ListManager.js | 37 | ||||
-rwxr-xr-x | build.sh | 1 | ||||
-rw-r--r-- | common/Storage.js (renamed from bg/Storage.js) | 62 | ||||
-rw-r--r-- | html/README (renamed from html/display_panel/content/README) | 0 | ||||
-rw-r--r-- | html/background-panel.png (renamed from html/display_panel/content/background-panel.png) | bin | 14814 -> 14814 bytes | |||
-rw-r--r-- | html/common.css | 29 | ||||
-rw-r--r-- | html/display_panel/content/display-panel.html | 5 | ||||
-rw-r--r-- | html/display_panel/content/librejs-title-old.png | bin | 2673 -> 0 bytes | |||
-rw-r--r-- | html/display_panel/content/main_panel.js | 27 | ||||
-rw-r--r-- | html/display_panel/content/panel-styles.css | 34 | ||||
-rw-r--r-- | html/librejs-title.png (renamed from html/display_panel/content/librejs-title.png) | bin | 14123 -> 14123 bytes | |||
-rw-r--r-- | html/preferences_panel/pref.js | 316 | ||||
-rw-r--r-- | html/preferences_panel/preferences_panel.html | 89 | ||||
-rw-r--r-- | html/preferences_panel/prefs.css | 91 | ||||
-rw-r--r-- | main_background.js | 190 | ||||
-rw-r--r-- | manifest.json | 5 |
16 files changed, 637 insertions, 249 deletions
diff --git a/bg/ListManager.js b/bg/ListManager.js index 34d9531..e0a85e9 100644 --- a/bg/ListManager.js +++ b/bg/ListManager.js @@ -23,27 +23,28 @@ A class to manage whitelist/blacklist operations */ -let {ListStore} = require("./Storage"); +let {ListStore} = require("../common/Storage"); class ListManager { constructor(whitelist, blacklist, builtInHashes) { this.lists = {whitelist, blacklist}; this.builtInHashes = new Set(builtInHashes); } - async whitelist(key) { - await this.lists.blacklist.remove(key); - await this.lists.whitelist.store(key); + + static async move(fromList, toList, ...keys) { + await Promise.all([fromList.remove(...keys), toList.store(...keys)]); } - async blacklist(key) { - await this.lists.whitelist.remove(key); - await this.lists.blacklist.store(key); + + async whitelist(...keys) { + ListManager.move(this.lists.blacklist, this.lists.whitelist, ...keys); } - async forget(key) { - for (let list of Object.values(this.lists)) { - await list.remove(key); - } + async blacklist(...keys) { + ListManager.move(this.lists.whitelist, this.lists.blacklist, ...keys); } - /* key is a string representing either a URL or an optional path + async forget(...keys) { + await Promise.all(Object.values(this.lists).map(l => l.remove(...keys))); + } + /* key is a string representing either a URL or an optional path with a trailing (hash). Returns "blacklisted", "whitelisted" or defValue */ @@ -53,16 +54,16 @@ class ListManager { if (!match) { let url = ListStore.urlItem(key); let site = ListStore.siteItem(key); - return (blacklist.contains(url) || blacklist.contains(site)) + return (blacklist.contains(url) || blacklist.contains(site)) ? "blacklisted" - : whitelist.contains(url) || whitelist.contains(site) - ? "whitelisted" : defValue; + : whitelist.contains(url) || whitelist.contains(site) + ? "whitelisted" : defValue; } - + let [hashItem, srcHash] = match; // (hash), hash - + return blacklist.contains(hashItem) ? "blacklisted" - : this.builtInHashes.has(srcHash) || whitelist.contains(hashItem) + : this.builtInHashes.has(srcHash) || whitelist.contains(hashItem) ? "whitelisted" : defValue; } @@ -12,6 +12,7 @@ mkdir ./build_temp cp -r icons ./build_temp cp -r ./html ./build_temp cp -r ./content ./build_temp +cp -r ./common ./build_temp cp manifest.json ./build_temp cp contact_finder.js ./build_temp cp bundle.js ./build_temp diff --git a/bg/Storage.js b/common/Storage.js index ecdc9e4..a83ce8f 100644 --- a/bg/Storage.js +++ b/common/Storage.js @@ -23,11 +23,14 @@ A tiny wrapper around extensions storage API, supporting CSV serialization for retro-compatibility */ +"use strict"; var Storage = { ARRAY: { - async load(key) { - let array = (await browser.storage.local.get(key))[key]; + async load(key, array = undefined) { + if (array === undefined) { + array = (await browser.storage.local.get(key))[key]; + } return array ? new Set(array) : new Set(); }, async save(key, list) { @@ -40,7 +43,7 @@ var Storage = { let csv = (await browser.storage.local.get(key))[key]; return csv ? new Set(csv.split(/\s*,\s*/)) : new Set(); }, - + async save(key, list) { return await browser.storage.local.set({[key]: [...list].join(",")}); } @@ -56,8 +59,13 @@ class ListStore { this.key = key; this.storage = storage; this.items = new Set(); + browser.storage.onChanged.addListener(changes => { + if (!this.saving && this.key in changes) { + this.load(changes[this.key].newValue); + } + }); } - + static hashItem(hash) { return hash.startsWith("(") ? hash : `(${hash})`; } @@ -73,32 +81,50 @@ class ListStore { return `${url}/*`; } } - + async save() { - return await this.storage.save(this.key, this.items); + this._saving = true; + try { + return await this.storage.save(this.key, this.items); + } finally { + this._saving = false; + } } - - async load() { + + async load(values = undefined) { try { - this.items = await this.storage.load(this.key); + this.items = await this.storage.load(this.key, values); } catch (e) { console.error(e); } return this.items; } - - async store(item) { + + async store(...items) { let size = this.items.size; - return (size !== this.items.add(item).size) && await this.save(); + let changed = false; + for (let item of items) { + if (size !== this.items.add(item).size) { + changed = true; + } + } + return changed && await this.save(); } - - async remove(item) { - return this.items.delete(item) && await this.save(); + + async remove(...items) { + let changed = false; + for (let item of items) { + if (this.items.delete(item)) { + changed = true; + } + } + return changed && await this.save(); } - + contains(item) { return this.items.has(item); } } - -module.exports = { ListStore, Storage }; +if (typeof module === "object") { + module.exports = { ListStore, Storage }; +} diff --git a/html/display_panel/content/README b/html/README index a56ea46..a56ea46 100644 --- a/html/display_panel/content/README +++ b/html/README diff --git a/html/display_panel/content/background-panel.png b/html/background-panel.png Binary files differindex 022ffb3..022ffb3 100644 --- a/html/display_panel/content/background-panel.png +++ b/html/background-panel.png diff --git a/html/common.css b/html/common.css new file mode 100644 index 0000000..cf2c5d1 --- /dev/null +++ b/html/common.css @@ -0,0 +1,29 @@ +html { + padding:0px; + margin:0px; + color:#000 !important; + background:url('background-panel.png') !important; +} +body { + padding:0; + margin:10px 30px 10px 20px; + color:#000; +} + +div.libre { + position: relative; +} + +.libre { + width:230px; + height:104px; + display:block; +} +h1.libre { + font-size:1.5em; + font-weight:normal; + padding:0; + font-weight:bold; + background:url('librejs-title.png') no-repeat top left; + text-indent:-1000px; +} 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 @@ <div> <a class="libre" id="ljs-settings" - href="javascript:void" - title="LibreJS Whitelist Settings"> + href= href="https://www.gnu.org/software/librejs/" + title="LibreJS Page Settings"> <h1 class="libre">LibreJS</h1> </a> </div> @@ -52,6 +52,7 @@ </div> <button id="complain">Complain to site owner</button> <button id="report-tab">Show this report in a new tab</button> + <button id="open-options">Settings...</button> </div> </div> <div id="info"> diff --git a/html/display_panel/content/librejs-title-old.png b/html/display_panel/content/librejs-title-old.png Binary files differdeleted file mode 100644 index 8a11527..0000000 --- a/html/display_panel/content/librejs-title-old.png +++ /dev/null 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/display_panel/content/panel-styles.css b/html/display_panel/content/panel-styles.css index 745c67f..cbf5cf5 100644 --- a/html/display_panel/content/panel-styles.css +++ b/html/display_panel/content/panel-styles.css @@ -17,38 +17,16 @@ * along with this program. If not, see <http://www.gnu.org/licenses/>. * */ -html { - padding:0px; - margin:0px; - color:#000 !important; - background:url('background-panel.png') !important; -} +@import url("/html/common.css"); + body { - padding:0; - margin:10px 30px 10px 20px; - color:#000; -width:500px; + width:500px; } - #header{ display:block; width:500px; } -.libre { - width:230px; - height:104px; - display:block; -} -h1.libre { - font-size:1.5em; - font-weight:normal; - padding:0; - font-weight:bold; - background:url('librejs-title.png') no-repeat top left; - text-indent:-1000px; - overflow:hidden; -} h2 { font-size:1.1em; font-weight:bold; @@ -171,3 +149,9 @@ span.blocked { width: 100%; text-align: center; } + + + +#complain { + display: none; /* TODO: Complaint to owner UI */ +} diff --git a/html/display_panel/content/librejs-title.png b/html/librejs-title.png Binary files differindex c1a911c..c1a911c 100644 --- a/html/display_panel/content/librejs-title.png +++ b/html/librejs-title.png 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 <http://www.gnu.org/licenses/>. */ -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..effb724 100644 --- a/html/preferences_panel/preferences_panel.html +++ b/html/preferences_panel/preferences_panel.html @@ -1,10 +1,13 @@ +<!doctype html> <html> - <head> +<head> +<meta charset="utf-8"/> <!-- /** * GNU LibreJS - A browser add-on to block nonfree nontrivial JavaScript. * * * Copyright (C) 2011, 2012, 2014 Loic J. Duros * Copyright (C) 2014, 2015 Nik Nyby + * Copyright (C) 2018 Giorgio Maone * * This file is part of GNU LibreJS. * @@ -25,43 +28,59 @@ <title> LibreJS preferences </title> + <link rel="stylesheet" type="text/css" href="./prefs.css"/> + <script type="text/javascript" src="/common/Storage.js"></script> + <script type="text/javascript" src="pref.js"></script> </head> - - <body> - <h3> - LibreJS Preferences - </h3> - <table> - <tr> - <td><p>Allow all scripts from pages with this text <br> in their URL. (Comma seperated, wildcard is *)</p></td> - <td><input id="pref_whitelist" type="text"></input></td> - </tr> - <!-- + <body> + <div class="libre"> + <a class="libre" + id="ljs-settings" + href="https://www.gnu.org/software/librejs/" + title="LibreJS Settings"> + <h1 class="libre">LibreJS</h1> + </a> + <h3>Settings</h3> + </div> + <div id="widgets"> + <fieldset id="section-lists"><legend>Allow or block scripts matching the following URLs ("*" matches any path)</legend> + <label>Type a new whitelist / blacklist entry:</label> + <div id="new-site"> + <input type="text" id="site" value="" placeholder="https://www.gnu.org/*"> + <button id="cmd-whitelist-site" class="white" title="Whitelist this site" default>Whitelist</button> + <button id="cmd-blacklist-site" class="red" title="Blacklist this site">Blacklist</button> + </div> + <div id="site-error" class="error-msg"></div> + <div id="lists"> + <div class="white list-container"> + <label>Whitelist (always allow)</label> + <select id="white" multiple size="10"></select> + </div> + <div id="commands"> + <button id="cmd-delete" title="Delete">x</button> + <button id="cmd-blacklist" title="Move to blacklist">»</button> + <button id="cmd-whitelist" title="Move to whitelist">«</button> + </div> + <div class="black list-container"> + <label>Blacklist (always block)</label> + <select id="black" multiple size="10"></select> + </div> + </div> + </fieldset> - <tr> - <td><p>Display complaint tab on sites where nonfree nontrivial Javascript detected</p></td> - <td><input id="pref_complaint_tab" type="checkbox"></input></td> - </tr> - <tr> - <td><p>Display notifications of the JavaScript code being analyzed by LibreJS</p></td> - <td><input id="pref_notify_analyze" type="checkbox"></input></td> - </tr> + <fieldset id="section-complaint"><legend>Complaint email defaults</legend> + <label for="pref_subject">Subject</label> + <input id="pref_subject" type="text" + value="Issues with Javascript on your website" + /> + <label for="pref_body">Body</label> + <textarea id="pref_body" rows="5" +>Please consider using a free license for the Javascript on your website. - --> - <tr> - <td><p>Default complaint email subject</p></td> - <td><input id="pref_subject" type="text"></input></td> - </tr> - <tr> - <td><p>Default complaint email body</p></td> - <td><input id="pref_body" type="text"></input></td> - </tr> - <tr> - <td><input type="button" value="Save changes" id="save_changes"></input></td> - <td></td> - </tr> - </table> - <script type="text/javascript" src="pref.js"></script> +[Message generated by LibreJS. See https://www.gnu.org/software/librejs/ for more information] +</textarea> + </fieldset> + </div> </body> </html> diff --git a/html/preferences_panel/prefs.css b/html/preferences_panel/prefs.css new file mode 100644 index 0000000..b52d6c5 --- /dev/null +++ b/html/preferences_panel/prefs.css @@ -0,0 +1,91 @@ +@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; +} + +#section-complaint { + display: none; /* TODO: Complaint to owner UI */ +} diff --git a/main_background.js b/main_background.js index d12ff54..9e08976 100644 --- a/main_background.js +++ b/main_background.js @@ -26,7 +26,7 @@ var jssha = require('jssha'); var walk = require("acorn/dist/walk"); var legacy_license_lib = require("./legacy_license_check.js"); var {ResponseProcessor} = require("./bg/ResponseProcessor"); -var {Storage, ListStore} = require("./bg/Storage"); +var {Storage, ListStore} = require("./common/Storage"); var {ListManager} = require("./bg/ListManager"); var {ExternalLicenses} = require("./bg/ExternalLicenses"); @@ -37,8 +37,8 @@ console.log("main_background.js"); * Also, it controls whether or not this part of the code logs to the console. * */ -var DEBUG = false; // debug the JS evaluation -var PRINT_DEBUG = false; // Everything else +var DEBUG = false; // debug the JS evaluation +var PRINT_DEBUG = false; // Everything else var time = Date.now(); function dbg_print(a,b){ @@ -134,16 +134,16 @@ function options_listener(changes, area){ // TODO: See if this can be minimized function flushed(){ dbg_print("cache flushed"); - } + } //var flushingCache = browser.webRequest.handlerBehaviorChanged(flushed); - + dbg_print("Items updated in area" + area +": "); var changedItems = Object.keys(changes); var changed_items = ""; for (var i = 0; i < changedItems.length; i++){ - var item = changedItems[i]; + var item = changedItems[i]; changed_items += item + ","; } dbg_print(changed_items); @@ -167,7 +167,7 @@ async function createReport(initializer = null) { template.url = (await browser.tabs.get(initializer.tabId)).url; } } - + template.site = ListStore.siteItem(template.url); template.siteStatus = listManager.getStatus(template.site); return template; @@ -224,7 +224,7 @@ function debug_print_local(){ * * NOTE: This WILL break if you provide inconsistent URLs to it. * Make sure it will use the right URL when refering to a certain script. -* +* */ async function updateReport(tabId, oldReport, updateUI = false){ let {url} = oldReport; @@ -253,8 +253,8 @@ async function updateReport(tabId, oldReport, updateUI = false){ * * Sends a message to the content script that adds a popup entry for a tab. * -* The action argument is an object with two properties: one named either -* "accepted","blocked", "whitelisted", "blacklisted" or "unknown", whose value +* The action argument is an object with two properties: one named either +* "accepted","blocked", "whitelisted", "blacklisted" or "unknown", whose value * is the array [scriptName, reason], and another named "url". Example: * action = { * "accepted": ["jquery.js (someHash)","Whitelisted by user"], @@ -269,9 +269,9 @@ async function updateReport(tabId, oldReport, updateUI = false){ */ async function addReportEntry(tabId, scriptHashOrUrl, action) { let report = activityReports[tabId]; - if (!report) report = activityReports[tabId] = + if (!report) report = activityReports[tabId] = await createReport({tabId}); - + let type, actionValue; for (type of ["accepted", "blocked", "whitelisted", "blacklisted"]) { if (type in action) { @@ -307,14 +307,14 @@ async function addReportEntry(tabId, scriptHashOrUrl, action) { console.error("action %o, type %s, entryType %s", action, type, entryType, e); entryType = "unknown"; } - + if (activeMessagePorts[tabId]) { try { activeMessagePorts[tabId].postMessage({show_info: report}); } catch(e) { } } - + browser.sessions.setTabValue(tabId, report.url, report); updateBadge(tabId, report); return entryType; @@ -347,21 +347,21 @@ function connected(p) { p.postMessage(items); } browser.storage.local.get(cb); - return; + return; } p.onMessage.addListener(async function(m) { var update = false; var contact_finder = false; - + for (let action of ["whitelist", "blacklist", "forget"]) { if (m[action]) { let [key] = m[action]; - if (m.site) key = ListStore.siteItem(key); + if (m.site) key = ListStore.siteItem(key); await listManager[action](key); update = true; } } - + if(m.report_tab){ openReportInTab(m.report_tab); } @@ -380,9 +380,9 @@ function connected(p) { console.log("Delete local storage"); debug_delete_local(); } - + let tabs = await browser.tabs.query({active: true, currentWindow: true}); - + if(contact_finder){ let tab = tabs.pop(); dbg_print(`[TABID:${tab.id}] Injecting contact finder`); @@ -397,13 +397,13 @@ function connected(p) { for(let tab of tabs) { if(activityReports[tab.id]){ // If we have some data stored here for this tabID, send it - dbg_print(`[TABID: ${tab.id}] Sending stored data associated with browser action'`); + dbg_print(`[TABID: ${tab.id}] Sending stored data associated with browser action'`); p.postMessage({"show_info": activityReports[tab.id]}); } else{ // create a new entry let report = activityReports[tab.id] = await createReport({"url": tab.url, tabId: tab.id}); - p.postMessage({show_info: report}); - dbg_print(`[TABID: ${tab.id}] No data found, creating a new entry for this window.`); + p.postMessage({show_info: report}); + dbg_print(`[TABID: ${tab.id}] No data found, creating a new entry for this window.`); } } } @@ -430,7 +430,7 @@ function delete_removed_tab_info(tab_id, remove_info){ /** * Called when the tab gets updated / activated * -* Here we check if new tab's url matches activityReports[tabId].url, and if +* Here we check if new tab's url matches activityReports[tabId].url, and if * it doesn't we use the session cached value (if any). * */ @@ -441,7 +441,7 @@ async function onTabUpdated(tabId, changedInfo, tab) { if (!(report && report.url === url)) { let cache = await browser.sessions.getTabValue(tabId, url); // on session restore tabIds may change - if (cache && cache.tabId !== tabId) cache.tabId = tabId; + if (cache && cache.tabId !== tabId) cache.tabId = tabId; updateBadge(tabId, activityReports[tabId] = cache); } } @@ -457,9 +457,9 @@ var fname_data = require("./fname_data.json").fname_data; //************************this part can be tested in the HTML file index.html's script test.js**************************** function full_evaluate(script){ - var res = true; + var res = true; if(script === undefined || script == ""){ - return [true,"Harmless null script"]; + return [true,"Harmless null script"]; } var ast = acorn.parse_dammit(script).body[0]; @@ -470,10 +470,10 @@ function full_evaluate(script){ var loopkeys = {"for":true,"if":true,"while":true,"switch":true}; var operators = {"||":true,"&&":true,"=":true,"==":true,"++":true,"--":true,"+=":true,"-=":true,"*":true}; try{ - var tokens = acorn_base.tokenizer(script); + var tokens = acorn_base.tokenizer(script); }catch(e){ console.warn("Tokenizer could not be initiated (probably invalid code)"); - return [false,"Tokenizer could not be initiated (probably invalid code)"]; + return [false,"Tokenizer could not be initiated (probably invalid code)"]; } try{ var toke = tokens.getToken(); @@ -512,27 +512,27 @@ function full_evaluate(script){ return script.charAt(end+i) == "["; } var error_count = 0; - while(toke !== undefined && toke.type != acorn_base.tokTypes.eof){ + while(toke !== undefined && toke.type != acorn_base.tokTypes.eof){ if(toke.type.keyword !== undefined){ //dbg_print("Keyword:"); //dbg_print(toke); - + // This type of loop detection ignores functional loop alternatives and ternary operators if(toke.type.keyword == "function"){ dbg_print("%c NONTRIVIAL: Function declaration.","color:red"); - if(DEBUG == false){ + if(DEBUG == false){ return [false,"NONTRIVIAL: Function declaration."]; - } + } } if(loopkeys[toke.type.keyword] !== undefined){ amtloops++; if(amtloops > 3){ dbg_print("%c NONTRIVIAL: Too many loops/conditionals.","color:red"); - if(DEBUG == false){ + if(DEBUG == false){ return [false,"NONTRIVIAL: Too many loops/conditionals."]; - } + } } } }else if(toke.value !== undefined && operators[toke.value] !== undefined){ @@ -540,39 +540,39 @@ function full_evaluate(script){ // kind of primitive (I.e. a number) }else if(toke.value !== undefined){ var status = fname_data[toke.value]; - if(status === true){ // is the identifier banned? + if(status === true){ // is the identifier banned? dbg_print("%c NONTRIVIAL: nontrivial token: '"+toke.value+"'","color:red"); - if(DEBUG == false){ + if(DEBUG == false){ return [false,"NONTRIVIAL: nontrivial token: '"+toke.value+"'"]; - } + } }else if(status === false){// is the identifier not banned? // Is there bracket suffix notation? if(is_bsn(toke.end)){ dbg_print("%c NONTRIVIAL: Bracket suffix notation on variable '"+toke.value+"'","color:red"); - if(DEBUG == false){ + if(DEBUG == false){ return [false,"%c NONTRIVIAL: Bracket suffix notation on variable '"+toke.value+"'"]; - } + } } }else if(status === undefined){// is the identifier user defined? // Are arguments being passed to a user defined variable? if(being_called(toke.end)){ dbg_print("%c NONTRIVIAL: User defined variable '"+toke.value+"' called as function","color:red"); - if(DEBUG == false){ + if(DEBUG == false){ return [false,"NONTRIVIAL: User defined variable '"+toke.value+"' called as function"]; - } + } } // Is there bracket suffix notation? if(is_bsn(toke.end)){ dbg_print("%c NONTRIVIAL: Bracket suffix notation on variable '"+toke.value+"'","color:red"); - if(DEBUG == false){ + if(DEBUG == false){ return [false,"NONTRIVIAL: Bracket suffix notation on variable '"+toke.value+"'"]; - } + } } }else{ dbg_print("trivial token:"+toke.value); } } - // If not a keyword or an identifier it's some kind of operator, field parenthesis, brackets + // If not a keyword or an identifier it's some kind of operator, field parenthesis, brackets try{ toke = tokens.getToken(); }catch(e){ @@ -603,7 +603,7 @@ function evaluate(script,name){ var scope_chars = "\{\}\]\[\(\)\,"; var trailing_chars = "\s*"+"\(\.\["; return new RegExp("(?:[^\\w\\d]|^|(?:"+arith_operators+"))"+object+'(?:\\s*?(?:[\\;\\,\\.\\(\\[])\\s*?)',"g"); - } + } reserved_object_regex("window"); var all_strings = new RegExp('".*?"'+"|'.*?'","gm"); var ml_comment = /\/\*([\s\S]+?)\*\//g; @@ -622,7 +622,7 @@ function evaluate(script,name){ var res = reserved_object_regex(reserved_objects[i]).exec(temp); if(res != null){ dbg_print("%c fail","color:red;"); - flag = false; + flag = false; reason = "Script uses a reserved object (" + reserved_objects[i] + ")"; } } @@ -644,7 +644,7 @@ function license_valid(matches){ return [false, "malformed or unrecognized license tag"]; } if(matches[1] != "@license"){ - return [false, "malformed or unrecognized license tag"]; + return [false, "malformed or unrecognized license tag"]; } if(licenses[matches[3]] === undefined){ return [false, "malformed or unrecognized license tag"]; @@ -659,14 +659,14 @@ function license_valid(matches){ * Evaluates the content of a script (license, if it is non-trivial) * * Returns -* [ +* [ * true (accepted) or false (denied), * edited content, -* reason text +* reason text * ] */ function license_read(script_src, name, external = false){ - + var reason_text = ""; var edited_src = ""; @@ -701,7 +701,7 @@ function license_read(script_src, name, external = false){ edited_src += "\n/*\nLIBREJS BLOCKED:"+nontrivial_status[1]+"\n*/\n"; } reason_text += "\n" + nontrivial_status[1]; - + if(parts_denied == true && parts_accepted == true){ reason_text = "Script was determined partly non-trivial after editing. (check source for details)\n"+reason_text; } @@ -709,7 +709,7 @@ function license_read(script_src, name, external = false){ return [false,edited_src,reason_text]; } else return [true,edited_src,reason_text]; - + } // sponge dbg_print("undedited_src:"); @@ -742,7 +742,7 @@ function license_read(script_src, name, external = false){ var license_res = license_valid(matches); if(license_res[0] == true){ edited_src = edited_src + unedited_src.substr(0,endtag_end_index); - reason_text += "\n" + license_res[1]; + reason_text += "\n" + license_res[1]; } else{ edited_src = edited_src + "\n/*\n"+license_res[1]+"\n*/\n"; reason_text += "\n" + license_res[1]; @@ -756,34 +756,34 @@ function license_read(script_src, name, external = false){ // TODO: Test if this script is being loaded from another domain compared to activityReports[tabid]["url"] /** -* Asynchronous function, returns the final edited script as a string, +* Asynchronous function, returns the final edited script as a string, * or an array containing it and the index, if the latter !== -1 */ async function get_script(response, url, tabId = -1, whitelisted = false, index = -1) { function result(scriptSource) { return index === -1 ? scriptSource : [scriptSource, index]; } - + let scriptName = url.split("/").pop(); if (whitelisted) { if (tabId !== -1) { - let site = ListStore.siteItem(url); + let site = ListStore.siteItem(url); // Accept without reading script, it was explicitly whitelisted let reason = whitelist.contains(site) - ? `All ${site} whitelisted by user` + ? `All ${site} whitelisted by user` : "Address whitelisted by user"; addReportEntry(tabId, url, {"whitelisted": [url, reason], url}); } return result(`/* LibreJS: script whitelisted by user preference. */\n${response}`); } - + let [verdict, editedSource, reason] = license_read(response, scriptName, index === -2); - + if (tabId < 0) { return result(verdict ? response : editedSource); } - + let sourceHash = hash(response); let domain = get_domain(url); let report = activityReports[tabId] || (activityReports[tabId] = await createReport({tabId})); @@ -792,17 +792,17 @@ async function get_script(response, url, tabId = -1, whitelisted = false, index let scriptSource = verdict ? response : editedSource; switch(category) { case "blacklisted": - case "whitelisted": + case "whitelisted": return result(`/* LibreJS: script ${category} by user. */\n${scriptSource}`); default: - return result(`/* LibreJS: script ${category}. */\n${scriptSource}`); + return result(`/* LibreJS: script ${category}. */\n${scriptSource}`); } } function updateBadge(tabId, report = null, forceRed = false) { let blockedCount = report ? report.blocked.length + report.blacklisted.length : 0; - let [text, color] = blockedCount > 0 || forceRed + let [text, color] = blockedCount > 0 || forceRed ? [blockedCount && blockedCount.toString() || "!" , "red"] : ["✓", "green"] browser.browserAction.setBadgeText({text, tabId}); browser.browserAction.setBadgeBackgroundColor({color, tabId}); @@ -857,21 +857,21 @@ var ResponseHandler = { async pre(response) { let {request} = response; let {url, type, tabId, frameId, documentUrl} = request; - + url = ListStore.urlItem(url); let site = ListStore.siteItem(url); - + let blacklistedSite = blacklist.contains(site); let blacklisted = blacklistedSite || blacklist.contains(url); let topUrl = request.frameAncestors && request.frameAncestors.pop() || documentUrl; - + if (blacklisted) { if (type === "script") { // abort the request before the response gets fetched - addReportEntry(tabId, url, {url: topUrl, + addReportEntry(tabId, url, {url: topUrl, "blacklisted": [url, blacklistedSite ? `User blacklisted ${site}` : "Blacklisted by user"]}); return ResponseProcessor.REJECT; - } + } // use CSP to restrict JavaScript execution in the page request.responseHeaders.unshift({ name: `Content-security-policy`, @@ -883,7 +883,7 @@ var ResponseHandler = { if (type === "script") { if (whitelisted) { // accept the script and stop processing - addReportEntry(tabId, url, {url: topUrl, + addReportEntry(tabId, url, {url: topUrl, "whitelisted": [url, whitelistedSite ? `User whitelisted ${site}` : "Whitelisted by user"]}); return ResponseProcessor.ACCEPT; } else { @@ -908,7 +908,7 @@ var ResponseHandler = { // let's keep processing return ResponseProcessor.CONTINUE; }, - + /** * Here we do the heavylifting, analyzing unknown scripts */ @@ -931,7 +931,7 @@ async function handle_script(response, whitelisted){ } /** -* Serializes HTMLDocument objects including the root element and +* Serializes HTMLDocument objects including the root element and * the DOCTYPE declaration */ function doc2HTML(doc) { @@ -955,7 +955,7 @@ function remove_noscripts(html_doc){ html_doc.getElementsByName("librejs-path")[i].outerHTML = html_doc.getElementsByName("librejs-path")[i].innerHTML; } } - + return doc2HTML(html_doc); } @@ -966,23 +966,23 @@ function remove_noscripts(html_doc){ function read_metadata(meta_element){ if(meta_element === undefined || meta_element === null){ - return; + return; } - console.log("metadata found"); - + console.log("metadata found"); + var metadata = {}; - - try{ + + try{ metadata = JSON.parse(meta_element.innerHTML); }catch(error){ console.log("Could not parse metadata on page.") return false; } - + var license_str = metadata["intrinsic-events"]; if(license_str === undefined){ - console.log("No intrinsic events license"); + console.log("No intrinsic events license"); return false; } console.log(license_str); @@ -992,7 +992,7 @@ function read_metadata(meta_element){ console.log("invalid (>2 tokens)"); return false; } - + // this should be adequete to escape the HTML escaping parts[0] = parts[0].replace(/&/g, '&'); @@ -1013,25 +1013,25 @@ function read_metadata(meta_element){ * Reads/changes the HTML of a page and the scripts within it. */ async function editHtml(html, documentUrl, tabId, frameId, whitelisted){ - + var parser = new DOMParser(); var html_doc = parser.parseFromString(html, "text/html"); - + // moves external licenses reference, if any, before any <SCRIPT> element - ExternalLicenses.optimizeDocument(html_doc, {tabId, frameId, documentUrl}); - + ExternalLicenses.optimizeDocument(html_doc, {tabId, frameId, documentUrl}); + let url = ListStore.urlItem(documentUrl); - + if (whitelisted) { // don't bother rewriting await get_script(html, url, tabId, whitelisted); // generates whitelisted report return null; } var scripts = html_doc.scripts; - + var meta_element = html_doc.getElementById("LibreJS-info"); var first_script_src = ""; - + // get the potential inline source that can contain a license for (let script of scripts) { // The script must be in-line and exist @@ -1052,7 +1052,7 @@ async function editHtml(html, documentUrl, tabId, frameId, whitelisted){ scripts = []; } else { let modified = false; - + // Deal with intrinsic events for (let element of html_doc.all) { let attributes = element.attributes; @@ -1074,7 +1074,7 @@ async function editHtml(html, documentUrl, tabId, frameId, whitelisted){ } } } - + let modifiedInline = false; for(let i = 0, len = scripts.length; i < len; i++) { let script = scripts[i]; @@ -1090,8 +1090,8 @@ async function editHtml(html, documentUrl, tabId, frameId, whitelisted){ } } if (modified) { - return modifiedInline - ? await remove_noscripts(html_doc) + return modifiedInline + ? await remove_noscripts(html_doc) : doc2HTML(html_doc); } } @@ -1105,7 +1105,7 @@ async function editHtml(html, documentUrl, tabId, frameId, whitelisted){ async function handle_html(response, whitelisted) { let {text, request} = response; let {url, tabId, frameId, type} = request; - if (type === "main_frame") { + if (type === "main_frame") { activityReports[tabId] = await createReport({url, tabId}); updateBadge(tabId); } @@ -1136,7 +1136,7 @@ async function init_addon(){ let all_types = [ "beacon", "csp_report", "font", "image", "imageset", "main_frame", "media", "object", "object_subrequest", "ping", "script", "stylesheet", "sub_frame", - "web_manifest", "websocket", "xbl", "xml_dtd", "xmlhttprequest", "xslt", + "web_manifest", "websocket", "xbl", "xml_dtd", "xmlhttprequest", "xslt", "other" ]; browser.webRequest.onBeforeRequest.addListener( @@ -1144,7 +1144,7 @@ async function init_addon(){ {urls: ["<all_urls>"], types: all_types}, ["blocking"] ); - + // Analyzes all the html documents and external scripts as they're loaded ResponseProcessor.install(ResponseHandler); diff --git a/manifest.json b/manifest.json index aecf9a8..03778d7 100644 --- a/manifest.json +++ b/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 2, "name": "GNU LibreJS [webExtensions]", "short_name": "LibreJS [experimental]", - "version": "7.16", + "version": "7.17", "author": "various", "description": "Only allows free and/or trivial Javascript to run.", "applications": { @@ -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" |