diff options
-rw-r--r-- | html/popup.html | 111 | ||||
-rw-r--r-- | html/popup.js | 140 | ||||
-rw-r--r-- | html/repo_query.html | 15 | ||||
-rw-r--r-- | html/repo_query.js | 15 | ||||
-rw-r--r-- | test/extension_crafting.py | 4 | ||||
-rw-r--r-- | test/unit/conftest.py | 1 | ||||
-rw-r--r-- | test/unit/test_popup.py | 215 | ||||
-rw-r--r-- | test/unit/test_repo_query.py | 10 |
8 files changed, 491 insertions, 20 deletions
diff --git a/html/popup.html b/html/popup.html new file mode 100644 index 0000000..ad6c258 --- /dev/null +++ b/html/popup.html @@ -0,0 +1,111 @@ +<!DOCTYPE html> +<!-- + SPDX-License-Identifier: GPL-3.0-or-later OR CC-BY-SA-4.0 + + Show details of how Haketilo handled given page and allow querying + repositories for custom scripts. + + This file is part of Haketilo. + + Copyright (C) 2022 Wojtek Kosior <koszko@koszko.org> + + File is dual-licensed. You can choose either GPLv3+, CC BY-SA or both. + + 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 <https://www.gnu.org/licenses/>. + + I, Wojtek Kosior, thereby promise not to sue for violation of this file's + licenses. 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. + --> +<html> + <head> + <meta charset="utf-8"/> + <title>Haketilo popup</title> +#LOADCSS html/reset.css +#LOADCSS html/base.css +#LOADCSS html/grid.css + <style> +#IF TEST + html { + background-color: #444; + } +#ENDIF + + html, body { + width: 400px; + overflow-x: hidden; + overflow-y: auto; + } + + #page_info_container { + padding: 0.4em; + } + + #info_form, #unprivileged_page_info { + display: grid; + grid-template-columns: auto; + text-align: center; + } + + #info_form * { + white-space: nowrap; + text-overflow: ellipsis; + overflow-x: hidden; + } + + #info_form label { + padding-bottom: 0.2em; + } + #info_form label+span, .top_but_container { + padding-bottom: 0.5em; + } + </style> + </head> + <body> + <!-- It contains just templates, we can include it at the top --> +#INCLUDE html/repo_query.html + <div id="page_info_container"> + <div id="loading_info"> + Loading page info... + </div> + <div id="info_form" class="hide"> + <label>Page URL:</label> + <span id="page_url"></span> + <label id="privileged_page_info" class="hide">Privileged page</label> + <div id="unprivileged_page_info" class="hide"> + <label>Scripts blocked:</label> + <span id="scripts_blocked"></span> + <label>Injected payload:</label> + <span id="injected_payload"></span> + <label>Mapping used:</label> + <span id="mapping_used"></span> + </div> + </div> + <div class="text_center top_but_container"> + <button id="search_resources_but" class="hide"> + Search for custom resources + </button> + </div> + <div class="text_center"> + <button id="settings_but"> + Open settings + </button> + </div> + </div> + <div id="repo_query_container" class="hide"> + <!-- Repo query view will be dynamically inserted here. --> + </div> +#LOADJS html/popup.js + </body> +</html> diff --git a/html/popup.js b/html/popup.js new file mode 100644 index 0000000..1efc4b0 --- /dev/null +++ b/html/popup.js @@ -0,0 +1,140 @@ +/** + * This file is part of Haketilo. + * + * Function: Show details of how Haketilo handled given page, drive popup. + * + * Copyright (C) 2021,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 <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 of this code in a + * proprietary program, I am not going to enforce this in court. + */ + +#FROM common/browser.js IMPORT browser +#FROM common/misc.js IMPORT is_privileged_url +#FROM html/DOM_helpers.js IMPORT by_id +#FROM html/repo_query.js IMPORT RepoQueryView + +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); + } +} + +async function get_page_info(tab_id) { + return browser.tabs.sendMessage(tab_id, ["page_info"]); +} + +function show_page_info(page_info) { + by_id("loading_info").remove(); + by_id("info_form").classList.remove("hide"); + by_id("page_url").innerText = page_info.url; + + if (page_info.privileged) { + by_id("privileged_page_info").classList.remove("hide"); + } else { + by_id("unprivileged_page_info").classList.remove("hide"); + + by_id("scripts_blocked").innerText = page_info.allow ? "no" : "yes"; + + by_id("injected_payload").innerText = page_info.payload ? + page_info.payload.identifier : "None"; + + const scripts_fate = page_info.allow ? "allowed" : "blocked"; + + if (page_info.mapping === "~allow") + var mapping = `None (scripts ${scripts_fate} by a rule)`; + else if (page_info.mapping) + var mapping = page_info.mapping; + else + var mapping = `None (scripts ${scripts_fate} by default policy)`; + by_id("mapping_used").innerText = mapping; + } +} + +function repo_query_showing(show) { + for (const [id, i] of [["repo_query", 0], ["page_info", 1]]) + by_id(`${id}_container`).classList[["add", "remove"][show ^ i]]("hide"); +} + +function prepare_repo_query_view(tab_id, page_info) { + const repo_query_view = new RepoQueryView(tab_id, + () => repo_query_showing(true), + () => repo_query_showing(false)); + by_id("repo_query_container").prepend(repo_query_view.main_div); + + let search_cb = () => repo_query_view.show(page_info.url); + search_cb = repo_query_view.when_hidden(search_cb); + by_id("search_resources_but").addEventListener("click", search_cb); + by_id("search_resources_but").classList.remove("hide"); +} + +async function main() { + const settings_opener = (e) => browser.runtime.openOptionsPage(); + by_id("settings_but").addEventListener("click", settings_opener); + + try { + var tab = await get_current_tab(); + var tab_id = tab.id; + + if (is_privileged_url(tab.url)) + var page_info = {privileged: true, url: tab.url}; + else + var page_info = await get_page_info(tab_id); + } catch(e) { + console.error(e); + } + + if (page_info) { + show_page_info(page_info); + if (!page_info.privileged) + prepare_repo_query_view(tab_id, page_info); + } else { + by_id("loading_info").innerText = + "Page info not avaialable. Try reloading the page."; + } +} + +main(); diff --git a/html/repo_query.html b/html/repo_query.html index 73b0f00..b9c9269 100644 --- a/html/repo_query.html +++ b/html/repo_query.html @@ -38,14 +38,16 @@ #LOADCSS html/reset.css #LOADCSS html/base.css -#LOADCSS html/grid.css <style> .repo_query_top_text { text-align: center; - margin: 0.4em; + padding: 0.4em; + text-overflow: ellipsis; + overflow: hidden; } .repo_queried_url { text-decoration: underline; + white-space: nowrap; } .repo_query_repo_li { @@ -77,10 +79,12 @@ flex: 1 1 auto; min-width: 0; } + .repo_query_entry_info > * { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; + padding-bottom: 0.1em; } .repo_query_entry button { white-space: nowrap; @@ -97,12 +101,11 @@ } </style> <template> - <div id="repo_query" data-template="main_div" - class="grid_1 repo_query_main_div"> + <div id="repo_query" data-template="main_div" class="repo_query_main_div"> <div data-template="repos_list_container"> <div class="repo_query_top_text"> - Browsing custom resources for - <span data-template="url_span" class="repo_queried_url"></span>. + Browsing custom resources for: + <span data-template="url_span" class="repo_queried_url"></span> </div> <ul data-template="repos_list"></ul> <div class="repo_query_bottom_buttons"> diff --git a/html/repo_query.js b/html/repo_query.js index 8f33356..a4b8890 100644 --- a/html/repo_query.js +++ b/html/repo_query.js @@ -118,16 +118,17 @@ function RepoEntry(query_view, repo_url) { return; } - this.info_span.remove(); - this.results_list.classList.remove("hide"); - this.result_entries = results.map(ref => new ResultEntry(this, ref)); - const to_append = this.result_entries.length > 0 ? - this.result_entries.map(re => re.main_li) : - ["No results :("]; + if (this.result_entries.length > 0) { + this.results_list.classList.remove("hide"); + this.info_span.remove(); - this.results_list.append(...to_append); + const to_append = this.result_entries.map(re => re.main_li); + this.results_list.append(...to_append); + } else { + this.info_span.innerText = "No results :("; + } } let show_results = () => { diff --git a/test/extension_crafting.py b/test/extension_crafting.py index efb2687..ed5792f 100644 --- a/test/extension_crafting.py +++ b/test/extension_crafting.py @@ -63,6 +63,10 @@ def manifest_template(): ], 'content_security_policy': "object-src 'none'; script-src 'self' https://serve.scrip.ts;", 'web_accessible_resources': ['testpage.html'], + 'options_ui': { + 'page': 'testpage.html', + 'open_in_tab': True + }, 'background': { 'persistent': True, 'scripts': ['__open_test_page.js', 'background.js'] diff --git a/test/unit/conftest.py b/test/unit/conftest.py index 9318f6e..a3064f1 100644 --- a/test/unit/conftest.py +++ b/test/unit/conftest.py @@ -59,6 +59,7 @@ 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() diff --git a/test/unit/test_popup.py b/test/unit/test_popup.py new file mode 100644 index 0000000..5319d72 --- /dev/null +++ b/test/unit/test_popup.py @@ -0,0 +1,215 @@ +# SPDX-License-Identifier: CC0-1.0 + +""" +Haketilo unit tests - repository querying +""" + +# This file is part of Haketilo +# +# Copyright (C) 2022 Wojtek Kosior <koszko@koszko.org> +# +# 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 +import json +from selenium.webdriver.support.ui import WebDriverWait + +from ..extension_crafting import ExtraHTML +from ..script_loader import load_script +from .utils import * + +def reload_with_target(driver, target): + current_url = driver.execute_script('return location.href') + driver.execute_script( + ''' + window.location.href = arguments[0]; + window.location.reload(); + ''', + f'{current_url}#{target}') + +unprivileged_page_info = { + 'url': 'https://example_a.com/something', + 'allow': False +} + +mocked_page_infos = { + 'privileged': { + 'url': 'moz-extension://<some-id>/file.html', + 'privileged': True + }, + 'blocked_default': unprivileged_page_info, + 'allowed_default': { + **unprivileged_page_info, + 'allow': True + }, + 'blocked_rule': { + **unprivileged_page_info, + 'mapping': '~allow' + }, + 'allowed_rule': { + **unprivileged_page_info, + 'allow': True, + 'mapping': '~allow' + }, + 'mapping': { + **unprivileged_page_info, + 'mapping': 'm1', + 'payload': {'identifier': 'res1'} + } +} + +tab_mock_js = ''' +; +const mocked_page_info = (%s)[/#mock_page_info-(.*)$/.exec(document.URL)[1]]; +browser.tabs.sendMessage = async function(tab_id, msg) { + const this_tab_id = (await browser.tabs.getCurrent()).id; + if (tab_id !== this_tab_id) + throw `not current tab id (${tab_id} instead of ${this_tab_id})`; + + if (msg[0] === "page_info") { + return mocked_page_info; + } else if (msg[0] === "repo_query") { + const response = await fetch(msg[1]); + if (!response) + return {error: "Something happened :o"}; + + const result = {ok: response.ok, status: response.status}; + try { + result.json = await response.json(); + } catch(e) { + result.error_json = "" + e; + } + return result; + } else { + throw `bad sendMessage message type: '${msg[0]}'`; + } +} + +const old_tabs_query = browser.tabs.query; +browser.tabs.query = async function(query) { + const tabs = await old_tabs_query(query); + tabs.forEach(t => t.url = mocked_page_info.url); + return tabs; +} +''' % json.dumps(mocked_page_infos) + +popup_ext_data = { + 'background_script': broker_js, + 'extra_html': ExtraHTML( + 'html/popup.html', + { + 'common/browser.js': tab_mock_js, + 'common/indexeddb.js': '; set_repo("https://hydril.la/");' + }, + wrap_into_htmldoc=False + ), + 'navigate_to': 'html/popup.html' +} + +@pytest.mark.ext_data(popup_ext_data) +@pytest.mark.usefixtures('webextension') +@pytest.mark.parametrize('page_info_key', ['', *mocked_page_infos.keys()]) +def test_popup_display(driver, execute_in_page, page_info_key): + """ + Test popup viewing while on a page. Test parametrized with different + possible values of page_info object passed in message from the content + script. + """ + reload_with_target(driver, f'mock_page_info-{page_info_key}') + + by_id = driver.execute_script(''' + const nodes = [...document.querySelectorAll("[id]")]; + return nodes.reduce((ob, node) => Object.assign(ob, {[node.id]: node}), {}); + '''); + + if page_info_key == '': + error_msg = 'Page info not avaialable. Try reloading the page.' + error_msg_shown = lambda d: by_id['loading_info'].text == error_msg + WebDriverWait(driver, 10).until(error_msg_shown) + return + + WebDriverWait(driver, 10).until(lambda d: by_id['info_form'].is_displayed()) + assert (page_info_key == 'privileged') == \ + by_id['privileged_page_info'].is_displayed() + assert (page_info_key == 'privileged') ^ \ + by_id['unprivileged_page_info'].is_displayed() + assert by_id['page_url'].text == mocked_page_infos[page_info_key]['url'] + assert not by_id['repo_query_container'].is_displayed() + + if 'blocked' in page_info_key or page_info_key == 'mapping': + assert by_id['scripts_blocked'].text.lower() == 'yes' + elif 'allowed' in page_info_key: + assert by_id['scripts_blocked'].text.lower() == 'no' + + if page_info_key == 'mapping': + assert by_id['injected_payload'].text == 'res1' + elif page_info_key != 'privileged': + assert by_id['injected_payload'].text == 'None' + + mapping_text = by_id['mapping_used'].text + if page_info_key == 'mapping': + assert mapping_text == 'm1' + + if 'allowed' in page_info_key: + 'None (scripts allowed by' in mapping_text + elif 'blocked' in page_info_key: + 'None (scripts blocked by' in mapping_text + + if 'rule' in page_info_key: + 'by a rule)' in mapping_text + elif 'default' in page_info_key: + 'by default_policy)' in mapping_text + +@pytest.mark.ext_data(popup_ext_data) +@pytest.mark.usefixtures('webextension') +def test_popup_repo_query(driver, execute_in_page): + """ + Test opening and closing the repo query view in popup. + """ + reload_with_target(driver, f'mock_page_info-blocked_rule') + + search_but = driver.find_element_by_id("search_resources_but") + WebDriverWait(driver, 10).until(lambda d: search_but.is_displayed()) + search_but.click() + containers = dict([(name, driver.find_element_by_id(f'{name}_container')) + for name in ('page_info', 'repo_query')]) + assert not containers['page_info'].is_displayed() + assert containers['repo_query'].is_displayed() + shown = lambda d: 'https://hydril.la/' in containers['repo_query'].text + WebDriverWait(driver, 10).until(shown) + + # Click the "Show results" button. + selector = '.repo_query_buttons > button:first-child' + driver.find_element_by_css_selector(selector).click() + shown = lambda d: 'MAPPING_A' in containers['repo_query'].text + WebDriverWait(driver, 10).until(shown) + + # Click the "Cancel" button + selector = '.repo_query_bottom_buttons > button' + driver.find_element_by_css_selector(selector).click() + assert containers['page_info'].is_displayed() + assert not containers['repo_query'].is_displayed() + +@pytest.mark.ext_data(popup_ext_data) +@pytest.mark.usefixtures('webextension') +def test_popup_settings_opening(driver, execute_in_page): + """ + Test opening the settings page from popup through button click. + """ + driver.find_element_by_id("settings_but").click() + + first_handle = driver.current_window_handle + WebDriverWait(driver, 10).until(lambda d: len(d.window_handles) == 2) + new_handle = [h for h in driver.window_handles if h != first_handle][0] + + driver.switch_to.window(new_handle) + driver.implicitly_wait(10) + assert "Extension's options page for testing" in \ + driver.find_element_by_tag_name("h1").text diff --git a/test/unit/test_repo_query.py b/test/unit/test_repo_query.py index dd57452..77c5e75 100644 --- a/test/unit/test_repo_query.py +++ b/test/unit/test_repo_query.py @@ -1,7 +1,7 @@ # SPDX-License-Identifier: CC0-1.0 """ -Haketilo unit tests - ............. +Haketilo unit tests - repository querying """ # This file is part of Haketilo @@ -18,14 +18,10 @@ Haketilo unit tests - ............. # CC0 1.0 Universal License for more details. import pytest -import json from selenium.webdriver.support.ui import WebDriverWait from ..extension_crafting import ExtraHTML from ..script_loader import load_script - -from ..extension_crafting import ExtraHTML -from ..script_loader import load_script from .utils import * repo_urls = [f'https://hydril.la/{s}' for s in ('', '1/', '2/', '3/', '4/')] @@ -172,7 +168,7 @@ def test_repo_query_messages(driver, execute_in_page, message): show_and_wait_for_repo_entry() elem = execute_in_page('returnval(view.url_span.parentNode);') - assert has_msg(f'Browsing custom resources for {queried_url}.', elem)(0) + assert has_msg(f'Browsing custom resources for: {queried_url}', elem)(0) elif message == 'no_repos': setup_view(execute_in_page, []) show_and_wait_for_repo_entry() @@ -272,7 +268,7 @@ def test_repo_query_messages(driver, execute_in_page, message): ''') show_and_wait_for_repo_entry() - elem = execute_in_page('returnval(view.repo_entries[0].results_list);') + elem = execute_in_page('returnval(view.repo_entries[0].info_span);') WebDriverWait(driver, 10).until(has_msg('No results :(', elem)) else: raise Exception('made a typo in test function params?') |