/** * 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. * * GNU LibreJS is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * GNU LibreJS is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with GNU LibreJS. If not, see . */ (() => { "use strict"; const LIST_NAMES = ["white", "black"]; var Model = { lists: {}, prefs: null, 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) { if (/^https?:\/\/\*\./.test(url)) { return this.malformedUrl(url.replace("*.", "")); } error = "Invalid URL"; if (url && !url.includes("://")) error += ": missing protocol, either http:// or https://"; else if (url.endsWith("://")) error += ": missing domain name"; } return error; }, 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; } }, 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; } } }; 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(); }, 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 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(); }); })();