aboutsummaryrefslogtreecommitdiff
path: root/html/display_panel.js
diff options
context:
space:
mode:
Diffstat (limited to 'html/display_panel.js')
-rw-r--r--html/display_panel.js586
1 files changed, 586 insertions, 0 deletions
diff --git a/html/display_panel.js b/html/display_panel.js
new file mode 100644
index 0000000..9d880ca
--- /dev/null
+++ b/html/display_panel.js
@@ -0,0 +1,586 @@
+/**
+ * This file is part of Haketilo.
+ *
+ * Function: Popup logic.
+ *
+ * Copyright (C) 2021 Wojtek Kosior
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * As additional permission under GNU GPL version 3 section 7, you
+ * may distribute forms of that code without the copy of the GNU
+ * GPL normally required by section 4, provided you include this
+ * license notice and, in case of non-source distribution, a URL
+ * through which recipients can access the Corresponding Source.
+ * If you modify file(s) with this exception, you may extend this
+ * exception to your version of the file(s), but you are not
+ * obligated to do so. If you do not wish to do so, delete this
+ * exception statement from your version.
+ *
+ * As a special exception to the GPL, any HTML file which merely
+ * makes function calls to this code, and for that purpose
+ * includes it by reference shall be deemed a separate work for
+ * copyright law purposes. If you modify this code, you may extend
+ * this exception to your version of the code, but you are not
+ * obligated to do so. If you do not wish to do so, delete this
+ * exception statement from your version.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * I, Wojtek Kosior, thereby promise not to sue for violation of this file's
+ * license. Although I request that you do not make use this code in a
+ * proprietary program, I am not going to enforce this in court.
+ */
+
+#IMPORT common/connection_types.js AS CONNECTION_TYPE
+
+#FROM common/browser.js IMPORT browser
+/*
+ * Using remote storage here seems inefficient, we only resort to that
+ * temporarily, before all storage access gets reworked.
+ */
+#FROM common/storage_client.js IMPORT get_remote_storage
+#FROM html/import_frame.js IMPORT get_import_frame
+#FROM common/settings_query.js IMPORT query_all
+#FROM common/misc.js IMPORT is_privileged_url, nice_name, \
+ open_in_settings
+#FROM common/stored_types.js IMPORT TYPE_PREFIX
+#FROM common/patterns.js IMPORT each_url_pattern
+#FROM html/DOM_helpers.js IMPORT by_id, clone_template
+
+let storage;
+let tab_url;
+
+#IF MOZILLA
+/* Force popup <html>'s reflow on stupid Firefox. */
+const reflow_forcer =
+ () => document.documentElement.style.width = "-moz-fit-content";
+for (const radio of document.querySelectorAll('[name="current_view"]'))
+ radio.addEventListener("change", reflow_forcer);
+#ENDIF
+
+const show_queried_view_radio = by_id("show_queried_view_radio");
+
+const tab_query = {currentWindow: true, active: true};
+
+async function get_current_tab()
+{
+#IF CHROMIUM
+ const callback = (cb) => browser.tabs.query(tab_query, tab => cb(tab));
+ const promise = new Promise(callback);
+#ELIF MOZILLA
+ const promise = browser.tabs.query(tab_query);
+#ENDIF
+
+ try {
+ return (await promise)[0];
+ } catch(e) {
+ console.log(e);
+ }
+}
+
+const page_url_heading = by_id("page_url_heading");
+const privileged_notice = by_id("privileged_notice");
+const page_state = by_id("page_state");
+
+/* Helper functions to convert string into a list of one-letter <span>'s. */
+function char_to_span(char, doc)
+{
+ const span = document.createElement("span");
+ span.textContent = char;
+ return span;
+}
+
+function to_spans(string, doc=document)
+{
+ return string.split("").map(c => char_to_span(c, doc));
+}
+
+async function show_page_activity_info()
+{
+ const tab = await get_current_tab();
+
+ if (tab === undefined) {
+ page_url_heading.textContent = "unknown page";
+ return;
+ }
+
+ tab_url = /^([^?#]*)/.exec(tab.url)[1];
+ to_spans(tab_url).forEach(s => page_url_heading.append(s));
+ if (is_privileged_url(tab_url)) {
+ privileged_notice.classList.remove("hide");
+ return;
+ }
+
+ populate_possible_patterns_list(tab_url);
+ page_state.classList.remove("hide");
+
+ try_to_connect(tab.id);
+}
+
+const possible_patterns_list = by_id("possible_patterns");
+const known_patterns = new Map();
+
+function add_pattern_to_list(pattern)
+{
+ const template = clone_template("pattern_entry");
+ template.name.textContent = pattern;
+
+ const settings_opener = () => open_in_settings(TYPE_PREFIX.PAGE, pattern);
+ template.button.addEventListener("click", settings_opener);
+
+ known_patterns.set(pattern, template);
+ possible_patterns_list.append(template.entry);
+
+ return template;
+}
+
+function style_possible_pattern_entry(pattern, exists_in_settings)
+{
+ const [text, class_action] = exists_in_settings ?
+ ["Edit", "add"] : ["Add", "remove"];
+ const entry_object = known_patterns.get(pattern);
+
+ if (entry_object) {
+ entry_object.button.textContent = `${text} setting`;
+ entry_object.entry.classList[class_action]("matched_pattern");
+ }
+}
+
+function handle_page_change(change)
+{
+ style_possible_pattern_entry(change.item, change.new_val !== undefined);
+}
+
+function populate_possible_patterns_list(url)
+{
+ for (const pattern of each_url_pattern(url))
+ add_pattern_to_list(pattern);
+
+ for (const [pattern, settings] of query_all(storage, url))
+ style_possible_pattern_entry(pattern, true);
+
+ storage.add_change_listener(handle_page_change, [TYPE_PREFIX.PAGE]);
+}
+
+const connected_chbx = by_id("connected_chbx");
+const query_pattern_but = by_id("query_pattern");
+
+var content_script_port;
+
+function try_to_connect(tab_id)
+{
+ /* This won't connect to iframes. We'll add support for them later */
+ const connect_info = {name: CONNECTION_TYPE.ACTIVITY_INFO, frameId: 0};
+ content_script_port = browser.tabs.connect(tab_id, connect_info);
+
+ const disconnect_cb = () => handle_disconnect(tab_id, start_querying_repos);
+ content_script_port.onDisconnect.addListener(disconnect_cb);
+ content_script_port.onMessage.addListener(handle_activity_report);
+
+ query_pattern_but.addEventListener("click", start_querying_repos);
+
+#IF MOZILLA
+ setTimeout(() => monitor_connecting(tab_id), 1000);
+#ENDIF
+}
+
+function start_querying_repos()
+{
+ query_pattern_but.removeEventListener("click", start_querying_repos);
+ const repo_urls = storage.get_all_names(TYPE_PREFIX.REPO);
+ if (content_script_port)
+ content_script_port.postMessage([TYPE_PREFIX.URL, tab_url, repo_urls]);
+}
+
+const loading_point = by_id("loading_point");
+const reload_notice = by_id("reload_notice");
+
+function handle_disconnect(tab_id, button_cb)
+{
+ query_pattern_but.removeEventListener("click", button_cb);
+ content_script_port = null;
+
+#IF CHROMIUM
+ if (!browser.runtime.lastError)
+ return;
+#ENDIF
+
+ /* return if error was not during connection initialization */
+ if (connected_chbx.checked)
+ return;
+
+ loading_point.classList.toggle("camouflage");
+ reload_notice.classList.remove("hide");
+
+ setTimeout(() => try_to_connect(tab_id), 1000);
+}
+
+function monitor_connecting(tab_id)
+{
+ if (connected_chbx.checked)
+ return;
+
+ if (content_script_port)
+ content_script_port.disconnect();
+ else
+ return;
+
+ loading_point.classList.toggle("camouflage");
+ reload_notice.classList.remove("hide");
+ try_to_connect(tab_id);
+}
+
+const pattern_span = by_id("pattern");
+const view_pattern_but = by_id("view_pattern");
+const blocked_span = by_id("blocked");
+const payload_span = by_id("payload");
+const payload_buttons_div = by_id("payload_buttons");
+const view_payload_but = by_id("view_payload");
+const view_injected_but = by_id("view_injected");
+const container_for_injected = by_id("container_for_injected");
+const content_type_cell = by_id("content_type");
+
+const queried_items = new Map();
+
+let max_injected_script_id = 0;
+
+function handle_activity_report(message)
+{
+ connected_chbx.checked = true;
+
+ const [type, data] = message;
+
+ if (type === "settings") {
+ const settings = data;
+
+ blocked_span.textContent = settings.allow ? "no" : "yes";
+
+ if (settings.pattern) {
+ pattern_span.textContent = settings.pattern;
+ const settings_opener =
+ () => open_in_settings(TYPE_PREFIX.PAGE, settings.pattern);
+ view_pattern_but.classList.remove("hide");
+ view_pattern_but.addEventListener("click", settings_opener);
+ } else {
+ pattern_span.textContent = "none";
+ blocked_span.textContent = blocked_span.textContent + " (default)";
+ }
+
+ if (settings.payload) {
+ payload_span.textContent = nice_name(...settings.payload);
+ payload_buttons_div.classList.remove("hide");
+ const settings_opener = () => open_in_settings(...settings.payload);
+ view_payload_but.addEventListener("click", settings_opener);
+ } else {
+ payload_span.textContent = "none";
+ }
+ }
+ if (type === "script") {
+ const template = clone_template("injected_script");
+ const chbx_id = `injected_script_${max_injected_script_id++}`;
+ template.chbx.id = chbx_id;
+ template.lbl.setAttribute("for", chbx_id);
+ template.script_contents.textContent = data;
+ container_for_injected.appendChild(template.div);
+ }
+ if (type === "is_html") {
+ if (!data)
+ content_type_cell.classList.remove("hide");
+ }
+ if (type === "repo_query_action") {
+ const key = data.prefix + data.item;
+ const results = queried_items.get(key) || {};
+ Object.assign(results, data.results);
+ queried_items.set(key, results);
+
+ const action = data.prefix === TYPE_PREFIX.URL ?
+ show_query_result : record_fetched_install_dep;
+
+ for (const [repo_url, result] of Object.entries(data.results))
+ action(data.prefix, data.item, repo_url, result);
+ }
+}
+
+const container_for_repo_responses = by_id("container_for_repo_responses");
+
+const results_lists = new Map();
+
+function create_results_list(url)
+{
+ const cloned_template = clone_template("multi_repos_query_result");
+ cloned_template.url_span.textContent = url;
+ container_for_repo_responses.appendChild(cloned_template.div);
+
+ cloned_template.by_repo = new Map();
+ results_lists.set(url, cloned_template);
+
+ return cloned_template;
+}
+
+function create_result_item(list_object, repo_url, result)
+{
+ const cloned_template = clone_template("single_repo_query_result");
+ cloned_template.repo_url.textContent = repo_url;
+ cloned_template.appended = null;
+
+ list_object.ul.appendChild(cloned_template.li);
+ list_object.by_repo.set(repo_url, cloned_template);
+
+ return cloned_template;
+}
+
+function set_appended(result_item, element)
+{
+ if (result_item.appended)
+ result_item.appended.remove();
+ result_item.appended = element;
+ result_item.li.appendChild(element);
+}
+
+function show_message(result_item, text)
+{
+ const div = document.createElement("div");
+ div.textContent = text;
+ set_appended(result_item, div);
+}
+
+function showcb(text)
+{
+ return item => show_message(item, text);
+}
+
+function unroll_chbx_first_checked(entry_object)
+{
+ if (!entry_object.chbx.checked)
+ return;
+
+ entry_object.chbx.removeEventListener("change", entry_object.unroll_cb);
+ delete entry_object.unroll_cb;
+
+ entry_object.unroll.innerHTML = "preview not implemented...<br />(consider contributing)";
+}
+
+let import_frame;
+let install_target = null;
+
+function install_abort(error_state)
+{
+ import_frame.show_error(`Error: ${error_state}`);
+ install_target = null;
+}
+
+/*
+ * Translate objects from the format in which they are sent by Hydrilla to the
+ * format in which they are stored in settings.
+ */
+
+function translate_script(script_object, repo_url)
+{
+ return {
+ [TYPE_PREFIX.SCRIPT + script_object.name]: {
+ hash: script_object.sha256,
+ url: `${repo_url}/content/${script_object.location}`
+ }
+ };
+}
+
+function translate_bag(bag_object)
+{
+ return {
+ [TYPE_PREFIX.BAG + bag_object.name]: bag_object.components
+ };
+}
+
+const format_translators = {
+ [TYPE_PREFIX.BAG]: translate_bag,
+ [TYPE_PREFIX.SCRIPT]: translate_script
+};
+
+function install_check_ready()
+{
+ if (install_target.to_fetch.size > 0)
+ return;
+
+ const page_key = [TYPE_PREFIX.PAGE + install_target.pattern];
+ const to_install = [{[page_key]: {components: install_target.payload}}];
+
+ for (const key of install_target.fetched) {
+ const old_object =
+ queried_items.get(key)[install_target.repo_url].response;
+ const new_object =
+ format_translators[key[0]](old_object, install_target.repo_url);
+ to_install.push(new_object);
+ }
+
+ import_frame.show_selection(to_install);
+}
+
+const possible_errors = ["connection_error", "parse_error"];
+
+function fetch_install_deps(components)
+{
+ const needed = [...components];
+ const processed = new Set();
+
+ while (needed.length > 0) {
+ const [prefix, item] = needed.pop();
+ const key = prefix + item;
+ processed.add(key);
+ const results = queried_items.get(key);
+ let relevant_result = null;
+
+ if (results)
+ relevant_result = results[install_target.repo_url];
+
+ if (!relevant_result) {
+ content_script_port.postMessage([prefix, item,
+ [install_target.repo_url]]);
+ install_target.to_fetch.add(key);
+ continue;
+ }
+
+ if (possible_errors.includes(relevant_result.state)) {
+ install_abort(relevant_result.state);
+ return false;
+ }
+
+ install_target.fetched.add(key);
+
+ if (prefix !== TYPE_PREFIX.BAG)
+ continue;
+
+ for (const dependency of relevant_result.response.components) {
+ if (processed.has(dependency.join('')))
+ continue;
+ needed.push(dependency);
+ }
+ }
+}
+
+function record_fetched_install_dep(prefix, item, repo_url, result)
+{
+ const key = prefix + item;
+
+ if (!install_target || repo_url !== install_target.repo_url ||
+ !install_target.to_fetch.has(key))
+ return;
+
+ if (possible_errors.includes(result.state)) {
+ install_abort(result.state);
+ return;
+ }
+
+ if (result.state !== "completed")
+ return;
+
+ install_target.to_fetch.delete(key);
+ install_target.fetched.add(key);
+
+ if (prefix === TYPE_PREFIX.BAG &&
+ fetch_install_deps(result.response.components) === false)
+ return;
+
+ install_check_ready();
+}
+
+function install_clicked(entry_object)
+{
+ import_frame.show_loading();
+
+ install_target = {
+ repo_url: entry_object.repo_url,
+ pattern: entry_object.match_object.pattern,
+ payload: entry_object.match_object.payload,
+ fetched: new Set(),
+ to_fetch: new Set()
+ };
+
+ fetch_install_deps([install_target.payload]);
+
+ install_check_ready();
+}
+
+var max_query_result_id = 0;
+
+function show_query_successful_result(result_item, repo_url, result)
+{
+ if (result.length === 0) {
+ show_message(result_item, "No results :(");
+ return;
+ }
+
+ const cloned_ul_template = clone_template("result_patterns_list");
+ set_appended(result_item, cloned_ul_template.ul);
+
+ for (const match of result) {
+ const entry_object = clone_template("query_match_li");
+
+ entry_object.pattern.textContent = match.pattern;
+
+ cloned_ul_template.ul.appendChild(entry_object.li);
+
+ if (!match.payload) {
+ entry_object.payload.textContent = "(none)";
+ for (const key of ["chbx", "triangle", "unroll"])
+ entry_object[key].remove();
+ continue;
+ }
+
+ entry_object.payload.textContent = nice_name(...match.payload);
+
+ const install_cb = () => install_clicked(entry_object);
+ entry_object.btn.addEventListener("click", install_cb);
+
+ const chbx_id = `query_result_${max_query_result_id++}`;
+ entry_object.chbx.id = chbx_id;
+ entry_object.lbl.setAttribute("for", chbx_id);
+
+ entry_object.unroll_cb = () => unroll_chbx_first_checked(entry_object);
+ entry_object.chbx.addEventListener("change", entry_object.unroll_cb);
+
+ entry_object.component_object = match.payload;
+ entry_object.match_object = match;
+ entry_object.repo_url = repo_url;
+ }
+}
+
+function show_query_result(url_prefix, url, repo_url, result)
+{
+ const results_list_object = results_lists.get(url) ||
+ create_results_list(url);
+ const result_item = results_list_object.by_repo.get(repo_url) ||
+ create_result_item(results_list_object, repo_url, result);
+
+ const completed_cb =
+ item => show_query_successful_result(item, repo_url, result.response);
+ const possible_actions = {
+ completed: completed_cb,
+ started: showcb("loading..."),
+ connection_error: showcb("Error when querying repository."),
+ parse_error: showcb("Bad data format received.")
+ };
+ possible_actions[result.state](result_item, repo_url);
+}
+
+by_id("settings_but")
+ .addEventListener("click", (e) => browser.runtime.openOptionsPage());
+
+async function main()
+{
+ storage = await get_remote_storage();
+ import_frame = await get_import_frame();
+ import_frame.onclose = () => show_queried_view_radio.checked = true;
+ show_page_activity_info();
+}
+
+main();