aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorhackademix <giorgio@maone.net>2018-09-13 15:56:29 +0200
committerhackademix <giorgio@maone.net>2018-09-13 15:56:29 +0200
commit4acf282ae6d5ae24a956908a87478d944f8519b9 (patch)
treeec3f865c6a00edc1adaaa2d5e4ead85e87c20e11
parentf87af55eb50a38ba44fcc0397d93e4989304bc8b (diff)
Brand new general settings page for white/black list management and other preferences.
-rw-r--r--html/display_panel/content/display-panel.html5
-rw-r--r--html/display_panel/content/main_panel.js27
-rw-r--r--html/preferences_panel/pref.js316
-rw-r--r--html/preferences_panel/preferences_panel.html89
-rw-r--r--html/preferences_panel/prefs.css87
-rw-r--r--manifest.json3
6 files changed, 435 insertions, 92 deletions
diff --git a/html/display_panel/content/display-panel.html b/html/display_panel/content/display-panel.html
index 2ed0c9c..df153b3 100644
--- a/html/display_panel/content/display-panel.html
+++ b/html/display_panel/content/display-panel.html
@@ -36,8 +36,8 @@
<div>
<a class="libre"
id="ljs-settings"
- href="javascript:void"
- title="LibreJS Whitelist Settings">
+ href= href="https://www.gnu.org/software/librejs/"
+ title="LibreJS Page Settings">
<h1 class="libre">LibreJS</h1>
</a>
</div>
@@ -52,6 +52,7 @@
</div>
<button id="complain">Complain to site owner</button>
<button id="report-tab">Show this report in a new tab</button>
+ <button id="open-options">Settings...</button>
</div>
</div>
<div id="info">
diff --git a/html/display_panel/content/main_panel.js b/html/display_panel/content/main_panel.js
index 930f7f2..c55b167 100644
--- a/html/display_panel/content/main_panel.js
+++ b/html/display_panel/content/main_panel.js
@@ -64,6 +64,11 @@ document.querySelector("#complain").onclick = e => {
close();
}
+document.querySelector("#open-options").onclick = e => {
+ browser.runtime.openOptionsPage();
+ close();
+}
+
document.querySelector("#reload").onclick = async e => {
let {tabId} = currentReport;
if (tabId) {
@@ -72,9 +77,9 @@ document.querySelector("#reload").onclick = async e => {
}
};
-/*
+/*
* Takes in the [[file_id, reason],...] array and the group name for one group
-* of scripts found in this tab, rendering it as a list with management buttons.
+* of scripts found in this tab, rendering it as a list with management buttons.
* Groups are "unknown", "blacklisted", "whitelisted", "accepted", and "blocked".
*/
function createList(data, group){
@@ -98,7 +103,7 @@ function createList(data, group){
let [scriptId, reason] = entry;
let li = liTemplate.cloneNode(true);
let a = li.querySelector("a");
- a.href = scriptId.split("(")[0];
+ a.href = scriptId.split("(")[0];
a.textContent = scriptId;
li.querySelector(".reason").textContent = reason;
let bySite = !!reason.match(/https?:\/\/[^/]+\/\*/);
@@ -116,7 +121,7 @@ function createList(data, group){
/**
* Updates scripts lists and buttons to act on them.
* If return_HTML is true, it returns the HTML of the popup window without updating it.
-* example report argument:
+* example report argument:
* {
* "accepted": [["FILENAME 1","REASON 1"],["FILENAME 2","REASON 2"]],
* "blocked": [["FILENAME 1","REASON 1"],["FILENAME 2","REASON 2"]],
@@ -131,29 +136,29 @@ function refreshUI(report) {
currentReport = report;
document.querySelector("#site").className = report.siteStatus || "";
- document.querySelector("#site h2").textContent =
+ document.querySelector("#site h2").textContent =
`This site ${report.site}`;
-
+
for (let toBeErased of document.querySelectorAll("#info h2:not(.site) > *, #info ul > *")) {
toBeErased.remove();
}
-
+
let scriptsCount = 0;
for (let group of ["unknown", "accepted", "whitelisted", "blocked", "blacklisted"]) {
if (group in report) createList(report, group);
scriptsCount += report[group].length;
}
-
+
for (let b of document.querySelectorAll(`.forget, .whitelist, .blacklist`)) {
b.disabled = false;
}
for (let b of document.querySelectorAll(
- `.unknown .forget, .accepted .forget, .blocked .forget,
+ `.unknown .forget, .accepted .forget, .blocked .forget,
.whitelisted .whitelist, .blacklisted .blacklist`
)) {
b.disabled = true;
- }
-
+ }
+
let noscript = scriptsCount === 0;
document.body.classList.toggle("empty", noscript);
}
diff --git a/html/preferences_panel/pref.js b/html/preferences_panel/pref.js
index 4642d32..9cecbb6 100644
--- a/html/preferences_panel/pref.js
+++ b/html/preferences_panel/pref.js
@@ -1,7 +1,8 @@
/**
* GNU LibreJS - A browser add-on to block nonfree nontrivial JavaScript.
-* *
+*
* Copyright (C) 2017 Nathan Nichols
+* Copyright (C) 2018 Giorgio maone
*
* This file is part of GNU LibreJS.
*
@@ -19,59 +20,288 @@
* along with GNU LibreJS. If not, see <http://www.gnu.org/licenses/>.
*/
-var store;
+(() => {
+ "use strict";
-function storage_got(items){
- var inputs = document.getElementsByTagName("input");
+ const LIST_NAMES = ["white", "black"];
- if(items["pref_whitelist"] == "undefined"){
- items["pref_whitelist"] = "";
- }
+ var Model = {
+ lists: {},
+ prefs: null,
- if(items["pref_subject"] == "undefined" || items["pref_subject"] == ""){
- items["pref_subject"] = "Issues with Javascript on your website";
- }
+ malformedUrl(url) {
+ let error = null;
+ try {
+ let objUrl = new URL(url);
+ url = objUrl.href;
+ if (!objUrl.protocol.startsWith("http")) {
+ error = "Please enter http:// or https:// URLs only";
+ } else if (!/^[^*]+\*?$/.test(url)) {
+ error = "Only one single trailing path wildcard (/*) allowed";
+ }
+ } catch (e) {
+ error = "Invalid URL";
+ if (url && !url.includes("://")) error += ": missing protocol, either http:// or https://";
+ else if (url.endsWith("://")) error += ": missing domain name";
+ }
+ return error;
+ },
- if(items["pref_body"] == "undefined" || items["pref_body"] == ""){
- items["pref_body"] = "Please consider using a free license for the Javascript on your website. [Message generated by LibreJS. See https://www.gnu.org/software/librejs/ for more information]";
- }
+ async save(prefs = this.prefs) {
+ if (prefs !== this.prefs) {
+ this.prefs = Object.assign(this.prefs, prefs);
+ }
+ this.saving = true;
+ try {
+ return await browser.storage.local.set(prefs);
+ } finally {
+ this.saving = false;
+ }
+ },
- for(var i = 0; i < inputs.length; i++){
- if(inputs[i].id.indexOf("pref_") != -1){
- if(inputs[i].type == "checkbox" && items[inputs[i].id]){
- inputs[i].checked = true;
+ async addToList(list, ...items) {
+ let other = list === Model.lists.black ? Model.lists.white : Model.lists.black;
+ this.saving = true;
+ try {
+ await Promise.all([
+ other.remove(...items),
+ list.store(...items)
+ ]);
+ } finally {
+ this.saving = false;
}
- if(inputs[i].type == "text" && items[inputs[i].id] != undefined){
- inputs[i].value = items[inputs[i].id];
- }
}
- }
+ };
+ Model.loading = (async () => {
+ let prefsNames = [
+ "whitelist",
+ "blacklist",
+ "subject",
+ "body"
+ ];
+ Model.prefs = await browser.storage.local.get(prefsNames.map(name => `pref_${name}`));
+
+ for (let listName of LIST_NAMES) {
+ let prefName = `pref_${listName}list`;
+ await (Model.lists[listName] = new ListStore(prefName, Storage.CSV))
+ .load(Model.prefs[prefName]);
+ }
+ })();
+
+ var Controller = {
+ init() {
+ let widgetsRoot = this.root = document.getElementById("widgets");
+ for (let widget of widgetsRoot.querySelectorAll('[id^="pref_"]')) {
+ if (widget.id in Model.lists) {
+ populateListUI(widget);
+ } else if (widget.id in Model.prefs) {
+ widget.value = Model.prefs[widget.id];
+ }
+ }
+
+ this.populateListUI();
+ this.syncAll();
+
+ for (let ev in Listeners) {
+ widgetsRoot.addEventListener(ev, Listeners[ev]);
+ }
+ document.getElementById("site").onfocus = e => {
+ if (!e.target.value.trim()) {
+ e.target.value = "https://";
+ }
+ };
+
+ browser.storage.onChanged.addListener(changes => {
+ if (!Model.saving &&
+ ("pref_whitelist" in changes || "pref_blacklist" in changes)) {
+ setTimeout(() => {
+ this.populateListUI();
+ this.syncAll();
+ }, 10);
+ }
+ });
+ },
+
+ async addSite(list) {
+ let url = document.getElementById("site").value.trim();
+
+ if (url && !Model.malformedUrl(url)) {
+ await this.addToList(list, url);
+ }
+ },
+ async addToList(list, ...items) {
+ await Model.addToList(list, ...items);
+ this.populateListUI();
+ this.syncAll();
+ },
+ async swapSelection(list) {
+ let origin = list === Model.lists.black ? "white" : "black";
+ await this.addToList(list, ...Array.map(
+ document.querySelectorAll(`select#${origin} option:checked`),
+ option => option.value)
+ );
+ },
+
+ syncAll() {
+ this.syncListsUI();
+ this.syncSiteUI();
+ },
+
+ syncSiteUI() {
+ let widget = document.getElementById("site");
+ let list2button = listName => document.getElementById(`cmd-${listName}list-site`);
+
+ for (let bi of LIST_NAMES.map(list2button)) {
+ bi.disabled = true;
+ }
+
+ let url = widget.value.trim();
+ let malformedUrl = url && Model.malformedUrl(url);
+ widget.classList.toggle("error", !!malformedUrl);
+ document.getElementById("site-error").textContent = malformedUrl || "";
+ if (!url) return;
+ if (url !== widget.value) {
+ widget.value = url;
+ }
+
+ for (let listName of LIST_NAMES) {
+ let list = Model.lists[listName];
+ if (!list.contains(url)) {
+ list2button(listName).disabled = false;
+ }
+ }
+ },
+
+ syncListsUI() {
+ let total = 0;
+ for (let id of ["black", "white"]) {
+ let selected = document.querySelectorAll(`select#${id} option:checked`).length;
+ let other = id === "black" ? "white" : "black";
+ document.getElementById(`cmd-${other}list`).disabled = selected === 0;
+ total += selected;
+ }
+ document.getElementById("cmd-delete").disabled = total === 0;
+ },
+ async deleteSelection() {
+ for (let id of ["black", "white"]) {
+ let selection = document.querySelectorAll(`select#${id} option:checked`);
+ await Model.lists[id].remove(...Array.map(selection, option => option.value));
+ }
+ this.populateListUI();
+ this.syncAll();
+ },
-}
-browser.storage.local.get(storage_got);
-
-document.getElementById("save_changes").addEventListener("click", function(){
- var inputs = document.getElementsByTagName("input");
- // TODO: validate/sanitize the user inputs
- var data = {};
- for(var i = 0; i < inputs.length; i++){
- if(inputs[i].id.indexOf("pref_") != -1){
- var input_val = "";
- if(inputs[i].type == "checkbox"){
- input_val = inputs[i].checked;
- } else{
- if(inputs[i.value] != "undefined"){
- input_val = inputs[i].value;
- } else{
- input_val = "";
+ populateListUI(widget) {
+ if (!widget) {
+ for(let id of ["white", "black"]) {
+ this.populateListUI(document.getElementById(id));
}
+ return;
+ }
+ widget.innerHTML = "";
+ let items = [...Model.lists[widget.id].items].sort();
+ let options = new DocumentFragment();
+ for (let item of items) {
+ let option = document.createElement("option");
+ option.value = option.textContent = option.title = item;
+ options.appendChild(option);
+ }
+ widget.appendChild(options);
+ }
+ };
+
+ var KeyEvents = {
+ Delete(e) {
+ if (e.target.matches("#lists select")) {
+ Controller.deleteSelection();
+ }
+ },
+ Enter(e) {
+ if (e.target.id === "site") {
+ e.target.parentElement.querySelector("button[default]").click();
+ }
+ },
+ KeyA(e) {
+ if (e.target.matches("select") && e.ctrlKey) {
+ for (let o of e.target.options) {
+ o.selected = true;
+ }
+ Controller.syncListsUI();
}
- var input_id = inputs[i].id;
- data[input_id] = input_val;
}
}
- console.log(data);
- browser.storage.local.set(data);
-});
+ var Listeners = {
+ async change(e) {
+ let {target} = e;
+ let {id} = target;
+
+ if (id in Model.lists) {
+ Controller.syncListsUI();
+ let selection = target.querySelectorAll("option:checked");
+ if (selection.length === 1) {
+ document.getElementById("site").value = selection[0].value;
+ }
+ return;
+ }
+ },
+
+ click(e) {
+ let {target} = e;
+
+ if (!/^cmd-(white|black|delete)(list-site)?/.test(target.id)) return;
+ e.preventDefault();
+ let cmd = RegExp.$1;
+ if (cmd === "delete") {
+ Controller.deleteSelection();
+ return;
+ }
+ let list = Model.lists[cmd];
+ if (list) {
+ Controller[RegExp.$2 ? "addSite" : "swapSelection"](list);
+ return;
+ }
+ },
+
+ keypress(e) {
+ let {code} = e;
+ if (code && typeof KeyEvents[code] === "function") {
+ if (KeyEvents[code](e) === false) {
+ e.preventDefault();
+ }
+ return;
+ }
+ },
+
+ async input(e) {
+ let {target} = e;
+ let {id} = target;
+ if (!id) return;
+
+ if (id === "site") {
+ Controller.syncSiteUI();
+ let url = target.value;
+ if (url) {
+ let o = document.querySelector(`#lists select option[value="${url}"]`);
+ if (o) {
+ o.scrollIntoView();
+ o.selected = true;
+ }
+ }
+ return;
+ }
+
+ if (id.startsWith("pref_")) {
+ await Model.save({[id]: target.value});
+ return;
+ }
+ }
+ };
+
+ window.addEventListener("DOMContentLoaded", async e => {
+ await Model.loading;
+ Controller.init();
+ });
+
+})();
diff --git a/html/preferences_panel/preferences_panel.html b/html/preferences_panel/preferences_panel.html
index 2d01f94..fff6f9c 100644
--- a/html/preferences_panel/preferences_panel.html
+++ b/html/preferences_panel/preferences_panel.html
@@ -1,10 +1,13 @@
+<!doctype html>
<html>
- <head>
+<head>
+<meta charset="utf-8"/>
<!-- /**
* GNU LibreJS - A browser add-on to block nonfree nontrivial JavaScript.
* *
* Copyright (C) 2011, 2012, 2014 Loic J. Duros
* Copyright (C) 2014, 2015 Nik Nyby
+ * Copyright (C) 2018 Giorgio Maone
*
* This file is part of GNU LibreJS.
*
@@ -25,43 +28,59 @@
<title>
LibreJS preferences
</title>
+ <link rel="stylesheet" type="text/css" href="./prefs.css"/>
+ <script type="text/javascript" src="/bg/Storage.js"></script>
+ <script type="text/javascript" src="pref.js"></script>
</head>
-
- <body>
- <h3>
- LibreJS Preferences
- </h3>
- <table>
- <tr>
- <td><p>Allow all scripts from pages with this text <br> in their URL. (Comma seperated, wildcard is *)</p></td>
- <td><input id="pref_whitelist" type="text"></input></td>
- </tr>
- <!--
+ <body>
+ <div class="libre">
+ <a class="libre"
+ id="ljs-settings"
+ href="https://www.gnu.org/software/librejs/"
+ title="LibreJS Settings">
+ <h1 class="libre">LibreJS</h1>
+ </a>
+ <h3>Settings</h3>
+ </div>
+ <div id="widgets">
+ <fieldset><legend>Allow or block scripts matching the following URLs ("*" matches any path)</legend>
+ <label>Type a new whitelist / blacklist entry:</label>
+ <div id="new-site">
+ <input type="text" id="site" value="" placeholder="https://www.gnu.org/*">
+ <button id="cmd-whitelist-site" class="white" title="Whitelist this site" default>Whitelist</button>
+ <button id="cmd-blacklist-site" class="red" title="Blacklist this site">Blacklist</button>
+ </div>
+ <div id="site-error" class="error-msg"></div>
+ <div id="lists">
+ <div class="white list-container">
+ <label>Whitelist (always allow)</label>
+ <select id="white" multiple size="10"></select>
+ </div>
+ <div id="commands">
+ <button id="cmd-delete" title="Delete">x</button>
+ <button id="cmd-blacklist" title="Move to blacklist">&raquo;</button>
+ <button id="cmd-whitelist" title="Move to whitelist">&laquo;</button>
+ </div>
+ <div class="black list-container">
+ <label>Blacklist (always block)</label>
+ <select id="black" multiple size="10"></select>
+ </div>
+ </div>
+ </fieldset>
- <tr>
- <td><p>Display complaint tab on sites where nonfree nontrivial Javascript detected</p></td>
- <td><input id="pref_complaint_tab" type="checkbox"></input></td>
- </tr>
- <tr>
- <td><p>Display notifications of the JavaScript code being analyzed by LibreJS</p></td>
- <td><input id="pref_notify_analyze" type="checkbox"></input></td>
- </tr>
+ <fieldset><legend>Complaint email defaults</legend>
+ <label for="pref_subject">Subject</label>
+ <input id="pref_subject" type="text"
+ value="Issues with Javascript on your website"
+ />
+ <label for="pref_body">Body</label>
+ <textarea id="pref_body" rows="5"
+>Please consider using a free license for the Javascript on your website.
- -->
- <tr>
- <td><p>Default complaint email subject</p></td>
- <td><input id="pref_subject" type="text"></input></td>
- </tr>
- <tr>
- <td><p>Default complaint email body</p></td>
- <td><input id="pref_body" type="text"></input></td>
- </tr>
- <tr>
- <td><input type="button" value="Save changes" id="save_changes"></input></td>
- <td></td>
- </tr>
- </table>
- <script type="text/javascript" src="pref.js"></script>
+[Message generated by LibreJS. See https://www.gnu.org/software/librejs/ for more information]
+</textarea>
+ </fieldset>
+ </div>
</body>
</html>
diff --git a/html/preferences_panel/prefs.css b/html/preferences_panel/prefs.css
new file mode 100644
index 0000000..3ac498e
--- /dev/null
+++ b/html/preferences_panel/prefs.css
@@ -0,0 +1,87 @@
+@import url("chrome://browser/content/extension.css");
+@import url("/html/common.css");
+h3 {
+ position: absolute;
+ bottom: 0px;
+ left: 240px;
+ font-size: 18px;
+}
+textarea {
+ width: 100%;
+}
+fieldset {
+ border: none;
+ padding: 0;
+ margin-top: 1em;
+ border-top: 1px solid #ccc;
+}
+legend {
+ font-weight: bold;
+ margin: 0;
+ padding: 0;
+}
+label, legend {
+ display: block;
+ font-size: 1.2em;
+}
+
+#lists {
+ display: flex;
+ flex-direction: row;
+}
+.list-container {
+ flex: 3;
+ flex-direction: row;
+}
+.list-container select {
+width: 100%
+}
+
+.black {
+ color: #600;
+}
+.white {
+ color: #060;
+}
+
+#commands {
+ display: flex;
+ justify-content: center;
+ flex: none;
+ flex-flow: column nowrap;
+}
+
+#commands button {
+ font-weight: bold;
+}
+input[type="text"] {
+ width: 100%;
+}
+
+#lists label {
+ font-weight: bold;
+}
+#lists select {
+ color: black;
+}
+#black {
+ background-color: #fcc;
+}
+#white {
+ background-color: #cfc;
+}
+
+#new-site {
+ display: flex;
+ flex 2;
+}
+.error-msg {
+ color: red;
+}
+.error-msg::after {
+ content: "\00A0";
+}
+.error {
+ background: #ffe;
+ color: #800;
+}
diff --git a/manifest.json b/manifest.json
index aecf9a8..e952f22 100644
--- a/manifest.json
+++ b/manifest.json
@@ -34,7 +34,8 @@
"default_popup": "html/display_panel/content/display-panel.html"
},
"options_ui": {
- "page": "html/preferences_panel/preferences_panel.html"
+ "page": "html/preferences_panel/preferences_panel.html",
+ "open_in_tab": true
},
"web_accessible_resources": [
"html/report_page/report.html"