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