aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorWojtek Kosior <koszko@koszko.org>2021-08-14 10:07:28 +0200
committerWojtek Kosior <koszko@koszko.org>2021-08-14 10:07:28 +0200
commit443bc095a72949adb4a007c9a19a43da7dd8843d (patch)
tree4b3e392057d8939961a3e707ae596cd85e6fa623
parentae1844f9ac935eb3c89314cd402b4ec2c3d0f537 (diff)
parent2fbab2f07d3cebde1fba0e801df4f3e9129e463b (diff)
downloadbrowser-extension-443bc095a72949adb4a007c9a19a43da7dd8843d.tar.gz
browser-extension-443bc095a72949adb4a007c9a19a43da7dd8843d.zip
merge facility to install from Hydrilla
-rw-r--r--background/main.js2
-rw-r--r--background/page_actions_server.js31
-rw-r--r--background/page_info_server.js74
-rw-r--r--background/policy_injector.js7
-rw-r--r--background/storage.js28
-rw-r--r--background/storage_server.js12
-rwxr-xr-xbuild.sh42
-rw-r--r--common/ajax.js39
-rw-r--r--common/connection_types.js3
-rw-r--r--common/misc.js14
-rw-r--r--common/observable.js36
-rw-r--r--common/sanitize_JSON.js400
-rw-r--r--common/settings_query.js (renamed from background/settings_query.js)33
-rw-r--r--common/storage_client.js17
-rw-r--r--common/stored_types.js7
-rw-r--r--content/activity_info_server.js35
-rw-r--r--content/repo_query.js113
-rw-r--r--copyright6
-rw-r--r--default_settings.json3
-rw-r--r--html/DOM_helpers.js41
-rw-r--r--html/base.css44
-rw-r--r--html/display-panel.html122
-rw-r--r--html/display-panel.js395
-rw-r--r--html/import_frame.html27
-rw-r--r--html/import_frame.js163
-rw-r--r--html/options.html65
-rw-r--r--html/options_main.js209
-rwxr-xr-xprocess_html_file.sh33
-rw-r--r--shell_utils.sh39
29 files changed, 1560 insertions, 480 deletions
diff --git a/background/main.js b/background/main.js
index ffa814e..7c50fd5 100644
--- a/background/main.js
+++ b/background/main.js
@@ -12,7 +12,6 @@
* IMPORT start_storage_server
* IMPORT start_page_actions_server
* IMPORT start_policy_injector
- * IMPORT start_page_info_server
* IMPORT browser
* IMPORTS_END
*/
@@ -20,7 +19,6 @@
start_storage_server();
start_page_actions_server();
start_policy_injector();
-start_page_info_server();
async function init_ext(install_details)
{
diff --git a/background/page_actions_server.js b/background/page_actions_server.js
index c6800a0..58a0073 100644
--- a/background/page_actions_server.js
+++ b/background/page_actions_server.js
@@ -13,19 +13,20 @@
* IMPORT browser
* IMPORT listen_for_connection
* IMPORT sha256
- * IMPORT get_query_best
+ * IMPORT query_best
+ * IMPORT make_ajax_request
* IMPORTS_END
*/
var storage;
-var query_best;
var handler;
function send_actions(url, port)
{
- let [pattern, settings] = query_best(url);
+ const [pattern, settings] = query_best(storage, url);
+ const repos = storage.get_all(TYPE_PREFIX.REPO);
- port.postMessage(["settings", [pattern, settings]]);
+ port.postMessage(["settings", [pattern, settings, repos]]);
if (settings === undefined)
return;
@@ -85,27 +86,6 @@ async function get_script_text(script_name)
}
}
-function ajax_callback()
-{
- if (this.readyState == 4)
- this.resolve_callback(this);
-}
-
-function initiate_ajax_request(resolve, method, url)
-{
- var xhttp = new XMLHttpRequest();
- xhttp.resolve_callback = resolve;
- xhttp.onreadystatechange = ajax_callback;
- xhttp.open(method, url, true);
- xhttp.send();
-}
-
-function make_ajax_request(method, url)
-{
- return new Promise((resolve, reject) =>
- initiate_ajax_request(resolve, method, url));
-}
-
async function fetch_remote_script(script_data)
{
try {
@@ -145,7 +125,6 @@ function new_connection(port)
async function start_page_actions_server()
{
storage = await get_storage();
- query_best = await get_query_best();
listen_for_connection(CONNECTION_TYPE.PAGE_ACTIONS, new_connection);
}
diff --git a/background/page_info_server.js b/background/page_info_server.js
deleted file mode 100644
index 6f02750..0000000
--- a/background/page_info_server.js
+++ /dev/null
@@ -1,74 +0,0 @@
-/**
- * part of Hachette
- * Serving of storage data corresponding to requested urls (server side).
- *
- * Copyright (C) 2021 Wojtek Kosior
- * Redistribution terms are gathered in the `copyright' file.
- */
-
-/*
- * IMPORTS_START
- * IMPORT listen_for_connection
- * IMPORT get_storage
- * IMPORT get_query_all
- * IMPORT TYPE_PREFIX
- * IMPORT CONNECTION_TYPE
- * IMPORT url_matches
- * IMPORTS_END
- */
-
-var storage;
-var query_all;
-
-function handle_change(connection_data, change)
-{
- if (change.prefix !== TYPE_PREFIX.PAGE)
- return;
-
- connection_data.port.postMessage(["change", change]);
-}
-
-async function handle_subscription(connection_data, message)
-{
- const [action, url] = message;
- if (action === "unsubscribe") {
- connection_data.subscribed.delete(url);
- return;
- }
-
- connection_data.subscribed.add(url);
- connection_data.port.postMessage(["new_url", query_all(url)]);
-}
-
-function new_connection(port)
-{
- console.log("new page info connection!");
-
- const connection_data = {
- subscribed : new Set(),
- port
- };
-
- let _handle_change = change => handle_change(connection_data, change);
-
- storage.add_change_listener(_handle_change);
-
- port.onMessage.addListener(m => handle_subscription(connection_data, m));
- port.onDisconnect.addListener(
- () => storage.remove_change_listener(_handle_change)
- );
-}
-
-async function start_page_info_server()
-{
- storage = await get_storage();
- query_all = await get_query_all();
-
- listen_for_connection(CONNECTION_TYPE.PAGE_INFO, new_connection);
-}
-
-/*
- * EXPORTS_START
- * EXPORT start_page_info_server
- * EXPORTS_END
- */
diff --git a/background/policy_injector.js b/background/policy_injector.js
index 80a0e3b..9725e99 100644
--- a/background/policy_injector.js
+++ b/background/policy_injector.js
@@ -16,15 +16,15 @@
* IMPORT gen_unique
* IMPORT gen_nonce
* IMPORT is_privileged_url
+ * IMPORT url_item
* IMPORT url_extract_target
* IMPORT sign_policy
- * IMPORT get_query_best
+ * IMPORT query_best
* IMPORT sanitize_csp_header
* IMPORTS_END
*/
var storage;
-var query_best;
const csp_header_names = new Set([
"content-security-policy",
@@ -56,7 +56,7 @@ function url_inject(details)
if (targets.policy)
targets.target = "";
- let [pattern, settings] = query_best(targets.base_url);
+ let [pattern, settings] = query_best(storage, targets.base_url);
/* Defaults */
if (!pattern)
settings = {};
@@ -162,7 +162,6 @@ function headers_inject(details)
async function start_policy_injector()
{
storage = await get_storage();
- query_best = await get_query_best();
let extra_opts = ["blocking", "responseHeaders"];
if (is_chrome)
diff --git a/background/storage.js b/background/storage.js
index 7229a2d..c2160b0 100644
--- a/background/storage.js
+++ b/background/storage.js
@@ -16,6 +16,7 @@
* IMPORT make_once
* IMPORT browser
* IMPORT is_chrome
+ * IMPORT observables
* IMPORTS_END
*/
@@ -98,13 +99,10 @@ async function list(prefix)
for (let item of await get_list_var(name))
map.set(item, await get(prefix + item));
- return {map, prefix, name, listeners : new Set(), lock : make_lock()};
+ return {map, prefix, name, observable: observables.make(),
+ lock: make_lock()};
}
-var pages;
-var bags;
-var scripts;
-
var list_by_prefix = {};
async function init()
@@ -125,7 +123,7 @@ exports.add_change_listener = function (cb, prefixes=list_prefixes)
prefixes = [prefixes];
for (let prefix of prefixes)
- list_by_prefix[prefix].listeners.add(cb);
+ observables.subscribe(list_by_prefix[prefix].observable, cb);
}
exports.remove_change_listener = function (cb, prefixes=list_prefixes)
@@ -134,13 +132,7 @@ exports.remove_change_listener = function (cb, prefixes=list_prefixes)
prefixes = [prefixes];
for (let prefix of prefixes)
- list_by_prefix[prefix].listeners.delete(cb);
-}
-
-function broadcast_change(change, list)
-{
- for (let listener_callback of list.listeners)
- listener_callback(change);
+ observables.unsubscribe(list_by_prefix[prefix].observable, cb);
}
/* Prepare some hepler functions to get elements of a list */
@@ -202,7 +194,7 @@ async function _set_item(item, value, list)
new_val : value
};
- broadcast_change(change, list);
+ observables.broadcast(list.observable, change);
return old_val;
}
@@ -240,7 +232,7 @@ async function _remove_item(item, list)
new_val : undefined
};
- broadcast_change(change, list);
+ observables.broadcast(list.observable, change);
return old_val;
}
@@ -286,7 +278,7 @@ async function _replace_item(old_item, new_item, list, new_val=undefined)
new_val : undefined
};
- broadcast_change(change, list);
+ observables.broadcast(list.observable, change);
list.map.set(new_item, new_val);
@@ -294,7 +286,7 @@ async function _replace_item(old_item, new_item, list, new_val=undefined)
change.old_val = undefined;
change.new_val = new_val;
- broadcast_change(change, list);
+ observables.broadcast(list.observable, change);
return old_val;
}
@@ -379,7 +371,7 @@ exports.clear = async function ()
for (let [item, val] of list_entries_it(list)) {
change.item = item;
change.old_val = val;
- broadcast_change(change, list);
+ observables.broadcast(list.observable, change);
}
list.map = new Map();
diff --git a/background/storage_server.js b/background/storage_server.js
index 554aff2..2252eb5 100644
--- a/background/storage_server.js
+++ b/background/storage_server.js
@@ -9,7 +9,7 @@
* IMPORTS_START
* IMPORT listen_for_connection
* IMPORT get_storage
- * IMPORT TYPE_PREFIX
+ * IMPORT list_prefixes
* IMPORT CONNECTION_TYPE
* IMPORTS_END
*/
@@ -38,11 +38,11 @@ function new_connection(port)
{
console.log("new remote storage connection!");
- port.postMessage({
- [TYPE_PREFIX.SCRIPT] : storage.get_all(TYPE_PREFIX.SCRIPT),
- [TYPE_PREFIX.BAG] : storage.get_all(TYPE_PREFIX.BAG),
- [TYPE_PREFIX.PAGE] : storage.get_all(TYPE_PREFIX.PAGE)
- });
+ const message = {};
+ for (const prefix of list_prefixes)
+ message[prefix] = storage.get_all(prefix);
+
+ port.postMessage(message);
let handle_change = change => port.postMessage(change);
diff --git a/build.sh b/build.sh
index 60c1863..675dc2c 100755
--- a/build.sh
+++ b/build.sh
@@ -3,36 +3,7 @@
# Copyright (C) 2021 Wojtek Kosior
# Redistribution terms are gathered in the `copyright' file.
-ENDL="
-"
-
-errcho() {
- echo "$@" >&2
-}
-
-map_set_instr() {
- echo "$1__$2='$3'"
-}
-
-map_set() {
- eval "$(map_set_instr "$@")"
-}
-
-map_get() {
- eval "echo \"\$$1__$2\""
-}
-
-map_del_instr() {
- echo "unset $1__$2"
-}
-
-map_del() {
- eval "$(map_del_instr "$@")"
-}
-
-sanitize() {
- echo "$1" | tr /.- _
-}
+. ./shell_utils.sh
handle_export_line() {
if [ "x$1" = "xEXPORTS_START" ]; then
@@ -259,11 +230,13 @@ s^_BGSCRIPTS_^$BGSCRIPTS^
s^_CONTENTSCRIPTS_^$CONTENTSCRIPTS^" \
< manifest.json > $BUILDDIR/manifest.json
- sed "s^_POPUPSCRIPTS_^$POPUPSCRIPTS^" \
- < html/display-panel.html > $BUILDDIR/html/display-panel.html
+ ./process_html_file.sh html/display-panel.html |
+ sed "s^_POPUPSCRIPTS_^$POPUPSCRIPTS^" \
+ > $BUILDDIR/html/display-panel.html
- sed "s^_OPTIONSSCRIPTS_^$OPTIONSSCRIPTS^" \
- < html/options.html > $BUILDDIR/html/options.html
+ ./process_html_file.sh html/options.html |
+ sed "s^_OPTIONSSCRIPTS_^$OPTIONSSCRIPTS^" \
+ > $BUILDDIR/html/options.html
for FILE in $SCRIPTS; do
FILEKEY=$(sanitize "$FILE")
@@ -300,6 +273,7 @@ $(map_get EXPORTCODES $FILEKEY)
fi
cp -r copyright licenses/ $BUILDDIR
+ cp html/*.css $BUILDDIR/html
mkdir $BUILDDIR/icons
cp icons/*.png $BUILDDIR/icons
}
diff --git a/common/ajax.js b/common/ajax.js
new file mode 100644
index 0000000..8082bbe
--- /dev/null
+++ b/common/ajax.js
@@ -0,0 +1,39 @@
+/**
+ * part of Hachette
+ * Wrapping XMLHttpRequest into a Promise.
+ *
+ * Copyright (C) 2021 Wojtek Kosior
+ * Redistribution terms are gathered in the `copyright' file.
+ */
+
+function ajax_callback()
+{
+ if (this.readyState == 4)
+ this.resolve_callback(this);
+}
+
+function initiate_ajax_request(resolve, reject, method, url)
+{
+ const xhttp = new XMLHttpRequest();
+ xhttp.resolve_callback = resolve;
+ xhttp.onreadystatechange = ajax_callback;
+ xhttp.open(method, url, true);
+ try {
+ xhttp.send();
+ } catch(e) {
+ console.log(e);
+ setTimeout(reject, 0);
+ }
+}
+
+function make_ajax_request(method, url)
+{
+ return new Promise((resolve, reject) =>
+ initiate_ajax_request(resolve, reject, method, url));
+}
+
+/*
+ * EXPORTS_START
+ * EXPORT make_ajax_request
+ * EXPORTS_END
+ */
diff --git a/common/connection_types.js b/common/connection_types.js
index 41bde75..88c6964 100644
--- a/common/connection_types.js
+++ b/common/connection_types.js
@@ -13,8 +13,7 @@
const CONNECTION_TYPE = {
REMOTE_STORAGE : "0",
PAGE_ACTIONS : "1",
- PAGE_INFO : "2",
- ACTIVITY_INFO : "3"
+ ACTIVITY_INFO : "2"
};
/*
diff --git a/common/misc.js b/common/misc.js
index fac88bb..3c7dc46 100644
--- a/common/misc.js
+++ b/common/misc.js
@@ -12,6 +12,7 @@
* IMPORT browser
* IMPORT is_chrome
* IMPORT TYPE_NAME
+ * IMPORT TYPE_PREFIX
* IMPORTS_END
*/
@@ -207,6 +208,18 @@ function sanitize_csp_header(header, rule, block)
return {name: header.name, value: new_policy.join('')};
}
+/* Regexes and objest to use as/in schemas for parse_json_with_schema(). */
+const nonempty_string_matcher = /.+/;
+
+const matchers = {
+ sha256: /^[0-9a-f]{64}$/,
+ nonempty_string: nonempty_string_matcher,
+ component: [
+ new RegExp(`^[${TYPE_PREFIX.SCRIPT}${TYPE_PREFIX.BAG}]$`),
+ nonempty_string_matcher
+ ]
+};
+
/*
* EXPORTS_START
* EXPORT gen_nonce
@@ -219,5 +232,6 @@ function sanitize_csp_header(header, rule, block)
* EXPORT open_in_settings
* EXPORT is_privileged_url
* EXPORT sanitize_csp_header
+ * EXPORT matchers
* EXPORTS_END
*/
diff --git a/common/observable.js b/common/observable.js
new file mode 100644
index 0000000..1fb0b0a
--- /dev/null
+++ b/common/observable.js
@@ -0,0 +1,36 @@
+/**
+ * part of Hachette
+ * Facilitate listening to events
+ *
+ * Copyright (C) 2021 Wojtek Kosior
+ * Redistribution terms are gathered in the `copyright' file.
+ */
+
+function make()
+{
+ return new Set();
+}
+
+function subscribe(observable, cb)
+{
+ observable.add(cb);
+}
+
+function unsubscribe(observable, cb)
+{
+ observable.delete(cb);
+}
+
+function broadcast(observable, event)
+{
+ for (const callback of observable)
+ callback(event);
+}
+
+const observables = {make, subscribe, unsubscribe, broadcast};
+
+/*
+ * EXPORTS_START
+ * EXPORT observables
+ * EXPORTS_END
+ */
diff --git a/common/sanitize_JSON.js b/common/sanitize_JSON.js
new file mode 100644
index 0000000..8b86d2d
--- /dev/null
+++ b/common/sanitize_JSON.js
@@ -0,0 +1,400 @@
+/**
+ * part of Hachette
+ * Powerful, full-blown format enforcer for externally-obtained JSON
+ *
+ * Copyright (C) 2021 Wojtek Kosior
+ * Redistribution terms are gathered in the `copyright' file.
+ */
+
+var error_path;
+var invalid_schema;
+
+function parse_json_with_schema(schema, json_string)
+{
+ error_path = [];
+ invalid_schema = false;
+
+ try {
+ return sanitize_unknown(schema, JSON.parse(json_string));
+ } catch (e) {
+ throw `Invalid JSON${invalid_schema ? " schema" : ""}: ${e}.`;
+ } finally {
+ /* Allow garbage collection. */
+ error_path = undefined;
+ }
+}
+
+function error_message(cause)
+{
+ return `object${error_path.join("")} ${cause}`;
+}
+
+function sanitize_unknown(schema, item)
+{
+ let error_msg = undefined;
+ let schema_options = [];
+ let has_default = false;
+ let _default = undefined;
+
+ if (!Array.isArray(schema) || schema[1] === "matchentry" ||
+ schema.length < 2 || !["ordefault", "or"].includes(schema[1]))
+ return sanitize_unknown_no_alternatives(schema, item);
+
+ if ((schema.length & 1) !== 1) {
+ invalid_schema = true;
+ throw error_message("was not understood");
+ }
+
+ for (let i = 0; i < schema.length; i++) {
+ if ((i & 1) !== 1) {
+ schema_options.push(schema[i]);
+ continue;
+ }
+
+ if (schema[i] === "or")
+ continue;
+ if (schema[i] === "ordefault" && schema.length === i + 2) {
+ has_default = true;
+ _default = schema[i + 1];
+ break;
+ }
+
+ invalid_schema = true;
+ throw error_message("was not understood");
+ }
+
+ for (const schema_option of schema_options) {
+ try {
+ return sanitize_unknown_no_alternatives(schema_option, item);
+ } catch (e) {
+ if (invalid_schema)
+ throw e;
+
+ if (has_default)
+ continue;
+
+ if (error_msg === undefined)
+ error_msg = e;
+ else
+ error_msg = `${error_msg}, or ${e}`;
+ }
+ }
+
+ if (has_default)
+ return _default;
+
+ throw error_msg;
+}
+
+function sanitize_unknown_no_alternatives(schema, item)
+{
+ for (const [schema_check, item_check, sanitizer, type_name] of checks) {
+ if (schema_check(schema)) {
+ if (item_check(item))
+ return sanitizer(schema, item);
+ throw error_message(`should be ${type_name} but is not`);
+ }
+ }
+
+ invalid_schema = true;
+ throw error_message("was not understood");
+}
+
+function key_error_path_segment(key)
+{
+ return /^[a-zA-Z_][a-zA-Z_0-9]*$/.exec(key) ?
+ `.${key}` : `[${JSON.stringify(key)}]`;
+}
+
+/*
+ * Generic object - one that can contain arbitrary keys (in addition to ones
+ * specified explicitly in the schema).
+ */
+function sanitize_genobj(schema, object)
+{
+ let max_matched_entries = Infinity;
+ let min_matched_entries = 0;
+ let matched_entries = 0;
+ const entry_schemas = [];
+ schema = [...schema];
+
+ if (schema[2] === "minentries") {
+ if (schema.length < 4) {
+ invalid_schema = true;
+ throw error_message("was not understood");
+ }
+
+ min_matched_entries = schema[3];
+ schema.splice(2, 2);
+ }
+
+ if (min_matched_entries < 0) {
+ invalid_schema = true;
+ throw error_message('specifies invalid "minentries" (should be a non-negative number)');
+ }
+
+ if (schema[2] === "maxentries") {
+ if (schema.length < 4) {
+ invalid_schema = true;
+ throw error_message("was not understood");
+ }
+
+ max_matched_entries = schema[3];
+ schema.splice(2, 2);
+ }
+
+ if (max_matched_entries < 0) {
+ invalid_schema = true;
+ throw error_message('specifies invalid "maxentries" (should be a non-negative number)');
+ }
+
+ while (schema.length > 2) {
+ let regex = /.+/;
+
+ if (schema.length > 3) {
+ regex = schema[2];
+ schema.splice(2, 1);
+ }
+
+ if (typeof regex === "string")
+ regex = new RegExp(regex);
+
+ entry_schemas.push([regex, schema[2]]);
+ schema.splice(2, 1);
+ }
+
+ const result = sanitize_object(schema[0], object);
+
+ for (const [key, entry] of Object.entries(object)) {
+ if (result.hasOwnProperty(key))
+ continue;
+
+ matched_entries += 1;
+ if (matched_entries > max_matched_entries)
+ throw error_message(`has more than ${max_matched_entries} matched entr${max_matched_entries === 1 ? "y" : "ies"}`);
+
+ error_path.push(key_error_path_segment(key));
+
+ let match = false;
+ for (const [key_regex, entry_schema] of entry_schemas) {
+ if (!key_regex.exec(key))
+ continue;
+
+ match = true;
+
+ sanitize_object_entry(result, key, entry_schema, object);
+ break;
+ }
+
+ if (!match) {
+ const regex_list = entry_schemas.map(i => i[0]).join(", ");
+ throw error_message(`does not match any of key regexes: [${regex_list}]`);
+ }
+
+ error_path.pop();
+ }
+
+ if (matched_entries < min_matched_entries)
+ throw error_message(`has less than ${min_matched_entries} matched entr${min_matched_entries === 1 ? "y" : "ies"}`);
+
+ return result;
+}
+
+function sanitize_array(schema, array)
+{
+ let min_length = 0;
+ let max_length = Infinity;
+ let repeat_length = 1;
+ let i = 0;
+ const result = [];
+
+ schema = [...schema];
+ if (schema[schema.length - 2] === "maxlen") {
+ max_length = schema[schema.length - 1];
+ schema.splice(schema.length - 2);
+ }
+
+ if (schema[schema.length - 2] === "minlen") {
+ min_length = schema[schema.length - 1];
+ schema.splice(schema.length - 2);
+ }
+
+ if (["repeat", "repeatfull"].includes(schema[schema.length - 2]))
+ repeat_length = schema.pop();
+ if (repeat_length < 1) {
+ invalid_schema = true;
+ throw error_message('specifies invalid "${schema[schema.length - 2]}" (should be number greater than 1)');
+ }
+ if (["repeat", "repeatfull"].includes(schema[schema.length - 1])) {
+ var repeat_directive = schema.pop();
+ var repeat = schema.splice(schema.length - repeat_length);
+ } else if (schema.length !== array.length) {
+ throw error_message(`does not have exactly ${schema.length} items`);
+ }
+
+ if (repeat_directive === "repeatfull" &&
+ (array.length - schema.length) % repeat_length !== 0)
+ throw error_message(`does not contain a full number of item group repetitions`);
+
+ if (array.length < min_length)
+ throw error_message(`has less than ${min_length} element${min_length === 1 ? "" : "s"}`);
+
+ if (array.length > max_length)
+ throw error_message(`has more than ${max_length} element${max_length === 1 ? "" : "s"}`);
+
+ for (const item of array) {
+ if (i >= schema.length) {
+ i = 0;
+ schema = repeat;
+ }
+
+ error_path.push(`[${i}]`);
+ const sanitized = sanitize_unknown(schema[i], item);
+ if (sanitized !== discard)
+ result.push(sanitized);
+ error_path.pop();
+
+ i++;
+ }
+
+ return result;
+}
+
+function sanitize_regex(schema, string)
+{
+ if (schema.test(string))
+ return string;
+
+ throw error_message(`does not match regex ${schema}`);
+}
+
+const string_spec_regex = /^string(:(.*))?$/;
+
+function sanitize_string(schema, string)
+{
+ const regex = string_spec_regex.exec(schema)[2];
+
+ if (regex === undefined)
+ return string;
+
+ return sanitize_regex(new RegExp(regex), string);
+}
+
+function sanitize_object(schema, object)
+{
+ const result = {};
+
+ for (let [key, entry_schema] of Object.entries(schema)) {
+ error_path.push(key_error_path_segment(key));
+ sanitize_object_entry(result, key, entry_schema, object);
+ error_path.pop();
+ }
+
+ return result;
+}
+
+function sanitize_object_entry(result, key, entry_schema, object)
+{
+ let optional = false;
+ let has_default = false;
+ let _default = undefined;
+
+ if (Array.isArray(entry_schema) && entry_schema.length > 1) {
+ if (entry_schema[0] === "optional") {
+ optional = true;
+ entry_schema = [...entry_schema].splice(1);
+
+ const idx_def = entry_schema.length - (entry_schema.length & 1) - 1;
+ if (entry_schema[idx_def] === "default") {
+ has_default = true;
+ _default = entry_schema[idx_def + 1];
+ entry_schema.splice(idx_def);
+ } else if ((entry_schema.length & 1) !== 1) {
+ invalid_schema = true;
+ throw error_message("was not understood");
+ }
+
+ if (entry_schema.length < 2)
+ entry_schema = entry_schema[0];
+ }
+ }
+
+ let unsanitized_value = object[key];
+ if (unsanitized_value === undefined) {
+ if (!optional)
+ throw error_message("is missing");
+
+ if (has_default)
+ result[key] = _default;
+
+ return;
+ }
+
+ const sanitized = sanitize_unknown(entry_schema, unsanitized_value);
+ if (sanitized !== discard)
+ result[key] = sanitized;
+}
+
+function take_literal(schema, item)
+{
+ return item;
+}
+
+/*
+ * This function is used like a symbol. Other parts of code do sth like
+ * `item === discard` to check if item was returned by this function.
+ */
+function discard(schema, item)
+{
+ return discard;
+}
+
+/*
+ * The following are some helper functions to categorize various
+ * schema item specifiers (used in the array below).
+ */
+
+function is_genobj_spec(item)
+{
+ return Array.isArray(item) && item[1] === "matchentry";
+}
+
+function is_regex(item)
+{
+ return typeof item === "object" && typeof item.test === "function";
+}
+
+function is_string_spec(item)
+{
+ return typeof item === "string" && string_spec_regex.test(item);
+}
+
+function is_object(item)
+{
+ return typeof item === "object";
+}
+
+function eq(what)
+{
+ return i => i === what;
+}
+
+/* Array and null checks must go before object check. */
+const checks = [
+ [is_genobj_spec, is_object, sanitize_genobj, "an object"],
+ [Array.isArray, Array.isArray, sanitize_array, "an array"],
+ [eq(null), i => i === null, take_literal, "null"],
+ [is_regex, i => typeof i === "string", sanitize_regex, "a string"],
+ [is_string_spec, i => typeof i === "string", sanitize_string, "a string"],
+ [is_object, is_object, sanitize_object, "an object"],
+ [eq("number"), i => typeof i === "number", take_literal, "a number"],
+ [eq("boolean"), i => typeof i === "boolean", take_literal, "a boolean"],
+ [eq("anything"), i => true, take_literal, "dummy"],
+ [eq("discard"), i => true, discard, "dummy"]
+];
+
+/*
+ * EXPORTS_START
+ * EXPORT parse_json_with_schema
+ * EXPORTS_END
+ */
diff --git a/background/settings_query.js b/common/settings_query.js
index d0d9511..e85ae63 100644
--- a/background/settings_query.js
+++ b/common/settings_query.js
@@ -7,23 +7,12 @@
/*
* IMPORTS_START
- * IMPORT make_once
- * IMPORT get_storage
* IMPORT TYPE_PREFIX
* IMPORT for_each_possible_pattern
* IMPORTS_END
*/
-var storage;
-
-async function init(fun)
-{
- storage = await get_storage();
-
- return fun;
-}
-
-function check_pattern(pattern, multiple, matched)
+function check_pattern(storage, pattern, multiple, matched)
{
const settings = storage.get(TYPE_PREFIX.PAGE, pattern);
@@ -36,30 +25,28 @@ function check_pattern(pattern, multiple, matched)
return false;
}
-function query(url, multiple)
+function query(storage, url, multiple)
{
const matched = [];
- for_each_possible_pattern(url, p => check_pattern(p, multiple, matched));
+ const cb = p => check_pattern(storage, p, multiple, matched);
+ for_each_possible_pattern(url, cb);
return multiple ? matched : (matched[0] || [undefined, undefined]);
}
-function query_best(url)
+function query_best(storage, url)
{
- return query(url, false);
+ return query(storage, url, false);
}
-function query_all(url)
+function query_all(storage, url)
{
- return query(url, true);
+ return query(storage, url, true);
}
-const get_query_best = make_once(() => init(query_best));
-const get_query_all = make_once(() => init(query_all));
-
/*
* EXPORTS_START
- * EXPORT get_query_best
- * EXPORT get_query_all
+ * EXPORT query_best
+ * EXPORT query_all
* EXPORTS_END
*/
diff --git a/common/storage_client.js b/common/storage_client.js
index 4849a65..2b2f495 100644
--- a/common/storage_client.js
+++ b/common/storage_client.js
@@ -8,7 +8,6 @@
/*
* IMPORTS_START
* IMPORT CONNECTION_TYPE
- * IMPORT TYPE_PREFIX
* IMPORT list_prefixes
* IMPORT make_once
* IMPORT browser
@@ -47,20 +46,10 @@ function handle_message(message)
setTimeout(resolve, 0, message.result);
}
-function list(name, prefix)
-{
- return {prefix, name, listeners : new Set()};
-}
-
-var scripts = list("scripts", TYPE_PREFIX.SCRIPT);
-var bags = list("bags", TYPE_PREFIX.BAG);
-var pages = list("pages", TYPE_PREFIX.PAGE);
+const list_by_prefix = {};
-const list_by_prefix = {
- [TYPE_PREFIX.SCRIPT] : scripts,
- [TYPE_PREFIX.BAG] : bags,
- [TYPE_PREFIX.PAGE] : pages
-};
+for (const prefix of list_prefixes)
+ list_by_prefix[prefix] = {prefix, listeners : new Set()};
var resolve_init;
diff --git a/common/stored_types.js b/common/stored_types.js
index a6f1f2f..bfceba6 100644
--- a/common/stored_types.js
+++ b/common/stored_types.js
@@ -14,19 +14,24 @@
*/
const TYPE_PREFIX = {
+ REPO: "r",
PAGE : "p",
BAG : "b",
SCRIPT : "s",
- VAR : "_"
+ VAR : "_",
+ /* Url prefix is not used in stored settings. */
+ URL : "u"
};
const TYPE_NAME = {
+ [TYPE_PREFIX.REPO] : "repo",
[TYPE_PREFIX.PAGE] : "page",
[TYPE_PREFIX.BAG] : "bag",
[TYPE_PREFIX.SCRIPT] : "script"
}
const list_prefixes = [
+ TYPE_PREFIX.REPO,
TYPE_PREFIX.PAGE,
TYPE_PREFIX.BAG,
TYPE_PREFIX.SCRIPT
diff --git a/content/activity_info_server.js b/content/activity_info_server.js
index 8435377..81a25fb 100644
--- a/content/activity_info_server.js
+++ b/content/activity_info_server.js
@@ -11,12 +11,20 @@
* IMPORTS_START
* IMPORT listen_for_connection
* IMPORT CONNECTION_TYPE
+ * IMPORT repo_query
+ * IMPORT subscribe_repo_query_results
+ * IMPORT unsubscribe_repo_query_results
* IMPORTS_END
*/
var activities = [];
var ports = new Set();
+function report_activity_oneshot(name, data, port)
+{
+ port.postMessage([name, data]);
+}
+
function report_activity(name, data)
{
const activity = [name, data];
@@ -36,6 +44,22 @@ function report_settings(settings)
report_activity("settings", settings);
}
+function report_repo_query_action(update, port)
+{
+ report_activity_oneshot("repo_query_action", update, port);
+}
+
+function trigger_repo_query(query_specifier)
+{
+ repo_query(...query_specifier);
+}
+
+function handle_disconnect(port, report_action)
+{
+ ports.delete(port)
+ unsubscribe_repo_query_results(report_action);
+}
+
function new_connection(port)
{
console.log("new activity info connection!");
@@ -44,6 +68,17 @@ function new_connection(port)
for (const activity of activities)
port.postMessage(activity);
+
+ const report_action = u => report_repo_query_action(u, port);
+ subscribe_repo_query_results(report_action);
+
+ /*
+ * So far the only thing we expect to receive is repo query order. Once more
+ * possibilities arrive, we will need to complicate this listener.
+ */
+ port.onMessage.addListener(trigger_repo_query);
+
+ port.onDisconnect.addListener(() => handle_disconnect(port, report_action));
}
function start_activity_info_server()
diff --git a/content/repo_query.js b/content/repo_query.js
new file mode 100644
index 0000000..3708108
--- /dev/null
+++ b/content/repo_query.js
@@ -0,0 +1,113 @@
+/**
+ * part of Hachette
+ * Getting available content for site from remote repositories.
+ *
+ * Copyright (C) 2021 Wojtek Kosior
+ * Redistribution terms are gathered in the `copyright' file.
+ */
+
+/*
+ * IMPORTS_START
+ * IMPORT make_ajax_request
+ * IMPORT observables
+ * IMPORT TYPE_PREFIX
+ * IMPORT parse_json_with_schema
+ * IMPORT matchers
+ * IMPORTS_END
+ */
+
+const paths = {
+ [TYPE_PREFIX.PAGE]: "/pattern",
+ [TYPE_PREFIX.BAG]: "/bag",
+ [TYPE_PREFIX.SCRIPT]: "/script",
+ [TYPE_PREFIX.URL]: "/query"
+};
+
+const queried_items = new Map();
+const observable = observables.make();
+
+function repo_query(prefix, item, repo_urls)
+{
+ const key = prefix + item;
+
+ const results = queried_items.get(key) || {};
+ queried_items.set(key, results);
+
+ for (const repo_url of repo_urls)
+ perform_query_against(key, repo_url, results);
+}
+
+const page_schema = {
+ pattern: matchers.nonempty_string,
+ payload: ["optional", matchers.component, "default", undefined]
+};
+const bag_schema = {
+ name: matchers.nonempty_string,
+ components: ["optional", [matchers.component, "repeat"], "default", []]
+};
+const script_schema = {
+ name: matchers.nonempty_string,
+ location: matchers.nonempty_string,
+ sha256: matchers.sha256,
+};
+const search_result_schema = [page_schema, "repeat"];
+
+const schemas = {
+ [TYPE_PREFIX.PAGE]: page_schema,
+ [TYPE_PREFIX.BAG]: bag_schema,
+ [TYPE_PREFIX.SCRIPT]: script_schema,
+ [TYPE_PREFIX.URL]: search_result_schema
+}
+
+async function perform_query_against(key, repo_url, results)
+{
+ if (results[repo_url] !== undefined)
+ return;
+
+ const prefix = key[0];
+ const item = key.substring(1);
+ const result = {state: "started"};
+ results[repo_url] = result;
+
+ const broadcast_msg = {prefix, item, results: {[repo_url]: result}};
+ observables.broadcast(observable, broadcast_msg);
+
+ let state = "connection_error";
+ const query_url =
+ `${repo_url}${paths[prefix]}?n=${encodeURIComponent(item)}`;
+
+ try {
+ let xhttp = await make_ajax_request("GET", query_url);
+ if (xhttp.status === 200) {
+ state = "parse_error";
+ result.response =
+ parse_json_with_schema(schemas[prefix], xhttp.responseText);
+ state = "completed";
+ }
+ } catch (e) {
+ console.log(e);
+ }
+
+ result.state = state;
+ observables.broadcast(observable, broadcast_msg);
+}
+
+function subscribe_repo_query_results(cb)
+{
+ observables.subscribe(observable, cb);
+ for (const [key, results] of queried_items.entries())
+ cb({prefix: key[0], item: key.substring(1), results});
+}
+
+function unsubscribe_repo_query_results(cb)
+{
+ observables.unsubscribe(observable, cb);
+}
+
+/*
+ * EXPORTS_START
+ * EXPORT repo_query
+ * EXPORT subscribe_repo_query_results
+ * EXPORT unsubscribe_repo_query_results
+ * EXPORTS_END
+ */
diff --git a/copyright b/copyright
index 36340e4..05a16aa 100644
--- a/copyright
+++ b/copyright
@@ -6,7 +6,7 @@ Files: *
Copyright: 2021 Wojtek Kosior <koszko@koszko.org>
License: GPL-3+-javascript or Alicense-1.0
-Files: re-generate_icons.sh build.sh
+Files: *.sh
Copyright: 2021 Wojtek Kosior <koszko@koszko.org>
2021 jahoti <jahoti@tilde.team>
License: CC0
@@ -20,11 +20,11 @@ Copyright: 2021 Wojtek Kosior <koszko@koszko.org>
2021 jahoti <jahoti@tilde.team>
License: GPL-3+-javascript or Alicense-1.0
-Files: README.txt copyright
+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
+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
diff --git a/default_settings.json b/default_settings.json
index 8656381..44fbca0 100644
--- a/default_settings.json
+++ b/default_settings.json
@@ -43,5 +43,8 @@
"phttps://www.worldcat.org/title/**": {
"components": ["s", "worldcat (library holdings)"]
}
+ },
+ {
+ "rhttps://api-demo.hachette-hydrilla.org": {}
}
]
diff --git a/html/DOM_helpers.js b/html/DOM_helpers.js
new file mode 100644
index 0000000..2bff966
--- /dev/null
+++ b/html/DOM_helpers.js
@@ -0,0 +1,41 @@
+/**
+ * Hachette operations on DOM elements
+ *
+ * Copyright (C) 2021 Wojtek Kosior
+ * Redistribution terms are gathered in the `copyright' file.
+ */
+
+function by_id(id)
+{
+ return document.getElementById(id);
+}
+
+function clone_template(template_id)
+{
+ const clone = document.getElementById(template_id).cloneNode(true);
+ const result_object = {};
+ const to_process = [clone];
+
+ while (to_process.length > 0) {
+ const element = to_process.pop();
+ const template_key = element.getAttribute("data-template");
+
+ if (template_key)
+ result_object[template_key] = element;
+
+ element.removeAttribute("id");
+ element.removeAttribute("template_key");
+
+ for (const child of element.children)
+ to_process.push(child);
+ }
+
+ return result_object;
+}
+
+/*
+ * EXPORTS_START
+ * EXPORT by_id
+ * EXPORT clone_template
+ * EXPORTS_END
+ */
diff --git a/html/base.css b/html/base.css
new file mode 100644
index 0000000..2256833
--- /dev/null
+++ b/html/base.css
@@ -0,0 +1,44 @@
+/**
+ * Hachette base styles
+ *
+ * Copyright (C) 2021 Wojtek Kosior
+ * Copyright (C) 2021 Nicholas Johnson
+ * Redistribution terms are gathered in the `copyright' file.
+ */
+
+input[type="checkbox"], input[type="radio"], .hide {
+ display: none;
+}
+
+.show_next:not(:checked)+* {
+ display: none;
+}
+
+.show_hide_next2:not(:checked)+* {
+ display: none;
+}
+
+.show_hide_next2:checked+*+* {
+ display: none;
+}
+
+button, .button {
+ background-color: #4CAF50;
+ border: none;
+ border-radius: 8px;
+ color: white;
+ text-align: center;
+ text-decoration: none;
+ display: inline-block;
+ padding: 6px 12px;
+ margin: 2px 0px;
+}
+
+button.slimbutton, .button.slimbutton {
+ padding: 2px 4px;
+ margin: 0;
+}
+
+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);
+}
diff --git a/html/display-panel.html b/html/display-panel.html
index 9b6d619..1b9c77b 100644
--- a/html/display-panel.html
+++ b/html/display-panel.html
@@ -7,37 +7,31 @@
<head>
<meta charset="utf-8"/>
<title>Hachette - page settings</title>
+ <link type="text/css" rel="stylesheet" href="base.css" />
<style>
- input[type="radio"], input[type="checkbox"] {
- display: none;
- }
-
body {
width: 300px;
height: 300px;
}
- .show_next:not(:checked)+* {
- display: none;
+ ul {
+ padding-inline-start: 15px;
}
- .hide {
- display: none;
+ .bold {
+ font-weight: bold;
}
- #possible_patterns_chbx:not(:checked)+label span#triangle:first-child+span,
- #possible_patterns_chbx:not(:checked)+label+*,
- #possible_patterns_chbx:checked+label span#triangle:first-child {
+ .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+* {
display: none;
}
#container_for_injected>#none_injected:not(:last-child) {
display: none;
}
-
- input#connected_chbx:checked+div+h3 {
- display: none;
- }
</style>
</head>
<body>
@@ -47,49 +41,85 @@
<span></span>
<button>View in settings</button>
</li>
+ <li id="query_match_li_template" class="queried_pattern_match" data-template="li">
+ <div>
+ <span>pattern:</span>
+ <span class="bold" data-template="pattern"></span>
+ <button data-template="btn">Install</button>
+ </div>
+ <div id="unrollable_component_template" data-template="unroll_container">
+ <span data-template="component_label">payload:</span>
+ <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>
+ </span>
+ <span data-template="component"></span>
+ </label>
+ <div data-template="unroll"></div>
+ </div>
+ </li>
</div>
- <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_install_view_chbx" type="checkbox" class="show_hide_next2"></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>
+ <div id="main_view">
+ <h2 id="page_url_heading"></h2>
- <input id="show_page_state_chbx" type="checkbox" class="show_next"></input>
- <div>
- <input id="possible_patterns_chbx" type="checkbox"></input>
- <label for="possible_patterns_chbx">
- <h3>
- <span id="triangle">&#x23F5;</span><span>&#x23F7;</span>
- Possible patterns
- </h3>
- </label>
- <ul id="possible_patterns"></ul>
+ <input id="show_privileged_notice_chbx" type="checkbox" class="show_next"></input>
+ <h3>Privileged page</h3>
- <input id="connected_chbx" type="checkbox" class="show_next"></input>
+ <input id="show_page_state_chbx" type="checkbox" class="show_next"></input>
<div>
- <h3>
- Matched pattern: <span id="pattern">...</span>
+ <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>
+ <div>
+ Matched pattern: <span id="pattern" class="bold">...</span>
<button id="view_pattern" class="hide">
- View in settings
+ View in settings
</button>
- </h3>
- <h3>
- Blocked: <span id="blocked">...</span>
- </h3>
- <h3>
- Payload: <span id="payload">...</span>
+ <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>
- <h3>Injected</h3>
- <div id="container_for_injected">
- <span id="none_injected">None</span>
+ <h3>Injected</h3>
+ <div id="container_for_injected">
+ <span id="none_injected">None</span>
+ </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>
+ <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>
- <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>_POPUPSCRIPTS_
+ <button id="settings_but" type="button" style="margin-top: 20px;">Settings</button>
+ </div>_POPUPSCRIPTS_
</body>
</html>
diff --git a/html/display-panel.js b/html/display-panel.js
index 650c234..b4d9abb 100644
--- a/html/display-panel.js
+++ b/html/display-panel.js
@@ -10,6 +10,11 @@
* IMPORT browser
* IMPORT is_chrome
* IMPORT is_mozilla
+ *** Using remote storage here seems inefficient, we only resort to that
+ *** temporarily, before all storage access gets reworked.
+ * IMPORT get_remote_storage
+ * IMPORT get_import_frame
+ * IMPORT query_all
* IMPORT CONNECTION_TYPE
* IMPORT url_item
* IMPORT is_privileged_url
@@ -17,13 +22,13 @@
* IMPORT nice_name
* IMPORT open_in_settings
* IMPORT for_each_possible_pattern
+ * IMPORT by_id
+ * IMPORT clone_template
* IMPORTS_END
*/
-function by_id(id)
-{
- return document.getElementById(id);
-}
+let storage;
+let tab_url;
const tab_query = {currentWindow: true, active: true};
@@ -55,28 +60,19 @@ async function show_page_activity_info()
return;
}
- const url = url_item(tab.url);
- page_url_heading.textContent = url;
- if (is_privileged_url(url)) {
+ tab_url = url_item(tab.url);
+ page_url_heading.textContent = tab_url;
+ if (is_privileged_url(tab_url)) {
show_privileged_notice_chbx.checked = true;
return;
}
- populate_possible_patterns_list(url);
+ populate_possible_patterns_list(tab_url);
show_page_state_chbx.checked = true;
try_to_connect(tab.id);
}
-function populate_possible_patterns_list(url)
-{
- for_each_possible_pattern(url, add_pattern_to_list);
-
- const port = browser.runtime.connect({name: CONNECTION_TYPE.PAGE_INFO});
- port.onMessage.addListener(handle_page_info);
- port.postMessage(["subscribe", url]);
-}
-
const possible_patterns_ul = by_id("possible_patterns");
const pattern_li_template = by_id("pattern_li_template");
pattern_li_template.removeAttribute("id");
@@ -121,51 +117,69 @@ function set_pattern_li_button_text(li_id, text)
by_id(li_id).firstElementChild.nextElementSibling.textContent = text;
}
-function handle_page_info(message)
+function handle_page_change(change)
{
- const [type, data] = message;
+ 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 (type === "change") {
- const li_id = ensure_pattern_exists(data.item);
- if (data.old_val === undefined)
- set_pattern_li_button_text(li_id, "Edit in settings");
- if (data.new_val === undefined)
- set_pattern_li_button_text(li_id, "Add setting");
- }
+function populate_possible_patterns_list(url)
+{
+ for_each_possible_pattern(url, add_pattern_to_list);
- if (type === "new_url") {
- for (const li_id of known_patterns.values())
- set_pattern_li_button_text(li_id, "Add setting");
- for (const [pattern, settings] of data) {
- set_pattern_li_button_text(ensure_pattern_exists(pattern),
- "Edit in settings")
- }
+ for (const [pattern, settings] of query_all(storage, url)) {
+ set_pattern_li_button_text(ensure_pattern_exists(pattern),
+ "Edit in settings");
}
+
+ storage.add_change_listener(handle_page_change, [TYPE_PREFIX.PAGE]);
}
const connected_chbx = by_id("connected_chbx");
+const query_pattern_but = by_id("query_pattern");
+
+var content_script_port;
function try_to_connect(tab_id)
{
/* This won't connect to iframes. We'll add support for them later */
const connect_info = {name: CONNECTION_TYPE.ACTIVITY_INFO, frameId: 0};
- const port = browser.tabs.connect(tab_id, connect_info);
+ content_script_port = browser.tabs.connect(tab_id, connect_info);
+
+ const disconnect_cb = () => handle_disconnect(tab_id, start_querying_repos);
+ content_script_port.onDisconnect.addListener(disconnect_cb);
+ content_script_port.onMessage.addListener(handle_activity_report);
- port.onDisconnect.addListener(port => handle_disconnect(tab_id));
- port.onMessage.addListener(handle_activity_report);
+ query_pattern_but.addEventListener("click", start_querying_repos);
if (is_mozilla)
- setTimeout(() => monitor_connecting(port, tab_id), 1000);
+ setTimeout(() => monitor_connecting(tab_id), 1000);
+}
+
+const query_started_chbx = by_id("query_started_chbx");
+
+function start_querying_repos(port)
+{
+ 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");
-function handle_disconnect(tab_id)
+function handle_disconnect(tab_id, button_cb)
{
+ query_pattern_but.removeEventListener("click", button_cb);
+ content_script_port = null;
+
if (is_chrome && !browser.runtime.lastError)
return;
- /* return if there was no connection initialization failure */
+ /* return if error was not during connection initialization */
if (connected_chbx.checked)
return;
@@ -173,12 +187,16 @@ function handle_disconnect(tab_id)
setTimeout(() => try_to_connect(tab_id), 1000);
}
-function monitor_connecting(port, tab_id)
+function monitor_connecting(tab_id)
{
if (connected_chbx.checked)
return;
- port.disconnect();
+ if (content_script_port)
+ content_script_port.disconnect();
+ else
+ return;
+
loading_chbx.checked = !loading_chbx.checked;
try_to_connect(tab_id);
}
@@ -190,6 +208,8 @@ const payload_span = by_id("payload");
const view_payload_but = by_id("view_payload");
const container_for_injected = by_id("container_for_injected");
+const queried_items = new Map();
+
function handle_activity_report(message)
{
connected_chbx.checked = true;
@@ -231,9 +251,300 @@ function handle_activity_report(message)
container_for_injected.appendChild(h4);
container_for_injected.appendChild(pre);
}
+ 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);
+ queried_items.set(key, results);
+
+ const action = data.prefix === TYPE_PREFIX.URL ?
+ show_query_result : record_fetched_install_dep;
+
+ for (const [repo_url, result] of Object.entries(data.results))
+ action(data.prefix, data.item, repo_url, result);
+ }
+}
+
+const container_for_repo_responses = by_id("container_for_repo_responses");
+
+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");
+
+ list_head.textContent = url;
+ list_div.appendChild(list_head);
+ list_div.appendChild(list);
+ container_for_repo_responses.appendChild(list_div);
+
+ const list_object = {list, by_repo: new Map()};
+
+ results_lists.set(url, list_object);
+
+ return list_object;
+}
+
+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);
+
+ list_object.list.appendChild(result_li);
+ list_object.by_repo.set(repo_url, result_item);
+
+ return result_item;
+}
+
+function set_appended(result_item, element)
+{
+ if (result_item.appended)
+ result_item.appended.remove();
+ result_item.appended = element;
+ result_item.result_li.appendChild(element);
+}
+
+function show_message(result_item, text)
+{
+ const div = document.createElement("div");
+ div.textContent = text;
+ set_appended(result_item, div);
+}
+
+function showcb(text)
+{
+ return item => show_message(item, text);
+}
+
+function unroll_chbx_first_checked(entry_object)
+{
+ if (!entry_object.chbx.checked)
+ return;
+
+ entry_object.chbx.removeEventListener("change", entry_object.unroll_cb);
+ delete entry_object.unroll_cb;
+
+ entry_object.unroll.textContent = "preview not implemented...";
+}
+
+const show_install_chbx = by_id("show_install_view_chbx");
+
+let import_frame;
+let install_target = null;
+
+function install_abort(error_state)
+{
+ import_frame.show_error(`Error: ${error_state}`);
+ install_target = null;
+}
+
+/*
+ * Translate objects from the format in which they are sent by Hydrilla to the
+ * format in which they are stored in settings.
+ */
+
+function translate_script(script_object, repo_url)
+{
+ return {
+ [TYPE_PREFIX.SCRIPT + script_object.name]: {
+ hash: script_object.sha256,
+ url: `${repo_url}/content/${script_object.location}`
+ }
+ };
+}
+
+function translate_bag(bag_object)
+{
+ return {
+ [TYPE_PREFIX.BAG + bag_object.name]: bag_object.components
+ };
+}
+
+const format_translators = {
+ [TYPE_PREFIX.BAG]: translate_bag,
+ [TYPE_PREFIX.SCRIPT]: translate_script
+};
+
+function install_check_ready()
+{
+ if (install_target.to_fetch.size > 0)
+ return;
+
+ const page_key = [TYPE_PREFIX.PAGE + install_target.pattern];
+ const to_install = [{[page_key]: {components: install_target.payload}}];
+
+ for (const key of install_target.fetched) {
+ const old_object =
+ queried_items.get(key)[install_target.repo_url].response;
+ const new_object =
+ format_translators[key[0]](old_object, install_target.repo_url);
+ to_install.push(new_object);
+ }
+
+ import_frame.show_selection(to_install);
+}
+
+const possible_errors = ["connection_error", "parse_error"];
+
+function fetch_install_deps(components)
+{
+ const needed = [...components];
+ const processed = new Set();
+
+ while (needed.length > 0) {
+ const [prefix, item] = needed.pop();
+ const key = prefix + item;
+ processed.add(key);
+ const results = queried_items.get(key);
+ let relevant_result = null;
+
+ if (results)
+ relevant_result = results[install_target.repo_url];
+
+ if (!relevant_result) {
+ content_script_port.postMessage([prefix, item,
+ [install_target.repo_url]]);
+ install_target.to_fetch.add(key);
+ continue;
+ }
+
+ if (possible_errors.includes(relevant_result.state)) {
+ install_abort(relevant_result.state);
+ return false;
+ }
+
+ install_target.fetched.add(key);
+
+ if (prefix !== TYPE_PREFIX.BAG)
+ continue;
+
+ for (const dependency of relevant_result.response.components) {
+ if (processed.has(dependency.join('')))
+ continue;
+ needed.push(dependency);
+ }
+ }
+}
+
+function record_fetched_install_dep(prefix, item, repo_url, result)
+{
+ const key = prefix + item;
+
+ if (!install_target || repo_url !== install_target.repo_url ||
+ !install_target.to_fetch.has(key))
+ return;
+
+ if (possible_errors.includes(result.state)) {
+ install_abort(result.state);
+ return;
+ }
+
+ if (result.state !== "completed")
+ return;
+
+ install_target.to_fetch.delete(key);
+ install_target.fetched.add(key);
+
+ if (prefix === TYPE_PREFIX.BAG &&
+ fetch_install_deps(result.response.components) === false)
+ return;
+
+ install_check_ready();
+}
+
+function install_clicked(entry_object)
+{
+ show_install_chbx.checked = true;
+ import_frame.show_loading();
+
+ install_target = {
+ repo_url: entry_object.repo_url,
+ pattern: entry_object.match_object.pattern,
+ payload: entry_object.match_object.payload,
+ fetched: new Set(),
+ to_fetch: new Set()
+ };
+
+ fetch_install_deps([install_target.payload]);
+
+ install_check_ready();
+}
+
+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);
+
+ for (const match of result) {
+ const entry_object = clone_template("query_match_li_template");
+
+ entry_object.pattern.textContent = match.pattern;
+
+ ul.appendChild(entry_object.li);
+
+ if (!match.payload) {
+ entry_object.payload.textContent = "(none)";
+ for (const key of ["chbx", "br", "triangle", "unroll"])
+ entry_object[key].remove();
+ continue;
+ }
+
+ entry_object.component.textContent = nice_name(...match.payload);
+
+ const install_cb = () => install_clicked(entry_object);
+ entry_object.btn.addEventListener("click", install_cb);
+
+ const chbx_id = `query_result_${max_query_result_id++}`;
+ entry_object.chbx.id = chbx_id;
+ entry_object.lbl.setAttribute("for", chbx_id);
+
+ entry_object.unroll_cb = () => unroll_chbx_first_checked(entry_object);
+ entry_object.chbx.addEventListener("change", entry_object.unroll_cb);
+
+ entry_object.component_object = match.payload;
+ entry_object.match_object = match;
+ entry_object.repo_url = repo_url;
+ }
+}
+
+function show_query_result(url_prefix, url, repo_url, result)
+{
+ const results_list_object = results_lists.get(url) ||
+ create_results_list(url);
+ const result_item = results_list_object.by_repo.get(repo_url) ||
+ create_result_item(results_list_object, repo_url, result);
+
+ const completed_cb =
+ item => show_query_successful_result(item, repo_url, result.response);
+ const possible_actions = {
+ completed: completed_cb,
+ started: showcb("loading..."),
+ connection_error: showcb("Error when querying repository."),
+ parse_error: showcb("Bad data format received.")
+ };
+ possible_actions[result.state](result_item, repo_url);
}
by_id("settings_but")
.addEventListener("click", (e) => browser.runtime.openOptionsPage());
-show_page_activity_info();
+async function main()
+{
+ storage = await get_remote_storage();
+ import_frame = await get_import_frame();
+ import_frame.onclose = () => show_install_chbx.checked = false;
+ show_page_activity_info();
+}
+
+main();
diff --git a/html/import_frame.html b/html/import_frame.html
new file mode 100644
index 0000000..c86c3de
--- /dev/null
+++ b/html/import_frame.html
@@ -0,0 +1,27 @@
+<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>
+<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>
+ <input id="import_errordetail_chbx" type="checkbox" class="show_next"></input>
+ <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>
+ <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
new file mode 100644
index 0000000..4075433
--- /dev/null
+++ b/html/import_frame.js
@@ -0,0 +1,163 @@
+/**
+ * Hachette HTML import frame script
+ *
+ * Copyright (C) 2021 Wojtek Kosior
+ * Redistribution terms are gathered in the `copyright' file.
+ */
+
+/*
+ * IMPORTS_START
+ * IMPORT get_remote_storage
+ * IMPORT by_id
+ * IMPORT nice_name
+ * IMPORT make_once
+ * IMPORTS_END
+ */
+
+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_chbxs_colliding = undefined;
+let settings_import_map = undefined;
+
+function add_import_li(prefix, name)
+{
+ let li = import_li_template.cloneNode(true);
+ let name_span = li.firstElementChild;
+ let chbx = name_span.nextElementSibling;
+ let warning_span = chbx.nextElementSibling;
+
+ li.setAttribute("data-prefix", prefix);
+ li.setAttribute("data-name", name);
+ li.id = import_li_id(prefix, name);
+ 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_ul.appendChild(li);
+}
+
+function check_all_imports()
+{
+ for (let li of import_ul.children)
+ li.firstElementChild.nextElementSibling.checked = true;
+}
+
+function uncheck_all_imports()
+{
+ for (let li of import_ul.children)
+ li.firstElementChild.nextElementSibling.checked = false;
+}
+
+function uncheck_colliding_imports()
+{
+ for (let chbx of import_chbxs_colliding)
+ chbx.checked = false;
+}
+
+function commit_import()
+{
+ for (let li of import_ul.children) {
+ let chbx = li.firstElementChild.nextElementSibling;
+
+ if (!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);
+ }
+
+ deactivate();
+}
+
+const import_loading_radio = by_id("import_loading_radio");
+
+function show_loading()
+{
+ import_loading_radio.checked = true;
+}
+
+const import_failed_radio = by_id("import_failed_radio");
+const import_errormsg = by_id("import_errormsg");
+const import_errordetail_chbx = by_id("import_errordetail_chbx");
+const import_errordetail = by_id("import_errordetail");
+
+function show_error(errormsg, errordetail)
+{
+ import_failed_radio.checked = true;
+ import_errormsg.textContent = errormsg;
+ import_errordetail_chbx.checked = errordetail;
+ import_errordetail.textContent = errordetail;
+}
+
+const import_selection_radio = by_id("import_selection_radio");
+
+function show_selection(settings)
+{
+ import_selection_radio.checked = true;
+
+ let old_children = import_ul.children;
+ while (old_children[0] !== undefined)
+ import_ul.removeChild(old_children[0]);
+
+ import_chbxs_colliding = [];
+ 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);
+ settings_import_map.set(key, value);
+ }
+}
+
+function deactivate()
+{
+ /* Let GC free some memory */
+ import_chbxs_colliding = undefined;
+ settings_import_map = undefined;
+
+ if (exports.onclose)
+ exports.onclose();
+}
+
+const exports = {show_loading, show_error, show_selection, deactivate};
+
+async function init()
+{
+ storage = await get_remote_storage();
+
+ by_id("commit_import_but").addEventListener("click", commit_import);
+ by_id("check_all_import_but").addEventListener("click", check_all_imports);
+ by_id("uncheck_all_import_but")
+ .addEventListener("click", uncheck_all_imports);
+ by_id("uncheck_colliding_import_but")
+ .addEventListener("click", uncheck_colliding_imports);
+ by_id("cancel_import_but").addEventListener("click", deactivate);
+ by_id("import_failok_but").addEventListener("click", deactivate);
+
+ return exports;
+}
+
+const get_import_frame = make_once(init);
+
+/*
+ * EXPORTS_START
+ * EXPORT get_import_frame
+ * EXPORTS_END
+ */
diff --git a/html/options.html b/html/options.html
index bfb9e52..2246f9a 100644
--- a/html/options.html
+++ b/html/options.html
@@ -8,11 +8,8 @@
<head>
<meta charset="utf-8"/>
<title>Hachette options</title>
+ <link type="text/css" rel="stylesheet" href="base.css" />
<style>
- input[type="checkbox"], input[type="radio"], .hide, .popup.hide {
- display: none;
- }
-
/* pages list */
#page_components_ul {
max-height: 80vh;
@@ -33,12 +30,14 @@
}
/* tabbed view */
+ #show_repos:not(:checked) ~ #repos,
#show_pages:not(:checked) ~ #pages,
#show_bags:not(:checked) ~ #bags,
#show_scripts:not(:checked) ~ #scripts {
display: none;
}
+ #show_repos:checked ~ #repos_lbl,
#show_pages:checked ~ #pages_lbl,
#show_bags:checked ~ #bags_lbl,
#show_scripts:checked ~ #scripts_lbl {
@@ -76,23 +75,6 @@
input[type="radio"]:not(:checked)+.import_window_content {
display: none;
}
-
- /* buttons */
- button {
- background-color: #4CAF50;
- border: none;
- border-radius: 8px;
- color: white;
- padding: 6px 12px;
- text-align: center;
- text-decoration: none;
- display: inline-block;
- margin: 2px 0px;
- }
-
- button:hover {
- box-shadow: 0 6px 8px 0 rgba(0,0,0,0.24), 0 17px 50px 0 rgba(0,0,0,0.19);
- }
</style>
</head>
<body>
@@ -116,17 +98,15 @@
<input type="radio" style="display: inline;" name="page_components"></input>
<span></span>
</li>
- <li id="import_li_template">
- <span></span>
- <input type="checkbox" style="display: inline;" checked></input>
- <span></span>
- </li>
</div>
<!-- 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"
@@ -134,6 +114,19 @@
<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>
+ <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">
@@ -225,25 +218,7 @@
<div id="import_window" class="hide popup" position="absolute">
<div class="popup_frame">
- <h2> Settings import </h2>
- <input id="import_loading_radio" type="radio" name="import_window_content"></input>
- <span class="import_window_content"> Loading... </span>
- <input id="import_failed_radio" type="radio" name="import_window_content"></input>
- <div class="import_window_content">
- <span> Bad file :( </span>
- <pre id="bad_file_errormsg"></pre>
- <button id="import_failok_but"> OK </button>
- </div>
- <input id="import_selection_radio" type="radio" name="import_window_content"></input>
- <div class="import_window_content">
- <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>
- <button id="commit_import_but"> OK </button>
- <button id="cancel_import_but"> Cancel </button>
- </div>
+ <IMPORT html/import_frame.html />
</div>
</div>
diff --git a/html/options_main.js b/html/options_main.js
index e1e6cbe..830c860 100644
--- a/html/options_main.js
+++ b/html/options_main.js
@@ -12,33 +12,31 @@
* IMPORT TYPE_NAME
* IMPORT list_prefixes
* IMPORT nice_name
+ * IMPORT parse_json_with_schema
+ * IMPORT by_id
+ * IMPORT matchers
+ * IMPORT get_import_frame
* IMPORTS_END
*/
var storage;
-function by_id(id)
-{
- return document.getElementById(id);
-}
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 import_li_template = by_id("import_li_template");
/* 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");
-import_li_template.removeAttribute("id");
function item_li_id(prefix, item)
{
return `li_${prefix}_${item}`;
}
-/* Insert into list of bags/pages/scripts */
+/* Insert into list of bags/pages/scripts/repos */
function add_li(prefix, item, at_the_end=false)
{
let ul = ul_by_prefix[prefix];
@@ -58,6 +56,8 @@ function add_li(prefix, item, at_the_end=false)
let export_button = remove_button.nextElementSibling;
export_button.addEventListener("click",
() => export_item(prefix, item));
+ if (prefix === TYPE_PREFIX.REPO)
+ export_button.remove();
if (!at_the_end) {
for (let element of ul.ul.children) {
@@ -89,7 +89,7 @@ function radio_li_id(prefix, item)
function add_chbx_li(prefix, name)
{
- if (prefix === TYPE_PREFIX.PAGE)
+ if (![TYPE_PREFIX.BAG, TYPE_PREFIX.SCRIPT].includes(prefix))
return;
let li = chbx_component_li_template.cloneNode(true);
@@ -109,7 +109,7 @@ var radio_component_none_li = by_id("radio_component_none_li");
function add_radio_li(prefix, name)
{
- if (prefix === TYPE_PREFIX.PAGE)
+ if (![TYPE_PREFIX.BAG, TYPE_PREFIX.SCRIPT].includes(prefix))
return;
let li = radio_component_li_template.cloneNode(true);
@@ -125,6 +125,18 @@ function add_radio_li(prefix, name)
radio_components_ul.insertBefore(li, radio_component_none_li);
}
+/* Used to reset edited repo. */
+function reset_work_repo_li(ul, item, _)
+{
+ ul.work_name_input.value = maybe_string(item);
+}
+
+/* Used to get repo data for saving */
+function work_repo_li_data(ul)
+{
+ return [ul.work_name_input.value, {}];
+}
+
const page_payload_span = by_id("page_payload");
function set_page_components(components)
@@ -461,6 +473,15 @@ const UL_STATE = {
};
const ul_by_prefix = {
+ [TYPE_PREFIX.REPO] : {
+ ul : by_id("repos_ul"),
+ work_li : by_id("work_repo_li"),
+ work_name_input : by_id("repo_url_field"),
+ reset_work_li : reset_work_repo_li,
+ get_work_li_data : work_repo_li_data,
+ state : UL_STATE.IDLE,
+ edited_item : undefined,
+ },
[TYPE_PREFIX.PAGE] : {
ul : by_id("pages_ul"),
work_li : by_id("work_page_li"),
@@ -494,12 +515,6 @@ const ul_by_prefix = {
}
}
-const import_window = by_id("import_window");
-const import_loading_radio = by_id("import_loading_radio");
-const import_failed_radio = by_id("import_failed_radio");
-const import_selection_radio = by_id("import_selection_radio");
-const bad_file_errormsg = by_id("bad_file_errormsg");
-
/*
* Newer browsers could utilise `text' method of File objects.
* Older ones require FileReader.
@@ -520,6 +535,31 @@ function read_file(file)
_read_file(file, resolve, reject));
}
+const url_regex = /^[a-z0-9]+:\/\/[^/]+\.[^/]{2,}(\/[^?#]*)?$/;
+const empty_regex = /^$/;
+
+const settings_schema = [
+ [{}, "matchentry", "minentries", 1,
+ new RegExp(`^${TYPE_PREFIX.SCRIPT}`), {
+ /* script data */
+ "url": ["optional", url_regex, "or", empty_regex],
+ "sha256": ["optional", matchers.sha256, "or", empty_regex],
+ "text": ["optional", "string"]
+ },
+ new RegExp(`^${TYPE_PREFIX.BAG}`), [
+ "optional",
+ [matchers.component, "repeat"],
+ "default", undefined
+ ],
+ new RegExp(`^${TYPE_PREFIX.PAGE}`), {
+ /* page data */
+ "components": ["optional", matchers.component]
+ }], "repeat"
+];
+
+const import_window = by_id("import_window");
+let import_frame;
+
async function import_from_file(event)
{
let files = event.target.files;
@@ -527,98 +567,17 @@ async function import_from_file(event)
return;
import_window.classList.remove("hide");
- import_loading_radio.checked = true;
-
- let result = undefined;
+ import_frame.show_loading();
try {
- result = JSON.parse(await read_file(files[0]));
+ const file = await read_file(files[0]);
+ var result = parse_json_with_schema(settings_schema, file);
} catch(e) {
- bad_file_errormsg.textContent = "" + e;
- import_failed_radio.checked = true;
- return;
- }
-
- let errormsg = validate_settings(result);
- if (errormsg !== false) {
- bad_file_errormsg.textContent = errormsg;
- import_failed_radio.checked = true;
+ import_frame.show_error("Bad file :(", "" + e);
return;
}
- populate_import_list(result);
- import_selection_radio.checked = true;
-}
-
-function validate_settings(settings)
-{
- // TODO
- return false;
-}
-
-function import_li_id(prefix, item)
-{
- return `ili_${prefix}_${item}`;
-}
-
-let import_ul = by_id("import_ul");
-let import_chbxs_colliding = undefined;
-let settings_import_map = undefined;
-
-function populate_import_list(settings)
-{
- let old_children = import_ul.children;
- while (old_children[0] !== undefined)
- import_ul.removeChild(old_children[0]);
-
- import_chbxs_colliding = [];
- 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);
- settings_import_map.set(key, value);
- }
-}
-
-function add_import_li(prefix, name)
-{
- let li = import_li_template.cloneNode(true);
- let name_span = li.firstElementChild;
- let chbx = name_span.nextElementSibling;
- let warning_span = chbx.nextElementSibling;
-
- li.setAttribute("data-prefix", prefix);
- li.setAttribute("data-name", name);
- li.id = import_li_id(prefix, name);
- 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_ul.appendChild(li);
-}
-
-function check_all_imports()
-{
- for (let li of import_ul.children)
- li.firstElementChild.nextElementSibling.checked = true;
-}
-
-function uncheck_all_imports()
-{
- for (let li of import_ul.children)
- li.firstElementChild.nextElementSibling.checked = false;
-}
-
-function uncheck_colliding_imports()
-{
- for (let chbx of import_chbxs_colliding)
- chbx.checked = false;
+ import_frame.show_selection(result);
}
const file_opener_form = by_id("file_opener_form");
@@ -626,9 +585,6 @@ const file_opener_form = by_id("file_opener_form");
function hide_import_window()
{
import_window.classList.add("hide");
- /* Let GC free some memory */
- import_chbxs_colliding = undefined;
- settings_import_map = undefined;
/*
* Reset file <input>. Without this, a second attempt to import the same
@@ -637,43 +593,16 @@ function hide_import_window()
file_opener_form.reset();
}
-function commit_import()
-{
- for (let li of import_ul.children) {
- let chbx = li.firstElementChild.nextElementSibling;
-
- if (!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);
- }
-
- hide_import_window();
-}
-
-function initialize_import_facility()
+async function initialize_import_facility()
{
let import_but = by_id("import_but");
let file_opener = by_id("file_opener");
- let import_failok_but = by_id("import_failok_but");
- let check_all_import_but = by_id("check_all_import_but");
- let uncheck_all_import_but = by_id("uncheck_all_import_but");
- let uncheck_existing_import_but = by_id("uncheck_existing_import_but");
- let commit_import_but = by_id("commit_import_but");
- let cancel_import_but = by_id("cancel_import_but");
+
import_but.addEventListener("click", () => file_opener.click());
file_opener.addEventListener("change", import_from_file);
- import_failok_but.addEventListener("click", hide_import_window);
- check_all_import_but.addEventListener("click", check_all_imports);
- uncheck_all_import_but.addEventListener("click", uncheck_all_imports);
- uncheck_colliding_import_but
- .addEventListener("click", uncheck_colliding_imports);
- commit_import_but.addEventListener("click", commit_import);
- cancel_import_but.addEventListener("click", hide_import_window);
+
+ import_frame = await get_import_frame();
+ import_frame.onclose = hide_import_window;
}
/*
@@ -727,7 +656,7 @@ async function main()
discard_but.addEventListener("click", () => cancel_work(prefix));
save_but.addEventListener("click", () => save_work(prefix));
- if (prefix === TYPE_PREFIX.SCRIPT)
+ if ([TYPE_PREFIX.REPO, TYPE_PREFIX.SCRIPT].includes(prefix))
continue;
let ul = ul_by_prefix[prefix];
@@ -745,9 +674,9 @@ async function main()
jump_to_item(document.URL);
- initialize_import_facility();
-
storage.add_change_listener(handle_change);
+
+ await initialize_import_facility();
}
function handle_change(change)
@@ -772,7 +701,7 @@ function handle_change(change)
let uls_creators = [[ul.ul, item_li_id]];
- if (change.prefix !== TYPE_PREFIX.PAGE) {
+ if ([TYPE_PREFIX.BAG, TYPE_PREFIX.SCRIPT].includes(change.prefix)) {
uls_creators.push([chbx_components_ul, chbx_li_id]);
uls_creators.push([radio_components_ul, radio_li_id]);
}
diff --git a/process_html_file.sh b/process_html_file.sh
new file mode 100755
index 0000000..1ed0295
--- /dev/null
+++ b/process_html_file.sh
@@ -0,0 +1,33 @@
+#!/bin/sh
+
+# Copyright (C) 2021 Wojtek Kosior
+# Redistribution terms are gathered in the `copyright' file.
+
+# Call like:
+# ./process_html_file.sh html/options.html
+
+. ./shell_utils.sh
+
+FILE="$1"
+FILEKEY=$(sanitize "$FILE")
+
+if [ "x$(map_get HTML_FILENAMES $FILEKEY)" = "xyes" ]; then
+ errcho "import loop on $FILE"
+ exit 1
+fi
+
+map_set_export HTML_FILENAMES $FILEKEY yes
+
+awk '\
+!/^[\t\r ]*<IMPORT[\t\r ]+([^\t\r ]+)[\t\r ]+\/>[\t\r ]*$/{
+ print $0;
+}
+/^[\t\r ]*<IMPORT[\t\r ]+([^\t\r ]+)[\t\r ]+\/>[\t\r ]*$/{
+ indent = substr($0, 1, index($0, "<") - 1);
+ command = "./process_html_file.sh " $2;
+ while (command | getline) {
+ print indent $0;
+ }
+ if (close(command) != 0)
+ exit 1;
+}' < "$FILE"
diff --git a/shell_utils.sh b/shell_utils.sh
new file mode 100644
index 0000000..95e0d4e
--- /dev/null
+++ b/shell_utils.sh
@@ -0,0 +1,39 @@
+# Copyright (C) 2021 Wojtek Kosior
+# Redistribution terms are gathered in the `copyright' file.
+
+# This file is meant to be sourced in sh.
+
+ENDL="
+"
+
+errcho() {
+ echo "$@" >&2
+}
+
+map_set_instr() {
+ echo "$1__$2='$3'"
+}
+
+map_set() {
+ eval "$(map_set_instr "$@")"
+}
+
+map_set_export() {
+ eval "export $(map_set_instr "$@")"
+}
+
+map_get() {
+ eval "echo \"\$$1__$2\""
+}
+
+map_del_instr() {
+ echo "unset $1__$2"
+}
+
+map_del() {
+ eval "$(map_del_instr "$@")"
+}
+
+sanitize() {
+ echo "$1" | tr /.- _
+}