aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorWojtek Kosior <koszko@koszko.org>2022-01-13 16:47:07 +0100
committerWojtek Kosior <koszko@koszko.org>2022-01-13 16:47:07 +0100
commit299864ee0901df8db2314cc7c07d6c481927c8aa (patch)
treedcd1056c39fa6e90214cb2296e70f1060ad9583a
parent5acb2499c1df14d6275b1ad9e139f02d1280cb9c (diff)
downloadbrowser-extension-299864ee0901df8db2314cc7c07d6c481927c8aa.tar.gz
browser-extension-299864ee0901df8db2314cc7c07d6c481927c8aa.zip
facilitate managing script blocking with a list of edtable entries
-rw-r--r--common/patterns.js30
-rw-r--r--html/payload_create.js17
-rw-r--r--html/text_entry_list.js134
-rw-r--r--test/unit/test_payload_create.py6
-rw-r--r--test/unit/test_text_entry_list.py159
-rw-r--r--test/unit/utils.py3
6 files changed, 281 insertions, 68 deletions
diff --git a/common/patterns.js b/common/patterns.js
index 1398961..6f5aa40 100644
--- a/common/patterns.js
+++ b/common/patterns.js
@@ -186,5 +186,31 @@ function* each_url_pattern(url)
}
#EXPORT each_url_pattern
-#EXPORT "https://hydrillabugs.koszko.org/projects/haketilo/wiki/URL_patterns" \
- AS patterns_doc_url
+const patterns_doc_url =
+ "https://hydrillabugs.koszko.org/projects/haketilo/wiki/URL_patterns";
+#EXPORT patterns_doc_url
+
+function reconstruct_url(deco)
+{
+ const domain = deco.domain.join(".");
+ const path = ["", ...deco.path].join("/");
+ const trail = deco.trailing_slash ? "/" : "";
+ return `${deco.proto}://${domain}${path}${trail}`;
+}
+#EXPORT reconstruct_url
+
+function validate_normalize_url_pattern(url_pattern)
+{
+ try {
+ return reconstruct_url(deconstruct_url(url_pattern));
+ } catch(e) {
+ const patterns_doc_link = document.createElement("a");
+ patterns_doc_link.href = patterns_doc_url;
+ patterns_doc_link.innerText = "here";
+ const msg = document.createElement("span");
+ msg.prepend(`'${url_pattern}' is not a valid URL pattern. See `,
+ patterns_doc_link, " for more details.");
+ throw msg;
+ }
+}
+#EXPORT validate_normalize_url_pattern
diff --git a/html/payload_create.js b/html/payload_create.js
index c1563ae..503a461 100644
--- a/html/payload_create.js
+++ b/html/payload_create.js
@@ -46,7 +46,8 @@
#FROM html/DOM_helpers.js IMPORT clone_template
#FROM common/sha256.js IMPORT sha256
-#FROM common/patterns.js IMPORT deconstruct_url, patterns_doc_url
+#FROM common/patterns.js IMPORT validate_normalize_url_pattern, \
+ patterns_doc_url
/* https://stackoverflow.com/questions/105034/how-to-create-a-guid-uuid */
/* This is a helper function used by uuidv4(). */
@@ -81,18 +82,8 @@ function collect_form_data(form_ctx)
const payloads = {};
- for (const pattern of url_patterns) {
- try {
- deconstruct_url(pattern);
- } catch(e) {
- const patterns_doc_link = document.createElement("a");
- patterns_doc_link.href = patterns_doc_url;
- patterns_doc_link.innerText = "here";
- const msg = document.createElement("span");
- msg.prepend(`'${pattern}' is not a valid URL pattern. See `,
- patterns_doc_link, " for more details.");
- throw msg;
- }
+ for (let pattern of url_patterns) {
+ pattern = validate_normalize_url_pattern(pattern);
if (pattern in payloads)
throw `Pattern '${pattern}' specified multiple times!`;
diff --git a/html/text_entry_list.js b/html/text_entry_list.js
index 4af19fd..ff63c79 100644
--- a/html/text_entry_list.js
+++ b/html/text_entry_list.js
@@ -46,7 +46,7 @@
#IMPORT common/indexeddb.js AS haketilodb
#FROM html/DOM_helpers.js IMPORT clone_template
-#FROM common/patterns.js IMPORT deconstruct_url, patterns_doc_url
+#FROM common/patterns.js IMPORT validate_normalize_url_pattern
const coll = new Intl.Collator();
@@ -232,7 +232,7 @@ function TextEntryList(dialog_ctx, destroy_cb,
async function repo_list(dialog_ctx) {
let list;
- function validate_normalize_repo_url(repo_url) {
+ function validate_normalize(repo_url) {
let error_msg;
/* In the future we might also try making a test connection. */
@@ -251,30 +251,32 @@ async function repo_list(dialog_ctx) {
}
async function remove_repo(repo_url) {
- dialog.loader(dialog_ctx, "Removing repository...");
+ dialog.loader(dialog_ctx, `Removing repository '${repo_url}'...`);
try {
await haketilodb.del_repo(repo_url);
var removing_ok = true;
} finally {
- if (!removing_ok)
- dialog.error(dialog_ctx, "Failed to remove repository :(");
+ if (!removing_ok) {
+ dialog.error(dialog_ctx,
+ `Failed to remove repository '${repo_url}' :(`);
+ }
dialog.close(dialog_ctx);
}
}
async function create_repo(repo_url) {
- repo_url = validate_normalize_repo_url(repo_url);
+ repo_url = validate_normalize(repo_url);
- dialog.loader(dialog_ctx, "Adding repository...");
+ dialog.loader(dialog_ctx, `Adding repository '${repo_url}'...`);
try {
await haketilodb.set_repo(repo_url);
var adding_ok = true;
} finally {
if (!adding_ok)
- dialog.error(dialog_ctx, "Failed to add repository :(");
+ dialog.error(dialog_ctx, `Failed to add repository '${repo_url}' :(`);
dialog.close(dialog_ctx);
}
@@ -284,7 +286,7 @@ async function repo_list(dialog_ctx) {
if (old_repo_url === new_repo_url)
return;
- new_repo_url = validate_normalize_repo_url(new_repo_url);
+ new_repo_url = validate_normalize(new_repo_url);
dialog.loader(dialog_ctx, "Replacing repository...");
@@ -319,3 +321,117 @@ async function repo_list(dialog_ctx) {
return list;
}
#EXPORT repo_list
+
+async function blocking_allowing_lists(dialog_ctx) {
+ function validate_normalize(url_pattern) {
+ try {
+ return validate_normalize_url_pattern(url_pattern);
+ } catch(e) {
+ dialog.error(dialog_ctx, e);
+ throw e;
+ }
+ }
+
+ async function default_allowing_on(url_pattern, allow) {
+ dialog.loader(dialog_ctx,
+ `Setting default scripts blocking policy on '${url_pattern}'...`);
+
+ try {
+ await haketilodb.set_default_allowing(url_pattern);
+ var default_allowing_ok = true;
+ } finally {
+ if (!default_allowing_ok) {
+ dialog.error(dialog_ctx,
+ `Failed to remove rule for '${url_pattern}' :(`);
+ }
+
+ dialog.close(dialog_ctx);
+ }
+ }
+
+ async function set_allowing_on(url_pattern, allow) {
+ url_pattern = validate_normalize(url_pattern);
+
+ const [action, action_cap] = allow ?
+ ["allowing", "Allowing"] : ["blocking", "Blocking"];
+ dialog.loader(dialog_ctx, `${action_cap} scripts on '${url_pattern}'...`);
+
+ try {
+ await haketilodb.set_allowed(url_pattern, allow);
+ var set_allowing_ok = true;
+ } finally {
+ if (!set_allowing_ok)
+ dialog.error(dialog_ctx,
+ `Failed to write ${action} rule for '${url_pattern}' :(`);
+
+ dialog.close(dialog_ctx);
+ }
+ }
+
+ async function replace_allowing_on(old_pattern, new_pattern, allow) {
+ new_pattern = validate_normalize(new_pattern);
+ if (old_pattern === new_pattern)
+ return;
+
+ const action = allow ? "allowing" : "blocking";
+ dialog.loader(dialog_ctx, `Rewriting script ${action} rule...`);
+
+ try {
+ await haketilodb.set_allowed(new_pattern, allow);
+ await haketilodb.set_default_allowing(old_pattern);
+ var replace_allowing_ok = true;
+ } finally {
+ if (!replace_allowing_ok)
+ dialog.error(dialog_ctx, `Failed to rewrite ${action} rule :(`);
+
+ dialog.close(dialog_ctx);
+ }
+ }
+
+ let blocking_list, allowing_list;
+
+ function onchange(change) {
+ if (change.new_val) {
+ if (change.new_val.allow)
+ var [to_add, to_remove] = [allowing_list, blocking_list];
+ else
+ var [to_add, to_remove] = [blocking_list, allowing_list];
+
+ to_add.add(change.key);
+ to_remove.remove(change.key);
+ } else {
+ blocking_list.remove(change.key);
+ allowing_list.remove(change.key);
+ }
+ }
+
+ dialog.loader(dialog_ctx, "Loading script blocking settings...");
+ const [tracking, items] = await haketilodb.track.blocking(onchange);
+ dialog.close(dialog_ctx);
+
+ let untrack_called = 0;
+ function untrack() {
+ if (++untrack_called === 2)
+ haketilodb.untrack(tracking);
+ }
+
+ const lists = [];
+ for (const allow of [false, true]) {
+ lists[allow + 0] =
+ new TextEntryList(dialog_ctx, untrack,
+ pattern => default_allowing_on(pattern),
+ pattern => set_allowing_on(pattern, allow),
+ (p1, p2) => replace_allowing_on(p1, p2, allow));
+ }
+ [blocking_list, allowing_list] = lists;
+
+ for (const item of items) {
+ if (item.allow)
+ allowing_list.add(item.pattern);
+ else
+ blocking_list.add(item.pattern);
+ }
+
+ return lists;
+}
+#EXPORT blocking_allowing_lists
diff --git a/test/unit/test_payload_create.py b/test/unit/test_payload_create.py
index 569d088..f07083d 100644
--- a/test/unit/test_payload_create.py
+++ b/test/unit/test_payload_create.py
@@ -93,7 +93,7 @@ def test_payload_create_normal_usage(driver, execute_in_page):
form_ctx.dialog_container]);
''')
- assert doc_url == \
+ assert patterns_doc_url == \
driver.find_element_by_link_text('URL patterns').get_attribute('href')
assert form_container.is_displayed()
@@ -176,8 +176,6 @@ def test_payload_create_normal_usage(driver, execute_in_page):
assert_db_contents()
-doc_url = 'https://hydrillabugs.koszko.org/projects/haketilo/wiki/URL_patterns'
-
@pytest.mark.ext_data({
'background_script': broker_js,
'extra_html': ExtraHTML('html/payload_create.html', {}),
@@ -215,7 +213,7 @@ def test_payload_create_errors(driver, execute_in_page):
# Verify patterns documentation <a> link.
if expected_msg == {'patterns': ':d'}:
doc_link_elem = driver.find_element_by_link_text('here')
- assert doc_link.get_attribute('href') == doc_url
+ assert doc_link.get_attribute('href') == patterns_doc_url
# Verify the form was NOT cleared upon failed saving.
execute_in_page('form_ctx.dialog_ctx.ok_but.click();')
diff --git a/test/unit/test_text_entry_list.py b/test/unit/test_text_entry_list.py
index 1951d53..e04f36c 100644
--- a/test/unit/test_text_entry_list.py
+++ b/test/unit/test_text_entry_list.py
@@ -28,12 +28,23 @@ from .utils import *
broker_js = lambda: load_script('background/broadcast_broker.js') + ';start();'
+list_code_template = '(await blocking_allowing_lists(%%s))[%d]'
+mode_parameters = [
+ #add_action del_action instantiate_code
+ ('set_repo', 'del_repo', 'await repo_list(%s)'),
+ ('set_disallowed', 'set_default_allowing', list_code_template % 0),
+ ('set_disallowed', 'set_allowed', list_code_template % 0),
+ ('set_allowed', 'set_default_allowing', list_code_template % 1),
+ ('set_allowed', 'set_disallowed', list_code_template % 1)
+]
+
def instantiate_list(to_return):
+ instantiate_code = inspect.stack()[1].frame.f_locals['instantiate_code']
return inspect.stack()[1].frame.f_locals['execute_in_page'](
f'''
let dialog_ctx = dialog.make(() => {{}}, () => {{}}), list;
async function make_list() {{
- list = await repo_list(dialog_ctx);
+ list = {instantiate_code % 'dialog_ctx'};
document.body.append(list.main_div, dialog_ctx.main_div);
return [{', '.join(to_return)}];
}}
@@ -49,10 +60,13 @@ dialog_html_test_ext_data = {
@pytest.mark.ext_data(dialog_html_test_ext_data)
@pytest.mark.usefixtures('webextension')
-def test_text_entry_list_ordering(driver, execute_in_page):
+@pytest.mark.parametrize('mode', mode_parameters)
+def test_text_entry_list_ordering(driver, execute_in_page, mode):
"""
- A test case of ordering of repo URLs in the list.
+ A test case of ordering of repo URLs or URL patterns in the list.
"""
+ add_action, del_action, instantiate_code = mode
+
execute_in_page(load_script('html/text_entry_list.js'))
endings = ['hyd/', 'hydrilla/', 'Hydrilla/', 'HYDRILLA/',
@@ -75,23 +89,26 @@ def test_text_entry_list_ordering(driver, execute_in_page):
def add_urls():
execute_in_page(
'''{
- async function add_urls(urls) {
+ async function add_urls(urls, add_action) {
for (const url of urls)
- await haketilodb.set_repo(url);
+ await haketilodb[add_action](url);
}
- returnval(add_urls(arguments[0]));
+ returnval(add_urls(...arguments));
}''',
- urls)
+ urls, add_action)
def wait_for_completed(wait_id):
"""
- We add an extra repo url to IndexedDB and wait for it to appear in
- the DOM list. Once this happes, we know other operations must have
- also finished.
+ We add an extra url to IndexedDB and wait for it to appear in the
+ DOM list. Once this happes, we know other operations must have also
+ finished.
"""
url = f'https://example.org/{iteration}/{wait_id}'
- execute_in_page('returnval(haketilodb.set_repo(arguments[0]));',
- url)
+ execute_in_page(
+ '''
+ returnval(haketilodb[arguments[1]](arguments[0]));
+ ''',
+ url, add_action)
WebDriverWait(driver, 10).until(lambda _: url in list_div.text)
def assert_order(indexes_present, empty_entry_expected=False):
@@ -122,13 +139,13 @@ def test_text_entry_list_ordering(driver, execute_in_page):
execute_in_page(
'''{
- async function remove_urls(urls) {
+ async function remove_urls(urls, del_action) {
for (const url of urls)
- await haketilodb.del_repo(url);
+ await haketilodb[del_action](url);
}
- returnval(remove_urls(arguments[0]));
+ returnval(remove_urls(...arguments));
}''',
- urls)
+ urls, del_action)
wait_for_completed(1)
assert_order(indexes_added.difference(to_include))
@@ -148,16 +165,20 @@ def existing(id, entry_nr=0):
return inspect.stack()[1].frame.f_locals['execute_in_page'](
'''
returnval(list.entries_by_text.get(list.shown_texts[arguments[0]])\
- [arguments[1]]);
+ [arguments[1]]);
''',
entry_nr, id)
@pytest.mark.ext_data(dialog_html_test_ext_data)
@pytest.mark.usefixtures('webextension')
-def test_text_entry_list_editing(driver, execute_in_page):
+@pytest.mark.parametrize('mode', [mp for mp in mode_parameters
+ if mp[1] != 'set_default_allowing'])
+def test_text_entry_list_editing(driver, execute_in_page, mode):
"""
A test case of editing entries in repo URLs list.
"""
+ add_action, _, instantiate_code = mode
+
execute_in_page(load_script('html/text_entry_list.js'))
execute_in_page(
@@ -173,7 +194,11 @@ def test_text_entry_list_editing(driver, execute_in_page):
list_div, new_entry_but = \
instantiate_list(['list.list_div', 'list.new_but'])
- assert last_loader_msg() == ['Loading repositories...']
+ if 'allow' in add_action:
+ assert last_loader_msg() == ['Loading script blocking settings...']
+ else:
+ assert last_loader_msg() == ['Loading repositories...']
+
assert execute_in_page('returnval(dialog_ctx.shown);') == False
# Test adding new item. Submit via button click.
@@ -184,28 +209,60 @@ def test_text_entry_list_editing(driver, execute_in_page):
assert active('cancel_but').is_displayed()
active('input').send_keys('https://example.com///')
active('add_but').click()
- WebDriverWait(driver, 10).until(lambda _: 'example.com/' in list_div.text)
+ WebDriverWait(driver, 10).until(lambda _: 'example.com' in list_div.text)
assert execute_in_page('returnval(list.list_div.children.length);') == 1
- assert last_loader_msg() == ['Adding repository...']
+ if 'disallow' in add_action:
+ assert last_loader_msg() == \
+ ["Blocking scripts on 'https://example.com/'..."]
+ elif 'allow' in add_action:
+ assert last_loader_msg() == \
+ ["Allowing scripts on 'https://example.com/'..."]
+ else:
+ assert last_loader_msg() == \
+ ["Adding repository 'https://example.com/'..."]
assert not existing('editable_view').is_displayed()
assert existing('text').is_displayed()
assert existing('remove_but').is_displayed()
- # Test editing item. Submit via 'Enter' hit.
+ # Test editing item. Submit via 'Enter' hit. Also test url pattern
+ # normalization.
existing('text').click()
assert not active('noneditable_view').is_displayed()
assert not active('add_but').is_displayed()
assert active('save_but').is_displayed()
assert active('cancel_but').is_displayed()
assert active('input.value') == 'https://example.com/'
- active('input').send_keys(Keys.BACKSPACE * 30 + 'https://example.org//'
+ active('input').send_keys(Keys.BACKSPACE * 30 + 'https://example.org//a//b'
+ Keys.ENTER)
- WebDriverWait(driver, 10).until(lambda _: 'example.org/' in list_div.text)
+ WebDriverWait(driver, 10).until(lambda _: 'example.org' in list_div.text)
assert execute_in_page('returnval(list.list_div.children.length);') == 1
- assert last_loader_msg() == ['Replacing repository...']
+ if 'disallow' in add_action:
+ assert last_loader_msg() == ['Rewriting script blocking rule...']
+ elif 'allow' in add_action:
+ assert last_loader_msg() == ['Rewriting script allowing rule...']
+ else:
+ assert last_loader_msg() == ['Replacing repository...']
+
+ # Test entry removal.
+ existing('remove_but').click()
+ WebDriverWait(driver, 10).until(lambda _: 'xample.org' not in list_div.text)
+ assert execute_in_page('returnval(list.list_div.children.length);') == 0
+ if 'allow' in add_action:
+ assert last_loader_msg() == \
+ ["Setting default scripts blocking policy on 'https://example.org/a/b'..."]
+ else:
+ assert last_loader_msg() == ["Removing repository 'https://example.org//a//b/'..."]
+
+ # The rest of this test remains the same regardless of mode. No point
+ # testing the same thing multiple times.
+ if 'repo' not in add_action:
+ return
# Test that clicking hidden buttons of item not being edited does nothing.
+ new_entry_but.click()
+ active('input').send_keys('https://example.foo' + Keys.ENTER)
+ WebDriverWait(driver, 10).until(lambda _: 'xample.foo/' in list_div.text)
existing('add_but.click()')
existing('save_but.click()')
existing('cancel_but.click()')
@@ -237,7 +294,7 @@ def test_text_entry_list_editing(driver, execute_in_page):
assert existing('editable_view').is_displayed()
assert execute_in_page('returnval(list.list_div.children.length);') == 1
- # Test that starting edit of another entry cancels edit of the other entry.
+ # Test that starting edit of another entry cancels edit of the first entry.
new_entry_but.click()
active('input').send_keys('https://example.net' + Keys.ENTER)
WebDriverWait(driver, 10).until(lambda _: 'example.net/' in list_div.text)
@@ -249,18 +306,16 @@ def test_text_entry_list_editing(driver, execute_in_page):
assert not existing('editable_view', 0).is_displayed()
assert existing('editable_view', 1).is_displayed()
- # Test entry removal.
- existing('remove_but', 0).click()
- WebDriverWait(driver, 10).until(lambda _: 'mple.net/' not in list_div.text)
- assert execute_in_page('returnval(list.list_div.children.length);') == 1
- assert last_loader_msg() == ['Removing repository...']
-
@pytest.mark.ext_data(dialog_html_test_ext_data)
@pytest.mark.usefixtures('webextension')
-def test_text_entry_list_errors(driver, execute_in_page):
+@pytest.mark.parametrize('mode', [mp for mp in mode_parameters
+ if mp[1] != 'set_default_allowing'])
+def test_text_entry_list_errors(driver, execute_in_page, mode):
"""
A test case of error dialogs shown by repo URL list.
"""
+ add_action, _, instantiate_code = mode
+
execute_in_page(load_script('html/text_entry_list.js'))
to_return = ['list.list_div', 'list.new_but', 'dialog_ctx.main_div']
@@ -275,9 +330,18 @@ def test_text_entry_list_errors(driver, execute_in_page):
clickable.click()
active('input').send_keys(Keys.BACKSPACE * 30 + 'ws://example'
+ Keys.ENTER)
- assert 'Repository URLs shoud use https:// schema.' in dialog_div.text
execute_in_page('dialog.close(dialog_ctx);')
+ if 'allow' in add_action:
+ assert "'ws://example' is not a valid URL pattern. See here for more details." \
+ in dialog_div.text
+ assert patterns_doc_url == \
+ driver.find_element_by_link_text('here').get_attribute('href')
+ continue
+ else:
+ assert 'Repository URLs shoud use https:// schema.' \
+ in dialog_div.text
+
active('input').send_keys(Keys.BACKSPACE * 30 + 'https://example'
+ Keys.ENTER)
assert 'Provided URL is not valid.' in dialog_div.text
@@ -286,25 +350,40 @@ def test_text_entry_list_errors(driver, execute_in_page):
# Mock errors to force error messages to appear.
execute_in_page(
'''
- for (const action of ["set_repo", "del_repo"])
+ for (const action of [
+ "set_repo", "del_repo", "set_allowed", "set_default_allowing"
+ ])
haketilodb[action] = () => {throw "reckless, limitless scope";};
''')
# Check database error dialogs.
- def check_reported_failure(what):
- fail = lambda _: f'Failed to {what} repository :(' in dialog_div.text
+ def check_reported_failure(txt):
+ fail = lambda _: txt in dialog_div.text
WebDriverWait(driver, 10).until(fail)
execute_in_page('dialog.close(dialog_ctx);')
existing('text').click()
active('input').send_keys(Keys.BACKSPACE * 30 + 'https://example.org'
+ Keys.ENTER)
- check_reported_failure('replace')
+ if 'disallow' in add_action:
+ check_reported_failure('Failed to rewrite blocking rule :(')
+ elif 'allow' in add_action:
+ check_reported_failure('Failed to rewrite allowing rule :(')
+ else:
+ check_reported_failure('Failed to replace repository :(')
active('cancel_but').click()
existing('remove_but').click()
- check_reported_failure('remove')
+ if 'allow' in add_action:
+ check_reported_failure("Failed to remove rule for 'https://example.com' :(")
+ else:
+ check_reported_failure("Failed to remove repository 'https://example.com/' :(")
new_entry_but.click()
active('input').send_keys('https://example.org' + Keys.ENTER)
- check_reported_failure('add')
+ if 'disallow' in add_action:
+ check_reported_failure("Failed to write blocking rule for 'https://example.org' :(")
+ elif 'allow' in add_action:
+ check_reported_failure("Failed to write allowing rule for 'https://example.org' :(")
+ else:
+ check_reported_failure("Failed to add repository 'https://example.org/' :(")
diff --git a/test/unit/utils.py b/test/unit/utils.py
index a35e587..6abb879 100644
--- a/test/unit/utils.py
+++ b/test/unit/utils.py
@@ -27,6 +27,9 @@ Various functions and objects that can be reused between unit tests
from hashlib import sha256
+patterns_doc_url = \
+ 'https://hydrillabugs.koszko.org/projects/haketilo/wiki/URL_patterns'
+
def make_hash_key(file_contents):
return f'sha256-{sha256(file_contents.encode()).digest().hex()}'