nsion to facilitate replacing sites' js with user-supplied scripts
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--background/cookie_filter.js45
-rw-r--r--background/main.js33
-rw-r--r--background/page_actions_server.js11
-rw-r--r--background/policy_injector.js33
-rw-r--r--background/storage.js117
-rwxr-xr-xbuild.sh11
-rw-r--r--common/misc.js50
-rw-r--r--common/observable.js28
-rw-r--r--common/patterns.js167
-rw-r--r--common/settings_query.js27
-rw-r--r--common/signing.js73
-rw-r--r--common/storage_light.js129
-rw-r--r--common/storage_raw.js54
-rw-r--r--content/activity_info_server.js6
-rw-r--r--content/freezer.js1
-rw-r--r--content/main.js114
-rw-r--r--content/page_actions.js35
-rw-r--r--copyright9
-rw-r--r--html/DOM_helpers.js23
-rw-r--r--html/MOZILLA_scrollbar_fix.css46
-rw-r--r--html/back_button.css50
-rw-r--r--html/base.css69
-rw-r--r--html/default_blocking_policy.html18
-rw-r--r--html/default_blocking_policy.js47
-rw-r--r--html/display-panel.html374
-rw-r--r--html/display-panel.js191
-rw-r--r--html/import_frame.html45
-rw-r--r--html/import_frame.js73
-rw-r--r--html/options.html421
-rw-r--r--html/options_main.js75
-rw-r--r--html/reset.css49
-rw-r--r--html/table.css45
-rw-r--r--manifest.json5
33 files changed, 1799 insertions, 675 deletions
diff --git a/background/cookie_filter.js b/background/cookie_filter.js
new file mode 100644
index 0000000..fea2d23
--- /dev/null
+++ b/background/cookie_filter.js
@@ -0,0 +1,45 @@
+/**
+ * part of Hachette
+ * Filtering request headers to remove hachette cookies that might have slipped
+ * through.
+ *
+ * Copyright (C) 2021 Wojtek Kosior
+ * Redistribution terms are gathered in the `copyright' file.
+ */
+
+/*
+ * IMPORTS_START
+ * IMPORT extract_signed
+ * IMPORTS_END
+ */
+
+function is_valid_hachette_cookie(cookie)
+{
+ const match = /^hachette-(\w*)=(.*)$/.exec(cookie);
+ if (!match)
+ return false;
+
+ return !extract_signed(match.slice(1, 3)).fail;
+}
+
+function remove_hachette_cookies(header)
+{
+ if (header.name !== "Cookie")
+ return header;
+
+ const cookies = header.value.split("; ");
+ const value = cookies.filter(c => !is_valid_hachette_cookie(c)).join("; ");
+
+ return value ? {name: "Cookie", value} : null;
+}
+
+function filter_cookie_headers(headers)
+{
+ return headers.map(remove_hachette_cookies).filter(h => h);
+}
+
+/*
+ * EXPORTS_START
+ * EXPORT filter_cookie_headers
+ * EXPORTS_END
+ */
diff --git a/background/main.js b/background/main.js
index 2e9fa50..03cd5d7 100644
--- a/background/main.js
+++ b/background/main.js
@@ -9,6 +9,7 @@
* IMPORTS_START
* IMPORT TYPE_PREFIX
* IMPORT get_storage
+ * IMPORT light_storage
* IMPORT start_storage_server
* IMPORT start_page_actions_server
* IMPORT browser
@@ -17,6 +18,7 @@
* IMPORT gen_nonce
* IMPORT inject_csp_headers
* IMPORT apply_stream_filter
+ * IMPORT filter_cookie_headers
* IMPORT is_chrome
* IMPORTS_END
*/
@@ -49,6 +51,7 @@ browser.runtime.onInstalled.addListener(init_ext);
let storage;
+let policy_observable = {};
function on_headers_received(details)
{
@@ -58,7 +61,8 @@ function on_headers_received(details)
const [pattern, settings] = query_best(storage, details.url);
const has_payload = !!(settings && settings.components);
- const allow = !has_payload && !!(settings && settings.allow);
+ const allow = !has_payload &&
+ !!(settings ? settings.allow : policy_observable.value);
const nonce = gen_nonce();
const policy = {allow, url, nonce, has_payload};
@@ -70,7 +74,7 @@ function on_headers_received(details)
skip = true;
}
- headers = inject_csp_headers(details, headers, policy);
+ headers = inject_csp_headers(headers, policy);
skip = skip || (details.statusCode >= 300 && details.statusCode < 400);
if (!skip) {
@@ -82,19 +86,40 @@ function on_headers_received(details)
return {responseHeaders: headers};
}
+function on_before_send_headers(details)
+{
+ let headers = details.requestHeaders;
+ headers = filter_cookie_headers(headers);
+ return {requestHeaders: headers};
+}
+
+const all_types = [
+ "main_frame", "sub_frame", "stylesheet", "script", "image", "font",
+ "object", "xmlhttprequest", "ping", "csp_report", "media", "websocket",
+ "other", "main_frame", "sub_frame"
+];
+
async function start_webRequest_operations()
{
storage = await get_storage();
- const extra_opts = ["blocking", "responseHeaders"];
+ const extra_opts = ["blocking"];
if (is_chrome)
extra_opts.push("extraHeaders");
browser.webRequest.onHeadersReceived.addListener(
on_headers_received,
{urls: ["<all_urls>"], types: ["main_frame", "sub_frame"]},
- extra_opts
+ extra_opts.concat("responseHeaders")
);
+
+ browser.webRequest.onBeforeSendHeaders.addListener(
+ on_before_send_headers,
+ {urls: ["<all_urls>"], types: all_types},
+ extra_opts.concat("requestHeaders")
+ );
+
+ policy_observable = await light_storage.observe_var("default_allow");
}
start_webRequest_operations();
diff --git a/background/page_actions_server.js b/background/page_actions_server.js
index 58a0073..b0db5f5 100644
--- a/background/page_actions_server.js
+++ b/background/page_actions_server.js
@@ -8,6 +8,7 @@
/*
* IMPORTS_START
* IMPORT get_storage
+ * IMPORT light_storage
* IMPORT TYPE_PREFIX
* IMPORT CONNECTION_TYPE
* IMPORT browser
@@ -20,17 +21,17 @@
var storage;
var handler;
+let policy_observable;
function send_actions(url, port)
{
- const [pattern, settings] = query_best(storage, url);
+ let [pattern, settings] = query_best(storage, url);
+ if (!settings)
+ settings = {allow: policy_observable && policy_observable.value};
const repos = storage.get_all(TYPE_PREFIX.REPO);
port.postMessage(["settings", [pattern, settings, repos]]);
- if (settings === undefined)
- return;
-
let components = settings.components;
let processed_bags = new Set();
@@ -127,6 +128,8 @@ async function start_page_actions_server()
storage = await get_storage();
listen_for_connection(CONNECTION_TYPE.PAGE_ACTIONS, new_connection);
+
+ policy_observable = await light_storage.observe_var("default_allow");
}
/*
diff --git a/background/policy_injector.js b/background/policy_injector.js
index 1d4db6f..72318d4 100644
--- a/background/policy_injector.js
+++ b/background/policy_injector.js
@@ -16,25 +16,27 @@
* IMPORTS_END
*/
-function inject_csp_headers(details, headers, policy)
+function inject_csp_headers(headers, policy)
{
- const url = details.url;
-
- let orig_csp_headers;
+ let csp_headers;
let old_signature;
let hachette_header;
for (const header of headers.filter(h => h.name === "x-hachette")) {
- const match = /^([^%])(%.*)$/.exec(header.value);
+ /* x-hachette header has format: <signature>_0_<data> */
+ const match = /^([^_]+)_(0_.*)$/.exec(header.value);
if (!match)
continue;
- const old_data = extract_signed(...match.splice(1, 2), [[0]]);
- if (!old_data || old_data.url !== url)
+ const result = extract_signed(...match.slice(1, 3));
+ if (result.fail)
continue;
+ /* This should succeed - it's our self-produced valid JSON. */
+ const old_data = JSON.parse(decodeURIComponent(result.data));
+
/* Confirmed- it's the originals, smuggled in! */
- orig_csp_headers = old_data.csp_headers;
+ csp_headers = old_data.csp_headers;
old_signature = old_data.policy_sig;
hachette_header = header;
@@ -46,24 +48,23 @@ function inject_csp_headers(details, headers, policy)
headers.push(hachette_header);
}
- orig_csp_headers = orig_csp_headers ||
+ csp_headers = csp_headers ||
headers.filter(h => is_csp_header_name(h.name));
/* When blocking remove report-only CSP headers that snitch on us. */
headers = headers.filter(h => !is_csp_header_name(h.name, !policy.allow));
if (old_signature)
- headers = headers.filter(h => h.name.search(old_signature) === -1);
+ headers = headers.filter(h => h.value.search(old_signature) === -1);
- const sanitizer = h => sanitize_csp_header(h, policy);
- headers.push(...orig_csp_headers.map(sanitizer));
+ headers.push(...csp_headers.map(h => sanitize_csp_header(h, policy)));
const policy_str = encodeURIComponent(JSON.stringify(policy));
- const policy_sig = sign_data(policy_str, new Date());
+ const signed_policy = sign_data(policy_str, new Date().getTime());
const later_30sec = new Date(new Date().getTime() + 30000).toGMTString();
headers.push({
name: "Set-Cookie",
- value: `hachette-${policy_sig}=${policy_str}; Expires=${later_30sec};`
+ value: `hachette-${signed_policy.join("=")}; Expires=${later_30sec};`
});
/*
@@ -71,9 +72,9 @@ function inject_csp_headers(details, headers, policy)
* These are signed with a time of 0, as it's not clear there is a limit on
* how long Firefox might retain headers in the cache.
*/
- let hachette_data = {csp_headers: orig_csp_headers, policy_sig, url};
+ let hachette_data = {csp_headers, policy_sig: signed_policy[0]};
hachette_data = encodeURIComponent(JSON.stringify(hachette_data));
- hachette_header.value = sign_data(hachette_data, 0) + hachette_data;
+ hachette_header.value = sign_data(hachette_data, 0).join("_");
/* To ensure there is a CSP header if required */
if (!policy.allow)
diff --git a/background/storage.js b/background/storage.js
index c2160b0..12c0c61 100644
--- a/background/storage.js
+++ b/background/storage.js
@@ -7,7 +7,7 @@
/*
* IMPORTS_START
- * IMPORT TYPE_PREFIX
+ * IMPORT raw_storage
* IMPORT TYPE_NAME
* IMPORT list_prefixes
* IMPORT make_lock
@@ -15,76 +15,17 @@
* IMPORT unlock
* IMPORT make_once
* IMPORT browser
- * IMPORT is_chrome
* IMPORT observables
* IMPORTS_END
*/
var exports = {};
-/* We're yet to decide how to handle errors... */
-
-/* Here are some basic wrappers for storage API functions */
-
-async function get(key)
-{
- try {
- /* Fix for fact that Chrome does not use promises here */
- let promise = is_chrome ?
- new Promise((resolve, reject) =>
- chrome.storage.local.get(key,
- val => resolve(val))) :
- browser.storage.local.get(key);
-
- return (await promise)[key];
- } catch (e) {
- console.log(e);
- }
-}
-
-async function set(key, value)
-{
- try {
- return browser.storage.local.set({[key]: value});
- } catch (e) {
- console.log(e);
- }
-}
-
-async function setn(keys_and_values)
-{
- let obj = Object();
- while (keys_and_values.length > 1) {
- let value = keys_and_values.pop();
- let key = keys_and_values.pop();
- obj[key] = value;
- }
-
- try {
- return browser.storage.local.set(obj);
- } catch (e) {
- console.log(e);
- }
-}
-
-async function set_var(name, value)
-{
- return set(TYPE_PREFIX.VAR + name, value);
-}
-
-async function get_var(name)
-{
- return get(TYPE_PREFIX.VAR + name);
-}
-
-/*
- * A special case of persisted variable is one that contains list
- * of items.
- */
+/* A special case of persisted variable is one that contains list of items. */
async function get_list_var(name)
{
- let list = await get_var(name);
+ let list = await raw_storage.get_var(name);
return list === undefined ? [] : list;
}
@@ -97,7 +38,7 @@ async function list(prefix)
let map = new Map();
for (let item of await get_list_var(name))
- map.set(item, await get(prefix + item));
+ map.set(item, await raw_storage.get(prefix + item));
return {map, prefix, name, observable: observables.make(),
lock: make_lock()};
@@ -175,19 +116,19 @@ async function set_item(item, value, list)
}
async function _set_item(item, value, list)
{
- let key = list.prefix + item;
- let old_val = list.map.get(item);
+ const key = list.prefix + item;
+ const old_val = list.map.get(item);
+ const set_obj = {[key]: value};
if (old_val === undefined) {
- let items = list_items(list);
+ const items = list_items(list);
items.push(item);
- await setn([key, value, "_" + list.name, items]);
- } else {
- await set(key, value);
+ set_obj["_" + list.name] = items;
}
- list.map.set(item, value)
+ await raw_storage.set(set_obj);
+ list.map.set(item, value);
- let change = {
+ const change = {
prefix : list.prefix,
item,
old_val,
@@ -212,20 +153,21 @@ async function remove_item(item, list)
}
async function _remove_item(item, list)
{
- let old_val = list.map.get(item);
+ const old_val = list.map.get(item);
if (old_val === undefined)
return;
- let key = list.prefix + item;
- let items = list_items(list);
- let index = items.indexOf(item);
+ const items = list_items(list);
+ const index = items.indexOf(item);
items.splice(index, 1);
- await setn([key, undefined, "_" + list.name, items]);
-
+ await raw_storage.set({
+ [list.prefix + item]: undefined,
+ ["_" + list.name]: items
+ });
list.map.delete(item);
- let change = {
+ const change = {
prefix : list.prefix,
item,
old_val,
@@ -247,11 +189,11 @@ async function replace_item(old_item, new_item, list, new_val=undefined)
}
async function _replace_item(old_item, new_item, list, new_val=undefined)
{
- let old_val = list.map.get(old_item);
+ const old_val = list.map.get(old_item);
if (new_val === undefined) {
if (old_val === undefined)
return;
- new_val = old_val
+ new_val = old_val;
} else if (new_val === old_val && new_item === old_item) {
return old_val;
}
@@ -261,17 +203,18 @@ async function _replace_item(old_item, new_item, list, new_val=undefined)
return old_val;
}
- let new_key = list.prefix + new_item;
- let old_key = list.prefix + old_item;
- let items = list_items(list);
- let index = items.indexOf(old_item);
+ const items = list_items(list);
+ const index = items.indexOf(old_item);
items[index] = new_item;
- await setn([old_key, undefined, new_key, new_val,
- "_" + list.name, items]);
+ await raw_storage.set({
+ [list.prefix + old_item]: undefined,
+ [list.prefix + new_item]: new_val,
+ ["_" + list.name]: items
+ });
list.map.delete(old_item);
- let change = {
+ const change = {
prefix : list.prefix,
item : old_item,
old_val,
diff --git a/build.sh b/build.sh
index 31f3dec..0659ed1 100755
--- a/build.sh
+++ b/build.sh
@@ -291,6 +291,17 @@ EOF
cp html/*.css $BUILDDIR/html
mkdir $BUILDDIR/icons
cp icons/*.png $BUILDDIR/icons
+
+ if [ "$BROWSER" = "chromium" ]; then
+ for MOZILLA_FILE in $(find $BUILDDIR -name "MOZILLA_*"); do
+ echo > "$MOZILLA_FILE"
+ done
+ fi
+ if [ "$BROWSER" = "mozilla" ]; then
+ for CHROMIUM_FILE in $(find $BUILDDIR -name "CHROMIUM_*"); do
+ echo > "$CHROMIUM_FILE"
+ done
+ fi
}
main "$@"
diff --git a/common/misc.js b/common/misc.js
index 30a9e77..91d60d2 100644
--- a/common/misc.js
+++ b/common/misc.js
@@ -8,9 +8,7 @@
/*
* IMPORTS_START
- * IMPORT sha256
* IMPORT browser
- * IMPORT is_chrome
* IMPORT TYPE_NAME
* IMPORT TYPE_PREFIX
* IMPORTS_END
@@ -45,32 +43,6 @@ function gen_nonce(length) // Default 16
return Uint8toHex(randomData);
}
-function get_secure_salt()
-{
- if (is_chrome)
- return browser.runtime.getManifest().key.substring(0, 50);
- else
- return browser.runtime.getURL("dummy");
-}
-
-function extract_signed(signature, data, times)
-{
- const now = new Date();
- times = times || [[now], [now, -1]];
-
- const reductor =
- (ok, time) => ok || signature === sign_data(data, ...time);
- if (!times.reduce(reductor, false))
- return undefined;
-
- try {
- return JSON.parse(decodeURIComponent(data));
- } catch (e) {
- /* This should not be reached - it's our self-produced valid JSON. */
- console.log("Unexpected internal error - invalid JSON smuggled!", e);
- }
-}
-
/* csp rule that blocks all scripts except for those injected by us */
function csp_rule(nonce)
{
@@ -89,7 +61,7 @@ const report_only_header_name = "content-security-policy-report-only";
function is_csp_header_name(string, include_report_only)
{
- string = string && string.toLowerCase() || "";
+ string = string && string.toLowerCase().trim() || "";
return (include_report_only && string === report_only_header_name) ||
csp_header_names.has(string);
@@ -112,17 +84,13 @@ function open_in_settings(prefix, name)
window.open(url, "_blank");
}
-/* Check if url corresponds to a browser's special page */
-function is_privileged_url(url)
-{
- return !!/^(chrome(-extension)?|moz-extension):\/\/|^about:/i.exec(url);
-}
-
-/* Sign a given string for a given time */
-function sign_data(data, now, hours_offset) {
- let time = Math.floor(now / 3600000) + (hours_offset || 0);
- return sha256(get_secure_salt() + time + data);
-}
+/*
+ * Check if url corresponds to a browser's special page (or a directory index in
+ * case of `file://' protocol).
+ */
+const privileged_reg =
+ /^(chrome(-extension)?|moz-extension):\/\/|^about:|^file:\/\/.*\/$/;
+const is_privileged_url = url => privileged_reg.test(url);
/* Parse a CSP header */
function parse_csp(csp) {
@@ -193,8 +161,6 @@ const matchers = {
/*
* EXPORTS_START
* EXPORT gen_nonce
- * EXPORT extract_signed
- * EXPORT sign_data
* EXPORT csp_rule
* EXPORT is_csp_header_name
* EXPORT nice_name
diff --git a/common/observable.js b/common/observable.js
index 1fb0b0a..02f1c1b 100644
--- a/common/observable.js
+++ b/common/observable.js
@@ -6,28 +6,22 @@
* Redistribution terms are gathered in the `copyright' file.
*/
-function make()
-{
- return new Set();
-}
+const make = (value=undefined) => ({value, listeners: new Set()});
+const subscribe = (observable, cb) => observable.listeners.add(cb);
+const unsubscribe = (observable, cb) => observable.listeners.delete(cb);
-function subscribe(observable, cb)
-{
- observable.add(cb);
-}
-
-function unsubscribe(observable, cb)
-{
- observable.delete(cb);
-}
+const silent_set = (observable, value) => observable.value = value;
+const broadcast = (observable, ...values) =>
+ observable.listeners.forEach(cb => cb(...values));
-function broadcast(observable, event)
+function set(observable, value)
{
- for (const callback of observable)
- callback(event);
+ const old_value = observable.value;
+ silent_set(observable, value);
+ broadcast(observable, value, old_value);
}
-const observables = {make, subscribe, unsubscribe, broadcast};
+const observables = {make, subscribe, unsubscribe, broadcast, silent_set, set};
/*
* EXPORTS_START
diff --git a/common/patterns.js b/common/patterns.js
index be7c650..ebb55ab 100644
--- a/common/patterns.js
+++ b/common/patterns.js
@@ -5,35 +5,48 @@
* Redistribution terms are gathered in the `copyright' file.
*/
-const proto_re = "[a-zA-Z]*:\/\/";
+const proto_regex = /^(\w+):\/\/(.*)$/;
+
+const user_re = "[^/?#@]+@"
const domain_re = "[^/?#]+";
-const segments_re = "/[^?#]*";
-const query_re = "\\?[^#]*";
-
-const url_regex = new RegExp(`\
-^\
-(${proto_re})\
-(${domain_re})\
-(${segments_re})?\
-(${query_re})?\
-#?.*\$\
-`);
+const path_re = "[^?#]*";
+const query_re = "\\??[^#]*";
+
+const http_regex = new RegExp(`^(${domain_re})(${path_re})(${query_re}).*`);
+
+const file_regex = new RegExp(`^(${path_re}).*`);
+
+const ftp_regex = new RegExp(`^(${user_re})?(${domain_re})(${path_re}).*`);
function deconstruct_url(url)
{
- const regex_match = url_regex.exec(url);
- if (regex_match === null)
+ const proto_match = proto_regex.exec(url);
+ if (proto_match === null)
return undefined;
- let [_, proto, domain, path, query] = regex_match;
+ const deco = {proto: proto_match[1]};
+
+ if (deco.proto === "file") {
+ deco.path = file_regex.exec(proto_match[2])[1];
+ } else if (deco.proto === "ftp") {
+ [deco.domain, deco.path] = ftp_regex.exec(proto_match[2]).slice(2, 4);
+ } else {
+ const http_match = http_regex.exec(proto_match[2]);
+ if (!http_match)
+ return undefined;
+ [deco.domain, deco.path, deco.query] = http_match.slice(1, 4);
+ }
+
+ if (deco.domain)
+ deco.domain = deco.domain.split(".");
- domain = domain.split(".");
- let path_trailing_dash =
- path && path[path.length - 1] === "/";
- path = (path || "").split("/").filter(s => s !== "");
- path.unshift("");
+ const leading_dash = deco.path[0] === "/";
+ deco.trailing_dash = deco.path[deco.path.length - 1] === "/";
+ deco.path = deco.path.split("/").filter(s => s !== "");
+ if (leading_dash || deco.path.length === 0)
+ deco.path.unshift("");
- return {proto, domain, path, query, path_trailing_dash};
+ return deco;
}
/* Be sane: both arguments should be arrays of length >= 2 */
@@ -104,84 +117,70 @@ function url_matches(url, pattern)
return false
}
- if (pattern_deco.proto !== url_deco.proto)
- return false;
-
- return domain_matches(url_deco.domain, pattern_deco.domain) &&
- path_matches(url_deco.path, url_deco.path_trailing_dash,
- pattern_deco.path, pattern_deco.path_trailing_dash);
+ return pattern_deco.proto === url_deco.proto &&
+ !(pattern_deco.proto === "file" && pattern_deco.trailing_dash) &&
+ !!url_deco.domain === !!pattern_deco.domain &&
+ (!url_deco.domain ||
+ domain_matches(url_deco.domain, pattern_deco.domain)) &&
+ path_matches(url_deco.path, url_deco.trailing_dash,
+ pattern_deco.path, pattern_deco.trailing_dash);
}
-/*
- * Call callback for every possible pattern that matches url. Return when there
- * are no more patterns or callback returns false.
- */
-function for_each_possible_pattern(url, callback)
+function* each_domain_pattern(domain_segments)
{
- const deco = deconstruct_url(url);
-
- if (deco === undefined) {
- console.log("bad url format", url);
- return;
+ for (let slice = 0; slice < domain_segments.length; slice++) {
+ const domain_part = domain_segments.slice(slice).join(".");
+ const domain_wildcards = [];
+ if (slice === 0)
+ yield domain_part;
+ if (slice === 1)
+ yield "*." + domain_part;
+ if (slice > 1)
+ yield "**." + domain_part;
+ yield "***." + domain_part;
}
+}
- for (let d_slice = 0; d_slice < deco.domain.length; d_slice++) {
- const domain_part = deco.domain.slice(d_slice).join(".");
- const domain_wildcards = [];
- if (d_slice === 0)
- domain_wildcards.push("");
- if (d_slice === 1)
- domain_wildcards.push("*.");
- if (d_slice > 0)
- domain_wildcards.push("**.");
- domain_wildcards.push("***.");
-
- for (const domain_wildcard of domain_wildcards) {
- const domain_pattern = domain_wildcard + domain_part;
-
- for (let s_slice = deco.path.length; s_slice > 0; s_slice--) {
- const path_part = deco.path.slice(0, s_slice).join("/");
- const path_wildcards = [];
- if (s_slice === deco.path.length) {
- if (deco.path_trailing_dash)
- path_wildcards.push("/");
- path_wildcards.push("");
- }
- if (s_slice === deco.path.length - 1 &&
- deco.path[s_slice] !== "*")
- path_wildcards.push("/*");
- if (s_slice < deco.path.length &&
- (deco.path[s_slice] !== "**" ||
- s_slice < deco.path.length - 1))
- path_wildcards.push("/**");
- if (deco.path[s_slice] !== "***" || s_slice < deco.path.length)
- path_wildcards.push("/***");
-
- for (const path_wildcard of path_wildcards) {
- const path_pattern = path_part + path_wildcard;
-
- const pattern = deco.proto + domain_pattern + path_pattern;
-
- if (callback(pattern) === false)
- return;
- }
- }
+function* each_path_pattern(path_segments, trailing_dash)
+{
+ for (let slice = path_segments.length; slice > 0; slice--) {
+ const path_part = path_segments.slice(0, slice).join("/");
+ const path_wildcards = [];
+ if (slice === path_segments.length) {
+ if (trailing_dash)
+ yield path_part + "/";
+ yield path_part;
}
+ if (slice === path_segments.length - 1 && path_segments[slice] !== "*")
+ yield path_part + "/*";
+ if (slice < path_segments.length - 1)
+ yield path_part + "/**";
+ if (slice < path_segments.length - 1 ||
+ path_segments[path_segments.length - 1] !== "***")
+ yield path_part + "/***";
}
}
-function possible_patterns(url)
+/* Generate every possible pattern that matches url. */
+function* each_url_pattern(url)
{
- const patterns = [];
- for_each_possible_pattern(url, patterns.push);
+ const deco = deconstruct_url(url);
- return patterns;
+ if (deco === undefined) {
+ console.log("bad url format", url);
+ return false;
+ }
+
+ const all_domains = deco.domain ? each_domain_pattern(deco.domain) : [""];
+ for (const domain of all_domains) {
+ for (const path of each_path_pattern(deco.path, deco.trailing_dash))
+ yield `${deco.proto}://${domain}${path}`;
+ }
}
/*
* EXPORTS_START
* EXPORT url_matches
- * EXPORT for_each_possible_pattern
- * EXPORT possible_patterns
+ * EXPORT each_url_pattern
* EXPORTS_END
*/
diff --git a/common/settings_query.js b/common/settings_query.js
index e85ae63..b54e580 100644
--- a/common/settings_query.js
+++ b/common/settings_query.js
@@ -8,30 +8,25 @@
/*
* IMPORTS_START
* IMPORT TYPE_PREFIX
- * IMPORT for_each_possible_pattern
+ * IMPORT each_url_pattern
* IMPORTS_END
*/
-function check_pattern(storage, pattern, multiple, matched)
-{
- const settings = storage.get(TYPE_PREFIX.PAGE, pattern);
-
- if (settings === undefined)
- return;
-
- matched.push([pattern, settings]);
-
- if (!multiple)
- return false;
-}
-
function query(storage, url, multiple)
{
const matched = [];
const cb = p => check_pattern(storage, p, multiple, matched);
- for_each_possible_pattern(url, cb);
+ for (const pattern of each_url_pattern(url)) {
+ const result = [pattern, storage.get(TYPE_PREFIX.PAGE, pattern)];
+ if (result[1] === undefined)
+ continue;
+
+ if (!multiple)
+ return result;
+ matched.push(result);
+ }
- return multiple ? matched : (matched[0] || [undefined, undefined]);
+ return multiple ? matched : [undefined, undefined];
}
function query_best(storage, url)
diff --git a/common/signing.js b/common/signing.js
new file mode 100644
index 0000000..2171714
--- /dev/null
+++ b/common/signing.js
@@ -0,0 +1,73 @@
+/**
+ * part of Hachette
+ * Functions related to "signing" of data, refactored to a separate file.
+ *
+ * Copyright (C) 2021 Wojtek Kosior
+ * Redistribution terms are gathered in the `copyright' file.
+ */
+
+/*
+ * IMPORTS_START
+ * IMPORT sha256
+ * IMPORT browser
+ * IMPORT is_chrome
+ * IMPORTS_END
+ */
+
+/*
+ * In order to make certain data synchronously accessible in certain contexts,
+ * hachette smuggles it in string form in places like cookies, URLs and headers.
+ * When using the smuggled data, we first need to make sure it isn't spoofed.
+ * For that, we use this pseudo-signing mechanism.
+ *
+ * Despite what name suggests, no assymetric cryptography is involved, as it
+ * would bring no additional benefits and would incur bigger performance
+ * overhead. Instead, we hash the string data together with some secret value
+ * that is supposed to be known only by this browser instance. Resulting hash
+ * sum plays the role of the signature. In the hash we also include current
+ * time. This way, even if signed data leaks (which shouldn't happen in the
+ * first place), an attacker won't be able to re-use it indefinitely.
+ *
+ * The secret shared between execution contexts has to be available
+ * synchronously. Under Mozilla, this is the extension's per-session id. Under
+ * Chromium, this is the key that resides in the manifest.
+ *
+ * An idea to (under Chromium) instead store the secret in a file fetched
+ * synchronously using XMLHttpRequest is being considered.
+ */
+
+function get_secret()
+{
+ if (is_chrome)
+ return browser.runtime.getManifest().key.substring(0, 50);
+ else
+ return browser.runtime.getURL("dummy");
+}
+
+function extract_signed(signature, signed_data)
+{
+ const match = /^([1-9][0-9]{12}|0)_(.*)$/.exec(signed_data);
+ if (!match)
+ return {fail: "bad format"};
+
+ const result = {time: parseInt(match[1]), data: match[2]};
+ if (sign_data(result.data, result.time)[0] !== signature)
+ result.fail = "bad signature";
+
+ return result;
+}
+
+/*
+ * Sign a given string for a given time. Time should be either 0 or in the range
+ * 10^12 <= time < 10^13.
+ */
+function sign_data(data, time) {
+ return [sha256(get_secret() + time + data), `${time}_${data}`];
+}
+
+/*
+ * EXPORTS_START
+ * EXPORT extract_signed
+ * EXPORT sign_data
+ * EXPORTS_END
+ */
diff --git a/common/storage_light.js b/common/storage_light.js
new file mode 100644
index 0000000..067bf0c
--- /dev/null
+++ b/common/storage_light.js
@@ -0,0 +1,129 @@
+/**
+ * part of Hachette
+ * Storage manager, lighter than the previous one.
+ *
+ * Copyright (C) 2021 Wojtek Kosior
+ * Redistribution terms are gathered in the `copyright' file.
+ */
+
+/*
+ * IMPORTS_START
+ * IMPORT TYPE_PREFIX
+ * IMPORT raw_storage
+ * IMPORT is_mozilla
+ * IMPORT observables
+ */
+
+const reg_spec = new Set(["\\", "[", "]", "(", ")", "{", "}", ".", "*", "+"]);
+const escape_reg_special = c => reg_spec.has(c) ? "\\" + c : c;
+
+function make_regex(name)
+{
+ return new RegExp(`^${name.split("").map(escape_reg_special).join("")}\$`);
+}
+
+const listeners_by_callback = new Map();
+
+function listen(callback, prefix, name)
+{
+ let by_prefix = listeners_by_callback.get(callback);
+ if (!by_prefix) {
+ by_prefix = new Map();
+ listeners_by_callback.set(callback, by_prefix);
+ }
+
+ let by_name = by_prefix.get(prefix);
+ if (!by_name) {
+ by_name = new Map();
+ by_prefix.set(prefix, by_name);
+ }
+
+ let name_reg = by_name.get(name);
+ if (!name_reg) {
+ name_reg = name.test ? name : make_regex(name);
+ by_name.set(name, name_reg);
+ }
+}
+
+function no_listen(callback, prefix, name)
+{
+ const by_prefix = listeners_by_callback.get(callback);
+ if (!by_prefix)
+ return;
+
+ const by_name = by_prefix.get(prefix);
+ if (!by_name)
+ return;
+
+ const name_reg = by_name.get(name);
+ if (!name_reg)
+ return;
+
+ by_name.delete(name);
+
+ if (by_name.size === 0)
+ by_prefix.delete(prefix);
+
+ if (by_prefix.size === 0)
+ listeners_by_callback.delete(callback);
+}
+
+function storage_change_callback(changes, area)
+{
+ if (is_mozilla && area !== "local")
+ {console.log("area", area);return;}
+
+ for (const item of Object.keys(changes)) {
+ for (const [callback, by_prefix] of listeners_by_callback.entries()) {
+ const by_name = by_prefix.get(item[0]);
+ if (!by_name)
+ continue;
+
+ for (const reg of by_name.values()) {
+ if (!reg.test(item.substring(1)))
+ continue;
+
+ try {
+ callback(item, changes[item]);
+ } catch(e) {
+ console.error(e);
+ }
+ }
+ }
+ }
+}
+
+raw_storage.listen(storage_change_callback);
+
+
+const created_observables = new Map();
+
+async function observe(prefix, name)
+{
+ const observable = observables.make();
+ const callback = (it, ch) => observables.set(observable, ch.newValue);
+ listen(callback, prefix, name);
+ created_observables.set(observable, [callback, prefix, name]);
+ observables.silent_set(observable, await raw_storage.get(prefix + name));
+
+ return observable;
+}
+
+const observe_var = name => observe(TYPE_PREFIX.VAR, name);
+
+function no_observe(observable)
+{
+ no_listen(...created_observables.get(observable) || []);
+ created_observables.delete(observable);
+}
+
+const light_storage = {};
+Object.assign(light_storage, raw_storage);
+Object.assign(light_storage,
+ {listen, no_listen, observe, observe_var, no_observe});
+
+/*
+ * EXPORTS_START
+ * EXPORT light_storage
+ * EXPORTS_END
+ */
diff --git a/common/storage_raw.js b/common/storage_raw.js
new file mode 100644
index 0000000..4c02ee4
--- /dev/null
+++ b/common/storage_raw.js
@@ -0,0 +1,54 @@
+/**
+ * part of Hachette
+ * Basic wrappers for storage API functions.
+ *
+ * Copyright (C) 2021 Wojtek Kosior
+ * Redistribution terms are gathered in the `copyright' file.
+ */
+
+/*
+ * IMPORTS_START
+ * IMPORT TYPE_PREFIX
+ * IMPORT browser
+ * IMPORT is_chrome
+ * IMPORTS_END
+ */
+
+async function get(key)
+{
+ /* Fix for fact that Chrome does not use promises here */
+ const promise = is_chrome ?
+ new Promise(resolve => chrome.storage.local.get(key, resolve)) :
+ browser.storage.local.get(key);
+
+ return (await promise)[key];
+}
+
+async function set(key_or_object, value)
+{
+ const arg = typeof key_or_object === "object" ?
+ key_or_object : {[key_or_object]: value};
+ return browser.storage.local.set(arg);
+}
+
+async function set_var(name, value)
+{
+ return set(TYPE_PREFIX.VAR + name, value);
+}
+
+async function get_var(name)
+{
+ return get(TYPE_PREFIX.VAR + name);
+}
+
+const on_changed = browser.storage.onChanged || browser.storage.local.onChanged;
+const listen = cb => on_changed.addListener(cb);
+const no_listen = cb => on_changed.removeListener(cb);
+
+const raw_storage = {get, set, get_var, set_var, listen, no_listen};
+
+/*
+ * EXPORTS_START
+ * EXPORT raw_storage
+ * EXPORTS_END
+ */
diff --git a/content/activity_info_server.js b/content/activity_info_server.js
index 81a25fb..beecb1a 100644
--- a/content/activity_info_server.js
+++ b/content/activity_info_server.js
@@ -44,6 +44,11 @@ function report_settings(settings)
report_activity("settings", settings);
}
+function report_content_type(content_type)
+{
+ report_activity("content_type", content_type);
+}
+
function report_repo_query_action(update, port)
{
report_activity_oneshot("repo_query_action", update, port);
@@ -91,5 +96,6 @@ function start_activity_info_server()
* EXPORT start_activity_info_server
* EXPORT report_script
* EXPORT report_settings
+ * EXPORT report_content_type
* EXPORTS_END
*/
diff --git a/content/freezer.js b/content/freezer.js
index 9dbc95e..0ea362e 100644
--- a/content/freezer.js
+++ b/content/freezer.js
@@ -49,6 +49,7 @@ function mozilla_suppress_scripts(e) {
console.log('Script suppressor has detached.');
return;
}
+ console.log("script event", e);
if (e.isTrusted && !e.target._hachette_payload) {
e.preventDefault();
console.log('Suppressed script', e.target);
diff --git a/content/main.js b/content/main.js
index 8440eb5..4fe6d43 100644
--- a/content/main.js
+++ b/content/main.js
@@ -10,6 +10,7 @@
* IMPORTS_START
* IMPORT handle_page_actions
* IMPORT extract_signed
+ * IMPORT sign_data
* IMPORT gen_nonce
* IMPORT is_privileged_url
* IMPORT mozilla_suppress_scripts
@@ -22,15 +23,67 @@
* IMPORTS_END
*/
-function accept_node(node, parent)
+function extract_cookie_policy(cookie, min_time)
{
- const clone = document.importNode(node, false);
- node.hachette_corresponding = clone;
- /*
- * TODO: Stop page's own issues like "Error parsing a meta element's
- * content:" from appearing as extension's errors.
- */
- parent.hachette_corresponding.appendChild(clone);
+ let best_result = {time: -1};
+ let policy = null;
+ const extracted_signatures = [];
+
+ for (const match of cookie.matchAll(/hachette-(\w*)=([^;]*)/g)) {
+ const new_result = extract_signed(...match.slice(1, 3));
+ if (new_result.fail)
+ continue;
+
+ extracted_signatures.push(match[1]);
+
+ if (new_result.time < Math.max(min_time, best_result.time))
+ continue;
+
+ /* This should succeed - it's our self-produced valid JSON. */
+ const new_policy = JSON.parse(decodeURIComponent(new_result.data));
+ if (new_policy.url !== document.URL)
+ continue;
+
+ best_result = new_result;
+ policy = new_policy;
+ }
+
+ return [policy, extracted_signatures];
+}
+
+function extract_url_policy(url, min_time)
+{
+ const [base_url, payload, anchor] =
+ /^([^#]*)#?([^#]*)(#?.*)$/.exec(url).splice(1, 4);
+
+ const match = /^hachette_([^_]+)_(.*)$/.exec(payload);
+ if (!match)
+ return [null, url];
+
+ const result = extract_signed(...match.slice(1, 3));
+ if (result.fail)
+ return [null, url];
+
+ const original_url = base_url + anchor;
+ const policy = result.time < min_time ? null :
+ JSON.parse(decodeURIComponent(result.data));
+
+ return [policy.url === original_url ? policy : null, original_url];
+}
+
+function employ_nonhttp_policy(policy)
+{
+ if (!policy.allow)
+ return;
+
+ policy.nonce = gen_nonce();
+ const [base_url, target] = /^([^#]*)(#?.*)$/.exec(policy.url).slice(1, 3);
+ const encoded_policy = encodeURIComponent(JSON.stringify(policy));
+ const payload = "hachette_" +
+ sign_data(encoded_policy, new Date().getTime()).join("_");
+ const resulting_url = `${base_url}#${payload}${target}`;
+ location.href = resulting_url;
+ location.reload();
}
/*
@@ -171,23 +224,44 @@ async function sanitize_document(doc, policy)
}
if (!is_privileged_url(document.URL)) {
- const reductor =
- (ac, [_, sig, pol]) => ac[0] && ac || [extract_signed(sig, pol), sig];
- const matches = [...document.cookie.matchAll(/hachette-(\w*)=([^;]*)/g)];
- let [policy, signature] = matches.reduce(reductor, []);
+ let policy_received_callback = () => undefined;
+ let policy;
+
+ /* Signature valid for half an hour. */
+ const min_time = new Date().getTime() - 1800 * 1000;
+
+ if (/^https?:/.test(document.URL)) {
+ let signatures;
+ [policy, signatures] = extract_cookie_policy(document.cookie, min_time);
+ for (const signature of signatures)
+ document.cookie = `hachette-${signature}=; Max-Age=-1;`;
+ } else {
+ const scheme = /^([^:]*)/.exec(document.URL)[1];
+ const known_scheme = ["file", "ftp"].includes(scheme);
+
+ if (!known_scheme)
+ console.warn(`Unknown url scheme: \`${scheme}'!`);
+
+ let original_url;
+ [policy, original_url] = extract_url_policy(document.URL, min_time);
+ history.replaceState(null, "", original_url);
+
+ if (known_scheme && !policy)
+ policy_received_callback = employ_nonhttp_policy;
+ }
- if (!policy || policy.url !== document.URL) {
- console.log("WARNING! Using default policy!!!");
+ if (!policy) {
+ console.warn("Using fallback policy!");
policy = {allow: false, nonce: gen_nonce()};
}
- if (signature)
- document.cookie = `hachette-${signature}=; Max-Age=-1;`;
-
- if (!policy.allow)
- sanitize_document(document, policy);
+ const doc_ready = Promise.all([
+ policy.allow ? Promise.resolve : sanitize_document(document, policy),
+ new Promise(cb => document.addEventListener("DOMContentLoaded",
+ cb, {once: true}))
+ ]);
- handle_page_actions(policy.nonce);
+ handle_page_actions(policy.nonce, policy_received_callback, doc_ready);
start_activity_info_server();
}
diff --git a/content/page_actions.js b/content/page_actions.js
index aff56b8..8057541 100644
--- a/content/page_actions.js
+++ b/content/page_actions.js
@@ -11,13 +11,18 @@
* IMPORT browser
* IMPORT report_script
* IMPORT report_settings
+ * IMPORT report_content_type
* IMPORTS_END
*/
-var port;
-var loaded = false;
-var scripts_awaiting = [];
-var nonce;
+let policy_received_callback;
+/* Snapshot url and content type early; these can be changed by other code. */
+let url;
+let is_html;
+let port;
+let loaded = false;
+let scripts_awaiting = [];
+let nonce;
function handle_message(message)
{
@@ -31,11 +36,13 @@ function handle_message(message)
scripts_awaiting.push(script_text);
}
}
- if (action === "settings")
+ if (action === "settings") {
report_settings(data);
+ policy_received_callback({url, allow: data[1].allow});
+ }
}
-function document_loaded(event)
+function document_ready(event)
{
loaded = true;
@@ -47,6 +54,9 @@ function document_loaded(event)
function add_script(script_text)
{
+ if (!is_html)
+ return;
+
let script = document.createElement("script");
script.textContent = script_text;
script.setAttribute("nonce", nonce);
@@ -56,11 +66,18 @@ function add_script(script_text)
report_script(script_text);
}
-function handle_page_actions(script_nonce) {
- document.addEventListener("DOMContentLoaded", document_loaded);
+function handle_page_actions(script_nonce, policy_received_cb,
+ doc_ready_promise) {
+ policy_received_callback = policy_received_cb;
+ url = document.URL;
+ is_html = /html/.test(document.contentType);
+ report_content_type(document.contentType);
+
+ doc_ready_promise.then(document_ready);
+
port = browser.runtime.connect({name : CONNECTION_TYPE.PAGE_ACTIONS});
port.onMessage.addListener(handle_message);
- port.postMessage({url: document.URL});
+ port.postMessage({url});
nonce = script_nonce;
}
diff --git a/copyright b/copyright
index 40126fe..58993a6 100644
--- a/copyright
+++ b/copyright
@@ -31,11 +31,20 @@ Files: *.html README.txt copyright
Copyright: 2021 Wojtek Kosior <koszko@koszko.org>
License: GPL-3+ or Alicense-1.0 or CC-BY-SA-4.0
+Files: html/*.css
+Copyright: 2021 Wojtek Kosior <koszko@koszko.org>
+License: GPL-3+ or Alicense-1.0 or CC-BY-SA-4.0
+
Files: html/base.css
Copyright: 2021 Wojtek Kosior <koszko@koszko.org>
2021 Nicholas Johnson <nicholasjohnson@posteo.org>
License: GPL-3+ or Alicense-1.0 or CC-BY-SA-4.0
+Files: html/reset.css
+Copyright: 2008,2011 Eric A. Meyer
+License: public-domain
+ This stylesheet has been place in public domain by its author.
+
Files: common/sha256.js
Copyright: 2014-2017 Chen, Yi-Cyuan <emn178@gmail.com>
License: Expat
diff --git a/html/DOM_helpers.js b/html/DOM_helpers.js
index 2bff966..01e2be9 100644
--- a/html/DOM_helpers.js
+++ b/html/DOM_helpers.js
@@ -10,9 +10,27 @@ function by_id(id)
return document.getElementById(id);
}
+const known_templates = new Map();
+
+function get_template(template_id)
+{
+ let template = known_templates.get(template_id) || null;
+ if (template)
+ return template;
+
+ for (const template_node of document.getElementsByTagName("TEMPLATE")) {
+ template = template_node.content.getElementById(template_id);
+ if (template)
+ break;
+ }
+
+ known_templates.set(template_id, template);
+ return template;
+}
+
function clone_template(template_id)
{
- const clone = document.getElementById(template_id).cloneNode(true);
+ const clone = get_template(template_id).cloneNode(true);
const result_object = {};
const to_process = [clone];
@@ -24,7 +42,7 @@ function clone_template(template_id)
result_object[template_key] = element;
element.removeAttribute("id");
- element.removeAttribute("template_key");
+ element.removeAttribute("data-template");
for (const child of element.children)
to_process.push(child);
@@ -36,6 +54,7 @@ function clone_template(template_id)
/*
* EXPORTS_START
* EXPORT by_id
+ * EXPORT get_template
* EXPORT clone_template
* EXPORTS_END
*/
diff --git a/html/MOZILLA_scrollbar_fix.css b/html/MOZILLA_scrollbar_fix.css
new file mode 100644
index 0000000..5feb7c3
--- /dev/null
+++ b/html/MOZILLA_scrollbar_fix.css
@@ -0,0 +1,46 @@
+/**
+ * Hachette
+ * Hacky fix for vertical scrollbar width being included in child's width.
+ *
+ * Copyright (C) 2021 Wojtek Kosior
+ * Redistribution terms are gathered in the `copyright' file.
+ */
+
+/*
+ * Under Mozilla browsers to avoid vertical scrollbar forcing horizontal
+ * scrollbar to appear in an element we add the `firefox_scrollbars_hacky_fix'
+ * class to an element for which width has to be reserved.
+ *
+ * This is a bit hacky and relies on some assumed width of Firefox scrollbar, I
+ * know. And must be excluded from Chromium builds.
+ *
+ * I came up with this hack when working on popup. Before that I had the
+ * scrollbar issue with tables in the options page and gave up there and made
+ * the scrollbal always visible. Now we could try applying this "fix" there,
+ * too!
+ */
+
+.firefox_scrollbars_hacky_fix {
+ font-size: 0;
+}
+
+.firefox_scrollbars_hacky_fix>div {
+ display: inline-block;
+ width: -moz-available;
+}
+
+.firefox_scrollbars_hacky_fix>*>* {
+ font-size: initial;
+}
+
+.firefox_scrollbars_hacky_fix::after {
+ content: "";
+ display: inline-block;
+ visibility: hidden;
+ font-size: initial;
+ width: 14px;
+}
+
+.firefox_scrollbars_hacky_fix.has_inline_content::after {
+ width: calc(14px - 0.3em);
+}
diff --git a/html/back_button.css b/html/back_button.css
new file mode 100644
index 0000000..1ddc5da
--- /dev/null
+++ b/html/back_button.css
@@ -0,0 +1,50 @@
+/**
+ * part of Hachette
+ * Style for a "back" button with a CSS arrow image.
+ *
+ * Copyright (C) 2021 Wojtek Kosior
+ * Redistribution terms are gathered in the `copyright' file.
+ */
+
+.back_button {
+ display: block;
+ width: auto;
+ height: auto;
+ background-color: white;
+ border: solid #454 0.4em;
+ border-left: none;
+ border-radius: 0 1.5em 1.5em 0;
+ cursor: pointer;
+}
+
+.back_button:hover {
+ box-shadow: 0 6px 8px 0 rgba(0,0,0,0.24), 0 17px 50px 0 rgba(0,0,0,0.19);
+}
+
+.back_button>div, .back_arrow {
+ width: 2em;
+ height: 0.5em;
+ background-color: #4CAF50;
+ border-radius: 0.3em;
+ margin: 1.15em 0.4em;
+}
+
+.back_button>div::after, .back_arrow::after,
+.back_button>div::before, .back_arrow::before {
+ content: "";
+ display: block;
+ position: relative;
+ background-color: inherit;
+ width: 1.3em;
+ height: 0.5em;
+ transform: rotate(45deg);
+ border-radius: 0.3em;
+ top: 0.3em;
+ right: 0.2em;
+ margin: 0 -1.3em -0.5em 0;
+}
+
+.back_button>div::before, .back_arrow::before {
+ transform: rotate(-45deg);
+ top: -0.3em;
+}
diff --git a/html/base.css b/html/base.css
index 2256833..df52f7c 100644
--- a/html/base.css
+++ b/html/base.css
@@ -6,10 +6,40 @@
* Redistribution terms are gathered in the `copyright' file.
*/
+body {
+ font-family: sans-serif;
+ background-color: #f0f0f0;
+ color: #555;
+ overflow: auto;
+}
+
+.bold, h2 {
+ font-weight: bold;
+}
+
+h2 {
+ margin: 8px;
+ font-size: 120%;
+}
+
+h3 {
+ padding: 5px;
+ font-size: 108%;
+ text-shadow: 0 0 0 #454;
+}
+
+textarea {
+ font-family: monospace;
+}
+
input[type="checkbox"], input[type="radio"], .hide {
display: none;
}
+.camouflage {
+ visibility: hidden;
+}
+
.show_next:not(:checked)+* {
display: none;
}
@@ -32,6 +62,10 @@ button, .button {
display: inline-block;
padding: 6px 12px;
margin: 2px 0px;
+ -moz-user-select: none;
+ user-select: none;
+ cursor: pointer;
+ font: 400 15px sans-serif;
}
button.slimbutton, .button.slimbutton {
@@ -42,3 +76,38 @@ button.slimbutton, .button.slimbutton {
button:hover, .button:hover {
box-shadow: 0 6px 8px 0 rgba(0,0,0,0.24), 0 17px 50px 0 rgba(0,0,0,0.19);
}
+
+aside {
+ background: #3f8dc6;
+ margin: 5px 0;
+ padding: 0.3em 1em;
+ border-radius: 3px;
+ color: #fff;
+}
+
+textarea: {
+ resize: none;
+}
+
+.has_bottom_line::after, .has_upper_line::before {
+ content: "";
+ display: block;
+ height: 8px;
+ background: linear-gradient(transparent, #555);
+}
+
+.has_bottom_line::after {
+ background: linear-gradient(#555, transparent);
+}
+
+.has_bottom_thin_line {
+ border-bottom: dashed #4CAF50 1px;
+}
+
+.has_upper_thin_line {
+ border-top: dashed #4CAF50 1px;
+}
+
+.nowrap {
+ white-space: nowrap;
+}
diff --git a/html/default_blocking_policy.html b/html/default_blocking_policy.html
new file mode 100644
index 0000000..50c19ca
--- /dev/null
+++ b/html/default_blocking_policy.html
@@ -0,0 +1,18 @@
+<!--
+ Copyright (C) 2021 Wojtek Kosior
+ Redistribution terms are gathered in the `copyright' file.
+
+ This is not a standalone page. This file is meant to be imported into other
+ HTML code.
+ -->
+<style>
+ #blocking_policy_div {
+ line-height: 2em;
+ }
+</style>
+<span id="blocking_policy_span">
+ Default policy for unmatched pages is to
+ <span id="current_policy_span" class="bold"></span>
+ their own scripts.
+ <button id="toggle_policy_but">Toggle policy</button>
+</span>
diff --git a/html/default_blocking_policy.js b/html/default_blocking_policy.js
new file mode 100644
index 0000000..2f49bac
--- /dev/null
+++ b/html/default_blocking_policy.js
@@ -0,0 +1,47 @@
+/**
+ * part of Hachette
+ * Default policy dialog logic.
+ *
+ * Copyright (C) 2021 Wojtek Kosior
+ * Redistribution terms are gathered in the `copyright' file.
+ */
+
+/*
+ * IMPORTS_START
+ * IMPORT by_id
+ * IMPORT light_storage
+ * IMPORT observables
+ * IMPORTS_END
+ */
+
+/*
+ * Used with `default_blocking_policy.html' to allow user to choose whether to
+ * block scripts globally or not.
+ */
+
+const blocking_policy_span = by_id("blocking_policy_span");
+const current_policy_span = by_id("current_policy_span");
+const toggle_policy_but = by_id("toggle_policy_but");
+
+let policy_observable;
+
+const update_policy =
+ allowed => current_policy_span.textContent = allowed ? "allow" : "block";
+const toggle_policy =
+ () => light_storage.set_var("default_allow", !policy_observable.value);
+
+async function init_default_policy_dialog()
+{
+ policy_observable = await light_storage.observe_var("default_allow");
+ update_policy(policy_observable.value);
+ observables.subscribe(policy_observable, update_policy);
+
+ toggle_policy_but.addEventListener("click", toggle_policy);
+ blocking_policy_span.classList.remove("hide");
+}
+
+/*
+ * EXPORTS_START
+ * EXPORT init_default_policy_dialog
+ * EXPORTS_END
+ */
diff --git a/html/display-panel.html b/html/display-panel.html
index 1b9c77b..3ed1b7a 100644
--- a/html/display-panel.html
+++ b/html/display-panel.html
@@ -7,119 +7,337 @@
<head>
<meta charset="utf-8"/>
<title>Hachette - page settings</title>
+ <link type="text/css" rel="stylesheet" href="reset.css" />
<link type="text/css" rel="stylesheet" href="base.css" />
+ <link type="text/css" rel="stylesheet" href="back_button.css" />
+ <link type="text/css" rel="stylesheet" href="table.css" />
+ <link type="text/css" rel="stylesheet" href="MOZILLA_scrollbar_fix.css" />
<style>
body {
- width: 300px;
- height: 300px;
+ width: max-content;
+ width: -moz-fit-content;
}
- ul {
- padding-inline-start: 15px;
+ .top>h2 {
+ padding-left: calc(0.8*3.2em - 8px);
}
- .bold {
- font-weight: bold;
+ .top {
+ line-height: calc(0.8*3.6em - 16px);
+ }
+
+ #main_view>.top>h2 {
+ padding-left: 0;
+ max-width: 550px
}
- .unroll_chbx:not(:checked)+*+label span.triangle:first-child+span.triangle,
- .unroll_chbx:checked+*+label span.triangle:first-child,
- .unroll_chbx:not(:checked)+*,
- .unroll_chbx:not(:checked)+*+label+* {
+ .unroll_chbx:not(:checked)+div>:not(:first-child) {
display: none;
}
+ .unroll_triangle {
+ height: 1em;
+ width: 1em;
+ display: inline-block;
+ }
+
+ .unroll_triangle::after {
+ content: "";
+ width: 0.6em;
+ height: 0.6em;
+ background: linear-gradient(-45deg, currentColor 50%, transparent 50%);
+ display: block;
+ position: relative;
+ transform: rotate(-45deg);
+ top: 0.3em;
+ }
+
+ .unroll_chbx:checked+div>:first-child .unroll_triangle::after {
+ transform: rotate(45deg);
+ left: 0.2em;
+ top: 0.2em;
+ }
+
+ .unroll_chbx:checked+div>:first-child .unroll_block {
+ display: block;
+ }
+
+ .unroll_chbx:checked+div>:first-child {
+ line-height: 1.4em;
+ }
+
+ .l2_ul {
+ border-left: solid #454 5px;
+ }
+
+ .l1_li {
+ margin-top: 0.3em;
+ margin-bottom: 0.3em;
+ }
+
+ .l1_li>div {
+ padding: 0.3em 0.3em 0.3em 0;
+ }
+
+ .l2_li {
+ padding: 0.3em;
+ }
+
+ #container_for_injected>*:nth-child(odd),
+ .l2_li:nth-child(odd) {
+ background-color: #e5e5e5;
+ }
+
#container_for_injected>#none_injected:not(:last-child) {
display: none;
}
+
+ #page_url_heading>span {
+ display: inline-block;
+ }
+
+ .back_button {
+ position: fixed;
+ z-index: 1;
+ top: 0;
+ left: 0;
+ /* The following scales the entire button. */
+ font-size: 80%;
+ }
+
+ #show_main_view_radio:checked~.back_button {
+ margin-left: -3.2em;
+ }
+
+ #show_main_view_radio:not(:checked)~.back_button {
+ transition: all 0.2s ease-out;
+ }
+
+ pre {
+ font-family: monospace;
+ background-color: white;
+ padding: 1px 5px;
+ }
+
+ .matched_pattern {
+ font-weight: bold;
+ }
+
+ tr.matched_pattern~tr {
+ color: #777;
+ font-size: 90%;
+ }
+
+ .padding_inline {
+ padding-left: 5px;
+ padding-right: 5px;
+ }
+
+ .padding_top {
+ padding-top: 5px;
+ }
+
+ .header {
+ padding-bottom: 0.3em;
+ margin-bottom: 0.5em;
+ text-align: center;
+ }
+
+ .middle {
+ margin-top: 0.5em;
+ margin-bottom: 0.5em;
+ }
+
+ .footer {
+ padding-top: 0.3em;
+ margin-top: 0.5em;
+ text-align: center;
+ }
+
+ .active_setting_table {
+ margin-bottom: 0.5em;
+ }
+
+ .active_setting_table td {
+ padding: 5px;
+ vertical-align: middle;
+ }
</style>
</head>
<body>
- <!-- The invisible div below is for elements that will be cloned. -->
- <div class="hide">
- <li id="pattern_li_template">
- <span></span>
- <button>View in settings</button>
- </li>
- <li id="query_match_li_template" class="queried_pattern_match" data-template="li">
+ <template>
+ <tr id="pattern_entry" class="nowrap" data-template="entry">
+ <td data-template="name"></td>
+ <td>
+ <div class="button" data-template="button">Add setting</div>
+ </td>
+ </tr>
+
+ <li id="query_match_li" class="l2_li" data-template="li">
<div>
<span>pattern:</span>
<span class="bold" data-template="pattern"></span>
- <button data-template="btn">Install</button>
+ <label class="button slimbutton" for="show_install_view_radio" data-template="btn">
+ Install
+ </label>
</div>
- <div id="unrollable_component_template" data-template="unroll_container">
- <span data-template="component_label">payload:</span>
+ <div id="unrollable_component" data-template="unroll_container">
<input type="checkbox" class="unroll_chbx" data-template="chbx"></input>
- <br data-template="br"/>
- <label class="bold" data-template="lbl">
- <span data-template="triangle">
- <span class="triangle">&#x23F5;</span>
- <span class="triangle">&#x23F7;</span>
+ <div>
+ <span>payload:
+ <label class="bold unroll_block" data-template="lbl">
+ <div data-template="triangle" class="unroll_triangle"></div>
+ <span data-template="payload"></span>
+ </label>
</span>
- <span data-template="component"></span>
+ <div data-template="unroll"></div>
+ </div>
+ </div>
+ </li>
+
+ <div id="injected_script" data-template="div">
+ <input type="checkbox" class="unroll_chbx" data-template="chbx"></input>
+ <div>
+ <label data-template="lbl">
+ <h3><div class="unroll_triangle"></div> script</h3>
</label>
- <div data-template="unroll"></div>
+ <pre class="has_bottom_thin_line has_upper_thin_line" data-template="script_contents"></pre>
+ </div>
+ </div>
+
+ <div id="multi_repos_query_result" data-template="div">
+ Results for <span class="bold" data-template="url_span"></span>
+ <ul class="l1_ul" data-template="ul"></ul>
+ </div>
+
+ <li id="single_repo_query_result" class="l1_li" data-template="li">
+ <div>
+ From <span class="bold" data-template="repo_url"></span>
</div>
</li>
- </div>
- <input id="show_install_view_chbx" type="checkbox" class="show_hide_next2"></input>
+ <ul id="result_patterns_list" class="l2_ul" data-template="ul">
+ </ul>
+ </template>
+
+ <input id="show_install_view_radio" type="radio" class="show_next" name="current_view"></input>
<div id="install_view">
- <IMPORT html/import_frame.html />
- <!--
- <div id="install_status"></div>
- <label for="show_install_chbx" class="bold">Cancel install</label>
- <button id="commit_install_but">Commit install</button>
- -->
+ <div class="top has_bottom_line"><h2> Site modifiers install </h2></div>
+ <div class="padding_inline">
+ <IMPORT html/import_frame.html />
+ </div>
</div>
- <div id="main_view">
- <h2 id="page_url_heading"></h2>
-
- <input id="show_privileged_notice_chbx" type="checkbox" class="show_next"></input>
- <h3>Privileged page</h3>
-
- <input id="show_page_state_chbx" type="checkbox" class="show_next"></input>
- <div>
- <input id="possible_patterns_chbx" type="checkbox" class="unroll_chbx"></input>
- <span></span>
- <label for="possible_patterns_chbx">
- <h3>
- <span class="triangle">&#x23F5;</span>
- <span class="triangle">&#x23F7;</span>
- Possible patterns
- </h3>
- </label>
- <ul id="possible_patterns"></ul>
-
- <input id="connected_chbx" type="checkbox" class="show_hide_next2"></input>
+
+ <input id="show_injected_view_radio" type="radio" class="show_next" name="current_view"></input>
+ <div id="injected_view">
+ <div class="top has_bottom_line"><h2>Injected scripts</h2></div>
+ <div id="container_for_injected">
+ <span id="none_injected">None</span>
+ </div>
+ </div>
+
+ <input id="show_patterns_view_radio" type="radio" class="show_next" name="current_view"></input>
+ <div>
+ <div class="top has_bottom_line"><h2>Possible patterns for this page</h2></div>
+ <div class="padding_inline">
+ <aside>
+ Patterns higher are more specific and override the ones below.
+ </aside>
+ </div>
+ <div class="table_wrapper firefox_scrollbars_hacky_fix">
<div>
- Matched pattern: <span id="pattern" class="bold">...</span>
- <button id="view_pattern" class="hide">
- View in settings
- </button>
- <br/>
- Blocked: <span id="blocked" class="bold">...</span>
- <br/>
- Payload: <span id="payload" class="bold">...</span>
- <button id="view_payload" class="hide">
- View in settings
- </button>
- <h3>Injected</h3>
- <div id="container_for_injected">
- <span id="none_injected">None</span>
+ <table>
+ <tbody id="possible_patterns">
+ </tbody>
+ </table>
+ </div>
+ </div>
+ <div class="padding_inline padding_top has_upper_thin_line firefox_scrollbars_hacky_fix has_inline_content">
+ <span class="nowrap">
+ <IMPORT html/default_blocking_policy.html />
+ </span>
+ </div>
+ </div>
+
+ <input id="show_queried_view_radio" type="radio" class="show_next" name="current_view"></input>
+ <div>
+ <div class="top has_bottom_line"><h2>Queried from repositories</h2></div>
+ <div id="container_for_repo_responses" class="padding_inline">
+ </div>
+ </div>
+
+ <input id="show_main_view_radio" type="radio" class="show_next" name="current_view" checked></input>
+ <div id="main_view">
+ <div class="top has_bottom_line"><h2 id="page_url_heading"></h2></div>
+ <h3 id="privileged_notice" class="middle hide">Privileged page</h3>
+
+ <div id="page_state" class="hide">
+ <div class="header padding_inline has_bottom_thin_line">
+ <label for="show_patterns_view_radio" class="button">
+ Edit settings for this page
+ </label>
+ </div>
+ <div class="middle padding_inline">
+ <input id="connected_chbx" type="checkbox" class="show_hide_next2"></input>
+ <div>
+ <table class="active_setting_table">
+ <tbody>
+ <tr class="nowrap">
+ <td>Matched pattern:</td>
+ <td id="pattern" class="bold">...</td>
+ <td>
+ <button id="view_pattern" class="hide">
+ View in settings
+ </button>
+ </td>
+ </tr>
+ <tr class="nowrap">
+ <td>Scripts blocked:</td>
+ <td id="blocked" class="bold">...</td>
+ <td></td>
+ </tr>
+ <tr class="nowrap">
+ <td>Injected payload:</td>
+ <td id="payload" class="bold">...</td>
+ <td id="payload_buttons" class="hide">
+ <button id="view_payload"> View in settings </button>
+ <br/>
+ <label id="view_injected" class="button" for="show_injected_view_radio">
+ View injected scripts
+ </label>
+ </td>
+ </tr>
+ <tr>
+ <td id="content_type" colspan="3" class="hide">
+ This is a non-HTML page. Chosen payload will not be injected.
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ <label id="query_pattern" for="show_queried_view_radio" class="button">
+ Install scripts for this page
+ </label>
</div>
- <input id="query_started_chbx" type="checkbox" class="show_hide_next2"></input>
- <div id="container_for_repo_responses">
- <h3>Queried from repositories</h3>
+ <div>
+ <h3>
+ Connecting to content script..<span id="loading_point">.</span>
+ </h3>
+ <aside id="reload_notice">
+ Try reloading the page.
+ </aside>
</div>
- <button id="query_pattern">
- Search for matching patterns
- </button>
</div>
- <h3>Trying to connect..<input id="loading_chbx" type="checkbox" class="show_next"></input><span>.</span></h3>
</div>
- <button id="settings_but" type="button" style="margin-top: 20px;">Settings</button>
- </div>_POPUPSCRIPTS_
+ <div class="footer padding_inline has_upper_thin_line">
+ <button id="settings_but" type="button">
+ Open Hachette settings
+ </button>
+ </div>
+ </div>
+
+ <div class="has_upper_line"></div>
+
+ <label for="show_main_view_radio" class="back_button"><div></div></label>_POPUPSCRIPTS_
</body>
</html>
diff --git a/html/display-panel.js b/html/display-panel.js
index 2539ded..66e51a6 100644
--- a/html/display-panel.js
+++ b/html/display-panel.js
@@ -14,13 +14,15 @@
*** temporarily, before all storage access gets reworked.
* IMPORT get_remote_storage
* IMPORT get_import_frame
+ * IMPORT init_default_policy_dialog
* IMPORT query_all
* IMPORT CONNECTION_TYPE
* IMPORT is_privileged_url
* IMPORT TYPE_PREFIX
* IMPORT nice_name
* IMPORT open_in_settings
- * IMPORT for_each_possible_pattern
+ * IMPORT url_matches
+ * IMPORT each_url_pattern
* IMPORT by_id
* IMPORT clone_template
* IMPORTS_END
@@ -29,6 +31,16 @@
let storage;
let tab_url;
+/* Force popup <html>'s reflow on stupid Firefox. */
+if (is_mozilla) {
+ const reflow_forcer =
+ () => document.documentElement.style.width = "-moz-fit-content";
+ for (const radio of document.querySelectorAll('[name="current_view"]'))
+ radio.addEventListener("change", reflow_forcer);
+}
+
+const show_queried_view_radio = by_id("show_queried_view_radio");
+
const tab_query = {currentWindow: true, active: true};
async function get_current_tab()
@@ -47,8 +59,21 @@ async function get_current_tab()
}
const page_url_heading = by_id("page_url_heading");
-const show_privileged_notice_chbx = by_id("show_privileged_notice_chbx");
-const show_page_state_chbx = by_id("show_page_state_chbx");
+const privileged_notice = by_id("privileged_notice");
+const page_state = by_id("page_state");
+
+/* Helper functions to convert string into a list of one-letter <span>'s. */
+function char_to_span(char, doc)
+{
+ const span = document.createElement("span");
+ span.textContent = char;
+ return span;
+}
+
+function to_spans(string, doc=document)
+{
+ return string.split("").map(c => char_to_span(c, doc));
+}
async function show_page_activity_info()
{
@@ -60,79 +85,74 @@ async function show_page_activity_info()
}
tab_url = /^([^?#]*)/.exec(tab.url)[1];
- page_url_heading.textContent = tab_url;
+ to_spans(tab_url).forEach(s => page_url_heading.append(s));
if (is_privileged_url(tab_url)) {
- show_privileged_notice_chbx.checked = true;
+ privileged_notice.classList.remove("hide");
return;
}
populate_possible_patterns_list(tab_url);
- show_page_state_chbx.checked = true;
+ page_state.classList.remove("hide");
try_to_connect(tab.id);
}
-const possible_patterns_ul = by_id("possible_patterns");
-const pattern_li_template = by_id("pattern_li_template");
-pattern_li_template.removeAttribute("id");
+const possible_patterns_list = by_id("possible_patterns");
const known_patterns = new Map();
function add_pattern_to_list(pattern)
{
- const li = pattern_li_template.cloneNode(true);
- li.id = `pattern_li_${known_patterns.size}`;
- known_patterns.set(pattern, li.id);
+ const template = clone_template("pattern_entry");
+ template.name.textContent = pattern;
- const span = li.firstElementChild;
- span.textContent = pattern;
-
- const button = span.nextElementSibling;
const settings_opener = () => open_in_settings(TYPE_PREFIX.PAGE, pattern);
- button.addEventListener("click", settings_opener);
+ template.button.addEventListener("click", settings_opener);
- possible_patterns_ul.appendChild(li)
+ known_patterns.set(pattern, template);
+ possible_patterns_list.append(template.entry);
- return li.id;
+ return template;
}
function ensure_pattern_exists(pattern)
{
- let id = known_patterns.get(pattern);
+ let entry_object = known_patterns.get(pattern);
/*
* As long as pattern computation works well, we should never get into this
* conditional block. This is just a safety measure. To be removed as part
* of a bigger rework when we start taking iframes into account.
*/
- if (id === undefined) {
+ if (entry_object === undefined) {
console.log(`unknown pattern: ${pattern}`);
- id = add_pattern_to_list(pattern);
+ entry_object = add_pattern_to_list(pattern);
}
- return id;
+ return entry_object;
}
-function set_pattern_li_button_text(li_id, text)
+function style_possible_pattern_entry(pattern, exists_in_settings)
{
- by_id(li_id).firstElementChild.nextElementSibling.textContent = text;
+ const [text, class_action] = exists_in_settings ?
+ ["Edit", "add"] : ["Add", "remove"];
+ const entry_object = ensure_pattern_exists(pattern);
+
+ entry_object.button.textContent = `${text} setting`;
+ entry_object.entry.classList[class_action]("matched_pattern");
}
function handle_page_change(change)
{
- const li_id = ensure_pattern_exists(change.item);
- if (change.old_val === undefined)
- set_pattern_li_button_text(li_id, "Edit in settings");
- if (change.new_val === undefined)
- set_pattern_li_button_text(li_id, "Add setting");
+ if (url_matches(tab_url, change.item))
+ style_possible_pattern_entry(change.item, change.new_val !== undefined);
}
function populate_possible_patterns_list(url)
{
- for_each_possible_pattern(url, add_pattern_to_list);
+ for (const pattern of each_url_pattern(url))
+ add_pattern_to_list(pattern);
- for (const [pattern, settings] of query_all(storage, url)) {
- set_pattern_li_button_text(ensure_pattern_exists(pattern),
- "Edit in settings");
- }
+ for (const [pattern, settings] of query_all(storage, url))
+ style_possible_pattern_entry(pattern, true);
storage.add_change_listener(handle_page_change, [TYPE_PREFIX.PAGE]);
}
@@ -158,17 +178,16 @@ function try_to_connect(tab_id)
setTimeout(() => monitor_connecting(tab_id), 1000);
}
-const query_started_chbx = by_id("query_started_chbx");
-
-function start_querying_repos(port)
+function start_querying_repos()
{
+ query_pattern_but.removeEventListener("click", start_querying_repos);
const repo_urls = storage.get_all_names(TYPE_PREFIX.REPO);
if (content_script_port)
content_script_port.postMessage([TYPE_PREFIX.URL, tab_url, repo_urls]);
- query_started_chbx.checked = true;
}
-const loading_chbx = by_id("loading_chbx");
+const loading_point = by_id("loading_point");
+const reload_notice = by_id("reload_notice");
function handle_disconnect(tab_id, button_cb)
{
@@ -182,7 +201,9 @@ function handle_disconnect(tab_id, button_cb)
if (connected_chbx.checked)
return;
- loading_chbx.checked = !loading_chbx.checked;
+ loading_point.classList.toggle("camouflage");
+ reload_notice.classList.remove("hide");
+
setTimeout(() => try_to_connect(tab_id), 1000);
}
@@ -196,7 +217,8 @@ function monitor_connecting(tab_id)
else
return;
- loading_chbx.checked = !loading_chbx.checked;
+ loading_point.classList.toggle("camouflage");
+ reload_notice.classList.remove("hide");
try_to_connect(tab_id);
}
@@ -204,11 +226,16 @@ const pattern_span = by_id("pattern");
const view_pattern_but = by_id("view_pattern");
const blocked_span = by_id("blocked");
const payload_span = by_id("payload");
+const payload_buttons_div = by_id("payload_buttons");
const view_payload_but = by_id("view_payload");
+const view_injected_but = by_id("view_injected");
const container_for_injected = by_id("container_for_injected");
+const content_type_cell = by_id("content_type");
const queried_items = new Map();
+let max_injected_script_id = 0;
+
function handle_activity_report(message)
{
connected_chbx.checked = true;
@@ -218,7 +245,6 @@ function handle_activity_report(message)
if (type === "settings") {
let [pattern, settings] = data;
- settings = settings || {};
blocked_span.textContent = settings.allow ? "no" : "yes";
if (pattern) {
@@ -229,30 +255,32 @@ function handle_activity_report(message)
view_pattern_but.addEventListener("click", settings_opener);
} else {
pattern_span.textContent = "none";
+ blocked_span.textContent = blocked_span.textContent + " (default)";
}
const components = settings.components;
if (components) {
payload_span.textContent = nice_name(...components);
+ payload_buttons_div.classList.remove("hide");
const settings_opener = () => open_in_settings(...components);
- view_payload_but.classList.remove("hide");
view_payload_but.addEventListener("click", settings_opener);
} else {
payload_span.textContent = "none";
}
}
if (type === "script") {
- const h4 = document.createElement("h4");
- const pre = document.createElement("pre");
- h4.textContent = "script";
- pre.textContent = data;
-
- container_for_injected.appendChild(h4);
- container_for_injected.appendChild(pre);
+ const template = clone_template("injected_script");
+ const chbx_id = `injected_script_${max_injected_script_id++}`;
+ template.chbx.id = chbx_id;
+ template.lbl.setAttribute("for", chbx_id);
+ template.script_contents.textContent = data;
+ container_for_injected.appendChild(template.div);
+ }
+ if (type === "content_type") {
+ if (!/html/.test(data))
+ content_type_cell.classList.remove("hide");
}
if (type === "repo_query_action") {
- query_started_chbx.checked = true;
-
const key = data.prefix + data.item;
const results = queried_items.get(key) || {};
Object.assign(results, data.results);
@@ -272,35 +300,26 @@ const results_lists = new Map();
function create_results_list(url)
{
- const list_div = document.createElement("div");
- const list_head = document.createElement("h4");
- const list = document.createElement("ul");
+ const cloned_template = clone_template("multi_repos_query_result");
+ cloned_template.url_span.textContent = url;
+ container_for_repo_responses.appendChild(cloned_template.div);
- list_head.textContent = url;
- list_div.appendChild(list_head);
- list_div.appendChild(list);
- container_for_repo_responses.appendChild(list_div);
+ cloned_template.by_repo = new Map();
+ results_lists.set(url, cloned_template);
- const list_object = {list, by_repo: new Map()};
-
- results_lists.set(url, list_object);
-
- return list_object;
+ return cloned_template;
}
function create_result_item(list_object, repo_url, result)
{
- const result_li = document.createElement("li");
- const repo_url_span = document.createElement("span");
- const result_item = {result_li, appended: null};
-
- repo_url_span.textContent = repo_url;
- result_li.appendChild(repo_url_span);
+ const cloned_template = clone_template("single_repo_query_result");
+ cloned_template.repo_url.textContent = repo_url;
+ cloned_template.appended = null;
- list_object.list.appendChild(result_li);
- list_object.by_repo.set(repo_url, result_item);
+ list_object.ul.appendChild(cloned_template.li);
+ list_object.by_repo.set(repo_url, cloned_template);
- return result_item;
+ return cloned_template;
}
function set_appended(result_item, element)
@@ -308,7 +327,7 @@ function set_appended(result_item, element)
if (result_item.appended)
result_item.appended.remove();
result_item.appended = element;
- result_item.result_li.appendChild(element);
+ result_item.li.appendChild(element);
}
function show_message(result_item, text)
@@ -331,11 +350,9 @@ function unroll_chbx_first_checked(entry_object)
entry_object.chbx.removeEventListener("change", entry_object.unroll_cb);
delete entry_object.unroll_cb;
- entry_object.unroll.textContent = "preview not implemented...";
+ entry_object.unroll.innerHTML = "preview not implemented...<br />(consider contributing)";
}
-const show_install_chbx = by_id("show_install_view_chbx");
-
let import_frame;
let install_target = null;
@@ -461,7 +478,6 @@ function record_fetched_install_dep(prefix, item, repo_url, result)
function install_clicked(entry_object)
{
- show_install_chbx.checked = true;
import_frame.show_loading();
install_target = {
@@ -481,25 +497,24 @@ var max_query_result_id = 0;
function show_query_successful_result(result_item, repo_url, result)
{
- const ul = document.createElement("ul");
-
- set_appended(result_item, ul);
+ const cloned_ul_template = clone_template("result_patterns_list");
+ set_appended(result_item, cloned_ul_template.ul);
for (const match of result) {
- const entry_object = clone_template("query_match_li_template");
+ const entry_object = clone_template("query_match_li");
entry_object.pattern.textContent = match.pattern;
- ul.appendChild(entry_object.li);
+ cloned_ul_template.ul.appendChild(entry_object.li);
if (!match.payload) {
entry_object.payload.textContent = "(none)";
- for (const key of ["chbx", "br", "triangle", "unroll"])
+ for (const key of ["chbx", "triangle", "unroll"])
entry_object[key].remove();
continue;
}
- entry_object.component.textContent = nice_name(...match.payload);
+ entry_object.payload.textContent = nice_name(...match.payload);
const install_cb = () => install_clicked(entry_object);
entry_object.btn.addEventListener("click", install_cb);
@@ -540,9 +555,11 @@ by_id("settings_but")
async function main()
{
+ init_default_policy_dialog();
+
storage = await get_remote_storage();
import_frame = await get_import_frame();
- import_frame.onclose = () => show_install_chbx.checked = false;
+ import_frame.onclose = () => show_queried_view_radio.checked = true;
show_page_activity_info();
}
diff --git a/html/import_frame.html b/html/import_frame.html
index c86c3de..754b289 100644
--- a/html/import_frame.html
+++ b/html/import_frame.html
@@ -1,13 +1,28 @@
-<div style="display: none;">
- <li id="import_li_template">
- <span></span>
- <input type="checkbox" style="display: inline;" checked></input>
- <span></span>
- </li>
-</div>
-<h2> Settings import </h2>
+<!--
+ Copyright (C) 2021 Wojtek Kosior
+ Redistribution terms are gathered in the `copyright' file.
+
+ This is not a standalone page. This file is meant to be imported into other
+ HTML code.
+ -->
+<style>
+ .padding_right {
+ padding-right: 0.3em;
+ }
+</style>
+<template>
+ <tr id="import_entry" class="nowrap" data-template="entry">
+ <td>
+ <input type="checkbox" style="display: inline;" checked data-template="chbx"></input>
+ <span data-template="name_span"></span>
+ </td>
+ <td class="bold padding_right" data-template="warning"></td>
+ </tr>
+</template>
+
<input id="import_loading_radio" type="radio" name="import_window_content" class="show_next"></input>
<span> Loading... </span>
+
<input id="import_failed_radio" type="radio" name="import_window_content" class="show_next"></input>
<div>
<span id="import_errormsg"></span>
@@ -15,13 +30,23 @@
<pre id="import_errordetail"></pre>
<button id="import_failok_but"> OK </button>
</div>
+
<input id="import_selection_radio" type="radio" name="import_window_content" class="show_next"></input>
<div>
<button id="check_all_import_but"> Check all </button>
<button id="uncheck_all_import_but"> Uncheck all </button>
<button id="uncheck_colliding_import_but"> Uncheck existing </button>
- <ul id="import_ul">
- </ul>
+ <aside id="existing_settings_note">
+ Settings that would owerwrite existing ones are marked "!".
+ </aside>
+ <div id="import_table_wrapper" class="table_wrapper">
+ <div>
+ <table>
+ <tbody id="import_list">
+ </tbody>
+ </table>
+ </div>
+ </div>
<button id="commit_import_but"> OK </button>
<button id="cancel_import_but"> Cancel </button>
</div>
diff --git a/html/import_frame.js b/html/import_frame.js
index 4075433..c0eb2f0 100644
--- a/html/import_frame.js
+++ b/html/import_frame.js
@@ -9,6 +9,7 @@
* IMPORTS_START
* IMPORT get_remote_storage
* IMPORT by_id
+ * IMPORT clone_template
* IMPORT nice_name
* IMPORT make_once
* IMPORTS_END
@@ -16,48 +17,38 @@
let storage;
-const import_li_template = by_id("import_li_template");
-import_li_template.removeAttribute("id");
-
-function import_li_id(prefix, item)
-{
- return `ili_${prefix}_${item}`;
-}
-
-let import_ul = by_id("import_ul");
+let import_list = by_id("import_list");
let import_chbxs_colliding = undefined;
+let entry_objects = undefined;
let settings_import_map = undefined;
-function add_import_li(prefix, name)
+function add_import_entry(prefix, name)
{
- let li = import_li_template.cloneNode(true);
- let name_span = li.firstElementChild;
- let chbx = name_span.nextElementSibling;
- let warning_span = chbx.nextElementSibling;
+ const cloned_template = clone_template("import_entry");
+ Object.assign(cloned_template, {prefix, name});
- li.setAttribute("data-prefix", prefix);
- li.setAttribute("data-name", name);
- li.id = import_li_id(prefix, name);
- name_span.textContent = nice_name(prefix, name);
+ cloned_template.name_span.textContent = nice_name(prefix, name);
if (storage.get(prefix, name) !== undefined) {
- import_chbxs_colliding.push(chbx);
- warning_span.textContent = "(will overwrite existing setting!)";
+ import_chbxs_colliding.push(cloned_template.chbx);
+ cloned_template.warning.textContent = "!";
}
- import_ul.appendChild(li);
+ import_list.appendChild(cloned_template.entry);
+
+ return cloned_template;
}
function check_all_imports()
{
- for (let li of import_ul.children)
- li.firstElementChild.nextElementSibling.checked = true;
+ for (const entry_object of entry_objects)
+ entry_object.chbx.checked = true;
}
function uncheck_all_imports()
{
- for (let li of import_ul.children)
- li.firstElementChild.nextElementSibling.checked = false;
+ for (const entry_object of entry_objects)
+ entry_object.chbx.checked = false;
}
function uncheck_colliding_imports()
@@ -68,17 +59,13 @@ function uncheck_colliding_imports()
function commit_import()
{
- for (let li of import_ul.children) {
- let chbx = li.firstElementChild.nextElementSibling;
-
- if (!chbx.checked)
+ for (const entry_object of entry_objects) {
+ if (!entry_object.chbx.checked)
continue;
- let prefix = li.getAttribute("data-prefix");
- let name = li.getAttribute("data-name");
- let key = prefix + name;
- let value = settings_import_map.get(key);
- storage.set(prefix, name, value);
+ const key = entry_object.prefix + entry_object.name;
+ const value = settings_import_map.get(key);
+ storage.set(entry_object.prefix, entry_object.name, value);
}
deactivate();
@@ -105,38 +92,48 @@ function show_error(errormsg, errordetail)
}
const import_selection_radio = by_id("import_selection_radio");
+const existing_settings_note = by_id("existing_settings_note");
function show_selection(settings)
{
import_selection_radio.checked = true;
- let old_children = import_ul.children;
+ let old_children = import_list.children;
while (old_children[0] !== undefined)
- import_ul.removeChild(old_children[0]);
+ import_list.removeChild(old_children[0]);
import_chbxs_colliding = [];
+ entry_objects = [];
settings_import_map = new Map();
for (let setting of settings) {
let [key, value] = Object.entries(setting)[0];
let prefix = key[0];
let name = key.substring(1);
- add_import_li(prefix, name);
+ entry_objects.push(add_import_entry(prefix, name));
settings_import_map.set(key, value);
}
+
+ const op = import_chbxs_colliding.length > 0 ? "remove" : "add";
+ existing_settings_note.classList[op]("hide");
}
function deactivate()
{
/* Let GC free some memory */
import_chbxs_colliding = undefined;
+ entry_objects = undefined;
settings_import_map = undefined;
if (exports.onclose)
exports.onclose();
}
-const exports = {show_loading, show_error, show_selection, deactivate};
+const wrapper = by_id("import_table_wrapper");
+const style_table = (...cls) => cls.forEach(c => wrapper.classList.add(c));
+
+const exports =
+ {show_loading, show_error, show_selection, deactivate, style_table};
async function init()
{
diff --git a/html/options.html b/html/options.html
index 2246f9a..f2a75e1 100644
--- a/html/options.html
+++ b/html/options.html
@@ -1,32 +1,18 @@
<!doctype html>
<!--
Copyright (C) 2021 Wojtek Kosior
- Copyright (C) 2021 Nicholas Johnson
Redistribution terms are gathered in the `copyright' file.
-->
<html>
<head>
<meta charset="utf-8"/>
<title>Hachette options</title>
+ <link type="text/css" rel="stylesheet" href="reset.css" />
<link type="text/css" rel="stylesheet" href="base.css" />
+ <link type="text/css" rel="stylesheet" href="table.css" />
<style>
- /* pages list */
- #page_components_ul {
- max-height: 80vh;
- overflow-y: auto;
- }
- #page_components_ul li.dragover_li {
- border-top: 2px solid blue;
- }
- #page_components_ul li {
- border-top: 2px solid white;
- }
- li[draggable=true] * {
- pointer-events: none;
- }
- li[draggable=true] label,
- li[draggable=true] button {
- pointer-events: auto;
+ body {
+ width: 100%;
}
/* tabbed view */
@@ -37,20 +23,38 @@
display: none;
}
- #show_repos:checked ~ #repos_lbl,
- #show_pages:checked ~ #pages_lbl,
- #show_bags:checked ~ #bags_lbl,
- #show_scripts:checked ~ #scripts_lbl {
+ #show_repos:checked ~ div #repos_lbl,
+ #show_pages:checked ~ div #pages_lbl,
+ #show_bags:checked ~ div #bags_lbl,
+ #show_scripts:checked ~ div #scripts_lbl {
background: #4CAF50;
color: white;
}
- body > div {
- border-top: 6px solid #4CAF50;
+ #tab_heads>* {
+ font-size: 130%;
+ padding: 10px;
+ display: inline-block;
+ cursor: pointer;
}
- .tab_head {
- display: inline-block;
+ #tab_heads {
+ -moz-user-select: none;
+ user-select: none;
+ }
+
+ #import_but {
+ font: unset;
+ font-size: 130%;
+ float: right;
+ margin: 0;
+ border-radius: 0;
+ }
+
+ div.tab {
+ min-width: 50vw;
+ width: fit-content;
+ padding-left: 6px;
}
/* popup window with list of selectable components */
@@ -60,7 +64,7 @@
height: 100vh;
left: 0;
top: 0;
- background-color: rgba(0,0,0,0.4);
+ background-color: rgba(60,60,60,0.4);
z-index: 1;
overflow: auto;
vertical-align: center;
@@ -68,135 +72,264 @@
}
.popup_frame {
- background-color: white;
- width: 50vw;
+ background-color: #f0f0f0;
+ margin: 5vh auto;
+ padding: 15px;
+ border: solid #333 4px;
+ border-radius: 15px;
+ width: -moz-fit-content;
+ width: fit-content;
}
- input[type="radio"]:not(:checked)+.import_window_content {
- display: none;
+ .work_li .table_wrapper::before {
+ background: linear-gradient(#e0f0f0, #555);
+ }
+
+ .work_li .table_wrapper::after {
+ background: linear-gradient(#555, #e0f0f0);
+ }
+
+ .table_wrapper.always_scrollbar>* {
+ border-left: solid #454 8px;
+ max-height: 80vh;
+ overflow-y: scroll;
+ }
+
+ .table_wrapper .table_wrapper.always_scrollbar>*,
+ .popup_frame .table_wrapper.always_scrollbar>* {
+ max-height: 60vh;
+ }
+
+ .popup_frame .table_wrapper table {
+ min-width: 30vw;
+ }
+
+ .popup_frame .table_wrapper {
+ margin: 0 auto;
+ }
+
+ td:first-child {
+ max-width: 70vw;
+ overflow: hidden;
+ }
+
+ tr.work_li>td:first-child {
+ padding-right: 0;
+ max-width: unset;
+ }
+
+ tr.work_li>td>div {
+ background-color: #e0f0f0;
+ border-left: solid #454 5px;
+ border-right: solid #454 2px;
+ padding: 5px 10px;
+ }
+
+ .form_grid {
+ display: grid;
+ grid-template-columns: auto auto;
+ }
+
+ .form_grid>label {
+ grid-column: 1 / span 1;
+ margin-right: 10px;
+ }
+
+ .form_grid label {
+ line-height: 34px; /* center text vertically */
+ }
+
+ .form_grid>input, .form_grid>span {
+ grid-column: 2 / span 1;
+ }
+
+ .form_grid>label[for="script_contents_field"],
+ .form_grid>* {
+ grid-column: 1 / span 2;
+ }
+
+ .form_grid>textarea {
+ min-width: 70vw;
+ resize: none;
}
</style>
</head>
<body>
- <!-- The invisible div below is for elements that will be cloned. -->
- <div class="hide">
- <li id="item_li_template">
- <span></span>
- <button> Edit </button>
- <button> Remove </button>
- <button> Export </button>
- </li>
- <li id="bag_component_li_template">
- <span></span>
- <button> Remove </button>
- </li>
- <li id="chbx_component_li_template">
- <input type="checkbox" style="display: inline;"></input>
- <span></span>
- </li>
- <li id="radio_component_li_template">
- <input type="radio" style="display: inline;" name="page_components"></input>
- <span></span>
- </li>
- </div>
+ <template>
+ <tr id="item_li" class="nowrap">
+ <td></td>
+ <td><div class="button"> Edit </div></td>
+ <td><div class="button"> Remove </div></td>
+ <td><div class="button"> Export </div></td>
+ </tr>
+ <tr id="bag_component_li" class="nowrap">
+ <td></td>
+ <td><div class="button"> Remove </div></td>
+ </tr>
+ <tr id="chbx_component_li" class="nowrap">
+ <td>
+ <input type="checkbox" style="display: inline;"></input>
+ <span></span>
+ </td>
+ </tr>
+ <tr id="radio_component_li" class="nowrap">
+ <td>
+ <input type="radio" style="display: inline;" name="page_components"></input>
+ <span></span>
+ </td>
+ </tr>
+ </template>
<!-- Mind the show_*s ids below - their format is assumed in js code -->
<input type="radio" name="tabs" id="show_repos"></input>
<input type="radio" name="tabs" id="show_pages" checked></input>
<input type="radio" name="tabs" id="show_bags"></input>
<input type="radio" name="tabs" id="show_scripts"></input>
- <label for="show_repos" id="repos_lbl"
- class="tab_head"> Repos </label>
- <label for="show_pages" id="pages_lbl"
- class="tab_head"> Pages </label>
- <label for="show_bags" id="bags_lbl"
- class="tab_head"> Bags </label>
- <label for="show_scripts" id="scripts_lbl"
- class="tab_head"> Scripts </label>
-
- <div id="repos">
- <ul id="repos_ul">
- <li id="work_repo_li" class="hide">
- <label for="repo_url_field">URL: </label>
- <input id="repo_url_field"></input>
- <br/>
- <button id="save_repo_but" type="button"> Save </button>
- <button id="discard_repo_but" type="button"> Cancel </button>
- </li>
- </ul>
+ <div id="tab_heads" class="has_bottom_line">
+ <label for="show_repos" id="repos_lbl"> Repos </label>
+ <label for="show_pages" id="pages_lbl"> Pages </label>
+ <label for="show_bags" id="bags_lbl"> Bags </label>
+ <label for="show_scripts" id="scripts_lbl"> Scripts </label>
+ <button id="import_but" style="margin-left: 40px;"> Import </button>
+ </div>
+ <div id="repos" class="tab">
+ <div class="table_wrapper tight_table has_bottom_line has_upper_line">
+ <div>
+ <table>
+ <tbody id="repos_ul">
+ <tr id="work_repo_li" class="hide work_li">
+ <td colspan="4">
+ <div class="form_grid">
+ <label for="repo_url_field">URL: </label>
+ <input id="repo_url_field"></input>
+ <div>
+ <button id="save_repo_but" type="button"> Save </button>
+ <button id="discard_repo_but" type="button"> Cancel </button>
+ </div>
+ </div>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ </div>
<button id="add_repo_but" type="button"> Add repository </button>
</div>
-
- <div id="pages">
- <ul id="pages_ul">
- <li id="work_page_li" class="hide">
- <label for="page_url_field">URL: </label>
- <input id="page_url_field"></input>
- <br/>
- <label>Payload: </label>
- <span id="page_payload"></span>
- <button id="select_page_components_but">
- Choose payload
- </button>
- <br/>
- <input id="page_allow_chbx" type="checkbox" style="display: inline;"></input>
- <label for="page_allow_chbx">Allow native scripts</label>
- <br/>
- <button id="save_page_but" type="button"> Save </button>
- <button id="discard_page_but" type="button"> Cancel </button>
- </li>
- </ul>
+ <div id="pages" class="tab">
+ <div class="table_wrapper tight_table has_bottom_line has_upper_line">
+ <div>
+ <table>
+ <tbody id="pages_ul">
+ <tr id="work_page_li" class="hide work_li">
+ <td colspan="4">
+ <div class="form_grid">
+ <label for="page_url_field">URL: </label>
+ <input id="page_url_field"></input>
+ <label>Payload: </label>
+ <span>
+ <span id="page_payload"></span>
+ <button id="select_page_components_but">
+ Choose payload
+ </button>
+ </span>
+ <div>
+ <input id="page_allow_chbx" type="checkbox" style="display: inline;"></input>
+ <label for="page_allow_chbx">Allow native scripts</label>
+ </div>
+ <div>
+ <button id="save_page_but" type="button"> Save </button>
+ <button id="discard_page_but" type="button"> Cancel </button>
+ </div>
+ </div>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ </div>
<button id="add_page_but" type="button"> Add page </button>
+ <IMPORT html/default_blocking_policy.html />
</div>
-
- <div id="bags">
- <ul id="bags_ul">
- <li id="work_bag_li" class="hide">
- <label for="bag_name_field"> Name: </label>
- <input id="bag_name_field"></input>
- <ul id="bag_components_ul">
- <li id="empty_bag_component_li" class="hide"></li>
- </ul>
- <button id="select_bag_components_but">
- Add scripts
- </button>
- <br/>
- <button id="save_bag_but"> Save </button>
- <button id="discard_bag_but"> Cancel </button>
- </li>
- </ul>
+ <div id="bags" class="tab">
+ <div class="table_wrapper tight_table has_bottom_line has_upper_line">
+ <div>
+ <table>
+ <tbody id="bags_ul">
+ <tr id="work_bag_li" class="hide work_li">
+ <td colspan="4">
+ <div class="form_grid">
+ <label for="bag_name_field"> Name: </label>
+ <input id="bag_name_field"></input>
+ <div class="table_wrapper tight_table has_bottom_line has_upper_line">
+ <div>
+ <table>
+ <tbody id="bag_components_ul">
+ <tr id="empty_bag_component_li" class="hide"></tr>
+ </tbody>
+ </table>
+ </div>
+ </div>
+ <div>
+ <button id="select_bag_components_but">
+ Add scripts
+ </button>
+ </div>
+ <div>
+ <button id="save_bag_but"> Save </button>
+ <button id="discard_bag_but"> Cancel </button>
+ </div>
+ </div>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ </div>
<button id="add_bag_but" type="button"> Add bag </button>
</div>
-
- <div id="scripts">
- <ul id="scripts_ul">
- <li id="work_script_li" class="hide">
- <label for="script_name_field"> Name: </label>
- <input id="script_name_field"></input>
- <br/>
- <label for="script_url_field"> URL: </label>
- <input id="script_url_field"></input>
- <br/>
- <label for="script_sha256_field"> sha256: </label>
- <input id="script_sha256_field"></input>
- <br/>
- <label for="script_contents_field"> contents: </label>
- <textarea id="script_contents_field" rows="20" cols="80"></textarea>
- <br/>
- <button id="save_script_but"> Save </button>
- <button id="discard_script_but"> Cancel </button>
- </li>
- </ul>
+ <div id="scripts" class="tab">
+ <div class="table_wrapper tight_table has_bottom_line has_upper_line">
+ <div>
+ <table>
+ <tbody id="scripts_ul">
+ <tr id="work_script_li" class="hide work_li">
+ <td colspan="4">
+ <div class="form_grid">
+ <label for="script_name_field"> Name: </label>
+ <input id="script_name_field"></input>
+ <label for="script_url_field"> URL: </label>
+ <input id="script_url_field"></input>
+ <label for="script_sha256_field"> SHA256: </label>
+ <input id="script_sha256_field"></input>
+ <aside>
+ Note: URL and SHA256 are ignored if script text is provided.
+ </aside>
+ <label for="script_contents_field"> contents: </label>
+ <textarea id="script_contents_field" rows="20" cols="80"></textarea>
+ <div>
+ <button id="save_script_but"> Save </button>
+ <button id="discard_script_but"> Cancel </button>
+ </div>
+ </div>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ </div>
<button id="add_script_but" type="button"> Add script </button>
</div>
- <button id="import_but" style="margin-top: 40px;"> Import </button>
-
<div id="chbx_components_window" class="hide popup" position="absolute">
<div class="popup_frame">
- <ul id="chbx_components_ul">
-
- </ul>
+ <div class="table_wrapper tight_table has_bottom_line has_upper_line">
+ <div>
+ <table>
+ <tbody id="chbx_components_ul">
+ </tbody>
+ </table>
+ </div>
+ </div>
<button id="commit_bag_components_but"> Add </button>
<button id="cancel_bag_components_but"> Cancel </button>
</div>
@@ -204,13 +337,20 @@
<div id="radio_components_window" class="hide popup" position="absolute">
<div class="popup_frame">
- <ul id="radio_components_ul">
-
- <li id="radio_component_none_li">
- <input id="radio_component_none_input" type="radio" style="display: inline;" name="page_components"></input>
- <span>(None)</span>
- </li>
- </ul>
+ <div class="table_wrapper tight_table always_scrollbar has_bottom_line has_upper_line">
+ <div>
+ <table>
+ <tbody id="radio_components_ul">
+ <tr id="radio_component_none_li">
+ <td>
+ <input id="radio_component_none_input" type="radio" style="display: inline;" name="page_components"></input>
+ <span>(None)</span>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ </div>
<button id="commit_page_components_but"> Choose </button>
<button id="cancel_page_components_but"> Cancel </button>
</div>
@@ -218,6 +358,7 @@
<div id="import_window" class="hide popup" position="absolute">
<div class="popup_frame">
+ <h2> Settings import </h2>
<IMPORT html/import_frame.html />
</div>
</div>
diff --git a/html/options_main.js b/html/options_main.js
index 830c860..2f4f154 100644
--- a/html/options_main.js
+++ b/html/options_main.js
@@ -13,24 +13,36 @@
* IMPORT list_prefixes
* IMPORT nice_name
* IMPORT parse_json_with_schema
+ * IMPORT get_template
* IMPORT by_id
* IMPORT matchers
* IMPORT get_import_frame
+ * IMPORT init_default_policy_dialog
* IMPORTS_END
*/
var storage;
-const item_li_template = by_id("item_li_template");
-const bag_component_li_template = by_id("bag_component_li_template");
-const chbx_component_li_template = by_id("chbx_component_li_template");
-const radio_component_li_template = by_id("radio_component_li_template");
+const item_li_template = get_template("item_li");
+const bag_component_li_template = get_template("bag_component_li");
+const chbx_component_li_template = get_template("chbx_component_li");
+const radio_component_li_template = get_template("radio_component_li");
/* Make sure they are later cloned without id. */
item_li_template.removeAttribute("id");
bag_component_li_template.removeAttribute("id");
chbx_component_li_template.removeAttribute("id");
radio_component_li_template.removeAttribute("id");
+function list_set_scrollbar(list_elem)
+{
+ const op = ((list_elem.children.length === 1 &&
+ list_elem.children[0].classList.contains("hide")) ||
+ list_elem.children.length < 1) ? "remove" : "add";
+ while (!list_elem.classList.contains("table_wrapper"))
+ list_elem = list_elem.parentElement;
+ list_elem.classList[op]("always_scrollbar");
+}
+
function item_li_id(prefix, item)
{
return `li_${prefix}_${item}`;
@@ -65,11 +77,17 @@ function add_li(prefix, item, at_the_end=false)
continue;
ul.ul.insertBefore(li, element);
- return;
+ break;
}
}
+ if (!li.parentElement) {
+ if (ul.work_li !== ul.ul.lastElementChild)
+ ul.ul.appendChild(li);
+ else
+ ul.work_li.before(li);
+ }
- ul.ul.appendChild(li);
+ list_set_scrollbar(ul.ul);
}
const chbx_components_ul = by_id("chbx_components_ul");
@@ -97,12 +115,13 @@ function add_chbx_li(prefix, name)
li.setAttribute("data-prefix", prefix);
li.setAttribute("data-name", name);
- let chbx = li.firstElementChild;
+ let chbx = li.firstElementChild.firstElementChild;
let span = chbx.nextElementSibling;
span.textContent = nice_name(prefix, name);
chbx_components_ul.appendChild(li);
+ list_set_scrollbar(chbx_components_ul);
}
var radio_component_none_li = by_id("radio_component_none_li");
@@ -117,12 +136,13 @@ function add_radio_li(prefix, name)
li.setAttribute("data-prefix", prefix);
li.setAttribute("data-name", name);
- let radio = li.firstElementChild;
+ let radio = li.firstElementChild.firstElementChild;
let span = radio.nextElementSibling;
span.textContent = nice_name(prefix, name);
- radio_components_ul.insertBefore(li, radio_component_none_li);
+ radio_component_none_li.before(li);
+ list_set_scrollbar(radio_components_ul);
}
/* Used to reset edited repo. */
@@ -190,6 +210,13 @@ function work_page_li_data(ul)
const empty_bag_component_li = by_id("empty_bag_component_li");
var bag_components_ul = by_id("bag_components_ul");
+function remove_bag_component_entry(entry)
+{
+ const list = entry.parentElement;
+ entry.remove();
+ list_set_scrollbar(list);
+}
+
/* Used to construct and update components list of edited bag. */
function add_bag_components(components)
{
@@ -198,15 +225,17 @@ function add_bag_components(components)
let li = bag_component_li_template.cloneNode(true);
li.setAttribute("data-prefix", prefix);
li.setAttribute("data-name", name);
+
let span = li.firstElementChild;
span.textContent = nice_name(prefix, name);
let remove_but = span.nextElementSibling;
- remove_but.addEventListener("click", () =>
- bag_components_ul.removeChild(li));
+ remove_but.addEventListener("click",
+ () => remove_bag_component_entry(li));
bag_components_ul.appendChild(li);
}
bag_components_ul.appendChild(empty_bag_component_li);
+ list_set_scrollbar(bag_components_ul);
}
/* Used to reset edited bag. */
@@ -218,8 +247,7 @@ function reset_work_bag_li(ul, item, components)
let old_components_ul = bag_components_ul;
bag_components_ul = old_components_ul.cloneNode(false);
- ul.work_li.insertBefore(bag_components_ul, old_components_ul);
- ul.work_li.removeChild(old_components_ul);
+ old_components_ul.replaceWith(bag_components_ul);
add_bag_components(components);
}
@@ -227,8 +255,7 @@ function reset_work_bag_li(ul, item, components)
/* Used to get edited bag data for saving. */
function work_bag_li_data(ul)
{
- let components_ul = ul.work_name_input.nextElementSibling;
- let component_li = components_ul.firstElementChild;
+ let component_li = bag_components_ul.firstElementChild;
let components = [];
@@ -284,6 +311,8 @@ function cancel_work(prefix)
}
ul.work_li.classList.add("hide");
+ ul.ul.append(ul.work_li);
+ list_set_scrollbar(ul.ul);
ul.state = UL_STATE.IDLE;
}
@@ -322,6 +351,7 @@ function edit_item(prefix, item)
ul.ul.insertBefore(ul.work_li, li);
ul.ul.removeChild(li);
ul.work_li.classList.remove("hide");
+ list_set_scrollbar(ul.ul);
ul.state = UL_STATE.EDITING_ENTRY;
ul.edited_item = item;
@@ -379,6 +409,7 @@ function add_new_item(prefix, name)
ul.reset_work_li(ul);
ul.work_li.classList.remove("hide");
ul.ul.appendChild(ul.work_li);
+ list_set_scrollbar(ul.ul);
if (name !== undefined)
ul.work_name_input.value = name;
@@ -393,7 +424,7 @@ function bag_components()
radio_components_window.classList.add("hide");
for (let li of chbx_components_ul.children) {
- let chbx = li.firstElementChild;
+ let chbx = li.firstElementChild.firstElementChild;
chbx.checked = false;
}
}
@@ -403,7 +434,7 @@ function commit_bag_components()
let selected = [];
for (let li of chbx_components_ul.children) {
- let chbx = li.firstElementChild;
+ let chbx = li.firstElementChild.firstElementChild;
if (!chbx.checked)
continue;
@@ -431,10 +462,11 @@ function page_components()
let [prefix, item] = components;
let li = by_id(radio_li_id(prefix, item));
+
if (li === null)
radio_component_none_input.checked = false;
else
- li.firstElementChild.checked = true;
+ li.firstElementChild.firstElementChild.checked = true;
}
function commit_page_components()
@@ -442,7 +474,7 @@ function commit_page_components()
let components = null;
for (let li of radio_components_ul.children) {
- let radio = li.firstElementChild;
+ let radio = li.firstElementChild.firstElementChild;
if (!radio.checked)
continue;
@@ -603,6 +635,8 @@ async function initialize_import_facility()
import_frame = await get_import_frame();
import_frame.onclose = hide_import_window;
+ import_frame.style_table("has_bottom_line", "always_scrollbar",
+ "has_upper_line", "tight_table");
}
/*
@@ -637,6 +671,8 @@ function jump_to_item(url_with_item)
async function main()
{
+ init_default_policy_dialog();
+
storage = await get_remote_storage();
for (let prefix of list_prefixes) {
@@ -709,6 +745,7 @@ function handle_change(change)
for (let [components_ul, id_creator] of uls_creators) {
let li = by_id(id_creator(change.prefix, change.item));
components_ul.removeChild(li);
+ list_set_scrollbar(components_ul);
}
}
diff --git a/html/reset.css b/html/reset.css
new file mode 100644
index 0000000..dab51cd
--- /dev/null
+++ b/html/reset.css
@@ -0,0 +1,49 @@
+/* http://meyerweb.com/eric/tools/css/reset/
+ v2.0 | 20110126
+ Copyright 2008,2011 Eric A. Meyer
+ Redistribution terms are gathered in the `copyright' file.
+*/
+
+html, body, div, span, applet, object, iframe,
+h1, h2, h3, h4, h5, h6, p, blockquote, pre,
+a, abbr, acronym, address, big, cite, code,
+del, dfn, em, img, ins, kbd, q, s, samp,
+small, strike, strong, sub, sup, tt, var,
+b, u, i, center,
+dl, dt, dd, ol, ul, li,
+fieldset, form, label, legend,
+table, caption, tbody, tfoot, thead, tr, th, td,
+article, aside, canvas, details, embed,
+figure, figcaption, footer, header, hgroup,
+menu, nav, output, ruby, section, summary,
+time, mark, audio, video {
+ margin: 0;
+ padding: 0;
+ border: 0;
+ font-size: 100%;
+ font: inherit;
+ vertical-align: baseline;
+}
+/* HTML5 display-role reset for older browsers */
+article, aside, details, figcaption, figure,
+footer, header, hgroup, menu, nav, section {
+ display: block;
+}
+body {
+ line-height: 1;
+}
+ol, ul {
+ list-style: none;
+}
+blockquote, q {
+ quotes: none;
+}
+blockquote:before, blockquote:after,
+q:before, q:after {
+ content: '';
+ content: none;
+}
+table {
+ border-collapse: collapse;
+ border-spacing: 0;
+}
diff --git a/html/table.css b/html/table.css
new file mode 100644
index 0000000..36f88bb
--- /dev/null
+++ b/html/table.css
@@ -0,0 +1,45 @@
+.table_wrapper {
+ display: block;
+ background-color: #f0f0f0;
+ margin: 6px 0;
+}
+
+.table_wrapper table {
+ border-collapse: unset;
+ width: 100%;
+}
+
+.table_wrapper.tight_table,
+.table_wrapper.tight_table>*,
+.table_wrapper.tight_table>*>table {
+ width: -moz-min-content;
+ width: min-content;
+}
+
+tr:nth-child(odd) {
+ background-color: #e5e5e5;
+}
+
+td {
+ vertical-align: middle;
+ min-width: fit-content;
+ min-width: -moz-fit-content;
+}
+
+.tight_table td {
+ width: 1%;
+}
+
+td:first-child {
+ padding: 3px 10px 6px;
+}
+
+.tight_table td:first-child {
+ width: 100%;
+}
+
+td>div.button {
+ margin-right: 4px;
+ white-space: nowrap;
+ float: right;
+}
diff --git a/manifest.json b/manifest.json
index 328b27f..bd963fe 100644
--- a/manifest.json
+++ b/manifest.json
@@ -6,7 +6,7 @@
"short_name": "Hachette",
"version": "0.0.1",_CHROMIUM_KEY_
"author": "various",
- "description": "Kill the web&js",_GECKO_APPLICATIONS_
+ "description": "Control your \"Web\" browsing.",_GECKO_APPLICATIONS_
"icons":{
"128": "icons/hachette128.png",
"64": "icons/hachette64.png",
@@ -23,7 +23,8 @@
"sessions",
"storage",
"tabs",
- "<all_urls>"
+ "<all_urls>",
+ "unlimitedStorage"
],
"browser_action": {
"browser_style": true,