From 4c6a2323d90e9321ec2b78e226167b3013ea69ab Mon Sep 17 00:00:00 2001 From: Wojtek Kosior Date: Sat, 29 Jan 2022 00:03:51 +0100 Subject: make Haketilo buildable again (for Mozilla) How cool it is to throw away 5755 lines of code... --- Makefile.in | 2 +- background/background.js | 67 +++ background/broadcast_broker.js | 7 +- background/main.js | 238 ---------- background/page_actions_server.js | 149 ------ background/patterns_query_manager.js | 28 +- background/policy_injector.js | 87 ---- background/storage.js | 359 -------------- background/storage_server.js | 95 ---- background/webrequest.js | 2 - common/ajax.js | 70 --- common/broadcast.js | 6 +- common/connection_types.js | 53 --- common/indexeddb.js | 2 +- common/lock.js | 94 ---- common/message_server.js | 2 +- common/misc.js | 56 --- common/observables.js | 61 --- common/once.js | 74 --- common/sanitize_JSON.js | 431 ----------------- common/settings_query.js | 66 --- common/storage_client.js | 208 -------- common/storage_light.js | 162 ------- common/storage_raw.js | 82 ---- common/stored_types.js | 80 ---- content/activity_info_server.js | 128 ----- content/content.js | 4 +- content/main.js | 357 -------------- content/page_actions.js | 113 ----- content/policy_enforcing.js | 4 +- content/repo_query.js | 140 ------ content/repo_query_cacher.js | 1 + default_settings.json | 74 ++- html/back_button.css | 71 --- html/display_panel.html | 370 --------------- html/display_panel.js | 586 ----------------------- html/import_frame.html | 79 ---- html/import_frame.js | 185 -------- html/mozilla_scrollbar_fix.css | 67 --- html/options.html | 416 ---------------- html/options_main.js | 781 ------------------------------- html/popup.html | 2 +- html/settings.html | 1 - html/table.css | 74 --- manifest.json | 28 +- test/conftest.py | 174 +++++++ test/data/pages/scripts_to_block_1.html | 1 + test/test_integration.py | 29 ++ test/unit/conftest.py | 163 ------- test/unit/test_basic.py | 4 +- test/unit/test_broadcast.py | 3 +- test/unit/test_content.py | 8 +- test/unit/test_indexeddb.py | 2 +- test/unit/test_patterns_query_manager.py | 19 +- test/unit/test_repo_query_cacher.py | 11 +- 55 files changed, 419 insertions(+), 5957 deletions(-) create mode 100644 background/background.js delete mode 100644 background/main.js delete mode 100644 background/page_actions_server.js delete mode 100644 background/policy_injector.js delete mode 100644 background/storage.js delete mode 100644 background/storage_server.js delete mode 100644 common/ajax.js delete mode 100644 common/connection_types.js delete mode 100644 common/lock.js delete mode 100644 common/observables.js delete mode 100644 common/once.js delete mode 100644 common/sanitize_JSON.js delete mode 100644 common/settings_query.js delete mode 100644 common/storage_client.js delete mode 100644 common/storage_light.js delete mode 100644 common/storage_raw.js delete mode 100644 common/stored_types.js delete mode 100644 content/activity_info_server.js delete mode 100644 content/main.js delete mode 100644 content/page_actions.js delete mode 100644 content/repo_query.js delete mode 100644 html/back_button.css delete mode 100644 html/display_panel.html delete mode 100644 html/display_panel.js delete mode 100644 html/import_frame.html delete mode 100644 html/import_frame.js delete mode 100644 html/mozilla_scrollbar_fix.css delete mode 100644 html/options.html delete mode 100644 html/options_main.js delete mode 100644 html/table.css create mode 100644 test/conftest.py create mode 100644 test/test_integration.py delete mode 100644 test/unit/conftest.py 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/background/background.js b/background/background.js new file mode 100644 index 0000000..90826ef --- /dev/null +++ b/background/background.js @@ -0,0 +1,67 @@ +/** + * This file is part of Haketilo. + * + * Function: Background scripts - main script. + * + * 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 + * 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 . + * + * 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 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 + +#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(); + + patterns_query_manager.start(secret); + webrequest.start(secret); +} + +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 - * 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 . - * - * 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= - */ - 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=. 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: [""], 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": [""], - "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 . - * - * 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 . - * - * - * 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 . - * - * 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 . - * - * 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/ajax.js b/common/ajax.js deleted file mode 100644 index 462e511..0000000 --- a/common/ajax.js +++ /dev/null @@ -1,70 +0,0 @@ -/** - * This file is part of Haketilo. - * - * Function: Wrapping XMLHttpRequest into a Promise. - * - * 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 . - * - * 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. - */ - -function ajax_callback() -{ - if (this.readyState == 4) - this.resolve_callback(this); -} - -function initiate_ajax_request(resolve, reject, method, url) -{ - const xhttp = new XMLHttpRequest(); - xhttp.resolve_callback = resolve; - xhttp.onreadystatechange = ajax_callback; - xhttp.open(method, url, true); - try { - xhttp.send(); - } catch(e) { - console.log(e); - setTimeout(reject, 0); - } -} - -function make_ajax_request(method, url) -{ - return new Promise((resolve, reject) => - initiate_ajax_request(resolve, reject, method, url)); -} - -#EXPORT make_ajax_request 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 . - * - * 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 . - * - * 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 . - * - * 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 . - * - * 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 . - * - * 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 . - * - * 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 . - * - * 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 . - * - * 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 . - * - * 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 . - * - * 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 . - * - * 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 . - * - * 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 CSP tags before - * they reach the document. - * 2. Only tags inside are considered valid by the browser and - * need to be considered. - * 3. We want to detach from document, wait until its completes - * loading, sanitize it and re-attach . - * 4. We shall wait for anything to appear in or after and take that as - * a sign has finished loading. - * 5. Otherwise, getting the `DOMContentLoaded' event on the document shall also - * be a sign that 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 ` 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 +# +# 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/conftest.py b/test/unit/conftest.py deleted file mode 100644 index a3064f1..0000000 --- a/test/unit/conftest.py +++ /dev/null @@ -1,163 +0,0 @@ -# SPDX-License-Identifier: GPL-3.0-or-later - -""" -Common fixtures for Haketilo unit tests -""" - -# This file is part of Haketilo. -# -# 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. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# -# 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 pytest -from pathlib import Path -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 - -@pytest.fixture(scope="package") -def proxy(): - httpd = do_an_internet() - yield httpd - httpd.shutdown() - -@pytest.fixture(scope="package") -def _driver(proxy): - with firefox_safe_mode() as driver: - yield driver - driver.quit() - -def close_all_but_one_window(driver): - while len(driver.window_handles) > 1: - driver.switch_to.window(driver.window_handles[-1]) - driver.close() - driver.switch_to.window(driver.window_handles[0]) - -@pytest.fixture() -def driver(_driver, request): - nav_target = request.node.get_closest_marker('get_page') - close_all_but_one_window(_driver) - _driver.get(nav_target.args[0] if nav_target else 'about:blank') - _driver.implicitly_wait(0) - yield _driver - -@pytest.fixture() -def webextension(driver, request): - ext_data = request.node.get_closest_marker('ext_data') - if ext_data is None: - raise Exception('"webextension" fixture requires "ext_data" marker to be set') - ext_data = ext_data.args[0].copy() - - navigate_to = ext_data.get('navigate_to') - if navigate_to is not None: - del ext_data['navigate_to'] - - driver.get('https://gotmyowndoma.in/') - ext_path = make_extension(Path(driver.firefox_profile.path), **ext_data) - addon_id = driver.install_addon(str(ext_path), temporary=True) - WebDriverWait(driver, 10).until( - EC.url_matches('^moz-extension://.*') - ) - - if navigate_to is not None: - testpage_url = driver.execute_script('return window.location.href;') - driver.get(testpage_url.replace('testpage.html', navigate_to)) - - yield - - close_all_but_one_window(driver) - driver.get('https://gotmyowndoma.in/') - driver.uninstall_addon(addon_id) - ext_path.unlink() - -script_injector_script = '''\ -/* - * Selenium by default executes scripts in some weird one-time context. We want - * separately-loaded scripts to be able to access global variables defined - * before, including those declared with `const` or `let`. To achieve that, we - * run our scripts by injecting them into the page with a