/** * 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 . * * 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 '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 '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...
(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();