From a2c93c2663d4f61b3c88d39f27fa492aca999455 Mon Sep 17 00:00:00 2001
From: hackademix <giorgio@maone.net>
Date: Sep 13 2018 13:47:27 +0000
Subject: [PATCH 1/6] Support for batch async list operations.


---

diff --git a/bg/ListManager.js b/bg/ListManager.js
index 34d9531..7284aca 100644
--- a/bg/ListManager.js
+++ b/bg/ListManager.js
@@ -30,20 +30,21 @@ class ListManager {
     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;
   	}
diff --git a/bg/Storage.js b/bg/Storage.js
index ecdc9e4..a83ce8f 100644
--- a/bg/Storage.js
+++ b/bg/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 };
+}

From f87af55eb50a38ba44fcc0397d93e4989304bc8b Mon Sep 17 00:00:00 2001
From: hackademix <giorgio@maone.net>
Date: Sep 13 2018 13:54:10 +0000
Subject: [PATCH 2/6] Refactored panel visual styles to be reused by the general settings page.


---

diff --git a/html/README b/html/README
new file mode 100644
index 0000000..a56ea46
--- /dev/null
+++ b/html/README
@@ -0,0 +1,21 @@
+/**
+ * GNU LibreJS - A browser add-on to block nonfree nontrivial JavaScript.
+ * *
+ * Copyright (C) 2011, 2012, 2014 Loic J. Duros
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see  <http://www.gnu.org/licenses/>.
+ *
+ */
+
+All images in this directory are free, released under the GPLv3 or later.
\ No newline at end of file
diff --git a/html/background-panel.png b/html/background-panel.png
new file mode 100644
index 0000000..022ffb3
Binary files /dev/null and b/html/background-panel.png differ
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/README b/html/display_panel/content/README
deleted file mode 100644
index a56ea46..0000000
--- a/html/display_panel/content/README
+++ /dev/null
@@ -1,21 +0,0 @@
-/**
- * GNU LibreJS - A browser add-on to block nonfree nontrivial JavaScript.
- * *
- * Copyright (C) 2011, 2012, 2014 Loic J. Duros
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see  <http://www.gnu.org/licenses/>.
- *
- */
-
-All images in this directory are free, released under the GPLv3 or later.
\ No newline at end of file
diff --git a/html/display_panel/content/background-panel.png b/html/display_panel/content/background-panel.png
deleted file mode 100644
index 022ffb3..0000000
Binary files a/html/display_panel/content/background-panel.png and /dev/null differ
diff --git a/html/display_panel/content/librejs-title-old.png b/html/display_panel/content/librejs-title-old.png
deleted file mode 100644
index 8a11527..0000000
Binary files a/html/display_panel/content/librejs-title-old.png and /dev/null differ
diff --git a/html/display_panel/content/librejs-title.png b/html/display_panel/content/librejs-title.png
deleted file mode 100644
index c1a911c..0000000
Binary files a/html/display_panel/content/librejs-title.png and /dev/null differ
diff --git a/html/display_panel/content/panel-styles.css b/html/display_panel/content/panel-styles.css
index 745c67f..0d849e4 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;
diff --git a/html/librejs-title.png b/html/librejs-title.png
new file mode 100644
index 0000000..c1a911c
Binary files /dev/null and b/html/librejs-title.png differ

From 4acf282ae6d5ae24a956908a87478d944f8519b9 Mon Sep 17 00:00:00 2001
From: hackademix <giorgio@maone.net>
Date: Sep 13 2018 13:56:29 +0000
Subject: [PATCH 3/6] Brand new general settings page for white/black list management and other preferences.


---

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/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 <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..fff6f9c 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="/bg/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><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">&raquo;</button>
+            <button id="cmd-whitelist" title="Move to whitelist">&laquo;</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><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..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"

From c2530fc59734b16a642723b3a9401f9da5033b2d Mon Sep 17 00:00:00 2001
From: hackademix <giorgio@maone.net>
Date: Sep 13 2018 14:18:43 +0000
Subject: [PATCH 4/6] Adjust directory layout and packaging to allow Storage.js to be shared with the settings page in the xpi release.


---

diff --git a/bg/ListManager.js b/bg/ListManager.js
index 7284aca..e0a85e9 100644
--- a/bg/ListManager.js
+++ b/bg/ListManager.js
@@ -23,7 +23,7 @@
   A class to manage whitelist/blacklist operations
 */
 
-let {ListStore} = require("./Storage");
+let {ListStore} = require("../common/Storage");
 
 class ListManager {
   constructor(whitelist, blacklist, builtInHashes) {
diff --git a/bg/Storage.js b/bg/Storage.js
deleted file mode 100644
index a83ce8f..0000000
--- a/bg/Storage.js
+++ /dev/null
@@ -1,130 +0,0 @@
-/**
-* GNU LibreJS - A browser add-on to block nonfree nontrivial JavaScript.
-*
-* Copyright (C) 2018 Giorgio Maone <giorgio@maone.net>
-*
-* This file is part of GNU LibreJS.
-*
-* GNU LibreJS is free software: you can redistribute it and/or modify
-* it under the terms of the GNU General Public License as published by
-* the Free Software Foundation, either version 3 of the License, or
-* (at your option) any later version.
-*
-* GNU LibreJS is distributed in the hope that it will be useful,
-* but WITHOUT ANY WARRANTY; without even the implied warranty of
-* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-* GNU General Public License for more details.
-*
-* You should have received a copy of the GNU General Public License
-* along with GNU LibreJS.  If not, see <http://www.gnu.org/licenses/>.
-*/
-
-/**
- A tiny wrapper around extensions storage API, supporting CSV serialization for
- retro-compatibility
-*/
-"use strict";
-
-var Storage = {
-  ARRAY: {
-    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) {
-      return await browser.storage.local.set({[key]: [...list]});
-    },
-  },
-
-  CSV: {
-    async load(key) {
-      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(",")});
-    }
-  }
-};
-
-/**
-  A class to hold and persist blacklists and whitelists
-*/
-
-class ListStore {
-  constructor(key, storage = Storage.ARRAY) {
-    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})`;
-  }
-  static urlItem(url) {
-    let queryPos = url.indexOf("?");
-    return queryPos === -1 ? url : url.substring(0, queryPos);
-  }
-  static siteItem(url) {
-    if (url.endsWith("/*")) return url;
-    try {
-      return `${new URL(url).origin}/*`;
-    } catch (e) {
-      return `${url}/*`;
-    }
-  }
-
-  async save() {
-    this._saving = true;
-    try {
-      return await this.storage.save(this.key, this.items);
-    } finally {
-      this._saving = false;
-    }
-  }
-
-  async load(values = undefined) {
-    try {
-      this.items = await this.storage.load(this.key, values);
-    } catch (e) {
-      console.error(e);
-    }
-    return this.items;
-  }
-
-  async store(...items) {
-    let size = this.items.size;
-    let changed = false;
-    for (let item of items) {
-      if (size !== this.items.add(item).size) {
-        changed = true;
-      }
-    }
-    return changed && 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);
-  }
-}
-if (typeof module === "object") {
-  module.exports = { ListStore, Storage };
-}
diff --git a/build.sh b/build.sh
index 49104b0..264eb00 100755
--- a/build.sh
+++ b/build.sh
@@ -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/common/Storage.js b/common/Storage.js
new file mode 100644
index 0000000..a83ce8f
--- /dev/null
+++ b/common/Storage.js
@@ -0,0 +1,130 @@
+/**
+* GNU LibreJS - A browser add-on to block nonfree nontrivial JavaScript.
+*
+* Copyright (C) 2018 Giorgio Maone <giorgio@maone.net>
+*
+* This file is part of GNU LibreJS.
+*
+* GNU LibreJS is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published by
+* the Free Software Foundation, either version 3 of the License, or
+* (at your option) any later version.
+*
+* GNU LibreJS is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+* GNU General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with GNU LibreJS.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+/**
+ A tiny wrapper around extensions storage API, supporting CSV serialization for
+ retro-compatibility
+*/
+"use strict";
+
+var Storage = {
+  ARRAY: {
+    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) {
+      return await browser.storage.local.set({[key]: [...list]});
+    },
+  },
+
+  CSV: {
+    async load(key) {
+      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(",")});
+    }
+  }
+};
+
+/**
+  A class to hold and persist blacklists and whitelists
+*/
+
+class ListStore {
+  constructor(key, storage = Storage.ARRAY) {
+    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})`;
+  }
+  static urlItem(url) {
+    let queryPos = url.indexOf("?");
+    return queryPos === -1 ? url : url.substring(0, queryPos);
+  }
+  static siteItem(url) {
+    if (url.endsWith("/*")) return url;
+    try {
+      return `${new URL(url).origin}/*`;
+    } catch (e) {
+      return `${url}/*`;
+    }
+  }
+
+  async save() {
+    this._saving = true;
+    try {
+      return await this.storage.save(this.key, this.items);
+    } finally {
+      this._saving = false;
+    }
+  }
+
+  async load(values = undefined) {
+    try {
+      this.items = await this.storage.load(this.key, values);
+    } catch (e) {
+      console.error(e);
+    }
+    return this.items;
+  }
+
+  async store(...items) {
+    let size = this.items.size;
+    let changed = false;
+    for (let item of items) {
+      if (size !== this.items.add(item).size) {
+        changed = true;
+      }
+    }
+    return changed && 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);
+  }
+}
+if (typeof module === "object") {
+  module.exports = { ListStore, Storage };
+}
diff --git a/html/preferences_panel/preferences_panel.html b/html/preferences_panel/preferences_panel.html
index fff6f9c..ab89d92 100644
--- a/html/preferences_panel/preferences_panel.html
+++ b/html/preferences_panel/preferences_panel.html
@@ -29,7 +29,7 @@
     LibreJS preferences
   </title>
   <link rel="stylesheet" type="text/css" href="./prefs.css"/>
-  <script type="text/javascript" src="/bg/Storage.js"></script>
+  <script type="text/javascript" src="/common/Storage.js"></script>
 	<script type="text/javascript" src="pref.js"></script>
   </head>
 
diff --git a/main_background.js b/main_background.js
index 08007f8..b003c90 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(/&amp;/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);
 

From 78af0be0cfd259e59474dc7ac79144ce205139dd Mon Sep 17 00:00:00 2001
From: hackademix <giorgio@maone.net>
Date: Sep 13 2018 20:36:41 +0000
Subject: [PATCH 5/6] Version bump: 7.17.


---

diff --git a/manifest.json b/manifest.json
index e952f22..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": {

From 3bf972bafeac738301623db6b08bedfd476abeca Mon Sep 17 00:00:00 2001
From: hackademix <giorgio@maone.net>
Date: Sep 13 2018 20:37:11 +0000
Subject: [PATCH 6/6] Temporary hiding complain to owner feature until ready for prime time.


---

diff --git a/html/display_panel/content/panel-styles.css b/html/display_panel/content/panel-styles.css
index 0d849e4..cbf5cf5 100644
--- a/html/display_panel/content/panel-styles.css
+++ b/html/display_panel/content/panel-styles.css
@@ -149,3 +149,9 @@ span.blocked {
   width: 100%;
   text-align: center;
 }
+
+
+
+#complain {
+  display: none; /* TODO: Complaint to owner UI */
+}
diff --git a/html/preferences_panel/preferences_panel.html b/html/preferences_panel/preferences_panel.html
index ab89d92..effb724 100644
--- a/html/preferences_panel/preferences_panel.html
+++ b/html/preferences_panel/preferences_panel.html
@@ -44,7 +44,7 @@
         <h3>Settings</h3>
     </div>
     <div id="widgets">
-      <fieldset><legend>Allow or block scripts matching the following URLs ("*" matches any path)</legend>
+      <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/*">
@@ -69,7 +69,7 @@
         </div>
       </fieldset>
 
-      <fieldset><legend>Complaint email defaults</legend>
+      <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"
diff --git a/html/preferences_panel/prefs.css b/html/preferences_panel/prefs.css
index 3ac498e..b52d6c5 100644
--- a/html/preferences_panel/prefs.css
+++ b/html/preferences_panel/prefs.css
@@ -85,3 +85,7 @@ input[type="text"] {
   background: #ffe;
   color: #800;
 }
+
+#section-complaint {
+  display: none; /* TODO: Complaint to owner UI */
+}

