#17 Brand new settings UI to manage whitelist, blacklist and other preferences.
Merged 5 years ago by quidam. Opened 5 years ago by gioma1.
gioma1/librejs features/settings-ui  into  master

file modified
+19 -18
@@ -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 @@ 

      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;

    	}

file modified
+1
@@ -12,6 +12,7 @@ 

  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

common/Storage.js bg/Storage.js
file renamed
+44 -18
@@ -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 @@ 

        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 @@ 

      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 @@ 

        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 };

+ }

html/README html/display_panel/content/README
file renamed
file was moved with no change to the file
html/background-panel.png html/display_panel/content/background-panel.png
file renamed
file was moved with no change to the file
file added
+29
@@ -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;

+ }

@@ -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">

@@ -64,6 +64,11 @@ 

    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 @@ 

    }

  };

  

- /* 

+ /*

  *	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 @@ 

     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 @@ 

  /**

  * 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 @@ 

    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);

  }

@@ -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 @@ 

    width: 100%;

    text-align: center;

  }

+ 

+ 

+ 

+ #complain {

+   display: none; /* TODO: Complaint to owner UI */

+ }

html/librejs-title.png html/display_panel/content/librejs-title.png
file renamed
file was moved with no change to the file
file modified
+273 -43
@@ -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();

+ 	});

+ 

+ })();

@@ -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">&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 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>

@@ -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 */

+ }

file modified
+95 -95
@@ -26,7 +26,7 @@ 

  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 @@ 

  *	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 @@ 

  	// 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 @@ 

  			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 @@ 

  *

  *	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 @@ 

  *

  *	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 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 @@ 

  		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 @@ 

  			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 @@ 

  			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 @@ 

  			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 @@ 

  /**

  *	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 @@ 

  	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 @@ 

  //************************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 @@ 

  		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 @@ 

  			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 @@ 

  				// 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 @@ 

  		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 @@ 

  		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 @@ 

  		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 @@ 

  *	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 @@ 

  				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 @@ 

  				return [false,edited_src,reason_text];

  			}

  			else return [true,edited_src,reason_text];

- 			

+ 

  		}

  		// sponge

  		dbg_print("undedited_src:");
@@ -742,7 +742,7 @@ 

  		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 @@ 

  // 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 @@ 

  	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 @@ 

  	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 @@ 

  			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 @@ 

  		//  let's keep processing

  		return ResponseProcessor.CONTINUE;

  	},

- 	

+ 

  	/**

  	*	Here we do the heavylifting, analyzing unknown scripts

  	*/
@@ -931,7 +931,7 @@ 

  }

  

  /**

- * 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 @@ 

  			html_doc.getElementsByName("librejs-path")[i].outerHTML = html_doc.getElementsByName("librejs-path")[i].innerHTML;

  		}

  	}

- 	

+ 

  	return doc2HTML(html_doc);

  }

  
@@ -966,23 +966,23 @@ 

  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 @@ 

  			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 @@ 

  * 	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 @@ 

  		scripts = [];

  	} else {

  		let modified = false;

- 		

+ 

  		// Deal with intrinsic events

  		for (let element of html_doc.all) {

  			let attributes = element.attributes;
@@ -1074,7 +1074,7 @@ 

  				}

  			}

  		}

- 		

+ 

  		let modifiedInline = false;

  		for(let i = 0, len = scripts.length; i < len; i++) {

  			let script = scripts[i];
@@ -1090,8 +1090,8 @@ 

  				}

  			}

  			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 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 @@ 

  	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 @@ 

  		{urls: ["<all_urls>"], types: all_types},

  		["blocking"]

  	);

- 	

+ 

  	// Analyzes all the html documents and external scripts as they're loaded

  	ResponseProcessor.install(ResponseHandler);

  

file modified
+3 -2
@@ -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"

Quite feature-rich CRUD interface to add, remove and move around whitelist and blacklist entries (also in asynchronous batches), plus real time updating of the complaint email defaults.

Keyboard shortcuts:
- Enter whitelists the currently typed URL, if feasible
- Ctrl+A selects all the entries of the currently focused list
- DELETE removes all the selected entries (from both the lists)

Use the tiny buttons on the center to delete entries or swap them between the two lists.

1 new commit added

  • Adjust directory layout and packaging to allow Storage.js to be shared with the settings page in the xpi release.
5 years ago

2 new commits added

  • Temporary hiding complain to owner feature until ready for prime time.
  • Version bump: 7.17.
5 years ago

Pull-Request has been merged by quidam

5 years ago