/** * Hachette display panel logic * * Copyright (C) 2021 Wojtek Kosior * Redistribution terms are gathered in the `copyright' file. */ /* * IMPORTS_START * IMPORT browser * IMPORT is_chrome * IMPORT is_mozilla *** Using remote storage here seems inefficient, we only resort to that *** temporarily, before all storage access gets reworked. * IMPORT get_remote_storage * IMPORT get_import_frame * IMPORT init_default_policy_dialog * IMPORT query_all * IMPORT CONNECTION_TYPE * IMPORT is_privileged_url * IMPORT TYPE_PREFIX * IMPORT nice_name * IMPORT open_in_settings * IMPORT url_matches * IMPORT each_url_pattern * IMPORT by_id * IMPORT clone_template * IMPORTS_END */ let storage; let tab_url; /* Force popup 's reflow on stupid Firefox. */ if (is_mozilla) { const reflow_forcer = () => document.documentElement.style.width = "-moz-fit-content"; for (const radio of document.querySelectorAll('[name="current_view"]')) radio.addEventListener("change", reflow_forcer); } const show_queried_view_radio = by_id("show_queried_view_radio"); 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 privileged_notice = by_id("privileged_notice"); const page_state = by_id("page_state"); /* Helper functions to convert string into a list of one-letter '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 ensure_pattern_exists(pattern) { let entry_object = 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 (entry_object === undefined) { console.log(`unknown pattern: ${pattern}`); entry_object = add_pattern_to_list(pattern); } return entry_object; } function style_possible_pattern_entry(pattern, exists_in_settings) { const [text, class_action] = exists_in_settings ? ["Edit", "add"] : ["Add", "remove"]; const entry_object = ensure_pattern_exists(pattern); entry_object.button.textContent = `${text} setting`; entry_object.entry.classList[class_action]("matched_pattern"); } function handle_page_change(change) { if (url_matches(tab_url, change.item)) 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 (is_mozilla) setTimeout(() => monitor_connecting(tab_id), 1000); } 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 (is_chrome && !browser.runtime.lastError) return; /* 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 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") { let [pattern, settings] = data; 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"; blocked_span.textContent = blocked_span.textContent + " (default)"; } const components = settings.components; if (components) { payload_span.textContent = nice_name(...components); payload_buttons_div.classList.remove("hide"); const settings_opener = () => open_in_settings(...components); 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 === "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...
(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) { 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() { init_default_policy_dialog(); 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();