aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorWojtek Kosior <koszko@koszko.org>2021-07-06 18:27:38 +0200
committerWojtek Kosior <koszko@koszko.org>2021-07-06 18:27:38 +0200
commitc86bdfcd9d56309faca69b58830cea1fa98870e0 (patch)
treeaba830cbebf3a3a7a3af94c65103f14585651441
parent2059fab6c01a0793e8a82f80d9518ed2bd6d1e29 (diff)
parentb7e2870ff58ef85370781aa04e9e0126988e39fd (diff)
downloadbrowser-extension-c86bdfcd9d56309faca69b58830cea1fa98870e0.tar.gz
browser-extension-c86bdfcd9d56309faca69b58830cea1fa98870e0.zip
Merge popup display
-rw-r--r--background/main.js2
-rw-r--r--background/page_actions_server.js16
-rw-r--r--background/page_info_server.js77
-rw-r--r--background/settings_query.js98
-rw-r--r--common/connection_types.js4
-rw-r--r--common/misc.js27
-rw-r--r--common/patterns.js187
-rw-r--r--content/activity_info_server.js60
-rw-r--r--content/main.js54
-rw-r--r--content/page_actions.js21
-rw-r--r--html/display-panel.html85
-rw-r--r--html/display-panel.js226
-rw-r--r--html/options_main.js6
13 files changed, 735 insertions, 128 deletions
diff --git a/background/main.js b/background/main.js
index 2fcdda3..da408f9 100644
--- a/background/main.js
+++ b/background/main.js
@@ -12,6 +12,7 @@
* IMPORT start_storage_server
* IMPORT start_page_actions_server
* IMPORT start_policy_injector
+ * IMPORT start_page_info_server
* IMPORT browser
* IMPORTS_END
*/
@@ -19,6 +20,7 @@
start_storage_server();
start_page_actions_server();
start_policy_injector();
+start_page_info_server();
async function init_myext(install_details)
{
diff --git a/background/page_actions_server.js b/background/page_actions_server.js
index f9773f6..2d9c333 100644
--- a/background/page_actions_server.js
+++ b/background/page_actions_server.js
@@ -21,9 +21,12 @@ var storage;
var query_best;
var handler;
-function send_scripts(url, port)
+function send_actions(url, port)
{
let [pattern, settings] = query_best(url);
+
+ port.postMessage(["settings", [pattern, settings]]);
+
if (settings === undefined)
return;
@@ -31,11 +34,11 @@ function send_scripts(url, port)
let processed_bags = new Set();
if (components !== undefined)
- send_scripts_rec([components], port, processed_bags);
+ send_scripts([components], port, processed_bags);
}
// TODO: parallelize script fetching
-async function send_scripts_rec(components, port, processed_bags)
+async function send_scripts(components, port, processed_bags)
{
for (let [prefix, name] of components) {
if (prefix === TYPE_PREFIX.BAG) {
@@ -52,14 +55,15 @@ async function send_scripts_rec(components, port, processed_bags)
}
processed_bags.add(name);
- await send_scripts_rec(bag, port, processed_bags);
+ 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]});
+ port.postMessage(["inject", [script_text]]);
}
}
}
@@ -127,7 +131,7 @@ function handle_message(port, message, handler)
port.onMessage.removeListener(handler[0]);
let url = message.url;
console.log({url});
- send_scripts(url, port);
+ send_actions(url, port);
}
function new_connection(port)
diff --git a/background/page_info_server.js b/background/page_info_server.js
new file mode 100644
index 0000000..49919fd
--- /dev/null
+++ b/background/page_info_server.js
@@ -0,0 +1,77 @@
+/**
+ * part of Hachette
+ * Serving of storage data corresponding to requested urls (server side).
+ *
+ * Copyright (C) 2021 Wojtek Kosior
+ * Redistribution terms are gathered in the `copyright' file.
+ */
+
+/*
+ * IMPORTS_START
+ * IMPORT listen_for_connection
+ * IMPORT get_storage
+ * IMPORT get_query_all
+ * IMPORT TYPE_PREFIX
+ * IMPORT CONNECTION_TYPE
+ * IMPORT url_matches
+ * IMPORTS_END
+ */
+
+var storage;
+var query_all;
+
+function handle_change(connection_data, change)
+{
+ if (change.prefix !== TYPE_PREFIX.PAGE)
+ return;
+
+ connection_data.port.postMessage(["change", change]);
+}
+
+async function handle_subscription(connection_data, message)
+{
+ const [action, url] = message;
+ if (action === "unsubscribe") {
+ connection_data.subscribed.delete(url);
+ return;
+ }
+
+ connection_data.subscribed.add(url);
+ connection_data.port.postMessage(["new_url", query_all(url)]);
+}
+
+function remove_storage_listener(cb)
+{
+ storage.remove_change_listener(cb);
+}
+
+function new_connection(port)
+{
+ console.log("new page info connection!");
+
+ const connection_data = {
+ subscribed : new Set(),
+ port
+ };
+
+ let _handle_change = change => handle_change(connection_data, change);
+
+ storage.add_change_listener(_handle_change);
+
+ port.onMessage.addListener(m => handle_subscription(connection_data, m));
+ port.onDisconnect.addListener(() => remove_storage_listener(handle_change));
+}
+
+async function start_page_info_server()
+{
+ storage = await get_storage();
+ query_all = await get_query_all();
+
+ listen_for_connection(CONNECTION_TYPE.PAGE_INFO, new_connection);
+}
+
+/*
+ * EXPORTS_START
+ * EXPORT start_page_info_server
+ * EXPORTS_END
+ */
diff --git a/background/settings_query.js b/background/settings_query.js
index ce01b80..d509b09 100644
--- a/background/settings_query.js
+++ b/background/settings_query.js
@@ -10,13 +10,12 @@
* IMPORT make_once
* IMPORT get_storage
* IMPORT TYPE_PREFIX
+ * IMPORT for_each_possible_pattern
* IMPORTS_END
*/
var storage;
-var exports = {};
-
async function init(fun)
{
storage = await get_storage();
@@ -24,94 +23,25 @@ async function init(fun)
return fun;
}
-// TODO: also support urls with specified ports
-function query(url, multiple)
+function check_pattern(pattern, multiple, matched)
{
- let proto_re = "[a-zA-Z]*:\/\/";
- let domain_re = "[^/?#]+";
- let segments_re = "/[^?#]*";
- let query_re = "\\?[^#]*";
-
- let url_regex = new RegExp(`\
-^\
-(${proto_re})\
-(${domain_re})\
-(${segments_re})?\
-(${query_re})?\
-#?.*\$\
-`);
-
- let regex_match = url_regex.exec(url);
- if (regex_match === null) {
- console.log("bad url format", url);
- return multiple ? [] : [undefined, undefined];
- }
-
- let [_, proto, domain, segments, query] = regex_match;
-
- domain = domain.split(".");
- let segments_trailing_dash =
- segments && segments[segments.length - 1] === "/";
- segments = (segments || "").split("/").filter(s => s !== "");
- segments.unshift("");
-
- let matched = [];
+ const settings = storage.get(TYPE_PREFIX.PAGE, pattern);
- for (let d_slice = 0; d_slice < domain.length; d_slice++) {
- let domain_part = domain.slice(d_slice).join(".");
- let domain_wildcards = [];
- if (d_slice === 0)
- domain_wildcards.push("");
- if (d_slice === 1)
- domain_wildcards.push("*.");
- if (d_slice > 0)
- domain_wildcards.push("**.");
- domain_wildcards.push("***.");
+ if (settings === undefined)
+ return;
- for (let domain_wildcard of domain_wildcards) {
- let domain_pattern = domain_wildcard + domain_part;
+ matched.push([pattern, settings]);
- for (let s_slice = segments.length; s_slice > 0; s_slice--) {
- let segments_part = segments.slice(0, s_slice).join("/");
- let segments_wildcards = [];
- if (s_slice === segments.length) {
- if (segments_trailing_dash)
- segments_wildcards.push("/");
- segments_wildcards.push("");
- }
- if (s_slice === segments.length - 1) {
- if (segments[s_slice] !== "*")
- segments_wildcards.push("/*");
- }
- if (s_slice < segments.length &&
- (segments[s_slice] !== "**" ||
- s_slice < segments.length - 1))
- segments_wildcards.push("/**");
- if (segments[s_slice] !== "***" ||
- s_slice < segments.length)
- segments_wildcards.push("/***");
-
- for (let segments_wildcard of segments_wildcards) {
- let segments_pattern =
- segments_part + segments_wildcard;
-
- let pattern = proto + domain_pattern + segments_pattern;
- console.log("trying", pattern);
- let settings = storage.get(TYPE_PREFIX.PAGE, pattern);
-
- if (settings === undefined)
- continue;
-
- if (!multiple)
- return [pattern, settings];
+ if (!multiple)
+ return false;
+}
- matched.push([pattern, settings]);
- }
- }
- }
- }
+function query(url, multiple)
+{
+ const matched = [];
+ for_each_possible_pattern(url, p => check_pattern(p, multiple, matched));
- return multiple ? matched : [undefined, undefined];
+ return multiple ? matched : (matched[0] || [undefined, undefined]);
}
function query_best(url)
diff --git a/common/connection_types.js b/common/connection_types.js
index e227532..ce63e55 100644
--- a/common/connection_types.js
+++ b/common/connection_types.js
@@ -12,7 +12,9 @@
const CONNECTION_TYPE = {
REMOTE_STORAGE : "0",
- PAGE_ACTIONS : "1"
+ PAGE_ACTIONS : "1",
+ PAGE_INFO : "2",
+ ACTIVITY_INFO : "3"
};
/*
diff --git a/common/misc.js b/common/misc.js
index 0a3a425..8b56e79 100644
--- a/common/misc.js
+++ b/common/misc.js
@@ -10,6 +10,7 @@
* IMPORT sha256
* IMPORT browser
* IMPORT is_chrome
+ * IMPORT TYPE_NAME
* IMPORTS_END
*/
@@ -72,10 +73,36 @@ function csp_rule(nonce)
}
/*
+ * Print item together with type, e.g.
+ * nice_name("s", "hello") → "hello (script)"
+ */
+function nice_name(prefix, name)
+{
+ return `${name} (${TYPE_NAME[prefix]})`;
+}
+
+/* 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");
+}
+
+/* Check if url corresponds to a browser's special page */
+function is_privileged_url(url)
+{
+ return !!/^(chrome(-extension)?|moz-extension):\/\/|^about:/i.exec(url);
+}
+
+/*
* EXPORTS_START
* EXPORT gen_unique
* EXPORT url_item
* EXPORT url_extract_target
* EXPORT csp_rule
+ * EXPORT nice_name
+ * EXPORT open_in_settings
+ * EXPORT is_privileged_url
* EXPORTS_END
*/
diff --git a/common/patterns.js b/common/patterns.js
new file mode 100644
index 0000000..f7da0ff
--- /dev/null
+++ b/common/patterns.js
@@ -0,0 +1,187 @@
+/**
+ * Hydrilla/Lernette operations on page url patterns
+ *
+ * Copyright (C) 2021 Wojtek Kosior
+ * Redistribution terms are gathered in the `copyright' file.
+ */
+
+const proto_re = "[a-zA-Z]*:\/\/";
+const domain_re = "[^/?#]+";
+const segments_re = "/[^?#]*";
+const query_re = "\\?[^#]*";
+
+const url_regex = new RegExp(`\
+^\
+(${proto_re})\
+(${domain_re})\
+(${segments_re})?\
+(${query_re})?\
+#?.*\$\
+`);
+
+function deconstruct_url(url)
+{
+ const regex_match = url_regex.exec(url);
+ if (regex_match === null)
+ return undefined;
+
+ let [_, proto, domain, path, query] = regex_match;
+
+ domain = domain.split(".");
+ let path_trailing_dash =
+ path && path[path.length - 1] === "/";
+ path = (path || "").split("/").filter(s => s !== "");
+ path.unshift("");
+
+ return {proto, domain, path, query, path_trailing_dash};
+}
+
+/* Be sane: both arguments should be arrays of length >= 2 */
+function domain_matches(url_domain, pattern_domain)
+{
+ const length_difference = url_domain.length - pattern_domain.length;
+
+ for (let i = 1; i <= url_domain.length; i++) {
+ const url_part = url_domain[url_domain.length - i];
+ const pattern_part = pattern_domain[pattern_domain.length - i];
+
+ if (pattern_domain.length === i) {
+ if (pattern_part === "*")
+ return length_difference === 0;
+ if (pattern_part === "**")
+ return length_difference > 0;
+ if (pattern_part === "***")
+ return true;
+ return length_difference === 0 && pattern_part === url_part;
+ }
+
+ if (pattern_part !== url_part)
+ return false;
+ }
+
+ return pattern_domain.length === url_domain.length + 1 &&
+ pattern_domain[0] === "***";
+}
+
+function path_matches(url_path, url_trailing_dash,
+ pattern_path, pattern_trailing_dash)
+{
+ const dashes_ok = !(pattern_trailing_dash && !url_trailing_dash);
+
+ if (pattern_path.length === 0)
+ return url_path.length === 0 && dashes_ok;
+
+ const length_difference = url_path.length - pattern_path.length;
+
+ for (let i = 0; i < url_path.length; i++) {
+ if (pattern_path.length === i + 1) {
+ if (pattern_path[i] === "*")
+ return length_difference === 0;
+ if (pattern_path[i] === "**") {
+ return length_difference > 0 ||
+ (url_path[i] === "**" && dashes_ok);
+ }
+ if (pattern_path[i] === "***")
+ return length_difference >= 0;
+ return length_difference === 0 &&
+ pattern_path[i] === url_path[i] && dashes_ok;
+ }
+
+ if (pattern_path[i] !== url_path[i])
+ return false;
+ }
+
+ return false;
+}
+
+function url_matches(url, pattern)
+{
+ const url_deco = deconstruct_url(url);
+ const pattern_deco = deconstruct_url(pattern);
+
+ if (url_deco === undefined || pattern_deco === undefined) {
+ console.log(`bad comparison: ${url} and ${pattern}`);
+ return false
+ }
+
+ if (pattern_deco.proto !== url_deco.proto)
+ return false;
+
+ return domain_matches(url_deco.domain, pattern_deco.domain) &&
+ path_matches(url_deco.path, url_deco.path_trailing_dash,
+ pattern_deco.path, pattern_deco.path_trailing_dash);
+}
+
+/*
+ * Call callback for every possible pattern that matches url. Return when there
+ * are no more patterns or callback returns false.
+ */
+function for_each_possible_pattern(url, callback)
+{
+ const deco = deconstruct_url(url);
+
+ if (deco === undefined) {
+ console.log("bad url format", url);
+ return;
+ }
+
+ for (let d_slice = 0; d_slice < deco.domain.length; d_slice++) {
+ const domain_part = deco.domain.slice(d_slice).join(".");
+ const domain_wildcards = [];
+ if (d_slice === 0)
+ domain_wildcards.push("");
+ if (d_slice === 1)
+ domain_wildcards.push("*.");
+ if (d_slice > 0)
+ domain_wildcards.push("**.");
+ domain_wildcards.push("***.");
+
+ for (const domain_wildcard of domain_wildcards) {
+ const domain_pattern = domain_wildcard + domain_part;
+
+ for (let s_slice = deco.path.length; s_slice > 0; s_slice--) {
+ const path_part = deco.path.slice(0, s_slice).join("/");
+ const path_wildcards = [];
+ if (s_slice === deco.path.length) {
+ if (deco.path_trailing_dash)
+ path_wildcards.push("/");
+ path_wildcards.push("");
+ }
+ if (s_slice === deco.path.length - 1 &&
+ deco.path[s_slice] !== "*")
+ path_wildcards.push("/*");
+ if (s_slice < deco.path.length &&
+ (deco.path[s_slice] !== "**" ||
+ s_slice < deco.path.length - 1))
+ path_wildcards.push("/**");
+ if (deco.path[s_slice] !== "***" || s_slice < deco.path.length)
+ path_wildcards.push("/***");
+
+ for (const path_wildcard of path_wildcards) {
+ const path_pattern = path_part + path_wildcard;
+
+ const pattern = deco.proto + domain_pattern + path_pattern;
+
+ if (callback(pattern) === false)
+ return;
+ }
+ }
+ }
+ }
+}
+
+function possible_patterns(url)
+{
+ const patterns = [];
+ for_each_possible_pattern(url, patterns.push);
+
+ return patterns;
+}
+
+/*
+ * EXPORTS_START
+ * EXPORT url_matches
+ * EXPORT for_each_possible_pattern
+ * EXPORT possible_patterns
+ * EXPORTS_END
+ */
diff --git a/content/activity_info_server.js b/content/activity_info_server.js
new file mode 100644
index 0000000..8435377
--- /dev/null
+++ b/content/activity_info_server.js
@@ -0,0 +1,60 @@
+/**
+ * part of Hachette
+ * Informing about activities performed by content script (script injection,
+ * script blocking).
+ *
+ * Copyright (C) 2021 Wojtek Kosior
+ * Redistribution terms are gathered in the `copyright' file.
+ */
+
+/*
+ * IMPORTS_START
+ * IMPORT listen_for_connection
+ * IMPORT CONNECTION_TYPE
+ * IMPORTS_END
+ */
+
+var activities = [];
+var ports = new Set();
+
+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);
+}
+
+function report_settings(settings)
+{
+ report_activity("settings", settings);
+}
+
+function new_connection(port)
+{
+ console.log("new activity info connection!");
+
+ ports.add(port);
+
+ for (const activity of activities)
+ port.postMessage(activity);
+}
+
+function start_activity_info_server()
+{
+ listen_for_connection(CONNECTION_TYPE.ACTIVITY_INFO, new_connection);
+}
+
+/*
+ * EXPORTS_START
+ * EXPORT start_activity_info_server
+ * EXPORT report_script
+ * EXPORT report_settings
+ * EXPORTS_END
+ */
diff --git a/content/main.js b/content/main.js
index 65ac008..5d88a6d 100644
--- a/content/main.js
+++ b/content/main.js
@@ -12,10 +12,12 @@
* IMPORT url_extract_target
* IMPORT gen_unique
* IMPORT csp_rule
+ * IMPORT is_privileged_url
* IMPORT sanitize_attributes
* IMPORT script_suppressor
* IMPORT is_chrome
* IMPORT is_mozilla
+ * IMPORT start_activity_info_server
* IMPORTS_END
*/
@@ -35,11 +37,14 @@ let unique = gen_unique(url);
const suppressor = script_suppressor(unique);
-function needs_blocking()
+
+function is_http()
{
- if (url.startsWith("https://") || url.startsWith("http://"))
- return false;
+ return !!/^https?:\/\//i.exec(document.URL);
+}
+function is_whitelisted()
+{
const parsed_url = url_extract_target(document.URL);
if (parsed_url.target !== undefined &&
@@ -49,12 +54,10 @@ function needs_blocking()
else
history.replaceState(null, "", parsed_url.base_url);
- console.log(["allowing whitelisted", document.URL]);
- return false;
+ return true;
}
- console.log(["disallowing", document.URL]);
- return true;
+ return false;
}
function handle_mutation(mutations, observer)
@@ -120,20 +123,27 @@ function inject_csp(head)
head.insertBefore(meta, head.firstElementChild);
}
-if (needs_blocking()) {
- block_nodes_recursively(document.documentElement);
-
- if (is_chrome) {
- var observer = new MutationObserver(handle_mutation);
- observer.observe(document.documentElement, {
- attributes: true,
- childList: true,
- subtree: true
- });
+if (!is_privileged_url(document.URL)) {
+ start_activity_info_server();
+ handle_page_actions(unique);
+
+ if (is_http()) {
+ /* rely on CSP injected through webRequest */
+ } else if (is_whitelisted()) {
+ /* do not block scripts at all */
+ } else {
+ block_nodes_recursively(document.documentElement);
+
+ if (is_chrome) {
+ var observer = new MutationObserver(handle_mutation);
+ observer.observe(document.documentElement, {
+ attributes: true,
+ childList: true,
+ subtree: true
+ });
+ }
+
+ if (is_mozilla)
+ addEventListener('beforescriptexecute', suppressor, true);
}
-
- if (is_mozilla)
- addEventListener('beforescriptexecute', suppressor, true);
}
-
-handle_page_actions(unique);
diff --git a/content/page_actions.js b/content/page_actions.js
index bc65449..fd405fe 100644
--- a/content/page_actions.js
+++ b/content/page_actions.js
@@ -9,6 +9,8 @@
* IMPORTS_START
* IMPORT CONNECTION_TYPE
* IMPORT browser
+ * IMPORT report_script
+ * IMPORT report_settings
* IMPORTS_END
*/
@@ -19,15 +21,18 @@ var nonce;
function handle_message(message)
{
- if (message.inject === undefined)
- return;
+ const [action, data] = message;
- for (let script_text of message.inject) {
- if (loaded)
- add_script(script_text);
- else
- scripts_awaiting.push(script_text);
+ if (action === "inject") {
+ for (let script_text of data) {
+ if (loaded)
+ add_script(script_text);
+ else
+ scripts_awaiting.push(script_text);
+ }
}
+ if (action === "settings")
+ report_settings(data);
}
function document_loaded(event)
@@ -46,6 +51,8 @@ function add_script(script_text)
script.textContent = script_text;
script.setAttribute("nonce", nonce);
document.body.appendChild(script);
+
+ report_script(script_text);
}
function handle_page_actions(script_nonce) {
diff --git a/html/display-panel.html b/html/display-panel.html
index 5e5580e..9b6d619 100644
--- a/html/display-panel.html
+++ b/html/display-panel.html
@@ -6,9 +6,90 @@
<html>
<head>
<meta charset="utf-8"/>
- <title>Myext popup</title>
+ <title>Hachette - page settings</title>
+ <style>
+ input[type="radio"], input[type="checkbox"] {
+ display: none;
+ }
+
+ body {
+ width: 300px;
+ height: 300px;
+ }
+
+ .show_next:not(:checked)+* {
+ display: none;
+ }
+
+ .hide {
+ display: none;
+ }
+
+ #possible_patterns_chbx:not(:checked)+label span#triangle:first-child+span,
+ #possible_patterns_chbx:not(:checked)+label+*,
+ #possible_patterns_chbx:checked+label span#triangle:first-child {
+ display: none;
+ }
+
+ #container_for_injected>#none_injected:not(:last-child) {
+ display: none;
+ }
+
+ input#connected_chbx:checked+div+h3 {
+ display: none;
+ }
+ </style>
</head>
<body>
- <button id="settings_but" type="button">Settings</button>_POPUPSCRIPTS_
+ <!-- The invisible div below is for elements that will be cloned. -->
+ <div class="hide">
+ <li id="pattern_li_template">
+ <span></span>
+ <button>View in settings</button>
+ </li>
+ </div>
+
+ <h2 id="page_url_heading"></h2>
+
+ <input id="show_privileged_notice_chbx" type="checkbox" class="show_next"></input>
+ <h3>Privileged page</h3>
+
+ <input id="show_page_state_chbx" type="checkbox" class="show_next"></input>
+ <div>
+ <input id="possible_patterns_chbx" type="checkbox"></input>
+ <label for="possible_patterns_chbx">
+ <h3>
+ <span id="triangle">&#x23F5;</span><span>&#x23F7;</span>
+ Possible patterns
+ </h3>
+ </label>
+ <ul id="possible_patterns"></ul>
+
+ <input id="connected_chbx" type="checkbox" class="show_next"></input>
+ <div>
+ <h3>
+ Matched pattern: <span id="pattern">...</span>
+ <button id="view_pattern" class="hide">
+ View in settings
+ </button>
+ </h3>
+ <h3>
+ Blocked: <span id="blocked">...</span>
+ </h3>
+ <h3>
+ Payload: <span id="payload">...</span>
+ <button id="view_payload" class="hide">
+ View in settings
+ </button>
+ </h3>
+ <h3>Injected</h3>
+ <div id="container_for_injected">
+ <span id="none_injected">None</span>
+ </div>
+ </div>
+ <h3>Trying to connect..<input id="loading_chbx" type="checkbox" class="show_next"></input><span>.</span></h3>
+ </div>
+
+ <button id="settings_but" type="button" style="margin-top: 20px;">Settings</button>_POPUPSCRIPTS_
</body>
</html>
diff --git a/html/display-panel.js b/html/display-panel.js
index 4a4cdcd..9ae35fd 100644
--- a/html/display-panel.js
+++ b/html/display-panel.js
@@ -8,8 +8,232 @@
/*
* IMPORTS_START
* IMPORT browser
+ * IMPORT is_chrome
+ * IMPORT is_mozilla
+ * IMPORT CONNECTION_TYPE
+ * IMPORT url_item
+ * IMPORT is_privileged_url
+ * IMPORT TYPE_PREFIX
+ * IMPORT nice_name
+ * IMPORT open_in_settings
+ * IMPORT for_each_possible_pattern
* IMPORTS_END
*/
-document.getElementById("settings_but")
+function by_id(id)
+{
+ return document.getElementById(id);
+}
+
+const tab_query = {currentWindow: true, active: true};
+
+async function get_current_tab()
+{
+ /* Fix for fact that Chrome does not use promises here */
+ const promise = is_chrome ?
+ new Promise((resolve, reject) =>
+ browser.tabs.query(tab_query, tab => resolve(tab))) :
+ browser.tabs.query(tab_query);
+
+ try {
+ return (await promise)[0];
+ } catch(e) {
+ console.log(e);
+ }
+}
+
+const page_url_heading = by_id("page_url_heading");
+const show_privileged_notice_chbx = by_id("show_privileged_notice_chbx");
+const show_page_state_chbx = by_id("show_page_state_chbx");
+
+async function show_page_activity_info()
+{
+ const tab = await get_current_tab();
+
+ if (tab === undefined) {
+ page_url_heading.textContent = "unknown page";
+ return;
+ }
+
+ const url = url_item(tab.url);
+ page_url_heading.textContent = url;
+ if (is_privileged_url(url)) {
+ show_privileged_notice_chbx.checked = true;
+ return;
+ }
+
+ populate_possible_patterns_list(url);
+ show_page_state_chbx.checked = true;
+
+ try_to_connect(tab.id);
+}
+
+function populate_possible_patterns_list(url)
+{
+ for_each_possible_pattern(url, add_pattern_to_list);
+
+ const port = browser.runtime.connect({name: CONNECTION_TYPE.PAGE_INFO});
+ port.onMessage.addListener(handle_page_info);
+ port.postMessage(["subscribe", url]);
+}
+
+const possible_patterns_ul = by_id("possible_patterns");
+const pattern_li_template = by_id("pattern_li_template");
+pattern_li_template.removeAttribute("id");
+const known_patterns = new Map();
+
+function add_pattern_to_list(pattern)
+{
+ const li = pattern_li_template.cloneNode(true);
+ li.id = `pattern_li_${known_patterns.size}`;
+ known_patterns.set(pattern, li.id);
+
+ const span = li.firstElementChild;
+ span.textContent = pattern;
+
+ const button = span.nextElementSibling;
+ const settings_opener = () => open_in_settings(TYPE_PREFIX.PAGE, pattern);
+ button.addEventListener("click", settings_opener);
+
+ possible_patterns_ul.appendChild(li)
+
+ return li.id;
+}
+
+function ensure_pattern_exists(pattern)
+{
+ let id = known_patterns.get(pattern);
+ /*
+ * As long as pattern computation works well, we should never get into this
+ * conditional block. This is just a safety measure. To be removed as part
+ * of a bigger rework when we start taking iframes into account.
+ */
+ if (id === undefined) {
+ console.log(`unknown pattern: ${pattern}`);
+ id = add_pattern_to_list(pattern);
+ }
+
+ return id;
+}
+
+function set_pattern_li_button_text(li_id, text)
+{
+ by_id(li_id).firstElementChild.nextElementSibling.textContent = text;
+}
+
+function handle_page_info(message)
+{
+ const [type, data] = message;
+
+ if (type === "change") {
+ const li_id = ensure_pattern_exists(data.item);
+ if (data.old_val === undefined)
+ set_pattern_li_button_text(li_id, "Edit in settings");
+ if (data.new_val === undefined)
+ set_pattern_li_button_text(li_id, "Add setting");
+ }
+
+ if (type === "new_url") {
+ for (const li_id of known_patterns.values())
+ set_pattern_li_button_text(li_id, "Add setting");
+ for (const [pattern, settings] of data) {
+ set_pattern_li_button_text(ensure_pattern_exists(pattern),
+ "Edit in settings")
+ }
+ }
+}
+
+const connected_chbx = by_id("connected_chbx");
+
+function try_to_connect(tab_id)
+{
+ /* This won't connect to iframes. We'll add support for them later */
+ const connect_info = {name: CONNECTION_TYPE.ACTIVITY_INFO, frameId: 0};
+ const port = browser.tabs.connect(tab_id, connect_info);
+
+ port.onDisconnect.addListener(port => handle_disconnect(tab_id));
+ port.onMessage.addListener(handle_activity_report);
+
+ if (is_mozilla)
+ setTimeout(() => monitor_connecting(port, tab_id), 1000);
+}
+
+const loading_chbx = by_id("loading_chbx");
+
+function handle_disconnect(tab_id)
+{
+ if (is_chrome && !browser.runtime.lastError)
+ return;
+
+ /* return if there was no connection initialization failure */
+ if (connected_chbx.checked)
+ return;
+
+ loading_chbx.checked = !loading_chbx.checked;
+ setTimeout(() => try_to_connect(tab_id), 1000);
+}
+
+function monitor_connecting(port, tab_id)
+{
+ if (connected_chbx.checked)
+ return;
+
+ port.disconnect();
+ loading_chbx.checked = !loading_chbx.checked;
+ try_to_connect(tab_id);
+}
+
+const pattern_span = by_id("pattern");
+const view_pattern_but = by_id("view_pattern");
+const blocked_span = by_id("blocked");
+const payload_span = by_id("payload");
+const view_payload_but = by_id("view_payload");
+const container_for_injected = by_id("container_for_injected");
+
+function handle_activity_report(message)
+{
+ connected_chbx.checked = true;
+
+ const [type, data] = message;
+
+ if (type === "settings") {
+ let [pattern, settings] = data;
+
+ settings = settings || {};
+ blocked_span.textContent = settings.allow ? "no" : "yes";
+
+ if (pattern) {
+ pattern_span.textContent = pattern;
+ const settings_opener =
+ () => open_in_settings(TYPE_PREFIX.PAGE, pattern);
+ view_pattern_but.classList.remove("hide");
+ view_pattern_but.addEventListener("click", settings_opener);
+ } else {
+ pattern_span.textContent = "none";
+ }
+
+ const components = settings.components;
+ if (components) {
+ payload_span.textContent = nice_name(...components);
+ const settings_opener = () => open_in_settings(...components);
+ view_payload_but.classList.remove("hide");
+ view_payload_but.addEventListener("click", settings_opener);
+ } else {
+ payload_span.textContent = "none";
+ }
+ }
+ if (type === "script") {
+ const h4 = document.createElement("h4");
+ const pre = document.createElement("pre");
+ h4.textContent = "script";
+ pre.textContent = data;
+
+ container_for_injected.appendChild(h4);
+ container_for_injected.appendChild(pre);
+ }
+}
+
+by_id("settings_but")
.addEventListener("click", (e) => browser.runtime.openOptionsPage());
+
+show_page_activity_info();
diff --git a/html/options_main.js b/html/options_main.js
index 9c66a27..f7adf39 100644
--- a/html/options_main.js
+++ b/html/options_main.js
@@ -12,6 +12,7 @@
* IMPORT TYPE_NAME
* IMPORT list_prefixes
* IMPORT url_extract_target
+ * IMPORT nice_name
* IMPORTS_END
*/
@@ -21,11 +22,6 @@ function by_id(id)
return document.getElementById(id);
}
-function nice_name(prefix, name)
-{
- return `${name} (${TYPE_NAME[prefix]})`;
-}
-
const item_li_template = by_id("item_li_template");
const bag_component_li_template = by_id("bag_component_li_template");
const chbx_component_li_template = by_id("chbx_component_li_template");