diff options
author | Wojtek Kosior <koszko@koszko.org> | 2022-01-29 00:03:51 +0100 |
---|---|---|
committer | Wojtek Kosior <koszko@koszko.org> | 2022-01-29 00:04:44 +0100 |
commit | 4c6a2323d90e9321ec2b78e226167b3013ea69ab (patch) | |
tree | 682ab3abd53a68d28d04f0470766699dcadd7294 | |
parent | ea9df6c7688613783ca114f0f11c6f6baf30036b (diff) | |
download | browser-extension-4c6a2323d90e9321ec2b78e226167b3013ea69ab.tar.gz browser-extension-4c6a2323d90e9321ec2b78e226167b3013ea69ab.zip |
make Haketilo buildable again (for Mozilla)
How cool it is to throw away 5755 lines of code...
53 files changed, 217 insertions, 5755 deletions
diff --git a/Makefile.in b/Makefile.in index bf0fdec..570260b 100644 --- a/Makefile.in +++ b/Makefile.in @@ -69,7 +69,7 @@ test/certs/rootCA.pem: test/certs/rootCA.key openssl req -x509 -new -nodes -key $< -days 1024 -out $@ \ -subj "/CN=Haketilo Test" -test: test/certs/rootCA.pem test/certs/site.key +test: test/certs/rootCA.pem test/certs/site.key $(default_target)-build.zip MOZ_HEADLESS=whatever pytest test-environment: test/certs/rootCA.pem test/certs/site.key diff --git a/common/ajax.js b/background/background.js index 462e511..90826ef 100644 --- a/common/ajax.js +++ b/background/background.js @@ -1,9 +1,9 @@ /** * This file is part of Haketilo. * - * Function: Wrapping XMLHttpRequest into a Promise. + * Function: Background scripts - main script. * - * Copyright (C) 2021 Wojtek Kosior + * Copyright (C) 2022 Wojtek Kosior * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -41,30 +41,27 @@ * proprietary program, I am not going to enforce this in court. */ -function ajax_callback() -{ - if (this.readyState == 4) - this.resolve_callback(this); -} +#IMPORT background/patterns_query_manager.js +#IMPORT background/webrequest.js +#IMPORT background/CORS_bypass_server.js +#IMPORT background/broadcast_broker.js +#IMPORT background/indexeddb_files_server.js -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); - } -} +#FROM common/misc.js IMPORT gen_nonce + +function main() { + const secret = gen_nonce(); + + /* + * Some other services depend on IndexedDB which depende on broadcast + * broker, hence we start it first. + */ + broadcast_broker.start(); + CORS_bypass_server.start(); + indexeddb_files_server.start(); -function make_ajax_request(method, url) -{ - return new Promise((resolve, reject) => - initiate_ajax_request(resolve, reject, method, url)); + patterns_query_manager.start(secret); + webrequest.start(secret); } -#EXPORT make_ajax_request +main(); diff --git a/background/broadcast_broker.js b/background/broadcast_broker.js index 79a1bb7..d8a0e28 100644 --- a/background/broadcast_broker.js +++ b/background/broadcast_broker.js @@ -42,8 +42,6 @@ * proprietary program, I am not going to enforce this in court. */ -#IMPORT common/connection_types.js AS CONNECTION_TYPE - #FROM common/message_server.js IMPORT listen_for_connection let next_id = 1; @@ -169,8 +167,7 @@ function remove_broadcast_sender(sender_ctx) function start() { - listen_for_connection(CONNECTION_TYPE.BROADCAST_SEND, new_broadcast_sender); - listen_for_connection(CONNECTION_TYPE.BROADCAST_LISTEN, - new_broadcast_listener); + listen_for_connection("broadcast_send", new_broadcast_sender); + listen_for_connection("broadcast_listen", new_broadcast_listener); } #EXPORT start diff --git a/background/main.js b/background/main.js deleted file mode 100644 index 61c96ac..0000000 --- a/background/main.js +++ /dev/null @@ -1,238 +0,0 @@ -/** - * This file is part of Haketilo. - * - * Function: Main background script. - * - * Copyright (C) 2021 Wojtek Kosior <koszko@koszko.org> - * Copyright (C) 2021 Jahoti <jahoti@envs.net> - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * As additional permission under GNU GPL version 3 section 7, you - * may distribute forms of that code without the copy of the GNU - * GPL normally required by section 4, provided you include this - * license notice and, in case of non-source distribution, a URL - * through which recipients can access the Corresponding Source. - * If you modify file(s) with this exception, you may extend this - * exception to your version of the file(s), but you are not - * obligated to do so. If you do not wish to do so, delete this - * exception statement from your version. - * - * As a special exception to the GPL, any HTML file which merely - * makes function calls to this code, and for that purpose - * includes it by reference shall be deemed a separate work for - * copyright law purposes. If you modify this code, you may extend - * this exception to your version of the code, but you are not - * obligated to do so. If you do not wish to do so, delete this - * exception statement from your version. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - * - * I, Wojtek Kosior, thereby promise not to sue for violation of this file's - * license. Although I request that you do not make use of this code in a - * proprietary program, I am not going to enforce this in court. - */ - -#IMPORT common/storage_light.js AS light_storage - -#IMPORT background/storage_server.js -#IMPORT background/page_actions_server.js -#IMPORT background/stream_filter.js - -#FROM common/browser.js IMPORT browser -#FROM common/stored_types.js IMPORT TYPE_PREFIX -#FROM background/storage.js IMPORT get_storage -#FROM common/misc.js IMPORT is_privileged_url -#FROM common/settings_query.js IMPORT query_best -#FROM background/policy_injector.js IMPORT inject_csp_headers - -const initial_data = ( -#INCLUDE_VERBATIM default_settings.json -); - -storage_server.start(); -page_actions_server.start(); - -async function init_ext(install_details) -{ - if (install_details.reason != "install") - return; - - let storage = await get_storage(); - - await storage.clear(); - - /* Below we add sample settings to the extension. */ - for (let setting of initial_data) { - let [key, value] = Object.entries(setting)[0]; - storage.set(key[0], key.substring(1), value); - } -} - -browser.runtime.onInstalled.addListener(init_ext); - -/* - * The function below implements a more practical interface for what it does by - * wrapping the old query_best() function. - */ -function decide_policy_for_url(storage, policy_observable, url) -{ - if (storage === undefined) - return {allow: false}; - - const settings = - {allow: policy_observable !== undefined && policy_observable.value}; - - const [pattern, queried_settings] = query_best(storage, url); - - if (queried_settings) { - settings.payload = queried_settings.components; - settings.allow = !!queried_settings.allow && !settings.payload; - settings.pattern = pattern; - } - - return settings; -} - -let storage; -let policy_observable = {}; - -function sanitize_web_page(details) -{ - const url = details.url; - if (is_privileged_url(details.url)) - return; - - const policy = - decide_policy_for_url(storage, policy_observable, details.url); - - let headers = details.responseHeaders; - - headers = inject_csp_headers(headers, policy); - - let skip = false; - for (const header of headers) { - if ((header.name.toLowerCase().trim() === "content-disposition" && - /^\s*attachment\s*(;.*)$/i.test(header.value))) - skip = true; - } - skip = skip || (details.statusCode >= 300 && details.statusCode < 400); - - if (!skip) { - /* Check for API availability. */ - if (browser.webRequest.filterResponseData) - headers = stream_filter.apply(details, headers, policy); - } - - return {responseHeaders: headers}; -} - -const request_url_regex = /^[^?]*\?url=(.*)$/; -const redirect_url_template = browser.runtime.getURL("dummy") + "?settings="; - -function synchronously_smuggle_policy(details) -{ - /* - * Content script will make a synchronous XmlHttpRequest to extension's - * `dummy` file to query settings for given URL. We smuggle that - * information in query parameter of the URL we redirect to. - * A risk of fingerprinting arises if a page with script execution allowed - * guesses the dummy file URL and makes an AJAX call to it. It is currently - * a problem in ManifestV2 Chromium-family port of Haketilo because Chromium - * uses predictable URLs for web-accessible resources. We plan to fix it in - * the future ManifestV3 port. - */ - if (details.type !== "xmlhttprequest") - return {cancel: true}; - - console.debug(`Settings queried using XHR for '${details.url}'.`); - - let policy = {allow: false}; - - try { - /* - * request_url should be of the following format: - * <url_for_extension's_dummy_file>?url=<valid_urlencoded_url> - */ - const match = request_url_regex.exec(details.url); - const queried_url = decodeURIComponent(match[1]); - - if (details.initiator && !queried_url.startsWith(details.initiator)) { - console.warn(`Blocked suspicious query of '${url}' by '${details.initiator}'. This might be the result of page fingerprinting the browser.`); - return {cancel: true}; - } - - policy = decide_policy_for_url(storage, policy_observable, queried_url); - } catch (e) { - console.warn(`Bad request! Expected ${browser.runtime.getURL("dummy")}?url=<valid_urlencoded_url>. Got ${request_url}. This might be the result of page fingerprinting the browser.`); - } - - const encoded_policy = encodeURIComponent(JSON.stringify(policy)); - - return {redirectUrl: redirect_url_template + encoded_policy}; -} - -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(); - -#IF CHROMIUM - const extra_opts = ["blocking", "extraHeaders"]; -#ELSE - const extra_opts = ["blocking"]; -#ENDIF - - browser.webRequest.onHeadersReceived.addListener( - sanitize_web_page, - {urls: ["<all_urls>"], types: ["main_frame", "sub_frame"]}, - extra_opts.concat("responseHeaders") - ); - - const dummy_url_pattern = browser.runtime.getURL("dummy") + "?url=*"; - browser.webRequest.onBeforeRequest.addListener( - synchronously_smuggle_policy, - {urls: [dummy_url_pattern], types: ["xmlhttprequest"]}, - extra_opts - ); - - policy_observable = await light_storage.observe_var("default_allow"); -} - -start_webRequest_operations(); - -#IF MOZILLA -const code = `\ -console.warn("Hi, I'm Mr Dynamic!"); - -console.debug("let's see how window.haketilo_exports looks like now"); - -console.log("haketilo_exports", window.haketilo_exports); -` - -async function test_dynamic_content_scripts() -{ - browser.contentScripts.register({ - "js": [{code}], - "matches": ["<all_urls>"], - "allFrames": true, - "runAt": "document_start" -}); -} - -test_dynamic_content_scripts(); -#ENDIF diff --git a/background/page_actions_server.js b/background/page_actions_server.js deleted file mode 100644 index 67c9b9e..0000000 --- a/background/page_actions_server.js +++ /dev/null @@ -1,149 +0,0 @@ -/** - * This file is part of Haketilo. - * - * Function: Serving page actions to content scripts. - * - * Copyright (C) 2021 Wojtek Kosior - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * As additional permission under GNU GPL version 3 section 7, you - * may distribute forms of that code without the copy of the GNU - * GPL normally required by section 4, provided you include this - * license notice and, in case of non-source distribution, a URL - * through which recipients can access the Corresponding Source. - * If you modify file(s) with this exception, you may extend this - * exception to your version of the file(s), but you are not - * obligated to do so. If you do not wish to do so, delete this - * exception statement from your version. - * - * As a special exception to the GPL, any HTML file which merely - * makes function calls to this code, and for that purpose - * includes it by reference shall be deemed a separate work for - * copyright law purposes. If you modify this code, you may extend - * this exception to your version of the code, but you are not - * obligated to do so. If you do not wish to do so, delete this - * exception statement from your version. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - * - * I, Wojtek Kosior, thereby promise not to sue for violation of this file's - * license. Although I request that you do not make use of this code in a - * proprietary program, I am not going to enforce this in court. - */ - -#IMPORT common/storage_light.js AS light_storage -#IMPORT common/connection_types.js AS CONNECTION_TYPE - -#FROM common/browser.js IMPORT browser -#FROM common/message_server.js IMPORT listen_for_connection -#FROM background/storage.js IMPORT get_storage -#FROM common/stored_types.js IMPORT TYPE_PREFIX -#FROM common/sha256.js IMPORT sha256 -#FROM common/ajax.js IMPORT make_ajax_request - -var storage; -var handler; - -// TODO: parallelize script fetching -async function send_scripts(components, port, processed_bags) -{ - for (let [prefix, name] of components) { - if (prefix === TYPE_PREFIX.BAG) { - if (processed_bags.has(name)) { - console.log(`preventing recursive inclusion of bag ${name}`); - continue; - } - - var bag = storage.get(TYPE_PREFIX.BAG, name); - - if (bag === undefined) { - console.log(`no bag in storage for key ${name}`); - continue; - } - - processed_bags.add(name); - await send_scripts(bag, port, processed_bags); - - processed_bags.delete(name); - } else { - let script_text = await get_script_text(name); - if (script_text === undefined) - continue; - - port.postMessage(["inject", [script_text]]); - } - } -} - -async function get_script_text(script_name) -{ - try { - let script_data = storage.get(TYPE_PREFIX.SCRIPT, script_name); - if (script_data === undefined) { - console.log(`missing data for ${script_name}`); - return; - } - let script_text = script_data.text; - if (!script_text) - script_text = await fetch_remote_script(script_data); - return script_text; - } catch (e) { - console.log(e); - } -} - -async function fetch_remote_script(script_data) -{ - try { - let xhttp = await make_ajax_request("GET", script_data.url); - if (xhttp.status === 200) { - let computed_hash = sha256(xhttp.responseText); - if (computed_hash !== script_data.hash) { - console.log(`Bad hash for ${script_data.url}\n got ${computed_hash} instead of ${script_data.hash}`); - return; - } - return xhttp.responseText; - } else { - console.log("script not fetched: " + script_data.url); - return; - } - } catch (e) { - console.log(e); - } -} - -function handle_message(port, message, handler) -{ - port.onMessage.removeListener(handler[0]); - console.debug(`Loading payload '${message.payload}'.`); - - const processed_bags = new Set(); - - send_scripts([message.payload], port, processed_bags); -} - -function new_connection(port) -{ - console.log("new page actions connection!"); - let handler = []; - handler.push(m => handle_message(port, m, handler)); - port.onMessage.addListener(handler[0]); -} - -async function start() -{ - storage = await get_storage(); - - listen_for_connection(CONNECTION_TYPE.PAGE_ACTIONS, new_connection); -} -#EXPORT start diff --git a/background/patterns_query_manager.js b/background/patterns_query_manager.js index 3b74ee9..9de9d35 100644 --- a/background/patterns_query_manager.js +++ b/background/patterns_query_manager.js @@ -64,8 +64,7 @@ let registered_script = null; let script_update_occuring = false; let script_update_needed; -async function update_content_script() -{ +async function update_content_script() { if (script_update_occuring) return; @@ -98,8 +97,7 @@ if (this.haketilo_content_script_main) } #ENDIF -function register(kind, object) -{ +function register(kind, object) { if (kind === "mappings") { for (const [pattern, resource] of Object.entries(object.payloads)) pqt.register(tree, pattern, object.identifier, resource); @@ -118,8 +116,7 @@ function register(kind, object) #ENDIF } -function changed(kind, change) -{ +function changed(kind, change) { const old_version = currently_registered.get(change.key); if (old_version !== undefined) { if (kind === "mappings") { @@ -143,8 +140,19 @@ function changed(kind, change) #ENDIF } -async function start(secret_) -{ +function setting_changed(change) { + if (change.key !== "default_allow") + return; + + default_allow.value = (change.new_val || {}).value; + +#IF MOZILLA || MV3 + script_update_needed = true; + setTimeout(update_content_script, 0); +#ENDIF +} + +async function start(secret_) { secret = secret_; const [mapping_tracking, initial_mappings] = @@ -155,9 +163,9 @@ async function start(secret_) initial_mappings.forEach(m => register("mappings", m)); initial_blocking.forEach(b => register("blocking", b)); - const set_allow_val = ch => default_allow.value = (ch.new_val || {}).value; const [setting_tracking, initial_settings] = - await haketilodb.track.settings(set_allow_val); + await haketilodb.track.settings(setting_changed); + for (const setting of initial_settings) { if (setting.name === "default_allow") Object.assign(default_allow, setting); diff --git a/background/policy_injector.js b/background/policy_injector.js deleted file mode 100644 index 36c950e..0000000 --- a/background/policy_injector.js +++ /dev/null @@ -1,87 +0,0 @@ -/** - * This file is part of Haketilo. - * - * Function: Injecting policy to page by modifying HTTP headers. - * - * Copyright (C) 2021, Wojtek Kosior - * Copyright (C) 2021, jahoti - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * As additional permission under GNU GPL version 3 section 7, you - * may distribute forms of that code without the copy of the GNU - * GPL normally required by section 4, provided you include this - * license notice and, in case of non-source distribution, a URL - * through which recipients can access the Corresponding Source. - * If you modify file(s) with this exception, you may extend this - * exception to your version of the file(s), but you are not - * obligated to do so. If you do not wish to do so, delete this - * exception statement from your version. - * - * As a special exception to the GPL, any HTML file which merely - * makes function calls to this code, and for that purpose - * includes it by reference shall be deemed a separate work for - * copyright law purposes. If you modify this code, you may extend - * this exception to your version of the code, but you are not - * obligated to do so. If you do not wish to do so, delete this - * exception statement from your version. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - * - * - * I, Wojtek Kosior, thereby promise not to sue for violation of this file's - * license. Although I request that you do not make use of this code in a - * proprietary program, I am not going to enforce this in court. - */ - -#FROM common/misc.js IMPORT csp_header_regex - -/* Re-enable the import below once nonce stuff here is ready */ -#IF NEVER -#FROM common/misc.js IMPORT gen_nonce -#ENDIF - -/* CSP rule that blocks scripts according to policy's needs. */ -function make_csp_rule(policy) -{ - let rule = "prefetch-src 'none'; script-src-attr 'none';"; - const script_src = policy.nonce !== undefined ? - `'nonce-${policy.nonce}'` : "'none'"; - rule += ` script-src ${script_src}; script-src-elem ${script_src};`; - return rule; -} - -function inject_csp_headers(headers, policy) -{ - let csp_headers; - - if (policy.payload) { - headers = headers.filter(h => !csp_header_regex.test(h.name)); - - // TODO: make CSP rules with nonces and facilitate passing them to - // content scripts via dynamic content script registration or - // synchronous XHRs - - // policy.nonce = gen_nonce(); - } - - if (!policy.allow && (policy.nonce || !policy.payload)) { - headers.push({ - name: "content-security-policy", - value: make_csp_rule(policy) - }); - } - - return headers; -} - -#EXPORT inject_csp_headers diff --git a/background/storage.js b/background/storage.js deleted file mode 100644 index fbd4a7e..0000000 --- a/background/storage.js +++ /dev/null @@ -1,359 +0,0 @@ -/** - * This file is part of Haketilo. - * - * Function: Storage manager. - * - * Copyright (C) 2021 Wojtek Kosior - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * As additional permission under GNU GPL version 3 section 7, you - * may distribute forms of that code without the copy of the GNU - * GPL normally required by section 4, provided you include this - * license notice and, in case of non-source distribution, a URL - * through which recipients can access the Corresponding Source. - * If you modify file(s) with this exception, you may extend this - * exception to your version of the file(s), but you are not - * obligated to do so. If you do not wish to do so, delete this - * exception statement from your version. - * - * As a special exception to the GPL, any HTML file which merely - * makes function calls to this code, and for that purpose - * includes it by reference shall be deemed a separate work for - * copyright law purposes. If you modify this code, you may extend - * this exception to your version of the code, but you are not - * obligated to do so. If you do not wish to do so, delete this - * exception statement from your version. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - * - * I, Wojtek Kosior, thereby promise not to sue for violation of this file's - * license. Although I request that you do not make use of this code in a - * proprietary program, I am not going to enforce this in court. - */ - -#IMPORT common/storage_raw.js AS raw_storage -#IMPORT common/observables.js - -#FROM common/stored_types.js IMPORT list_prefixes, TYPE_NAME -#FROM common/lock.js IMPORT lock, unlock, make_lock -#FROM common/once.js IMPORT make_once -#FROM common/browser.js IMPORT browser - -var exports = {}; - -/* A special case of persisted variable is one that contains list of items. */ - -async function get_list_var(name) -{ - let list = await raw_storage.get_var(name); - - return list === undefined ? [] : list; -} - -/* We maintain in-memory copies of some stored lists. */ - -async function list(prefix) -{ - let name = TYPE_NAME[prefix] + "s"; /* Make plural. */ - let map = new Map(); - - for (let item of await get_list_var(name)) - map.set(item, await raw_storage.get(prefix + item)); - - return {map, prefix, name, observable: observables.make(), - lock: make_lock()}; -} - -var list_by_prefix = {}; - -async function init() -{ - for (let prefix of list_prefixes) - list_by_prefix[prefix] = await list(prefix); - - return exports; -} - -/* - * Facilitate listening to changes - */ - -exports.add_change_listener = function (cb, prefixes=list_prefixes) -{ - if (typeof(prefixes) === "string") - prefixes = [prefixes]; - - for (let prefix of prefixes) - observables.subscribe(list_by_prefix[prefix].observable, cb); -} - -exports.remove_change_listener = function (cb, prefixes=list_prefixes) -{ - if (typeof(prefixes) === "string") - prefixes = [prefixes]; - - for (let prefix of prefixes) - observables.unsubscribe(list_by_prefix[prefix].observable, cb); -} - -/* Prepare some hepler functions to get elements of a list */ - -function list_items_it(list, with_values=false) -{ - return with_values ? list.map.entries() : list.map.keys(); -} - -function list_entries_it(list) -{ - return list_items_it(list, true); -} - -function list_items(list, with_values=false) -{ - let array = []; - - for (let item of list_items_it(list, with_values)) - array.push(item); - - return array; -} - -function list_entries(list) -{ - return list_items(list, true); -} - -/* - * Below we make additional effort to update map of given kind of items - * every time an item is added/removed to keep everything coherent. - */ -async function set_item(item, value, list) -{ - await lock(list.lock); - let result = await _set_item(...arguments); - unlock(list.lock) - return result; -} -async function _set_item(item, value, list) -{ - const key = list.prefix + item; - const old_val = list.map.get(item); - const set_obj = {[key]: value}; - if (old_val === undefined) { - const items = list_items(list); - items.push(item); - set_obj["_" + list.name] = items; - } - - await raw_storage.set(set_obj); - list.map.set(item, value); - - const change = { - prefix : list.prefix, - item, - old_val, - new_val : value - }; - - observables.broadcast(list.observable, change); - - return old_val; -} - -// TODO: The actual idea to set value to undefined is good - this way we can -// also set a new list of items in the same API call. But such key -// is still stored in the storage. We need to somehow remove it later. -// For that, we're going to have to store 1 more list of each kind. -async function remove_item(item, list) -{ - await lock(list.lock); - let result = await _remove_item(...arguments); - unlock(list.lock) - return result; -} -async function _remove_item(item, list) -{ - const old_val = list.map.get(item); - if (old_val === undefined) - return; - - const items = list_items(list); - const index = items.indexOf(item); - items.splice(index, 1); - - await raw_storage.set({ - [list.prefix + item]: undefined, - ["_" + list.name]: items - }); - list.map.delete(item); - - const change = { - prefix : list.prefix, - item, - old_val, - new_val : undefined - }; - - observables.broadcast(list.observable, change); - - return old_val; -} - -// TODO: same as above applies here -async function replace_item(old_item, new_item, list, new_val=undefined) -{ - await lock(list.lock); - let result = await _replace_item(...arguments); - unlock(list.lock) - return result; -} -async function _replace_item(old_item, new_item, list, new_val=undefined) -{ - const old_val = list.map.get(old_item); - if (new_val === undefined) { - if (old_val === undefined) - return; - new_val = old_val; - } else if (new_val === old_val && new_item === old_item) { - return old_val; - } - - if (old_item === new_item || old_val === undefined) { - await _set_item(new_item, new_val, list); - return old_val; - } - - const items = list_items(list); - const index = items.indexOf(old_item); - items[index] = new_item; - - await raw_storage.set({ - [list.prefix + old_item]: undefined, - [list.prefix + new_item]: new_val, - ["_" + list.name]: items - }); - list.map.delete(old_item); - - const change = { - prefix : list.prefix, - item : old_item, - old_val, - new_val : undefined - }; - - observables.broadcast(list.observable, change); - - list.map.set(new_item, new_val); - - change.item = new_item; - change.old_val = undefined; - change.new_val = new_val; - - observables.broadcast(list.observable, change); - - return old_val; -} - -/* - * For scripts, item name is chosen by user, data should be - * an object containing: - * - script's url and hash or - * - script's text or - * - all three - */ - -/* - * For bags, item name is chosen by user, data is an array of 2-element - * arrays with type prefix and script/bag names. - */ - -/* - * For pages data argument is an object with properties `allow' - * and `components'. Item name is url. - */ - -exports.set = async function (prefix, item, data) -{ - return set_item(item, data, list_by_prefix[prefix]); -} - -exports.get = function (prefix, item) -{ - return list_by_prefix[prefix].map.get(item); -} - -exports.remove = async function (prefix, item) -{ - return remove_item(item, list_by_prefix[prefix]); -} - -exports.replace = async function (prefix, old_item, new_item, - new_data=undefined) -{ - return replace_item(old_item, new_item, list_by_prefix[prefix], - new_data); -} - -exports.get_all_names = function (prefix) -{ - return list_items(list_by_prefix[prefix]); -} - -exports.get_all_names_it = function (prefix) -{ - return list_items_it(list_by_prefix[prefix]); -} - -exports.get_all = function (prefix) -{ - return list_entries(list_by_prefix[prefix]); -} - -exports.get_all_it = function (prefix) -{ - return list_entries_it(list_by_prefix[prefix]); -} - -/* Finally, a quick way to wipe all the data. */ -// TODO: maybe delete items in such order that none of them ever references -// an already-deleted one? -exports.clear = async function () -{ - let lists = list_prefixes.map((p) => list_by_prefix[p]); - - for (let list of lists) - await lock(list.lock); - - for (let list of lists) { - - let change = { - prefix : list.prefix, - new_val : undefined - }; - - for (let [item, val] of list_entries_it(list)) { - change.item = item; - change.old_val = val; - observables.broadcast(list.observable, change); - } - - list.map = new Map(); - } - - await browser.storage.local.clear(); - - for (let list of lists) - unlock(list.lock); -} - -#EXPORT make_once(init) AS get_storage diff --git a/background/storage_server.js b/background/storage_server.js deleted file mode 100644 index 5aa07bf..0000000 --- a/background/storage_server.js +++ /dev/null @@ -1,95 +0,0 @@ -/** - * This file is part of Haketilo. - * - * Function: Storage through messages (server side). - * - * Copyright (C) 2021 Wojtek Kosior - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * As additional permission under GNU GPL version 3 section 7, you - * may distribute forms of that code without the copy of the GNU - * GPL normally required by section 4, provided you include this - * license notice and, in case of non-source distribution, a URL - * through which recipients can access the Corresponding Source. - * If you modify file(s) with this exception, you may extend this - * exception to your version of the file(s), but you are not - * obligated to do so. If you do not wish to do so, delete this - * exception statement from your version. - * - * As a special exception to the GPL, any HTML file which merely - * makes function calls to this code, and for that purpose - * includes it by reference shall be deemed a separate work for - * copyright law purposes. If you modify this code, you may extend - * this exception to your version of the code, but you are not - * obligated to do so. If you do not wish to do so, delete this - * exception statement from your version. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - * - * I, Wojtek Kosior, thereby promise not to sue for violation of this file's - * license. Although I request that you do not make use of this code in a - * proprietary program, I am not going to enforce this in court. - */ - -#IMPORT common/connection_types.js AS CONNECTION_TYPE - -#FROM common/message_server.js IMPORT listen_for_connection -#FROM background/storage.js IMPORT get_storage -#FROM common/stored_types.js IMPORT list_prefixes - -var storage; - -async function handle_remote_call(port, message) -{ - let [call_id, func, args] = message; - - try { - let result = await Promise.resolve(storage[func](...args)); - port.postMessage({call_id, result}); - } catch (error) { - error = error + ''; - port.postMessage({call_id, error}); - } -} - -function remove_storage_listener(cb) -{ - storage.remove_change_listener(cb); -} - -function new_connection(port) -{ - console.log("new remote storage connection!"); - - const message = {}; - for (const prefix of list_prefixes) - message[prefix] = storage.get_all(prefix); - - port.postMessage(message); - - let handle_change = change => port.postMessage(change); - - storage.add_change_listener(handle_change); - - port.onMessage.addListener(m => handle_remote_call(port, m)); - port.onDisconnect.addListener(() => - remove_storage_listener(handle_change)); -} - -async function start() -{ - storage = await get_storage(); - - listen_for_connection(CONNECTION_TYPE.REMOTE_STORAGE, new_connection); -} -#EXPORT start diff --git a/background/webrequest.js b/background/webrequest.js index bd091dc..cb89a3d 100644 --- a/background/webrequest.js +++ b/background/webrequest.js @@ -172,7 +172,5 @@ async function start(secret_) extra_opts ); #ENDIF - - await track_default_allow(); } #EXPORT start diff --git a/common/broadcast.js b/common/broadcast.js index ce4ac08..4dcac2b 100644 --- a/common/broadcast.js +++ b/common/broadcast.js @@ -41,14 +41,12 @@ * proprietary program, I am not going to enforce this in court. */ -#IMPORT common/connection_types.js AS CONNECTION_TYPE - #FROM common/message_server.js IMPORT connect_to_background function sender_connection() { return { - port: connect_to_background(CONNECTION_TYPE.BROADCAST_SEND) + port: connect_to_background("broadcast_send") }; } #EXPORT sender_connection @@ -94,7 +92,7 @@ function flush(sender_conn) function listener_connection(cb) { const conn = { - port: connect_to_background(CONNECTION_TYPE.BROADCAST_LISTEN) + port: connect_to_background("broadcast_listen") }; conn.port.onMessage.addListener(cb); diff --git a/common/connection_types.js b/common/connection_types.js deleted file mode 100644 index 6ed2e4a..0000000 --- a/common/connection_types.js +++ /dev/null @@ -1,53 +0,0 @@ -/** - * This file is part of Haketilo. - * - * Function: Define an "enum" of message connection types. - * - * Copyright (C) 2021 Wojtek Kosior - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * As additional permission under GNU GPL version 3 section 7, you - * may distribute forms of that code without the copy of the GNU - * GPL normally required by section 4, provided you include this - * license notice and, in case of non-source distribution, a URL - * through which recipients can access the Corresponding Source. - * If you modify file(s) with this exception, you may extend this - * exception to your version of the file(s), but you are not - * obligated to do so. If you do not wish to do so, delete this - * exception statement from your version. - * - * As a special exception to the GPL, any HTML file which merely - * makes function calls to this code, and for that purpose - * includes it by reference shall be deemed a separate work for - * copyright law purposes. If you modify this code, you may extend - * this exception to your version of the code, but you are not - * obligated to do so. If you do not wish to do so, delete this - * exception statement from your version. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - * - * I, Wojtek Kosior, thereby promise not to sue for violation of this file's - * license. Although I request that you do not make use of this code in a - * proprietary program, I am not going to enforce this in court. - */ - -/* - * Those need to be strings so they can be used as 'name' parameter - * to browser.runtime.connect() - */ - -#EXPORT "0" AS REMOTE_STORAGE -#EXPORT "1" AS PAGE_ACTIONS -#EXPORT "2" AS ACTIVITY_INFO -#EXPORT "3" AS BROADCAST_SEND -#EXPORT "4" AS BROADCAST_LISTEN diff --git a/common/indexeddb.js b/common/indexeddb.js index 1b8e574..271dfce 100644 --- a/common/indexeddb.js +++ b/common/indexeddb.js @@ -48,7 +48,7 @@ let initial_data = ( #IF UNIT_TEST {} #ELSE -#INCLUDE_VERBATIM default_settings.json +#INCLUDE default_settings.json #ENDIF ); diff --git a/common/lock.js b/common/lock.js deleted file mode 100644 index f577481..0000000 --- a/common/lock.js +++ /dev/null @@ -1,94 +0,0 @@ -/** - * This file is part of Haketilo. - * - * Function: Implement a lock (aka binary semaphore aka mutex). - * - * Copyright (C) 2021 Wojtek Kosior - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * As additional permission under GNU GPL version 3 section 7, you - * may distribute forms of that code without the copy of the GNU - * GPL normally required by section 4, provided you include this - * license notice and, in case of non-source distribution, a URL - * through which recipients can access the Corresponding Source. - * If you modify file(s) with this exception, you may extend this - * exception to your version of the file(s), but you are not - * obligated to do so. If you do not wish to do so, delete this - * exception statement from your version. - * - * As a special exception to the GPL, any HTML file which merely - * makes function calls to this code, and for that purpose - * includes it by reference shall be deemed a separate work for - * copyright law purposes. If you modify this code, you may extend - * this exception to your version of the code, but you are not - * obligated to do so. If you do not wish to do so, delete this - * exception statement from your version. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - * - * I, Wojtek Kosior, thereby promise not to sue for violation of this file's - * license. Although I request that you do not make use of this code in a - * proprietary program, I am not going to enforce this in court. - */ - -/* - * Javascript runs single-threaded, with an event loop. Because of that, - * explicit synchronization is often not needed. An exception is when we use - * an API function that must wait. Ajax is an example. Callback passed to ajax - * call doesn't get called immediately, but after some time. In the meantime - * some other piece of code might get to execute and modify some variables. - * Access to WebExtension local storage is another situation where this problem - * can occur. - * - * This is a solution. A lock object, that can be used to delay execution of - * some code until other code finishes its critical work. Locking is wrapped - * in a promise. - */ - -#EXPORT () => ({free: true, queue: []}) AS make_lock - -function _lock(lock, cb) { - if (lock.free) { - lock.free = false; - setTimeout(cb); - } else { - lock.queue.push(cb); - } -} - -#EXPORT lock => new Promise(resolve => _lock(lock, resolve)) AS lock - -function try_lock(lock) -{ - if (lock.free) { - lock.free = false; - return true; - } - - return false; -} -#EXPORT try_lock - -function unlock(lock) { - if (lock.free) - throw new Exception("Attempting to release a free lock"); - - if (lock.queue.length === 0) { - lock.free = true; - } else { - let cb = lock.queue[0]; - lock.queue.splice(0, 1); - setTimeout(cb); - } -} -#EXPORT unlock diff --git a/common/message_server.js b/common/message_server.js index 80cefd5..a67b6ee 100644 --- a/common/message_server.js +++ b/common/message_server.js @@ -97,7 +97,7 @@ function connect_to_background(magic) return browser.runtime.connect({name: magic}); if (!(magic in listeners)) - throw `no listener for '${magic}'` + throw `no listener for '${magic}'`; const ports = [new Port(magic), new Port(magic)]; ports[0].other = ports[1]; diff --git a/common/misc.js b/common/misc.js index f8e0812..c06052a 100644 --- a/common/misc.js +++ b/common/misc.js @@ -42,9 +42,6 @@ * proprietary program, I am not going to enforce this in court. */ -#FROM common/browser.js IMPORT browser -#FROM common/stored_types.js IMPORT TYPE_NAME, TYPE_PREFIX - /* uint8_to_hex is a separate function used in cryptographic functions. */ const uint8_to_hex = array => [...array].map(b => ("0" + b.toString(16)).slice(-2)).join(""); @@ -83,15 +80,6 @@ const csp_header_regex = */ #EXPORT (prefix, name) => `${name} (${TYPE_NAME[prefix]})` AS nice_name -/* Open settings tab with given item's editing already on. */ -function open_in_settings(prefix, name) -{ - name = encodeURIComponent(name); - const url = browser.runtime.getURL("html/options.html#" + prefix + name); - window.open(url, "_blank"); -} -#EXPORT open_in_settings - /* * Check if url corresponds to a browser's special page (or a directory index in * case of `file://' protocol). @@ -102,47 +90,3 @@ const priv_reg = /^moz-extension:\/\/|^about:|^file:\/\/[^?#]*\/([?#]|$)/; const priv_reg = /^chrome(-extension)?:\/\/|^about:|^file:\/\/[^?#]*\/([?#]|$)/; #ENDIF #EXPORT url => priv_reg.test(url) AS is_privileged_url - -/* Parse a CSP header */ -function parse_csp(csp) { - let directive, directive_array; - let directives = {}; - for (directive of csp.split(';')) { - directive = directive.trim(); - if (directive === '') - continue; - - directive_array = directive.split(/\s+/); - directive = directive_array.shift(); - /* The "true" case should never occur; nevertheless... */ - directives[directive] = directive in directives ? - directives[directive].concat(directive_array) : - directive_array; - } - return directives; -} - -/* Regexes and objects 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 - ] -}; -#EXPORT matchers - -/* - * Facilitates checking if there aren't any keys in object. This does *NOT* - * account for pathological cases like redefined properties of Object prototype. - */ -function is_object_empty(object) -{ - for (const key in object) - return false; - return true; -} -#EXPORT is_object_empty diff --git a/common/observables.js b/common/observables.js deleted file mode 100644 index e73b9f7..0000000 --- a/common/observables.js +++ /dev/null @@ -1,61 +0,0 @@ -/** - * This file is part of Haketilo. - * - * Function: Facilitate listening to (internal, self-generated) events. - * - * Copyright (C) 2021 Wojtek Kosior - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * As additional permission under GNU GPL version 3 section 7, you - * may distribute forms of that code without the copy of the GNU - * GPL normally required by section 4, provided you include this - * license notice and, in case of non-source distribution, a URL - * through which recipients can access the Corresponding Source. - * If you modify file(s) with this exception, you may extend this - * exception to your version of the file(s), but you are not - * obligated to do so. If you do not wish to do so, delete this - * exception statement from your version. - * - * As a special exception to the GPL, any HTML file which merely - * makes function calls to this code, and for that purpose - * includes it by reference shall be deemed a separate work for - * copyright law purposes. If you modify this code, you may extend - * this exception to your version of the code, but you are not - * obligated to do so. If you do not wish to do so, delete this - * exception statement from your version. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - * - * I, Wojtek Kosior, thereby promise not to sue for violation of this file's - * license. Although I request that you do not make use of this code in a - * proprietary program, I am not going to enforce this in court. - */ - -#EXPORT (value=undefined) => ({value, listeners: new Set()}) AS make -#EXPORT (observable, cb) => observable.listeners.add(cb) AS subscribe -#EXPORT (observable, cb) => observable.listeners.delete(cb) AS unsubscribe - -const silent_set = (observable, value) => observable.value = value; -#EXPORT silent_set - -const broadcast = (observable, ...values) => - observable.listeners.forEach(cb => cb(...values)); -#EXPORT broadcast - -function set(observable, value) -{ - const old_value = observable.value; - silent_set(observable, value); - broadcast(observable, value, old_value); -} -#EXPORT set diff --git a/common/once.js b/common/once.js deleted file mode 100644 index 01216bd..0000000 --- a/common/once.js +++ /dev/null @@ -1,74 +0,0 @@ -/** - * This file is part of Haketilo. - * - * Function: Wrap APIs that depend on some asynchronous initialization into - * promises. - * - * Copyright (C) 2021 Wojtek Kosior - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * As additional permission under GNU GPL version 3 section 7, you - * may distribute forms of that code without the copy of the GNU - * GPL normally required by section 4, provided you include this - * license notice and, in case of non-source distribution, a URL - * through which recipients can access the Corresponding Source. - * If you modify file(s) with this exception, you may extend this - * exception to your version of the file(s), but you are not - * obligated to do so. If you do not wish to do so, delete this - * exception statement from your version. - * - * As a special exception to the GPL, any HTML file which merely - * makes function calls to this code, and for that purpose - * includes it by reference shall be deemed a separate work for - * copyright law purposes. If you modify this code, you may extend - * this exception to your version of the code, but you are not - * obligated to do so. If you do not wish to do so, delete this - * exception statement from your version. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - * - * I, Wojtek Kosior, thereby promise not to sue for violation of this file's - * license. Although I request that you do not make use of this code in a - * proprietary program, I am not going to enforce this in court. - */ - -/* - * This module provides an easy way to wrap an async function into a promise - * so that it only gets executed once. - */ - -async function assign_result(state, result_producer) -{ - state.result = await result_producer(); - state.ready = true; - for (let cb of state.waiting) - setTimeout(cb, 0, state.result); - state.waiting = undefined; -} - -async function get_result(state) -{ - if (state.ready) - return state.result; - - return new Promise((resolve, reject) => state.waiting.push(resolve)); -} - -function make_once(result_producer) -{ - let state = {waiting : [], ready : false, result : undefined}; - assign_result(state, result_producer); - return () => get_result(state); -} - -#EXPORT make_once diff --git a/common/sanitize_JSON.js b/common/sanitize_JSON.js deleted file mode 100644 index e03e396..0000000 --- a/common/sanitize_JSON.js +++ /dev/null @@ -1,431 +0,0 @@ -/** - * This file is part of Haketilo. - * - * Function: Powerful, full-blown format enforcer for externally-obtained JSON. - * - * Copyright (C) 2021 Wojtek Kosior - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * As additional permission under GNU GPL version 3 section 7, you - * may distribute forms of that code without the copy of the GNU - * GPL normally required by section 4, provided you include this - * license notice and, in case of non-source distribution, a URL - * through which recipients can access the Corresponding Source. - * If you modify file(s) with this exception, you may extend this - * exception to your version of the file(s), but you are not - * obligated to do so. If you do not wish to do so, delete this - * exception statement from your version. - * - * As a special exception to the GPL, any HTML file which merely - * makes function calls to this code, and for that purpose - * includes it by reference shall be deemed a separate work for - * copyright law purposes. If you modify this code, you may extend - * this exception to your version of the code, but you are not - * obligated to do so. If you do not wish to do so, delete this - * exception statement from your version. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - * - * I, Wojtek Kosior, thereby promise not to sue for violation of this file's - * license. Although I request that you do not make use of this code in a - * proprietary program, I am not going to enforce this in court. - */ - -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"] -]; - -#EXPORT parse_json_with_schema diff --git a/common/settings_query.js b/common/settings_query.js deleted file mode 100644 index b8c3a25..0000000 --- a/common/settings_query.js +++ /dev/null @@ -1,66 +0,0 @@ -/** - * This file is part of Haketilo. - * - * Function: Querying page settings. - * - * Copyright (C) 2021 Wojtek Kosior - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * As additional permission under GNU GPL version 3 section 7, you - * may distribute forms of that code without the copy of the GNU - * GPL normally required by section 4, provided you include this - * license notice and, in case of non-source distribution, a URL - * through which recipients can access the Corresponding Source. - * If you modify file(s) with this exception, you may extend this - * exception to your version of the file(s), but you are not - * obligated to do so. If you do not wish to do so, delete this - * exception statement from your version. - * - * As a special exception to the GPL, any HTML file which merely - * makes function calls to this code, and for that purpose - * includes it by reference shall be deemed a separate work for - * copyright law purposes. If you modify this code, you may extend - * this exception to your version of the code, but you are not - * obligated to do so. If you do not wish to do so, delete this - * exception statement from your version. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - * - * I, Wojtek Kosior, thereby promise not to sue for violation of this file's - * license. Although I request that you do not make use of this code in a - * proprietary program, I am not going to enforce this in court. - */ - - -#FROM common/stored_types.js IMPORT TYPE_PREFIX -#FROM common/patterns.js IMPORT each_url_pattern - -function query(storage, url, multiple) -{ - const matched = []; - const cb = p => check_pattern(storage, p, multiple, matched); - 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 : [undefined, undefined]; -} - -#EXPORT (storage, url) => query(storage, url, false) AS query_best -#EXPORT (storage, url) => query(storage, url, true) AS query_all diff --git a/common/storage_client.js b/common/storage_client.js deleted file mode 100644 index fe8d6e6..0000000 --- a/common/storage_client.js +++ /dev/null @@ -1,208 +0,0 @@ -/** - * This file is part of Haketilo. - * - * Function: Storage through messages (client side). - * - * Copyright (C) 2021 Wojtek Kosior - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * As additional permission under GNU GPL version 3 section 7, you - * may distribute forms of that code without the copy of the GNU - * GPL normally required by section 4, provided you include this - * license notice and, in case of non-source distribution, a URL - * through which recipients can access the Corresponding Source. - * If you modify file(s) with this exception, you may extend this - * exception to your version of the file(s), but you are not - * obligated to do so. If you do not wish to do so, delete this - * exception statement from your version. - * - * As a special exception to the GPL, any HTML file which merely - * makes function calls to this code, and for that purpose - * includes it by reference shall be deemed a separate work for - * copyright law purposes. If you modify this code, you may extend - * this exception to your version of the code, but you are not - * obligated to do so. If you do not wish to do so, delete this - * exception statement from your version. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - * - * I, Wojtek Kosior, thereby promise not to sue for violation of this file's - * license. Although I request that you do not make use of this code in a - * proprietary program, I am not going to enforce this in court. - */ - -#IMPORT common/connection_types.js AS CONNECTION_TYPE - -#FROM common/browser.js IMPORT browser -#FROM common/stored_types.js IMPORT list_prefixes -#FROM common/once.js IMPORT make_once - -var call_id = 0; -var port; -var calls_waiting = new Map(); - -function set_call_callback(resolve, reject, func, args) -{ - port.postMessage([call_id, func, args]); - calls_waiting.set(call_id++, [resolve, reject]); -} - -async function remote_call(func, args) -{ - return new Promise((resolve, reject) => - set_call_callback(resolve, reject, func, args)); -} - -function handle_message(message) -{ - let callbacks = calls_waiting.get(message.call_id); - if (callbacks === undefined) { - handle_change(message); - return; - } - - let [resolve, reject] = callbacks; - calls_waiting.delete(message.call_id); - if (message.error !== undefined) - setTimeout(reject, 0, message.error); - else - setTimeout(resolve, 0, message.result); -} - -const list_by_prefix = {}; - -for (const prefix of list_prefixes) - list_by_prefix[prefix] = {prefix, listeners : new Set()}; - -var resolve_init; - -function handle_first_message(message) -{ - for (let prefix of Object.keys(message)) - list_by_prefix[prefix].map = new Map(message[prefix]); - - port.onMessage.removeListener(handle_first_message); - port.onMessage.addListener(handle_message); - - resolve_init(); -} - -function handle_change(change) -{ - let list = list_by_prefix[change.prefix]; - - if (change.new_val === undefined) - list.map.delete(change.item); - else - list.map.set(change.item, change.new_val); - - for (let listener_callback of list.listeners) - listener_callback(change); -} - -var exports = {}; - -function start_connection(resolve) -{ - resolve_init = resolve; - port = browser.runtime.connect({name : CONNECTION_TYPE.REMOTE_STORAGE}); - port.onMessage.addListener(handle_first_message); -} - -async function init() { - await new Promise((resolve, reject) => start_connection(resolve)); - return exports; -} - -for (let call_name of ["set", "remove", "replace", "clear"]) - exports [call_name] = (...args) => remote_call(call_name, args); - -// TODO: Much of the code below is copy-pasted from /background/storage.mjs. -// This should later be refactored into a separate module -// to avoid duplication. - -/* - * Facilitate listening to changes - */ - -exports.add_change_listener = function (cb, prefixes=list_prefixes) -{ - if (typeof(prefixes) === "string") - prefixes = [prefixes]; - - for (let prefix of prefixes) - list_by_prefix[prefix].listeners.add(cb); -} - -exports.remove_change_listener = function (cb, prefixes=list_prefixes) -{ - if (typeof(prefixes) === "string") - prefixes = [prefixes]; - - for (let prefix of prefixes) - list_by_prefix[prefix].listeners.delete(cb); -} - -/* Prepare some hepler functions to get elements of a list */ - -function list_items_it(list, with_values=false) -{ - return with_values ? list.map.entries() : list.map.keys(); -} - -function list_entries_it(list) -{ - return list_items_it(list, true); -} - -function list_items(list, with_values=false) -{ - let array = []; - - for (let item of list_items_it(list, with_values)) - array.push(item); - - return array; -} - -function list_entries(list) -{ - return list_items(list, true); -} - -exports.get = function (prefix, item) -{ - return list_by_prefix[prefix].map.get(item); -} - -exports.get_all_names = function (prefix) -{ - return list_items(list_by_prefix[prefix]); -} - -exports.get_all_names_it = function (prefix) -{ - return list_items_it(list_by_prefix[prefix]); -} - -exports.get_all = function (prefix) -{ - return list_entries(list_by_prefix[prefix]); -} - -exports.get_all_it = function (prefix) -{ - return list_entries_it(list_by_prefix[prefix]); -} - -#EXPORT make_once(init) AS get_remote_storage diff --git a/common/storage_light.js b/common/storage_light.js deleted file mode 100644 index 35dfae2..0000000 --- a/common/storage_light.js +++ /dev/null @@ -1,162 +0,0 @@ -/** - * This file is part of Haketilo. - * - * Function: Storage manager, lighter than the previous one. - * - * Copyright (C) 2021 Wojtek Kosior - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * As additional permission under GNU GPL version 3 section 7, you - * may distribute forms of that code without the copy of the GNU - * GPL normally required by section 4, provided you include this - * license notice and, in case of non-source distribution, a URL - * through which recipients can access the Corresponding Source. - * If you modify file(s) with this exception, you may extend this - * exception to your version of the file(s), but you are not - * obligated to do so. If you do not wish to do so, delete this - * exception statement from your version. - * - * As a special exception to the GPL, any HTML file which merely - * makes function calls to this code, and for that purpose - * includes it by reference shall be deemed a separate work for - * copyright law purposes. If you modify this code, you may extend - * this exception to your version of the code, but you are not - * obligated to do so. If you do not wish to do so, delete this - * exception statement from your version. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - * - * I, Wojtek Kosior, thereby promise not to sue for violation of this file's - * license. Although I request that you do not make use of this code in a - * proprietary program, I am not going to enforce this in court. - */ - -#IMPORT common/storage_raw.js AS raw_storage -#IMPORT common/observables.js - -#FROM common/stored_types.js IMPORT TYPE_PREFIX - -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); - } -} -#EXPORT listen - -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); -} -#EXPORT no_listen - -function storage_change_callback(changes, area) -{ -#IF MOZILLA - if (area !== "local") { - console.warn("change in storage area", area); - return; - } -#ENDIF - - 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; -} -#EXPORT observe - -#EXPORT name => observe(TYPE_PREFIX.VAR, name) AS observe_var - -function no_observe(observable) -{ - no_listen(...created_observables.get(observable) || []); - created_observables.delete(observable); -} -#EXPORT no_observe - -#EXPORT raw_storage.set AS set -#EXPORT raw_storage.set_var AS set_var -#EXPORT raw_storage.get_var AS get_var diff --git a/common/storage_raw.js b/common/storage_raw.js deleted file mode 100644 index ddb21b5..0000000 --- a/common/storage_raw.js +++ /dev/null @@ -1,82 +0,0 @@ -/** - * This file is part of Haketilo. - * - * Function: Basic wrappers for storage API functions. - * - * Copyright (C) 2021 Wojtek Kosior - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * As additional permission under GNU GPL version 3 section 7, you - * may distribute forms of that code without the copy of the GNU - * GPL normally required by section 4, provided you include this - * license notice and, in case of non-source distribution, a URL - * through which recipients can access the Corresponding Source. - * If you modify file(s) with this exception, you may extend this - * exception to your version of the file(s), but you are not - * obligated to do so. If you do not wish to do so, delete this - * exception statement from your version. - * - * As a special exception to the GPL, any HTML file which merely - * makes function calls to this code, and for that purpose - * includes it by reference shall be deemed a separate work for - * copyright law purposes. If you modify this code, you may extend - * this exception to your version of the code, but you are not - * obligated to do so. If you do not wish to do so, delete this - * exception statement from your version. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - * - * I, Wojtek Kosior, thereby promise not to sue for violation of this file's - * license. Although I request that you do not make use of this code in a - * proprietary program, I am not going to enforce this in court. - */ - -#FROM common/browser.js IMPORT browser -#FROM common/stored_types.js IMPORT TYPE_PREFIX - -async function get(key) -{ -#IF CHROMIUM - const promise = new Promise(cb => browser.storage.local.get(key, cb)); -#ELIF MOZILLA - const promise = browser.storage.local.get(key); -#ENDIF - - return (await promise)[key]; -} -#EXPORT get - -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); -} -#EXPORT set - -async function set_var(name, value) -{ - return set(TYPE_PREFIX.VAR + name, value); -} -#EXPORT set_var - -async function get_var(name) -{ - return get(TYPE_PREFIX.VAR + name); -} -#EXPORT get_var - -const on_changed = browser.storage.onChanged || browser.storage.local.onChanged; - -#EXPORT cb => on_changed.addListener(cb) AS listen -#EXPORT cb => on_changed.removeListener(cb) AS no_listen diff --git a/common/stored_types.js b/common/stored_types.js deleted file mode 100644 index d1ce0b2..0000000 --- a/common/stored_types.js +++ /dev/null @@ -1,80 +0,0 @@ -/** - * This file is part of Haketilo. - * - * Function: Define an "enum" of stored item types. - * - * Copyright (C) 2021 Wojtek Kosior - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * As additional permission under GNU GPL version 3 section 7, you - * may distribute forms of that code without the copy of the GNU - * GPL normally required by section 4, provided you include this - * license notice and, in case of non-source distribution, a URL - * through which recipients can access the Corresponding Source. - * If you modify file(s) with this exception, you may extend this - * exception to your version of the file(s), but you are not - * obligated to do so. If you do not wish to do so, delete this - * exception statement from your version. - * - * As a special exception to the GPL, any HTML file which merely - * makes function calls to this code, and for that purpose - * includes it by reference shall be deemed a separate work for - * copyright law purposes. If you modify this code, you may extend - * this exception to your version of the code, but you are not - * obligated to do so. If you do not wish to do so, delete this - * exception statement from your version. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - * - * I, Wojtek Kosior, thereby promise not to sue for violation of this file's - * license. Although I request that you do not make use of this code in a - * proprietary program, I am not going to enforce this in court. - */ - -/* - * Key for item that is stored in quantity (script, page) is constructed by - * prepending its name with first letter of its list name. However, we also - * need to store some items that don't belong to any list. Let's call them - * persisted variables. In such case item's key is its "name" prepended with - * an underscore. - */ - -const TYPE_PREFIX = { - REPO: "r", - PAGE : "p", - BAG : "b", - SCRIPT : "s", - VAR : "_", - /* Url prefix is not used in stored settings. */ - URL : "u" -}; - -#EXPORT TYPE_PREFIX - -const TYPE_NAME = { - [TYPE_PREFIX.REPO] : "repo", - [TYPE_PREFIX.PAGE] : "page", - [TYPE_PREFIX.BAG] : "bag", - [TYPE_PREFIX.SCRIPT] : "script" -} - -#EXPORT TYPE_NAME - -const list_prefixes = [ - TYPE_PREFIX.REPO, - TYPE_PREFIX.PAGE, - TYPE_PREFIX.BAG, - TYPE_PREFIX.SCRIPT -]; - -#EXPORT list_prefixes diff --git a/content/activity_info_server.js b/content/activity_info_server.js deleted file mode 100644 index 6c0badc..0000000 --- a/content/activity_info_server.js +++ /dev/null @@ -1,128 +0,0 @@ -/** - * This file is part of Haketilo. - * - * Function: Informing the popup about what happens in the content script - * (script injection, script blocking, etc.). - * - * Copyright (C) 2021 Wojtek Kosior - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * As additional permission under GNU GPL version 3 section 7, you - * may distribute forms of that code without the copy of the GNU - * GPL normally required by section 4, provided you include this - * license notice and, in case of non-source distribution, a URL - * through which recipients can access the Corresponding Source. - * If you modify file(s) with this exception, you may extend this - * exception to your version of the file(s), but you are not - * obligated to do so. If you do not wish to do so, delete this - * exception statement from your version. - * - * As a special exception to the GPL, any HTML file which merely - * makes function calls to this code, and for that purpose - * includes it by reference shall be deemed a separate work for - * copyright law purposes. If you modify this code, you may extend - * this exception to your version of the code, but you are not - * obligated to do so. If you do not wish to do so, delete this - * exception statement from your version. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - * - * I, Wojtek Kosior, thereby promise not to sue for violation of this file's - * license. Although I request that you do not make use of this code in a - * proprietary program, I am not going to enforce this in court. - */ - -#IMPORT common/connection_types.js AS CONNECTION_TYPE -#IMPORT content/repo_query.js - -#FROM common/message_server.js IMPORT listen_for_connection - -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]; - activities.push(activity); - - for (const port of ports) - port.postMessage(activity); -} - -function report_script(script_data) -{ - report_activity("script", script_data); -} -#EXPORT report_script - -function report_settings(settings) -{ - const settings_clone = {}; - Object.assign(settings_clone, settings) - report_activity("settings", settings_clone); -} -#EXPORT report_settings - -function report_document_type(is_html) -{ - report_activity("is_html", is_html); -} -#EXPORT report_document_type - -function report_repo_query_action(update, port) -{ - report_activity_oneshot("repo_query_action", update, port); -} - -function trigger_repo_query(query_specifier) -{ - repo_query.query(...query_specifier); -} - -function handle_disconnect(port, report_action) -{ - ports.delete(port) - repo_query.unsubscribe_results(report_action); -} - -function new_connection(port) -{ - console.log("new activity info connection!"); - - ports.add(port); - - for (const activity of activities) - port.postMessage(activity); - - const report_action = u => report_repo_query_action(u, port); - repo_query.subscribe_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() -{ - listen_for_connection(CONNECTION_TYPE.ACTIVITY_INFO, new_connection); -} -#EXPORT start diff --git a/content/content.js b/content/content.js index feef5db..c501187 100644 --- a/content/content.js +++ b/content/content.js @@ -74,7 +74,7 @@ globalThis.haketilo_content_script_main = async function() { const policy = decide_policy(globalThis.haketilo_pattern_tree, document.URL, - globalThis.haketilo_defualt_allow, + globalThis.haketilo_default_allow, globalThis.haketilo_secret); const page_info = Object.assign({url: document.URL}, policy); ["csp", "nonce"].forEach(prop => delete page_info[prop]); @@ -93,7 +93,7 @@ globalThis.haketilo_content_script_main = async function() { resolve_page_info(Object.assign(page_info, script_response)); return; } else { - for (const script_contents of script_response) { + for (const script_contents of script_response.files) { const html_ns = "http://www.w3.org/1999/xhtml"; const script = document.createElementNS(html_ns, "script"); diff --git a/content/main.js b/content/main.js deleted file mode 100644 index 8b24323..0000000 --- a/content/main.js +++ /dev/null @@ -1,357 +0,0 @@ -/** - * This file is part of Haketilo. - * - * Function: Main content script that runs in all frames. - * - * Copyright (C) 2021 Wojtek Kosior - * Copyright (C) 2021 jahoti - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * As additional permission under GNU GPL version 3 section 7, you - * may distribute forms of that code without the copy of the GNU - * GPL normally required by section 4, provided you include this - * license notice and, in case of non-source distribution, a URL - * through which recipients can access the Corresponding Source. - * If you modify file(s) with this exception, you may extend this - * exception to your version of the file(s), but you are not - * obligated to do so. If you do not wish to do so, delete this - * exception statement from your version. - * - * As a special exception to the GPL, any HTML file which merely - * makes function calls to this code, and for that purpose - * includes it by reference shall be deemed a separate work for - * copyright law purposes. If you modify this code, you may extend - * this exception to your version of the code, but you are not - * obligated to do so. If you do not wish to do so, delete this - * exception statement from your version. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - * - * I, Wojtek Kosior, thereby promise not to sue for violation of this file's - * license. Although I request that you do not make use of this code in a - * proprietary program, I am not going to enforce this in court. - */ - -#IMPORT content/activity_info_server.js - -#FROM content/page_actions.js IMPORT handle_page_actions -#FROM common/misc.js IMPORT gen_nonce, is_privileged_url, \ - csp_header_regex -#FROM common/browser.js IMPORT browser - -/* CSP rule that blocks scripts according to policy's needs. */ -function make_csp_rule(policy) -{ - let rule = "prefetch-src 'none'; script-src-attr 'none';"; - const script_src = policy.nonce !== undefined ? - `'nonce-${policy.nonce}'` : "'none'"; - rule += ` script-src ${script_src}; script-src-elem ${script_src};`; - return rule; -} - -document.content_loaded = document.readyState === "complete"; -const wait_loaded = e => e.content_loaded ? Promise.resolve() : - new Promise(c => e.addEventListener("DOMContentLoaded", c, {once: true})); - -wait_loaded(document).then(() => document.content_loaded = true); - -/* - * In the case of HTML documents: - * 1. When injecting some payload we need to sanitize <meta> CSP tags before - * they reach the document. - * 2. Only <meta> tags inside <head> are considered valid by the browser and - * need to be considered. - * 3. We want to detach <html> from document, wait until its <head> completes - * loading, sanitize it and re-attach <html>. - * 4. We shall wait for anything to appear in or after <body> and take that as - * a sign <head> has finished loading. - * 5. Otherwise, getting the `DOMContentLoaded' event on the document shall also - * be a sign that <head> is fully loaded. - */ - -function make_body_start_observer(DOM_element, waiting) -{ - const observer = new MutationObserver(() => try_body_started(waiting)); - observer.observe(DOM_element, {childList: true}); - return observer; -} - -function try_body_started(waiting) -{ - const body = waiting.detached_html.querySelector("body"); - - if ((body && (body.firstChild || body.nextSibling)) || - waiting.doc.documentElement.nextSibling) { - finish_waiting(waiting); - return true; - } - - if (body && waiting.observers.length < 2) - waiting.observers.push(make_body_start_observer(body, waiting)); -} - -function finish_waiting(waiting) -{ - if (waiting.finished) - return; - waiting.finished = true; - waiting.observers.forEach(observer => observer.disconnect()); - setTimeout(waiting.callback, 0); -} - -function _wait_for_head(doc, detached_html, callback) -{ - const waiting = {doc, detached_html, callback, observers: []}; - - if (try_body_started(waiting)) - return; - - waiting.observers = [make_body_start_observer(detached_html, waiting)]; - - wait_loaded(doc).then(() => finish_waiting(waiting)); -} - -function wait_for_head(doc, detached_html) -{ - return new Promise(cb => _wait_for_head(doc, detached_html, cb)); -} - -const blocked_str = "blocked"; - -function block_attribute(node, attr, ns=null) -{ - const [hasa, geta, seta, rema] = ["has", "get", "set", "remove"] - .map(m => (n, ...args) => typeof ns === "string" ? - n[`${m}AttributeNS`](ns, ...args) : n[`${m}Attribute`](...args)); - /* - * Disabling attributes by prepending `-blocked' allows them to still be - * relatively easily accessed in case they contain some useful data. - */ - const construct_name = [attr]; - while (hasa(node, construct_name.join(""))) - construct_name.unshift(blocked_str); - - while (construct_name.length > 1) { - construct_name.shift(); - const name = construct_name.join(""); - seta(node, `${blocked_str}-${name}`, geta(node, name)); - } - - rema(node, attr); -} - -/* - * Used to disable `<script>'s and `<meta>'s that have not yet been added to - * live DOM (doesn't work for those already added). - */ -function sanitize_meta(meta) -{ - if (csp_header_regex.test(meta.httpEquiv) && meta.content) - block_attribute(meta, "content"); -} - -function sanitize_script(script) -{ - script.haketilo_blocked_type = script.getAttribute("type"); - script.type = "text/plain"; -} - -/* - * Executed after `<script>' has been connected to the DOM, when it is no longer - * eligible for being executed by the browser. - */ -function desanitize_script(script) -{ - script.setAttribute("type", script.haketilo_blocked_type); - - if ([null, undefined].includes(script.haketilo_blocked_type)) - script.removeAttribute("type"); - - delete script.haketilo_blocked_type; -} - -const bad_url_reg = /^data:([^,;]*ml|unknown-content-type)/i; -function sanitize_urls(element) -{ - for (const attr of [...element.attributes || []] - .filter(attr => /^(href|src|data)$/i.test(attr.localName)) - .filter(attr => bad_url_reg.test(attr.value))) - block_attribute(element, attr.localName, attr.namespaceURI); -} - -function start_data_urls_sanitizing(doc) -{ - doc.querySelectorAll("*[href], *[src], *[data]").forEach(sanitize_urls); - if (!doc.content_loaded) { - const mutation_handler = m => m.addedNodes.forEach(sanitize_urls); - const mo = new MutationObserver(ms => ms.forEach(mutation_handler)); - mo.observe(doc, {childList: true, subtree: true}); - wait_loaded(doc).then(() => mo.disconnect()); - } -} - -/* - * Normally, we block scripts with CSP. However, Mozilla does optimizations that - * cause part of the DOM to be loaded when our content scripts get to run. Thus, - * before the CSP rules we inject (for non-HTTP pages) become effective, we need - * to somehow block the execution of `<script>'s and intrinsics that were - * already there. Additionally, some browsers (IceCat 60) seem to have problems - * applying this CSP to non-inline `<scripts>' in certain scenarios. - */ -function prevent_script_execution(event) -{ - if (!event.target.haketilo_payload) - event.preventDefault(); -} - -function mozilla_initial_block(doc) -{ - doc.addEventListener("beforescriptexecute", prevent_script_execution); - - for (const elem of doc.querySelectorAll("*")) { - [...elem.attributes].map(attr => attr.localName) - .filter(attr => /^on/.test(attr) && elem.wrappedJSObject[attr]) - .forEach(attr => elem.wrappedJSObject[attr] = null); - } -} - -/* - * Here we block all scripts of a document which might be either and - * HTMLDocument or an XMLDocument. Modifying an XML document might disrupt - * Mozilla's XML preview. This is an unfortunate thing we have to accept for - * now. XML documents *have to* be sanitized as well because they might - * contain `<script>' tags (or on* attributes) with namespace declared as - * "http://www.w3.org/1999/xhtml" or "http://www.w3.org/2000/svg" which allows - * javascript execution. - */ -async function sanitize_document(doc, policy) -{ -#IF MOZILLA - /* - * Blocking of scripts that are in the DOM from the beginning. Needed for - * Mozilla. - */ - mozilla_initial_block(doc); -#ENDIF - - /* - * Ensure our CSP rules are employed from the beginning. This CSP injection - * method is, when possible, going to be applied together with CSP rules - * injected using webRequest. - * Using elements namespaced as HTML makes this CSP injection also work for - * non-HTML documents. - */ - const html = new DOMParser().parseFromString(`<html><head><meta \ -http-equiv="Content-Security-Policy" content="${make_csp_rule(policy)}"\ -/></head><body>Loading...</body></html>`, "text/html").documentElement; - - /* - * Root node gets hijacked now, to be re-attached after <head> is loaded - * and sanitized. - */ - const root = doc.documentElement; - root.replaceWith(html); - - /* - * When we don't inject payload, we neither block document's CSP `<meta>' - * tags nor wait for `<head>' to be parsed. - */ - if (policy.has_payload) { - await wait_for_head(doc, root); - - root.querySelectorAll("head meta") - .forEach(m => sanitize_meta(m, policy)); - } - - root.querySelectorAll("script").forEach(s => sanitize_script(s, policy)); - html.replaceWith(root); - root.querySelectorAll("script").forEach(s => desanitize_script(s, policy)); - - start_data_urls_sanitizing(doc); -} - -async function _disable_service_workers() -{ - if (!navigator.serviceWorker) - return; - - const registrations = await navigator.serviceWorker.getRegistrations(); - if (registrations.length === 0) - return; - - console.warn("Service Workers detected on this page! Unregistering and reloading."); - - try { - await Promise.all(registrations.map(r => r.unregister())); - } finally { - location.reload(); - } - - /* Never actually return! */ - return new Promise(() => 0); -} - -/* - * Trying to use servce workers APIs might result in exceptions, for example - * when in a non-HTML document. Because of this, we wrap the function that does - * the actual work in a try {} block. - */ -async function disable_service_workers() -{ - try { - await _disable_service_workers() - } catch (e) { - console.debug("Exception thrown during an attempt to detect and disable service workers.", e); - } -} - -function synchronously_get_policy(url) -{ - const encoded_url = encodeURIComponent(url); - const request_url = `${browser.runtime.getURL("dummy")}?url=${encoded_url}`; - - try { - var xhttp = new XMLHttpRequest(); - xhttp.open("GET", request_url, false); - xhttp.send(); - } catch(e) { - console.error("Failure to synchronously fetch policy for url.", e); - return {allow: false}; - } - - const policy = /^[^?]*\?settings=(.*)$/.exec(xhttp.responseURL)[1]; - return JSON.parse(decodeURIComponent(policy)); -} - -if (!is_privileged_url(document.URL)) { - const policy = synchronously_get_policy(document.URL); - - if (!(document instanceof HTMLDocument)) - delete policy.payload; - - console.debug("current policy", policy); - - activity_info_server.report_settings(policy); - - policy.nonce = gen_nonce(); - - const doc_ready = Promise.all([ - policy.allow ? Promise.resolve() : sanitize_document(document, policy), - policy.allow ? Promise.resolve() : disable_service_workers(), - wait_loaded(document) - ]); - - handle_page_actions(policy, doc_ready); - - activity_info_server.start(); -} diff --git a/content/page_actions.js b/content/page_actions.js deleted file mode 100644 index b1ecd42..0000000 --- a/content/page_actions.js +++ /dev/null @@ -1,113 +0,0 @@ -/** - * This file is part of Haketilo. - * - * Function: Handle page actions in a content script. - * - * Copyright (C) 2021 Wojtek Kosior - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * As additional permission under GNU GPL version 3 section 7, you - * may distribute forms of that code without the copy of the GNU - * GPL normally required by section 4, provided you include this - * license notice and, in case of non-source distribution, a URL - * through which recipients can access the Corresponding Source. - * If you modify file(s) with this exception, you may extend this - * exception to your version of the file(s), but you are not - * obligated to do so. If you do not wish to do so, delete this - * exception statement from your version. - * - * As a special exception to the GPL, any HTML file which merely - * makes function calls to this code, and for that purpose - * includes it by reference shall be deemed a separate work for - * copyright law purposes. If you modify this code, you may extend - * this exception to your version of the code, but you are not - * obligated to do so. If you do not wish to do so, delete this - * exception statement from your version. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - * - * I, Wojtek Kosior, thereby promise not to sue for violation of this file's - * license. Although I request that you do not make use of this code in a - * proprietary program, I am not going to enforce this in court. - */ - -#IMPORT common/connection_types.js AS CONNECTION_TYPE - -#FROM common/browser.js IMPORT browser -#FROM content/activity_info_server.js IMPORT report_script, report_document_type - -let policy; -/* 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 = []; - -function handle_message(message) -{ - const [action, data] = message; - - if (action === "inject") { - for (let script_text of data) { - if (loaded) - add_script(script_text); - else - scripts_awaiting.push(script_text); - } - } - else { - console.error(`Bad page action '${action}'.`); - } -} - -function document_ready(event) -{ - loaded = true; - - for (let script_text of scripts_awaiting) - add_script(script_text); - - scripts_awaiting = undefined; -} - -function add_script(script_text) -{ - if (!is_html) - return; - - let script = document.createElement("script"); - script.textContent = script_text; - script.setAttribute("nonce", policy.nonce); - script.haketilo_payload = true; - document.body.appendChild(script); - - report_script(script_text); -} - -function handle_page_actions(_policy, doc_ready_promise) { - policy = _policy; - - url = document.URL; - is_html = document instanceof HTMLDocument; - report_document_type(is_html); - - doc_ready_promise.then(document_ready); - - if (policy.payload) { - port = browser.runtime.connect({name : CONNECTION_TYPE.PAGE_ACTIONS}); - port.onMessage.addListener(handle_message); - port.postMessage({payload: policy.payload}); - } -} -#EXPORT handle_page_actions diff --git a/content/policy_enforcing.js b/content/policy_enforcing.js index 8e26afb..320b6d0 100644 --- a/content/policy_enforcing.js +++ b/content/policy_enforcing.js @@ -43,7 +43,7 @@ * proprietary program, I am not going to enforce this in court. */ -#FROM common/misc.js IMPORT gen_nonce +#FROM common/misc.js IMPORT gen_nonce, csp_header_regex document.content_loaded = document.readyState === "complete"; const wait_loaded = e => e.content_loaded ? Promise.resolve() : @@ -237,7 +237,7 @@ function prevent_script_execution(event) { #ENDIF /* - * Here we block all scripts of a document which might be either and + * Here we block all scripts of a document which might be either an * HTMLDocument or an XMLDocument. Modifying an XML document might disrupt * Mozilla's XML preview. This is an unfortunate thing we have to accept for * now. XML documents *have to* be sanitized as well because they might diff --git a/content/repo_query.js b/content/repo_query.js deleted file mode 100644 index 5f1adda..0000000 --- a/content/repo_query.js +++ /dev/null @@ -1,140 +0,0 @@ -/** - * This file is part of Haketilo. - * - * Function: Getting available content for site from remote repositories. - * - * Copyright (C) 2021 Wojtek Kosior - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * As additional permission under GNU GPL version 3 section 7, you - * may distribute forms of that code without the copy of the GNU - * GPL normally required by section 4, provided you include this - * license notice and, in case of non-source distribution, a URL - * through which recipients can access the Corresponding Source. - * If you modify file(s) with this exception, you may extend this - * exception to your version of the file(s), but you are not - * obligated to do so. If you do not wish to do so, delete this - * exception statement from your version. - * - * As a special exception to the GPL, any HTML file which merely - * makes function calls to this code, and for that purpose - * includes it by reference shall be deemed a separate work for - * copyright law purposes. If you modify this code, you may extend - * this exception to your version of the code, but you are not - * obligated to do so. If you do not wish to do so, delete this - * exception statement from your version. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - * - * I, Wojtek Kosior, thereby promise not to sue for violation of this file's - * license. Although I request that you do not make use of this code in a - * proprietary program, I am not going to enforce this in court. - */ - -#IMPORT common/observables.js - -#FROM common/ajax.js IMPORT make_ajax_request -#FROM common/stored_types.js IMPORT TYPE_PREFIX -#FROM common/sanitize_JSON.js IMPORT parse_json_with_schema -#FROM common/misc.js IMPORT matchers - -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); -} -#EXPORT repo_query AS query - -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_results(cb) -{ - observables.subscribe(observable, cb); - for (const [key, results] of queried_items.entries()) - cb({prefix: key[0], item: key.substring(1), results}); -} -#EXPORT subscribe_results - -function unsubscribe_results(cb) -{ - observables.unsubscribe(observable, cb); -} -#EXPORT unsubscribe_results diff --git a/content/repo_query_cacher.js b/content/repo_query_cacher.js index bdba189..41487e1 100644 --- a/content/repo_query_cacher.js +++ b/content/repo_query_cacher.js @@ -82,3 +82,4 @@ function on_repo_query_request([type, url], sender, respond_cb) { function start() { browser.runtime.onMessage.addListener(on_repo_query_request); } +#EXPORT start diff --git a/default_settings.json b/default_settings.json index e7ea2c3..d66ced1 100644 --- a/default_settings.json +++ b/default_settings.json @@ -1 +1,73 @@ -[{"shaketilo demo script":{"url":"","hash":"","text":"/**\n * Haketilo demo script.\n *\n * Copyright (C) Wojtek Kosior\n * Available under the terms of Creative Commons Zero\n * <https://creativecommons.org/publicdomain/zero/1.0/legalcode>\n */\n\nconst banner = document.createElement(\"h2\");\n\nbanner.textContent = \"Hoooray! Haketilo works :D\"\n\nbanner.setAttribute(\"style\", `\\\nmargin: 1em; \\\nborder-radius: 1em 0px; \\\nbackground-color: #474; \\\npadding: 10px 20px; \\\ncolor: #eee; \\\nbox-shadow: 0 6px 8px 0 rgba(0,0,0,0.24), 0 17px 50px 0 rgba(0,0,0,0.19); \\\ndisplay: inline-block;\\\n`);\n\ndocument.body.prepend(banner);"}},{"bhaketilo demo bag":[["s","haketilo demo script"]]},{"phttps://hachette-hydrilla.org":{"components":["b","haketilo demo bag"],"allow":false}},{"rhttps://api-demo.hachette-hydrilla.org":{}}] +#IF KEEP_DEFAULT_SETTINGS_COMMENT +// SPDX-License-Identifier: CC0-1.0 +// +// This file contains definitions of default mapping(s) and resource(s) to +// include in Haketilo. +// +// Copyright (C) 2022 Wojtek Kosior <koszko@koszko.org> +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the CC0 1.0 Universal License as published by +// the Creative Commons Corporation. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// CC0 1.0 Universal License for more details. +// +// +// The text of CC0 1.0 Universal License is also included in this file. This +// text is copyrighted by the Creative Commons Corporation and is not subject to +// the notice above. +#ENDIF +{ + "resources": { + "haketilo-demo-script": { + "2022.1.28": { + "source_name": "haketilo-default-settings", + "source_copyright": [{ + "file": "CC0-1.0.txt", + "hash_key": "sha256-a2010f343487d3f7618affe54f789f5487602331c0a8d03f49e9a7c547cf0499" + }], + "type": "resource", + "identifier": "haketilo-demo-script", + "long_name": "Haketilo demonstrational script", + "uuid": "9dccba55-4b18-4a01-933f-feaabd62fe95", + "version": [2022, 1, 28], + "revision": 1, + "description": "Demonstrational script that displays a message with green background on given page.", + "dependencies": [], + "scripts": [{ + "file": "demo-script.js", + "hash_key": "sha256-f1d9dc9f2f4edaaebd9167eb8b79a6da6434313df47921aa4ea8ae278f7739e3" + }] + } + } + }, + "mappings": { + "haketilo-demo-message": { + "2022.1.28": { + "source_name": "haketilo-default-settings", + "source_copyright": [{ + "file": "CC0-1.0.txt", + "hash_key": "sha256-a2010f343487d3f7618affe54f789f5487602331c0a8d03f49e9a7c547cf0499" + }], + "type": "mapping", + "identifier": "haketilo-demo-message", + "long_name": "Haketilo demonstrational message", + "uuid": "2caf5000-cc22-40e4-ba3e-e263f196a1ac", + "version": [2022, 1, 28], + "description": "Demonstrate Haketilo's capabilities by displaying a simple message on hachette-hydrilla.org", + "payloads": { + "https://hachette-hydrilla.org": { + "identifier": "haketilo-demo-script" + }, + } + } + } + }, + "files": { + "sha256-a2010f343487d3f7618affe54f789f5487602331c0a8d03f49e9a7c547cf0499": "Creative Commons Legal Code\n\nCC0 1.0 Universal\n\n CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE\n LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN\n ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS\n INFORMATION ON AN \"AS-IS\" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES\n REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS\n PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM\n THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED\n HEREUNDER.\n\nStatement of Purpose\n\nThe laws of most jurisdictions throughout the world automatically confer\nexclusive Copyright and Related Rights (defined below) upon the creator\nand subsequent owner(s) (each and all, an \"owner\") of an original work of\nauthorship and/or a database (each, a \"Work\").\n\nCertain owners wish to permanently relinquish those rights to a Work for\nthe purpose of contributing to a commons of creative, cultural and\nscientific works (\"Commons\") that the public can reliably and without fear\nof later claims of infringement build upon, modify, incorporate in other\nworks, reuse and redistribute as freely as possible in any form whatsoever\nand for any purposes, including without limitation commercial purposes.\nThese owners may contribute to the Commons to promote the ideal of a free\nculture and the further production of creative, cultural and scientific\nworks, or to gain reputation or greater distribution for their Work in\npart through the use and efforts of others.\n\nFor these and/or other purposes and motivations, and without any\nexpectation of additional consideration or compensation, the person\nassociating CC0 with a Work (the \"Affirmer\"), to the extent that he or she\nis an owner of Copyright and Related Rights in the Work, voluntarily\nelects to apply CC0 to the Work and publicly distribute the Work under its\nterms, with knowledge of his or her Copyright and Related Rights in the\nWork and the meaning and intended legal effect of CC0 on those rights.\n\n1. Copyright and Related Rights. A Work made available under CC0 may be\nprotected by copyright and related or neighboring rights (\"Copyright and\nRelated Rights\"). Copyright and Related Rights include, but are not\nlimited to, the following:\n\n i. the right to reproduce, adapt, distribute, perform, display,\n communicate, and translate a Work;\n ii. moral rights retained by the original author(s) and/or performer(s);\niii. publicity and privacy rights pertaining to a person's image or\n likeness depicted in a Work;\n iv. rights protecting against unfair competition in regards to a Work,\n subject to the limitations in paragraph 4(a), below;\n v. rights protecting the extraction, dissemination, use and reuse of data\n in a Work;\n vi. database rights (such as those arising under Directive 96/9/EC of the\n European Parliament and of the Council of 11 March 1996 on the legal\n protection of databases, and under any national implementation\n thereof, including any amended or successor version of such\n directive); and\nvii. other similar, equivalent or corresponding rights throughout the\n world based on applicable law or treaty, and any national\n implementations thereof.\n\n2. Waiver. To the greatest extent permitted by, but not in contravention\nof, applicable law, Affirmer hereby overtly, fully, permanently,\nirrevocably and unconditionally waives, abandons, and surrenders all of\nAffirmer's Copyright and Related Rights and associated claims and causes\nof action, whether now known or unknown (including existing as well as\nfuture claims and causes of action), in the Work (i) in all territories\nworldwide, (ii) for the maximum duration provided by applicable law or\ntreaty (including future time extensions), (iii) in any current or future\nmedium and for any number of copies, and (iv) for any purpose whatsoever,\nincluding without limitation commercial, advertising or promotional\npurposes (the \"Waiver\"). Affirmer makes the Waiver for the benefit of each\nmember of the public at large and to the detriment of Affirmer's heirs and\nsuccessors, fully intending that such Waiver shall not be subject to\nrevocation, rescission, cancellation, termination, or any other legal or\nequitable action to disrupt the quiet enjoyment of the Work by the public\nas contemplated by Affirmer's express Statement of Purpose.\n\n3. Public License Fallback. Should any part of the Waiver for any reason\nbe judged legally invalid or ineffective under applicable law, then the\nWaiver shall be preserved to the maximum extent permitted taking into\naccount Affirmer's express Statement of Purpose. In addition, to the\nextent the Waiver is so judged Affirmer hereby grants to each affected\nperson a royalty-free, non transferable, non sublicensable, non exclusive,\nirrevocable and unconditional license to exercise Affirmer's Copyright and\nRelated Rights in the Work (i) in all territories worldwide, (ii) for the\nmaximum duration provided by applicable law or treaty (including future\ntime extensions), (iii) in any current or future medium and for any number\nof copies, and (iv) for any purpose whatsoever, including without\nlimitation commercial, advertising or promotional purposes (the\n\"License\"). The License shall be deemed effective as of the date CC0 was\napplied by Affirmer to the Work. Should any part of the License for any\nreason be judged legally invalid or ineffective under applicable law, such\npartial invalidity or ineffectiveness shall not invalidate the remainder\nof the License, and in such case Affirmer hereby affirms that he or she\nwill not (i) exercise any of his or her remaining Copyright and Related\nRights in the Work or (ii) assert any associated claims and causes of\naction with respect to the Work, in either case contrary to Affirmer's\nexpress Statement of Purpose.\n\n4. Limitations and Disclaimers.\n\n a. No trademark or patent rights held by Affirmer are waived, abandoned,\n surrendered, licensed or otherwise affected by this document.\n b. Affirmer offers the Work as-is and makes no representations or\n warranties of any kind concerning the Work, express, implied,\n statutory or otherwise, including without limitation warranties of\n title, merchantability, fitness for a particular purpose, non\n infringement, or the absence of latent or other defects, accuracy, or\n the present or absence of errors, whether or not discoverable, all to\n the greatest extent permissible under applicable law.\n c. Affirmer disclaims responsibility for clearing rights of other persons\n that may apply to the Work or any use thereof, including without\n limitation any person's Copyright and Related Rights in the Work.\n Further, Affirmer disclaims responsibility for obtaining any necessary\n consents, permissions or other rights required for any use of the\n Work.\n d. Affirmer understands and acknowledges that Creative Commons is not a\n party to this document and has no duty or obligation with respect to\n this CC0 or use of the Work.\n", + "sha256-f1d9dc9f2f4edaaebd9167eb8b79a6da6434313df47921aa4ea8ae278f7739e3": "/**\n * Haketilo demo script.\n *\n * Copyright (C) 2021 Wojtek Kosior\n * Available under the terms of Creative Commons Zero\n * <https://creativecommons.org/publicdomain/zero/1.0/legalcode>\n */\n\n{\n const banner = document.createElement(\"h2\");\n \n banner.textContent = \"Hoooray! Haketilo works :D\";\n \n banner.setAttribute(\"style\", `\\\n margin: 1em; \\\n border-radius: 1em 0px; \\\n background-color: #474; \\\n padding: 10px 20px; \\\n color: #eee; \\\n box-shadow: 0 6px 8px 0 rgba(0,0,0,0.24), 0 17px 50px 0 rgba(0,0,0,0.19); \\\n display: inline-block;\\\n `);\n \n document.body.prepend(banner);\n}\n" + } +} diff --git a/html/back_button.css b/html/back_button.css deleted file mode 100644 index 38740d7..0000000 --- a/html/back_button.css +++ /dev/null @@ -1,71 +0,0 @@ -/* - * SPDX-License-Identifier: GPL-3.0-or-later OR CC-BY-SA-4.0 - * - * Style for a "back" button with a CSS arrow image - * - * This file is part of Haketilo. - * - * Copyright (C) 2021 Wojtek Kosior <koszko@koszko.org> - * - * File is dual-licensed. You can choose either GPLv3+, CC BY-SA or both. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - * - * I, Wojtek Kosior, thereby promise not to sue for violation of this file's - * licenses. Although I request that you do not make use of this code in a - * proprietary program, I am not going to enforce this in court. - */ - -.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/display_panel.html b/html/display_panel.html deleted file mode 100644 index 1468368..0000000 --- a/html/display_panel.html +++ /dev/null @@ -1,370 +0,0 @@ -<!doctype html> -<!-- - SPDX-License-Identifier: GPL-3.0-or-later OR CC-BY-SA-4.0 - - Extension's popup page - - This file is part of Haketilo. - - Copyright (C) 2021 Wojtek Kosior <koszko@koszko.org> - - File is dual-licensed. You can choose either GPLv3+, CC BY-SA or both. - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see <https://www.gnu.org/licenses/>. - - I, Wojtek Kosior, thereby promise not to sue for violation of this file's - licenses. Although I request that you do not make use of this code in a - proprietary program, I am not going to enforce this in court. - --> -<html> - <head> - <meta charset="utf-8"/> - <title>Haketilo - page settings</title> -#LOADCSS html/reset.css -#LOADCSS html/base.css -#LOADCSS html/back_button.css -#LOADCSS html/table.css -#IF MOZILLA -#LOADCSS html/mozilla_scrollbar_fix.css -#ENDIF - <style> - body { - width: max-content; - width: -moz-fit-content; - } - - .top>h2 { - padding-left: calc(0.8*3.2em - 8px); - } - - .top { - line-height: calc(0.8*3.6em - 16px); - } - - #main_view>.top>h2 { - padding-left: 0; - max-width: 550px - } - - .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> - <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> - <label class="button slimbutton" for="show_install_view_radio" data-template="btn"> - Install - </label> - </div> - <div id="unrollable_component" data-template="unroll_container"> - <input type="checkbox" class="unroll_chbx" data-template="chbx"></input> - <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> - <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> - <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> - - <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"> - <div class="top has_bottom_line"><h2> Site modifiers install </h2></div> - <div class="padding_inline"> -#INCLUDE html/import_frame.html - </div> - </div> - - <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> - <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"> -#INCLUDE 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> - <div> - <h3> - Connecting to content script..<span id="loading_point">.</span> - </h3> - <aside id="reload_notice"> - Try reloading the page. - </aside> - </div> - </div> - </div> - - <div class="footer padding_inline has_upper_thin_line"> - <button id="settings_but" type="button"> - Open Haketilo settings - </button> - </div> - </div> - - <div class="has_upper_line"></div> - - <label for="show_main_view_radio" class="back_button"><div></div></label> -#LOADJS html/display_panel.js - </body> -</html> diff --git a/html/display_panel.js b/html/display_panel.js deleted file mode 100644 index 1cd77e6..0000000 --- a/html/display_panel.js +++ /dev/null @@ -1,586 +0,0 @@ -/** - * This file is part of Haketilo. - * - * Function: Popup logic. - * - * Copyright (C) 2021 Wojtek Kosior - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * As additional permission under GNU GPL version 3 section 7, you - * may distribute forms of that code without the copy of the GNU - * GPL normally required by section 4, provided you include this - * license notice and, in case of non-source distribution, a URL - * through which recipients can access the Corresponding Source. - * If you modify file(s) with this exception, you may extend this - * exception to your version of the file(s), but you are not - * obligated to do so. If you do not wish to do so, delete this - * exception statement from your version. - * - * As a special exception to the GPL, any HTML file which merely - * makes function calls to this code, and for that purpose - * includes it by reference shall be deemed a separate work for - * copyright law purposes. If you modify this code, you may extend - * this exception to your version of the code, but you are not - * obligated to do so. If you do not wish to do so, delete this - * exception statement from your version. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - * - * I, Wojtek Kosior, thereby promise not to sue for violation of this file's - * license. Although I request that you do not make use of this code in a - * proprietary program, I am not going to enforce this in court. - */ - -#IMPORT common/connection_types.js AS CONNECTION_TYPE - -#FROM common/browser.js IMPORT browser -/* - * Using remote storage here seems inefficient, we only resort to that - * temporarily, before all storage access gets reworked. - */ -#FROM common/storage_client.js IMPORT get_remote_storage -#FROM html/import_frame.js IMPORT get_import_frame -#FROM common/settings_query.js IMPORT query_all -#FROM common/misc.js IMPORT is_privileged_url, nice_name, \ - open_in_settings -#FROM common/stored_types.js IMPORT TYPE_PREFIX -#FROM common/patterns.js IMPORT each_url_pattern -#FROM html/DOM_helpers.js IMPORT by_id, clone_template - -let storage; -let tab_url; - -#IF MOZILLA -/* Force popup <html>'s reflow on stupid Firefox. */ -const reflow_forcer = - () => document.documentElement.style.width = "-moz-fit-content"; -for (const radio of document.querySelectorAll('[name="current_view"]')) - radio.addEventListener("change", reflow_forcer); -#ENDIF - -const show_queried_view_radio = by_id("show_queried_view_radio"); - -const tab_query = {currentWindow: true, active: true}; - -async function get_current_tab() -{ -#IF CHROMIUM - const callback = (cb) => browser.tabs.query(tab_query, tab => cb(tab)); - const promise = new Promise(callback); -#ELIF MOZILLA - const promise = browser.tabs.query(tab_query); -#ENDIF - - try { - return (await promise)[0]; - } catch(e) { - console.log(e); - } -} - -const page_url_heading = by_id("page_url_heading"); -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() -{ - const tab = await get_current_tab(); - - if (tab === undefined) { - page_url_heading.textContent = "unknown page"; - return; - } - - tab_url = /^([^?#]*)/.exec(tab.url)[1]; - to_spans(tab_url).forEach(s => page_url_heading.append(s)); - if (is_privileged_url(tab_url)) { - privileged_notice.classList.remove("hide"); - return; - } - - populate_possible_patterns_list(tab_url); - page_state.classList.remove("hide"); - - try_to_connect(tab.id); -} - -const possible_patterns_list = by_id("possible_patterns"); -const known_patterns = new Map(); - -function add_pattern_to_list(pattern) -{ - const template = clone_template("pattern_entry"); - template.name.textContent = pattern; - - const settings_opener = () => open_in_settings(TYPE_PREFIX.PAGE, pattern); - template.button.addEventListener("click", settings_opener); - - known_patterns.set(pattern, template); - possible_patterns_list.append(template.entry); - - return template; -} - -function style_possible_pattern_entry(pattern, exists_in_settings) -{ - const [text, class_action] = exists_in_settings ? - ["Edit", "add"] : ["Add", "remove"]; - const entry_object = known_patterns.get(pattern); - - if (entry_object) { - entry_object.button.textContent = `${text} setting`; - entry_object.entry.classList[class_action]("matched_pattern"); - } -} - -function handle_page_change(change) -{ - style_possible_pattern_entry(change.item, change.new_val !== undefined); -} - -function populate_possible_patterns_list(url) -{ - for (const pattern of each_url_pattern(url)) - add_pattern_to_list(pattern); - - 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]); -} - -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}; - 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); - - query_pattern_but.addEventListener("click", start_querying_repos); - -#IF MOZILLA - setTimeout(() => monitor_connecting(tab_id), 1000); -#ENDIF -} - -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]); -} - -const loading_point = by_id("loading_point"); -const reload_notice = by_id("reload_notice"); - -function handle_disconnect(tab_id, button_cb) -{ - query_pattern_but.removeEventListener("click", button_cb); - content_script_port = null; - -#IF CHROMIUM - if (!browser.runtime.lastError) - return; -#ENDIF - - /* return if error was not during connection initialization */ - if (connected_chbx.checked) - return; - - loading_point.classList.toggle("camouflage"); - reload_notice.classList.remove("hide"); - - setTimeout(() => try_to_connect(tab_id), 1000); -} - -function monitor_connecting(tab_id) -{ - if (connected_chbx.checked) - return; - - if (content_script_port) - content_script_port.disconnect(); - else - return; - - loading_point.classList.toggle("camouflage"); - reload_notice.classList.remove("hide"); - try_to_connect(tab_id); -} - -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; - - const [type, data] = message; - - if (type === "settings") { - const settings = data; - - blocked_span.textContent = settings.allow ? "no" : "yes"; - - if (settings.pattern) { - pattern_span.textContent = settings.pattern; - const settings_opener = - () => open_in_settings(TYPE_PREFIX.PAGE, settings.pattern); - view_pattern_but.classList.remove("hide"); - view_pattern_but.addEventListener("click", settings_opener); - } else { - pattern_span.textContent = "none"; - blocked_span.textContent = blocked_span.textContent + " (default)"; - } - - if (settings.payload) { - payload_span.textContent = nice_name(...settings.payload); - payload_buttons_div.classList.remove("hide"); - const settings_opener = () => open_in_settings(...settings.payload); - view_payload_but.addEventListener("click", settings_opener); - } else { - payload_span.textContent = "none"; - } - } - if (type === "script") { - 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 === "is_html") { - if (!data) - content_type_cell.classList.remove("hide"); - } - if (type === "repo_query_action") { - 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 cloned_template = clone_template("multi_repos_query_result"); - cloned_template.url_span.textContent = url; - container_for_repo_responses.appendChild(cloned_template.div); - - cloned_template.by_repo = new Map(); - results_lists.set(url, cloned_template); - - return cloned_template; -} - -function create_result_item(list_object, repo_url, result) -{ - const cloned_template = clone_template("single_repo_query_result"); - cloned_template.repo_url.textContent = repo_url; - cloned_template.appended = null; - - list_object.ul.appendChild(cloned_template.li); - list_object.by_repo.set(repo_url, cloned_template); - - return cloned_template; -} - -function set_appended(result_item, element) -{ - if (result_item.appended) - result_item.appended.remove(); - result_item.appended = element; - result_item.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.innerHTML = "preview not implemented...<br />(consider contributing)"; -} - -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) -{ - 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) -{ - if (result.length === 0) { - show_message(result_item, "No results :("); - return; - } - - 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"); - - entry_object.pattern.textContent = match.pattern; - - cloned_ul_template.ul.appendChild(entry_object.li); - - if (!match.payload) { - entry_object.payload.textContent = "(none)"; - for (const key of ["chbx", "triangle", "unroll"]) - entry_object[key].remove(); - continue; - } - - entry_object.payload.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()); - -async function main() -{ - storage = await get_remote_storage(); - import_frame = await get_import_frame(); - import_frame.onclose = () => show_queried_view_radio.checked = true; - show_page_activity_info(); -} - -main(); diff --git a/html/import_frame.html b/html/import_frame.html deleted file mode 100644 index bac98a8..0000000 --- a/html/import_frame.html +++ /dev/null @@ -1,79 +0,0 @@ -<!-- - SPDX-License-Identifier: GPL-3.0-or-later OR CC-BY-SA-4.0 - - Scripts import dialog - - This file is part of Haketilo. - - Copyright (C) 2021 Wojtek Kosior <koszko@koszko.org> - - File is dual-licensed. You can choose either GPLv3+, CC BY-SA or both. - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see <https://www.gnu.org/licenses/>. - - I, Wojtek Kosior, thereby promise not to sue for violation of this file's - licenses. Although I request that you do not make use of this code in a - proprietary program, I am not going to enforce this in court. - --> - -<!-- - 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> - <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> - <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 deleted file mode 100644 index 659d420..0000000 --- a/html/import_frame.js +++ /dev/null @@ -1,185 +0,0 @@ -/** - * This file is part of Haketilo. - * - * Function: Logic for the settings import frame. - * - * Copyright (C) 2021 Wojtek Kosior - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * As additional permission under GNU GPL version 3 section 7, you - * may distribute forms of that code without the copy of the GNU - * GPL normally required by section 4, provided you include this - * license notice and, in case of non-source distribution, a URL - * through which recipients can access the Corresponding Source. - * If you modify file(s) with this exception, you may extend this - * exception to your version of the file(s), but you are not - * obligated to do so. If you do not wish to do so, delete this - * exception statement from your version. - * - * As a special exception to the GPL, any HTML file which merely - * makes function calls to this code, and for that purpose - * includes it by reference shall be deemed a separate work for - * copyright law purposes. If you modify this code, you may extend - * this exception to your version of the code, but you are not - * obligated to do so. If you do not wish to do so, delete this - * exception statement from your version. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - * - * I, Wojtek Kosior, thereby promise not to sue for violation of this file's - * license. Although I request that you do not make use of this code in a - * proprietary program, I am not going to enforce this in court. - */ - -#FROM common/storage_client.js IMPORT get_remote_storage -#FROM html/DOM_helpers.js IMPORT by_id, clone_template -#FROM common/misc.js IMPORT nice_name -#FROM common/once.js IMPORT make_once - -let storage; - -let import_list = by_id("import_list"); -let import_chbxs_colliding = undefined; -let entry_objects = undefined; -let settings_import_map = undefined; - -function add_import_entry(prefix, name) -{ - const cloned_template = clone_template("import_entry"); - Object.assign(cloned_template, {prefix, name}); - - cloned_template.name_span.textContent = nice_name(prefix, name); - - if (storage.get(prefix, name) !== undefined) { - import_chbxs_colliding.push(cloned_template.chbx); - cloned_template.warning.textContent = "!"; - } - - import_list.appendChild(cloned_template.entry); - - return cloned_template; -} - -function check_all_imports() -{ - for (const entry_object of entry_objects) - entry_object.chbx.checked = true; -} - -function uncheck_all_imports() -{ - for (const entry_object of entry_objects) - entry_object.chbx.checked = false; -} - -function uncheck_colliding_imports() -{ - for (let chbx of import_chbxs_colliding) - chbx.checked = false; -} - -function commit_import() -{ - for (const entry_object of entry_objects) { - if (!entry_object.chbx.checked) - continue; - - 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(); -} - -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"); -const existing_settings_note = by_id("existing_settings_note"); - -function show_selection(settings) -{ - import_selection_radio.checked = true; - - let old_children = import_list.children; - while (old_children[0] !== undefined) - 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); - 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 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() -{ - 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; -} - -#EXPORT make_once(init) AS get_import_frame diff --git a/html/mozilla_scrollbar_fix.css b/html/mozilla_scrollbar_fix.css deleted file mode 100644 index 08cdc93..0000000 --- a/html/mozilla_scrollbar_fix.css +++ /dev/null @@ -1,67 +0,0 @@ -/* - * SPDX-License-Identifier: GPL-3.0-or-later OR CC-BY-SA-4.0 - * - * Hacky fix for vertical scrollbar width being included in child's width - * - * This file is part of Haketilo. - * - * Copyright (C) 2021 Wojtek Kosior <koszko@koszko.org> - * - * File is dual-licensed. You can choose either GPLv3+, CC BY-SA or both. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - * - * I, Wojtek Kosior, thereby promise not to sue for violation of this file's - * licenses. Although I request that you do not make use of this code in a - * proprietary program, I am not going to enforce this in court. - */ - -/* - * 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/options.html b/html/options.html deleted file mode 100644 index e317032..0000000 --- a/html/options.html +++ /dev/null @@ -1,416 +0,0 @@ -<!DOCTYPE html> -<!-- - SPDX-License-Identifier: GPL-3.0-or-later OR CC-BY-SA-4.0 - - Extension's settings page - - This file is part of Haketilo. - - Copyright (C) 2021 Wojtek Kosior <koszko@koszko.org> - - File is dual-licensed. You can choose either GPLv3+, CC BY-SA or both. - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see <https://www.gnu.org/licenses/>. - - I, Wojtek Kosior, thereby promise not to sue for violation of this file's - licenses. Although I request that you do not make use of this code in a - proprietary program, I am not going to enforce this in court. - --> -<html> - <head> - <meta charset="utf-8"/> - <title>Haketilo options</title> -#LOADCSS html/reset.css -#LOADCSS html/base.css -#LOADCSS html/table.css - <style> - body { - width: 100%; - } - - /* 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 ~ 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; - } - - #tab_heads>* { - font-size: 130%; - padding: 10px; - display: inline-block; - cursor: pointer; - } - - #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 */ - .popup { - position: fixed; - width: 100vw; - height: 100vh; - left: 0; - top: 0; - background-color: rgba(60,60,60,0.4); - z-index: 1; - overflow: auto; - vertical-align: center; - horizontal-align: center; - } - - .popup_frame { - background-color: #f0f0f0; - margin: 5vh auto; - padding: 15px; - border: solid #333 4px; - border-radius: 15px; - width: -moz-fit-content; - width: fit-content; - } - - .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; - } - - .form_disabled>* { - opacity: 0.5; - pointer-events: none; - } - - .form_disabled_msg { - display: none; - font-style: italic; - } - - .form_disabled .form_disabled_msg { - opacity: initial; - pointer-events: initial; - display: initial; - } - </style> - </head> - <body> - <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> - <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" 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 class="nowrap"> - <span id="page_payload"></span> - <button id="select_page_components_but"> - Choose payload - </button> - </span> - <div id="allow_native_scripts_container" class="nowrap"> - <input id="page_allow_chbx" type="checkbox" style="display: inline;"></input> - <label for="page_allow_chbx">Allow native scripts</label> - <span class="form_disabled_msg"> - (only possible when no payload is used) - </span> - </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> - <br/> -#INCLUDE html/default_blocking_policy.html - </div> - <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" 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> - - <div id="chbx_components_window" class="hide popup" position="absolute"> - <div class="popup_frame"> - <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> - </div> - - <div id="radio_components_window" class="hide popup" position="absolute"> - <div class="popup_frame"> - <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> - </div> - - <div id="import_window" class="hide popup" position="absolute"> - <div class="popup_frame"> - <h2> Settings import </h2> -#INCLUDE html/import_frame.html - </div> - </div> - - <a id="file_downloader" class="hide"></a> - <form id="file_opener_form" style="visibility: hidden;"> - <input type="file" id="file_opener"></input> - </form> -#LOADJS html/options_main.js - </body> -</html> diff --git a/html/options_main.js b/html/options_main.js deleted file mode 100644 index 4b828f0..0000000 --- a/html/options_main.js +++ /dev/null @@ -1,781 +0,0 @@ -/** - * This file is part of Haketilo. - * - * Function: Settings page logic. - * - * Copyright (C) 2021 Wojtek Kosior - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * As additional permission under GNU GPL version 3 section 7, you - * may distribute forms of that code without the copy of the GNU - * GPL normally required by section 4, provided you include this - * license notice and, in case of non-source distribution, a URL - * through which recipients can access the Corresponding Source. - * If you modify file(s) with this exception, you may extend this - * exception to your version of the file(s), but you are not - * obligated to do so. If you do not wish to do so, delete this - * exception statement from your version. - * - * As a special exception to the GPL, any HTML file which merely - * makes function calls to this code, and for that purpose - * includes it by reference shall be deemed a separate work for - * copyright law purposes. If you modify this code, you may extend - * this exception to your version of the code, but you are not - * obligated to do so. If you do not wish to do so, delete this - * exception statement from your version. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - * - * I, Wojtek Kosior, thereby promise not to sue for violation of this file's - * license. Although I request that you do not make use of this code in a - * proprietary program, I am not going to enforce this in court. - */ - -#FROM common/storage_client.js IMPORT get_remote_storage -#FROM common/stored_types.js IMPORT TYPE_PREFIX, TYPE_NAME, \ - list_prefixes -#FROM common/misc.js IMPORT nice_name, matchers -#FROM common/sanitize_JSON.js IMPORT parse_json_with_schema -#FROM html/DOM_helpers.js IMPORT get_template, by_id -#FROM html/import_frame.js IMPORT get_import_frame - -var storage; - -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}`; -} - -/* Insert into list of bags/pages/scripts/repos */ -function add_li(prefix, item, at_the_end=false) -{ - let ul = ul_by_prefix[prefix]; - let li = item_li_template.cloneNode(true); - li.id = item_li_id(prefix, item); - - let span = li.firstElementChild; - span.textContent = item; - - let edit_button = span.nextElementSibling; - edit_button.addEventListener("click", () => edit_item(prefix, item)); - - let remove_button = edit_button.nextElementSibling; - remove_button.addEventListener("click", - () => storage.remove(prefix, item)); - - 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) { - if (element.id < li.id || element.id.startsWith("work_")) - continue; - - ul.ul.insertBefore(li, element); - break; - } - } - if (!li.parentElement) { - if (ul.work_li !== ul.ul.lastElementChild) - ul.ul.appendChild(li); - else - ul.work_li.before(li); - } - - list_set_scrollbar(ul.ul); -} - -const chbx_components_ul = by_id("chbx_components_ul"); -const radio_components_ul = by_id("radio_components_ul"); - -function chbx_li_id(prefix, item) -{ - return `cli_${prefix}_${item}`; -} - -function radio_li_id(prefix, item) -{ - return `rli_${prefix}_${item}`; -} - -//TODO: refactor the 2 functions below - -function add_chbx_li(prefix, name) -{ - if (![TYPE_PREFIX.BAG, TYPE_PREFIX.SCRIPT].includes(prefix)) - return; - - let li = chbx_component_li_template.cloneNode(true); - li.id = chbx_li_id(prefix, name); - li.setAttribute("data-prefix", prefix); - li.setAttribute("data-name", name); - - 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"); - -function add_radio_li(prefix, name) -{ - if (![TYPE_PREFIX.BAG, TYPE_PREFIX.SCRIPT].includes(prefix)) - return; - - let li = radio_component_li_template.cloneNode(true); - li.id = radio_li_id(prefix, name); - li.setAttribute("data-prefix", prefix); - li.setAttribute("data-name", name); - - let radio = li.firstElementChild.firstElementChild; - let span = radio.nextElementSibling; - - span.textContent = nice_name(prefix, name); - - radio_component_none_li.before(li); - list_set_scrollbar(radio_components_ul); -} - -/* 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 allow_native_scripts_container = by_id("allow_native_scripts_container"); -const page_payload_span = by_id("page_payload"); - -function set_page_components(components) -{ - if (components === undefined) { - page_payload_span.setAttribute("data-payload", "no"); - page_payload_span.textContent = "(None)"; - allow_native_scripts_container.classList.remove("form_disabled"); - } else { - page_payload_span.setAttribute("data-payload", "yes"); - let [prefix, name] = components; - page_payload_span.setAttribute("data-prefix", prefix); - page_payload_span.setAttribute("data-name", name); - page_payload_span.textContent = nice_name(prefix, name); - allow_native_scripts_container.classList.add("form_disabled"); - } -} - -const page_allow_chbx = by_id("page_allow_chbx"); - -/* Used to reset edited page. */ -function reset_work_page_li(ul, item, settings) -{ - ul.work_name_input.value = maybe_string(item); - settings = settings || {allow: false, components: undefined}; - page_allow_chbx.checked = !!settings.allow; - - set_page_components(settings.components); -} - -function work_page_li_components() -{ - if (page_payload_span.getAttribute("data-payload") === "no") - return undefined; - - let prefix = page_payload_span.getAttribute("data-prefix"); - let name = page_payload_span.getAttribute("data-name"); - return [prefix, name]; -} - -/* Used to get edited page data for saving. */ -function work_page_li_data(ul) -{ - let url = ul.work_name_input.value; - let settings = { - components : work_page_li_components(), - allow : !!page_allow_chbx.checked - }; - - return [url, settings]; -} - -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) -{ - for (let component of components) { - let [prefix, name] = component; - 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", - () => 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. */ -function reset_work_bag_li(ul, item, components) -{ - components = components || []; - - ul.work_name_input.value = maybe_string(item); - let old_components_ul = bag_components_ul; - bag_components_ul = old_components_ul.cloneNode(false); - - old_components_ul.replaceWith(bag_components_ul); - - add_bag_components(components); -} - -/* Used to get edited bag data for saving. */ -function work_bag_li_data(ul) -{ - let component_li = bag_components_ul.firstElementChild; - - let components = []; - - /* Last list element is empty li with id set. */ - while (component_li.id === '') { - components.push([component_li.getAttribute("data-prefix"), - component_li.getAttribute("data-name")]); - component_li = component_li.nextElementSibling; - } - - return [ul.work_name_input.value, components]; -} - -const script_url_input = by_id("script_url_field"); -const script_sha256_input = by_id("script_sha256_field"); -const script_contents_field = by_id("script_contents_field"); - -function maybe_string(maybe_defined) -{ - return maybe_defined === undefined ? "" : maybe_defined + ""; -} - -/* Used to reset edited script. */ -function reset_work_script_li(ul, name, data) -{ - ul.work_name_input.value = maybe_string(name); - if (data === undefined) - data = {}; - script_url_input.value = maybe_string(data.url); - script_sha256_input.value = maybe_string(data.hash); - script_contents_field.value = maybe_string(data.text); -} - -/* Used to get edited script data for saving. */ -function work_script_li_data(ul) -{ - return [ul.work_name_input.value, { - url : script_url_input.value, - hash : script_sha256_input.value, - text : script_contents_field.value - }]; -} - -function cancel_work(prefix) -{ - let ul = ul_by_prefix[prefix]; - - if (ul.state === UL_STATE.IDLE) - return; - - if (ul.state === UL_STATE.EDITING_ENTRY) { - add_li(prefix, ul.edited_item); - } - - ul.work_li.classList.add("hide"); - ul.ul.append(ul.work_li); - list_set_scrollbar(ul.ul); - ul.state = UL_STATE.IDLE; -} - -function save_work(prefix) -{ - let ul = ul_by_prefix[prefix]; - - if (ul.state === UL_STATE.IDLE) - return; - - let [item, data] = ul.get_work_li_data(ul); - - /* Here we fire promises and return without waiting. */ - - if (ul.state === UL_STATE.EDITING_ENTRY) - storage.replace(prefix, ul.edited_item, item, data); - if (ul.state === UL_STATE.ADDING_ENTRY) - storage.set(prefix, item, data); - - cancel_work(prefix); -} - -function edit_item(prefix, item) -{ - cancel_work(prefix); - - let ul = ul_by_prefix[prefix]; - let li = by_id(item_li_id(prefix, item)); - - if (li === null) { - add_new_item(prefix, item); - return; - } - - ul.reset_work_li(ul, item, storage.get(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; -} - -const file_downloader = by_id("file_downloader"); - -function recursively_export_item(prefix, name, added_items, items_data) -{ - let key = prefix + name; - - if (added_items.has(key)) - return; - - let data = storage.get(prefix, name); - if (data === undefined) { - console.log(`${TYPE_NAME[prefix]} '${name}' for export not found`); - return; - } - - if (prefix !== TYPE_PREFIX.SCRIPT) { - let components = prefix === TYPE_PREFIX.BAG ? - data : [data.components]; - - for (let [comp_prefix, comp_name] of components) { - recursively_export_item(comp_prefix, comp_name, - added_items, items_data); - } - } - - items_data.push({[key]: data}); - added_items.add(key); -} - -function export_item(prefix, name) -{ - let added_items = new Set(); - let items_data = []; - recursively_export_item(prefix, name, added_items, items_data); - let file = new Blob([JSON.stringify(items_data)], - {type: "application/json"}); - let url = URL.createObjectURL(file); - file_downloader.setAttribute("href", url); - file_downloader.setAttribute("download", prefix + name + ".json"); - file_downloader.click(); - file_downloader.removeAttribute("href"); - URL.revokeObjectURL(url); -} - -function add_new_item(prefix, name) -{ - cancel_work(prefix); - - let ul = ul_by_prefix[prefix]; - 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; - ul.state = UL_STATE.ADDING_ENTRY; -} - -const chbx_components_window = by_id("chbx_components_window"); - -function bag_components() -{ - chbx_components_window.classList.remove("hide"); - radio_components_window.classList.add("hide"); - - for (let li of chbx_components_ul.children) { - let chbx = li.firstElementChild.firstElementChild; - chbx.checked = false; - } -} - -function commit_bag_components() -{ - let selected = []; - - for (let li of chbx_components_ul.children) { - let chbx = li.firstElementChild.firstElementChild; - if (!chbx.checked) - continue; - - selected.push([li.getAttribute("data-prefix"), - li.getAttribute("data-name")]); - } - - add_bag_components(selected); - cancel_components(); -} - -const radio_components_window = by_id("radio_components_window"); -var radio_component_none_input = by_id("radio_component_none_input"); - -function page_components() -{ - radio_components_window.classList.remove("hide"); - chbx_components_window.classList.add("hide"); - - radio_component_none_input.checked = true; - - let components = work_page_li_components(); - if (components === undefined) - return; - - 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.firstElementChild.checked = true; -} - -function commit_page_components() -{ - let components = null; - - for (let li of radio_components_ul.children) { - let radio = li.firstElementChild.firstElementChild; - if (!radio.checked) - continue; - - components = [li.getAttribute("data-prefix"), - li.getAttribute("data-name")]; - - if (radio.id === "radio_component_none_input") - components = undefined; - - break; - } - - if (components !== null) - set_page_components(components); - cancel_components(); -} - -function cancel_components() -{ - chbx_components_window.classList.add("hide"); - radio_components_window.classList.add("hide"); -} - -const UL_STATE = { - EDITING_ENTRY : 0, - ADDING_ENTRY : 1, - IDLE : 2 -}; - -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"), - work_name_input : by_id("page_url_field"), - reset_work_li : reset_work_page_li, - get_work_li_data : work_page_li_data, - select_components : page_components, - commit_components : commit_page_components, - state : UL_STATE.IDLE, - edited_item : undefined, - }, - [TYPE_PREFIX.BAG] : { - ul : by_id("bags_ul"), - work_li : by_id("work_bag_li"), - work_name_input : by_id("bag_name_field"), - reset_work_li : reset_work_bag_li, - get_work_li_data : work_bag_li_data, - select_components : bag_components, - commit_components : commit_bag_components, - state : UL_STATE.IDLE, - edited_item : undefined, - }, - [TYPE_PREFIX.SCRIPT] : { - ul : by_id("scripts_ul"), - work_li : by_id("work_script_li"), - work_name_input : by_id("script_name_field"), - reset_work_li : reset_work_script_li, - get_work_li_data : work_script_li_data, - state : UL_STATE.IDLE, - edited_item : undefined, - } -} - -/* - * Newer browsers could utilise `text' method of File objects. - * Older ones require FileReader. - */ - -function _read_file(file, resolve, reject) -{ - let reader = new FileReader(); - - reader.onload = () => resolve(reader.result); - reader.onerror = () => reject(reader.error); - reader.readAsText(file); -} - -function read_file(file) -{ - return new Promise((resolve, reject) => - _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; - if (files.length < 1) - return; - - import_window.classList.remove("hide"); - import_frame.show_loading(); - - try { - const file = await read_file(files[0]); - var result = parse_json_with_schema(settings_schema, file); - } catch(e) { - import_frame.show_error("Bad file :(", "" + e); - return; - } - - import_frame.show_selection(result); -} - -const file_opener_form = by_id("file_opener_form"); - -function hide_import_window() -{ - import_window.classList.add("hide"); - - /* - * Reset file <input>. Without this, a second attempt to import the same - * file would result in "change" event not happening on <input> element. - */ - file_opener_form.reset(); -} - -async function initialize_import_facility() -{ - let import_but = by_id("import_but"); - let file_opener = by_id("file_opener"); - - import_but.addEventListener("click", () => file_opener.click()); - file_opener.addEventListener("change", import_from_file); - - 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"); -} - -/* - * If url has a target appended, e.g. - * chrome-extension://hnhmbnpohhlmhehionjgongbnfdnabdl/html/options.html#smyhax - * that target will be split into prefix and item name (e.g. "s" and "myhax") - * and editing of that respective item will be started. - * - * We don't need to worry about the state of the page (e.g. some editing being - * in progress) in jump_to_item() - this function is called at the beginning, - * together with callbacks being assigned to buttons, so it is safe to assume - * lists are initialized with items and page is in its virgin state with regard - * to everything else. - */ -function jump_to_item(url_with_item) -{ - const [dummy1, base_url, dummy2, target] = - /^([^#]*)(#(.*))?$/i.exec(url_with_item); - if (target === undefined) - return; - - const prefix = target.substring(0, 1); - - if (!list_prefixes.includes(prefix)) { - history.replaceState(null, "", base_url); - return; - } - - by_id(`show_${TYPE_NAME[prefix]}s`).checked = true; - edit_item(prefix, decodeURIComponent(target.substring(1))); -} - -async function main() -{ - storage = await get_remote_storage(); - - for (let prefix of list_prefixes) { - for (let item of storage.get_all_names(prefix).sort()) { - add_li(prefix, item, true); - add_chbx_li(prefix, item); - add_radio_li(prefix, item); - } - - let name = TYPE_NAME[prefix]; - - let add_but = by_id(`add_${name}_but`); - let discard_but = by_id(`discard_${name}_but`); - let save_but = by_id(`save_${name}_but`); - - add_but.addEventListener("click", () => add_new_item(prefix)); - discard_but.addEventListener("click", () => cancel_work(prefix)); - save_but.addEventListener("click", () => save_work(prefix)); - - if ([TYPE_PREFIX.REPO, TYPE_PREFIX.SCRIPT].includes(prefix)) - continue; - - let ul = ul_by_prefix[prefix]; - - let commit_components_but = by_id(`commit_${name}_components_but`); - let cancel_components_but = by_id(`cancel_${name}_components_but`); - let select_components_but = by_id(`select_${name}_components_but`); - - commit_components_but - .addEventListener("click", ul.commit_components); - select_components_but - .addEventListener("click", ul.select_components); - cancel_components_but.addEventListener("click", cancel_components); - } - - jump_to_item(document.URL); - - storage.add_change_listener(handle_change); - - await initialize_import_facility(); -} - -function handle_change(change) -{ - if (change.old_val === undefined) { - add_li(change.prefix, change.item); - add_chbx_li(change.prefix, change.item); - add_radio_li(change.prefix, change.item); - - return; - } - - if (change.new_val !== undefined) - return; - - let ul = ul_by_prefix[change.prefix]; - if (ul.state === UL_STATE.EDITING_ENTRY && - ul.edited_item === change.item) { - ul.state = UL_STATE.ADDING_ENTRY; - return; - } - - let uls_creators = [[ul.ul, item_li_id]]; - - 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]); - } - - 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); - } -} - -main(); diff --git a/html/popup.html b/html/popup.html index ad6c258..bb30425 100644 --- a/html/popup.html +++ b/html/popup.html @@ -55,10 +55,10 @@ #info_form, #unprivileged_page_info { display: grid; grid-template-columns: auto; - text-align: center; } #info_form * { + text-align: center; white-space: nowrap; text-overflow: ellipsis; overflow-x: hidden; diff --git a/html/settings.html b/html/settings.html index ce19e55..7abb870 100644 --- a/html/settings.html +++ b/html/settings.html @@ -33,7 +33,6 @@ <title>Haketilo options</title> #LOADCSS html/reset.css #LOADCSS html/base.css -#LOADCSS html/table.css #LOADCSS html/grid.css <style> /* Style top menu items. */ diff --git a/html/table.css b/html/table.css deleted file mode 100644 index 48792c1..0000000 --- a/html/table.css +++ /dev/null @@ -1,74 +0,0 @@ -/* - * SPDX-License-Identifier: GPL-3.0-or-later OR CC-BY-SA-4.0 - * - * Table styling used in some Haketilo internal HTML pages - * - * This file is part of Haketilo. - * - * Copyright (C) 2021 Wojtek Kosior <koszko@koszko.org> - * - * File is dual-licensed. You can choose either GPLv3+, CC BY-SA or both. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - * - * I, Wojtek Kosior, thereby promise not to sue for violation of this file's - * licenses. Although I request that you do not make use of this code in a - * proprietary program, I am not going to enforce this in court. - */ - -.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 ec94c6e..25b5a1a 100644 --- a/manifest.json +++ b/manifest.json @@ -1,6 +1,8 @@ +// SPDX-License-Identifier: CC0-1.0 +// // This is the manifest file of Haketilo. // -// Copyright (C) 2021 Wojtek Kosior +// Copyright (C) 2021,2022 Wojtek Kosior <koszko@koszko.org> // // This program is free software: you can redistribute it and/or modify // it under the terms of the CC0 1.0 Universal License as published by @@ -20,12 +22,13 @@ "manifest_version": 2, #ELIF MV3 "manifest_version": 3, +#ERROR Manifest version 3 is not yet supported! Please define 'MV2'. #ELSE #ERROR Manifest version not selected! Please define 'MV2'. #ENDIF "name": "Haketilo", "short_name": "Haketilo", - "version": "0.1", + "version": "0.999", "author": "Wojtek Kosior & contributors", "description": "Control your \"Web\" browsing.", #IF MOZILLA @@ -49,13 +52,8 @@ "16": "icons/haketilo16.png" }, "permissions": [ - "contextMenus", "webRequest", "webRequestBlocking", - "activeTab", - "notifications", - "sessions", - "storage", "tabs", "<all_urls>", "unlimitedStorage" @@ -70,30 +68,30 @@ "16": "icons/haketilo16.png" }, "default_title": "Haketilo", -#LOADHTML html/display_panel.html - "default_popup": "html/display_panel.html" +#LOADHTML html/popup.html + "default_popup": "html/popup.html" }, "options_ui": { -#LOADHTML html/options.html - "page": "html/options.html", - "open_in_tab": true +#LOADHTML html/settings.html + "page": "html/settings.html", + "open_in_tab": true, + "browser_style": false }, #COPY_FILE dummy "web_accessible_resources": ["dummy"], "background": { "persistent": true, "scripts": [ -#LOADJS background/main.js +#LOADJS background/background.js ] }, "content_scripts": [ { "run_at": "document_start", "matches": ["<all_urls>"], - "match_about_blank": true, "all_frames": true, "js": [ -#LOADJS content/main.js +#LOADJS content/content.js ] } ] diff --git a/test/unit/conftest.py b/test/conftest.py index a3064f1..4eea714 100644 --- a/test/unit/conftest.py +++ b/test/conftest.py @@ -27,22 +27,24 @@ Common fixtures for Haketilo unit tests import pytest from pathlib import Path +from tempfile import TemporaryDirectory from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC -from ..profiles import firefox_safe_mode -from ..server import do_an_internet -from ..extension_crafting import make_extension -from ..world_wide_library import start_serving_script, dump_scripts +from .profiles import firefox_safe_mode +from .server import do_an_internet +from .extension_crafting import make_extension +from .world_wide_library import start_serving_script, dump_scripts +from .misc_constants import here -@pytest.fixture(scope="package") +@pytest.fixture(scope="session") def proxy(): httpd = do_an_internet() yield httpd httpd.shutdown() -@pytest.fixture(scope="package") +@pytest.fixture(scope="session") def _driver(proxy): with firefox_safe_mode() as driver: yield driver @@ -91,6 +93,15 @@ def webextension(driver, request): driver.uninstall_addon(addon_id) ext_path.unlink() +@pytest.fixture() +def haketilo(driver): + addon_id = driver.install_addon(str(here.parent / 'mozilla-build.zip'), + temporary=True) + + yield + + driver.uninstall_addon(addon_id) + script_injector_script = '''\ /* * Selenium by default executes scripts in some weird one-time context. We want diff --git a/test/data/pages/scripts_to_block_1.html b/test/data/pages/scripts_to_block_1.html index 6d868dd..1aa49ee 100644 --- a/test/data/pages/scripts_to_block_1.html +++ b/test/data/pages/scripts_to_block_1.html @@ -19,6 +19,7 @@ --> <html> <head> + <meta name="charset" value="latin1"> <script> window.__run = [...(window.__run || []), 'inline']; </script> diff --git a/test/test_integration.py b/test/test_integration.py new file mode 100644 index 0000000..db5ae43 --- /dev/null +++ b/test/test_integration.py @@ -0,0 +1,29 @@ +# SPDX-License-Identifier: CC0-1.0 + +""" +Haketilo integration tests +""" + +# This file is part of Haketilo +# +# Copyright (C) 2022 Wojtek Kosior <koszko@koszko.org> +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the CC0 1.0 Universal License as published by +# the Creative Commons Corporation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# CC0 1.0 Universal License for more details. + +import pytest +from selenium.webdriver.support.ui import WebDriverWait + +@pytest.mark.usefixtures('haketilo') +def test_integration(driver): + #from time import sleep + #sleep(100000) + + # TODO!!! + pass diff --git a/test/unit/test_basic.py b/test/unit/test_basic.py index 5f42f5d..6ec54cc 100644 --- a/test/unit/test_basic.py +++ b/test/unit/test_basic.py @@ -40,9 +40,9 @@ def test_script_loader(execute_in_page): A trivial test case that verifies Haketilo's .js files can be properly loaded into a test page together with their dependencies. """ - execute_in_page(load_script('common/stored_types.js')) + execute_in_page(load_script('common/indexeddb.js')) - assert execute_in_page('returnval(TYPE_PREFIX.VAR);') == '_' + assert 'mapping' in execute_in_page('returnval(stores.map(s => s[0]));') @pytest.mark.ext_data({}) @pytest.mark.usefixtures('webextension') diff --git a/test/unit/test_broadcast.py b/test/unit/test_broadcast.py index 7de6c80..7c2c051 100644 --- a/test/unit/test_broadcast.py +++ b/test/unit/test_broadcast.py @@ -18,6 +18,7 @@ Haketilo unit tests - message broadcasting # CC0 1.0 Universal License for more details. import pytest +from selenium.webdriver.support.ui import WebDriverWait from ..script_loader import load_script from .utils import broker_js @@ -55,8 +56,8 @@ def test_broadcast(driver, execute_in_page, wait_elem_text): window.open(window.location.href, "_blank"); window.open(window.location.href, "_blank"); ''') + WebDriverWait(driver, 10).until(lambda d: len(d.window_handles) == 3) windows = [*driver.window_handles] - assert len(windows) == 3 # Let's first test if a simple message can be successfully broadcasted driver.switch_to.window(windows[0]) diff --git a/test/unit/test_content.py b/test/unit/test_content.py index 35ab027..8220160 100644 --- a/test/unit/test_content.py +++ b/test/unit/test_content.py @@ -33,7 +33,7 @@ dynamic_script = \ '''; this.haketilo_secret = "abracadabra"; this.haketilo_pattern_tree = {}; - this.haketilo_defualt_allow = false; + this.haketilo_default_allow = false; if (this.haketilo_content_script_main) this.haketilo_content_script_main(); @@ -69,7 +69,7 @@ content_script = \ async function mock_payload_ok([type, res_id]) { if (type === "indexeddb_files") - return [1, 2].map(n => `window.haketilo_injected_${n} = ${n}${n};`); + return {files: [1, 2].map(n => `window.hak_injected_${n} = ${n};`)}; } if (/payload_error/.test(document.URL)) { @@ -162,7 +162,7 @@ def test_content_unprivileged_page(driver, execute_in_page, target1, target2): def vars_made_by_payload(driver): vars_values = driver.execute_script( - 'return [1, 2].map(n => window[`haketilo_injected_${n}`]);' + 'return [1, 2].map(n => window[`hak_injected_${n}`]);' ) if vars_values != [None, None]: return vars_values @@ -174,7 +174,7 @@ def test_content_unprivileged_page(driver, execute_in_page, target1, target2): } elif target2 == 'payload_ok': vars_values = WebDriverWait(driver, 10).until(vars_made_by_payload) - assert vars_values == [11, 22] + assert vars_values == [1, 2] @pytest.mark.ext_data({'content_script': content_script}) @pytest.mark.usefixtures('webextension') diff --git a/test/unit/test_indexeddb.py b/test/unit/test_indexeddb.py index b320cff..550b923 100644 --- a/test/unit/test_indexeddb.py +++ b/test/unit/test_indexeddb.py @@ -318,8 +318,8 @@ def test_haketilodb_track(driver, execute_in_page, wait_elem_text): # will be used to make changes to IndexedDB and window 0 to "track" those # changes. driver.execute_script('window.open(window.location.href, "_blank");') + WebDriverWait(driver, 10).until(lambda d: len(d.window_handles) == 2) windows = [*driver.window_handles] - assert len(windows) == 2 # Create elements that will have tracked data inserted under them. driver.switch_to.window(windows[0]) diff --git a/test/unit/test_patterns_query_manager.py b/test/unit/test_patterns_query_manager.py index c6ebb81..5daf3a0 100644 --- a/test/unit/test_patterns_query_manager.py +++ b/test/unit/test_patterns_query_manager.py @@ -150,14 +150,14 @@ def test_pqm_tree_building(driver, execute_in_page): all([m['identifier'] in last_script for m in sample_mappings])) execute_in_page( - ''' + '''{ const new_setting_val = {name: "default_allow", value: false}; settingchange({key: "default_allow", new_val: new_setting_val}); for (const mapping of arguments[0]) mappingchange({key: mapping.identifier, new_val: mapping}); for (const blocking of arguments[1]) blockingchange({key: blocking.pattern, new_val: blocking}); - ''', + }''', sample_mappings[2:], sample_blocking[2:]) WebDriverWait(driver, 10).until(condition_all_added) @@ -201,6 +201,19 @@ def test_pqm_tree_building(driver, execute_in_page): WebDriverWait(driver, 10).until(condition_all_removed) + def condition_default_allowed_again(driver): + content_script = execute_in_page('returnval(last_script);') + cs_values = get_content_script_values(driver, content_script) + return cs_values['haketilo_default_allow'] == True + + execute_in_page( + '''{ + const new_setting_val = {name: "default_allow", value: true}; + settingchange({key: "default_allow", new_val: new_setting_val}); + }''') + + WebDriverWait(driver, 10).until(condition_default_allowed_again) + content_js = ''' let already_run = false; this.haketilo_content_script_main = function() { @@ -229,8 +242,8 @@ def test_pqm_script_injection(driver, execute_in_page): # Let's open a normal page in a second window. Window 0 will be used to make # changed to IndexedDB and window 1 to test the working of content scripts. driver.execute_script('window.open("about:blank", "_blank");') + WebDriverWait(driver, 10).until(lambda d: len(d.window_handles) == 2) windows = [*driver.window_handles] - assert len(windows) == 2 def run_content_script(): driver.switch_to.window(windows[1]) diff --git a/test/unit/test_repo_query_cacher.py b/test/unit/test_repo_query_cacher.py index b1ce4c8..5fbc5cd 100644 --- a/test/unit/test_repo_query_cacher.py +++ b/test/unit/test_repo_query_cacher.py @@ -65,18 +65,19 @@ def run_content_script_in_new_window(driver, url): Open the provided url in a new tab, find its tab id and return it, with current window changed back to the initial one. """ - initial_handle = driver.current_window_handle - handles = driver.window_handles + handle0 = driver.current_window_handle + initial_handles = [*driver.window_handles] driver.execute_script('window.open(arguments[0], "_blank");', url) - WebDriverWait(driver, 10).until(lambda d: d.window_handles is not handles) - new_handle = [h for h in driver.window_handles if h not in handles][0] + window_added = lambda d: set(d.window_handles) != set(initial_handles) + WebDriverWait(driver, 10).until(window_added) + new_handle = [*set(driver.window_handles).difference(initial_handles)][0] driver.switch_to.window(new_handle) get_tab_id = lambda d: d.execute_script('return window.haketilo_tab;') tab_id = WebDriverWait(driver, 10).until(get_tab_id) - driver.switch_to.window(initial_handle) + driver.switch_to.window(handle0) return tab_id @pytest.mark.ext_data({ |