Project

General

Profile

« Previous | Next » 

Revision fd9f2fc4

Added by koszko over 1 year ago

fix out-of-source builds

View differences:

Makefile.in
17 17

  
18 18
version = <<VERSION>>
19 19
extension_files = background/ common/ content/ html/ licenses/ \
20
        copyright default_settings.json manifest.json
20
	icons/ copyright default_settings.json manifest.json
21 21

  
22
metafiles = build.sh configure Makefile.in process_html_file.sh README.txt \
22

  
23
metafiles = build.sh configure Makefile.in compute_scripts.awk README.txt \
23 24
        re-generate_icons.sh shell_utils.sh upload_amo.sh write_makefile.sh
24 25

  
25 26
# Configuration gets included here by write_makefile.sh
......
47 48
unpacked: $(default_target)-unpacked Makefile
48 49
all-unpacked: mozilla-unpacked chromium-unpacked
49 50
%-unpacked: $(extension_files) icons/haketilo16.png
50
	$(srcdir)/build.sh $* $(srcdir) $(UPDATE_URL)
51
	"$(srcdir)/build.sh" $* $(srcdir) $(UPDATE_URL)
51 52

  
52 53
install install-strip: $(default_target)-unpacked
53 54
	cp -R $(default_target)-unpacked \
54
	"$(DESTDIR)/{6fe13369-88e9-440f-b837-5012fb3bedec}"
55
		"$(DESTDIR)/{6fe13369-88e9-440f-b837-5012fb3bedec}"
55 56

  
56 57
uninstall:
57 58
	rm -r "$(DESTDIR)/{6fe13369-88e9-440f-b837-5012fb3bedec}"
......
59 60
%-build.zip: %-unpacked Makefile
60 61
	cd $< && zip -q -r ../$@ *
61 62

  
62
test/:
63
	mkdir $@
64

  
65
test/certs/: | test/
63
certs/:
66 64
	mkdir $@
67 65

  
68
test/certs/%.key: | test/certs/
66
certs/%.key: | certs/
69 67
	openssl genrsa -out $@ 2048
70 68

  
71
test/certs/rootCA.pem: test/certs/rootCA.key
69
certs/rootCA.pem: certs/rootCA.key
72 70
	openssl req -x509 -new -nodes -key $< -days 1024 -out $@ \
73 71
		 -subj "/CN=Haketilo Test"
74 72

  
75
test: test/certs/rootCA.pem test/certs/site.key $(default_target)-build.zip
76
	MOZ_HEADLESS=whatever $(PYTHON) -m pytest
73
pytest.ini: pytest.ini.in
74
	sed "s|<<SRCDIR>>|$(srcdir)|" <$< > $@
77 75

  
78
test-environment: test/certs/rootCA.pem test/certs/site.key
79
	python3 -m test
76
test: certs/rootCA.pem certs/site.key $(default_target)-build.zip \
77
		pytest.ini
78
	PYTHONPYCACHEPREFIX=$$(pwd)/test__pycache__ MOZ_HEADLESS=whatever \
79
		"$(PYTHON)" -m pytest
80 80

  
81
test-environment-with-haketilo: test/certs/rootCA.pem test/certs/site.key \
81
test-environment: certs/rootCA.pem certs/site.key
82
	"$(PYTHON)" -m test
83

  
84
test-environment-with-haketilo: certs/rootCA.pem certs/site.key \
82 85
		$(default_target)-build.zip
83
	python3 -m test --load-haketilo
86
	"$(PYTHON)" -m test --load-haketilo
84 87

  
85 88
# helper targets
86 89
clean mostlyclean:
87 90
	rm -rf mozilla-unpacked chromium-unpacked haketilo-[1-9]*
88 91
	rm -f mozilla-build.zip chromium-build.zip exports_init.js
89
	rm -rf test/certs
90
	rm -rf $$(find . -name geckodriver.log)
91
	rm -rf $$(find . -type d -name __pycache__)
92
	rm -rf $$(find . -type d -name injected_scripts)
92
	rm -rf pytest.ini certs injected_scripts geckodriver.log
93
	rm -rf certs/ test__pycache__/ .pytest_cache/
93 94

  
94 95
distclean: clean
95 96
	rm -f Makefile config.status record.conf
......
99 100
	@echo 'deletes files that may need special tools to rebuild.'
100 101
	rm -f "$(srcdir)"/icons/*.png
101 102

  
102
dist: $(extension_files) $(metafiles) icons/haketilo16.png
103
	test -d haketilo-$(version) || mkdir haketilo-$(version)
104
	for file in $(extension_files) $(metafiles) icons/; do \
105
	    cp -R "$(srcdir)"/$$file haketilo-$(version); \
106
	done
107
	tar cf haketilo-$(version).tar haketilo-$(version)
108
	gzip haketilo-$(version).tar
109

  
110 103
# Files for constructing the makefile
111 104
Makefile: config.status Makefile.in record.conf
112 105
	./config.status
......
115 108
	cp "$(srcdir)"/write_makefile.sh config.status
116 109

  
117 110
# Unused GNU-specified targets
111
dist:
118 112
install-html:
119 113
install-dvi:
120 114
install-pdf:
build.sh
15 15

  
16 16
set -e
17 17

  
18
. ./shell_utils.sh
19

  
20 18
print_usage() {
21 19
    printf 'usage:  %s mozilla|chromium [source directory] [update url]\n' \
22 20
	   "$0" >&2
pytest.ini
1
#!/usr/bin/env pytest
2

  
3
# This file is part of Haketilo
4
#
5
# Copyright (C) 2021, Wojtek Kosior
6
#
7
# This program is free software: you can redistribute it and/or modify
8
# it under the terms of the CC0 1.0 Universal License as published by
9
# the Creative Commons Corporation.
10
#
11
# This program is distributed in the hope that it will be useful,
12
# but WITHOUT ANY WARRANTY; without even the implied warranty of
13
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14
# CC0 1.0 Universal License for more details.
15

  
16
[pytest]
17
markers =
18
    ext_data: define a custom testing extension for `webextension` fixture.
19
    get_page: define a url the `driver` fixture should navigate the browser to.
20
    second_driver: tell `driver` fixture to spawn a separate browser instance fr this test.
pytest.ini.in
1
#!/usr/bin/env pytest
2

  
3
# This file is part of Haketilo
4
#
5
# Copyright (C) 2021, 2022 Wojtek Kosior <koszko@koszko.org>
6
#
7
# This program is free software: you can redistribute it and/or modify
8
# it under the terms of the CC0 1.0 Universal License as published by
9
# the Creative Commons Corporation.
10
#
11
# This program is distributed in the hope that it will be useful,
12
# but WITHOUT ANY WARRANTY; without even the implied warranty of
13
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14
# CC0 1.0 Universal License for more details.
15

  
16
[pytest]
17
markers =
18
    ext_data: define a custom testing extension for `webextension` fixture.
19
    get_page: define a url the `driver` fixture should navigate the browser to.
20
    second_driver: tell `driver` fixture to spawn a separate browser instance for this test.
21
testpaths =
22
    <<SRCDIR>>/test/haketilo_test
test/__init__.py
1
# SPDX-License-Identifier: CC0-1.0
2
# Copyright (C) 2021 Wojtek Kosior
test/__main__.py
1
# SPDX-License-Identifier: AGPL-3.0-or-later
2

  
3
"""
4
Run a Firefox-type browser with WebDriver attached and Python console open
5
"""
6

  
7
# This file is part of Haketilo.
8
#
9
# Copyright (C) 2021 jahoti <jahoti@tilde.team>
10
# Copyright (C) 2021 Wojtek Kosior <koszko@koszko.org>
11
#
12
# This program is free software: you can redistribute it and/or modify
13
# it under the terms of the GNU Affero General Public License as
14
# published by the Free Software Foundation, either version 3 of the
15
# License, or (at your option) any later version.
16
#
17
# This program is distributed in the hope that it will be useful,
18
# but WITHOUT ANY WARRANTY; without even the implied warranty of
19
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
20
# GNU Affero General Public License for more details.
21
#
22
# You should have received a copy of the GNU Affero General Public License
23
# along with this program.  If not, see <https://www.gnu.org/licenses/>.
24
#
25
#
26
# I, Wojtek Kosior, thereby promise not to sue for violation of this
27
# file's license. Although I request that you do not make use of this code
28
# in a proprietary program, I am not going to enforce this in court.
29

  
30
import sys
31
import time
32
import code
33
from rlcompleter import Completer
34
import readline
35

  
36
from .server import do_an_internet
37
from .misc_constants import *
38
from .profiles import firefox_safe_mode
39
from .extension_crafting import get_extension_base_url
40

  
41
def fail(msg, error_code):
42
    print('Error:', msg)
43
    print('Usage:', sys.argv[0], '[--load-haketilo]', '[certificates_directory] [proxy_port]')
44
    sys.exit(error_code)
45

  
46
load_haketilo = False
47
argv_idx = 1
48
if len(sys.argv) > argv_idx and sys.argv[argv_idx] == '--load-haketilo':
49
    load_haketilo = True
50
    argv_idx += 1
51

  
52
certdir = Path(sys.argv[argv_idx]).resolve() if len(sys.argv) > argv_idx \
53
    else default_cert_dir
54

  
55
if not certdir.is_dir():
56
    fail('selected certificate directory does not exist.', 2)
57

  
58
argv_idx += 1
59

  
60
port = sys.argv[argv_idx] if len(sys.argv) > argv_idx \
61
    else str(default_proxy_port)
62

  
63
if not port.isnumeric():
64
    fail('port must be an integer.', 3)
65

  
66
httpd = do_an_internet(certdir, int(port))
67
driver = firefox_safe_mode(proxy_port=int(port))
68

  
69
if load_haketilo:
70
    driver.install_addon(str(here.parent / 'mozilla-build.zip'), temporary=True)
71
    driver.get(get_extension_base_url(driver) + 'html/settings.html')
72

  
73
print("You can now control the browser through 'driver' object")
74

  
75
# Here we enable readline-enhanced editing:
76
# https://stackoverflow.com/questions/35115208/is-there-any-way-to-combine-readline-rlcompleter-and-interactiveconsole-in-pytho#answer-35116399
77
readline.parse_and_bind('tab: complete');
78
console_locals = globals()
79
readline.set_completer(Completer(console_locals).complete)
80
code.InteractiveConsole(locals=globals()).interact()
81

  
82
driver.quit()
83
httpd.shutdown()
test/conftest.py
1
# SPDX-License-Identifier: GPL-3.0-or-later
2

  
3
"""
4
Common fixtures for Haketilo unit tests
5
"""
6

  
7
# This file is part of Haketilo.
8
#
9
# Copyright (C) 2021 Wojtek Kosior <koszko@koszko.org>
10
#
11
# This program is free software: you can redistribute it and/or modify
12
# it under the terms of the GNU General Public License as published by
13
# the Free Software Foundation, either version 3 of the License, or
14
# (at your option) any later version.
15
#
16
# This program is distributed in the hope that it will be useful,
17
# but WITHOUT ANY WARRANTY; without even the implied warranty of
18
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
19
# GNU General Public License for more details.
20
#
21
# You should have received a copy of the GNU General Public License
22
# along with this program.  If not, see <https://www.gnu.org/licenses/>.
23
#
24
# I, Wojtek Kosior, thereby promise not to sue for violation of this file's
25
# license. Although I request that you do not make use of this code in a
26
# proprietary program, I am not going to enforce this in court.
27

  
28
import pytest
29
from pathlib import Path
30
from tempfile import TemporaryDirectory
31
from selenium.webdriver.common.by import By
32
from selenium.webdriver.support.ui import WebDriverWait
33
from selenium.webdriver.support import expected_conditions as EC
34

  
35
from .profiles           import firefox_safe_mode
36
from .server             import do_an_internet
37
from .extension_crafting import make_extension
38
from .world_wide_library import start_serving_script, dump_scripts
39
from .misc_constants     import here
40

  
41
@pytest.fixture(scope="session")
42
def proxy():
43
    httpd = do_an_internet()
44
    yield httpd
45
    httpd.shutdown()
46

  
47
@pytest.fixture(scope="session")
48
def _driver(proxy):
49
    with firefox_safe_mode() as driver:
50
        yield driver
51
        driver.quit()
52

  
53
def close_all_but_one_window(driver):
54
    while len(driver.window_handles) > 1:
55
        driver.switch_to.window(driver.window_handles[-1])
56
        driver.close()
57
    driver.switch_to.window(driver.window_handles[0])
58

  
59
@pytest.fixture()
60
def driver(_driver, request):
61
    nav_target = request.node.get_closest_marker('get_page')
62
    nav_target = nav_target.args[0] if nav_target else 'about:blank'
63

  
64
    second_driver = request.node.get_closest_marker('second_driver')
65

  
66
    if second_driver:
67
        with firefox_safe_mode() as _driver:
68
            _driver.get(nav_target)
69
            yield _driver
70
            _driver.quit()
71
    else:
72
        close_all_but_one_window(_driver)
73
        _driver.get(nav_target)
74
        _driver.implicitly_wait(0)
75
        yield _driver
76

  
77
@pytest.fixture()
78
def webextension(driver, request):
79
    ext_data = request.node.get_closest_marker('ext_data')
80
    if ext_data is None:
81
        raise Exception('"webextension" fixture requires "ext_data" marker to be set')
82
    ext_data = ext_data.args[0].copy()
83

  
84
    navigate_to = ext_data.get('navigate_to')
85
    if navigate_to is not None:
86
        del ext_data['navigate_to']
87

  
88
    driver.get('https://gotmyowndoma.in/')
89
    ext_path = make_extension(Path(driver.firefox_profile.path), **ext_data)
90
    addon_id = driver.install_addon(str(ext_path), temporary=True)
91
    get_url = lambda d: d.execute_script('return window.ext_page_url;')
92
    ext_page_url = WebDriverWait(driver, 10).until(get_url)
93
    driver.get(ext_page_url)
94

  
95
    if navigate_to is not None:
96
        driver.get(driver.current_url.replace('testpage.html', navigate_to))
97

  
98
    yield
99

  
100
    # Unloading an extension might cause its windows to vanish. Make sure
101
    # there's at least one window navigated to some other page before
102
    # uninstalling the addon. Otherwise, we could be left with a windowless
103
    # browser :c
104
    driver.switch_to.window(driver.window_handles[-1])
105
    driver.get('https://gotmyowndoma.in/')
106
    driver.uninstall_addon(addon_id)
107
    ext_path.unlink()
108

  
109
@pytest.fixture()
110
def haketilo(driver):
111
    addon_id = driver.install_addon(str(here.parent / 'mozilla-build.zip'),
112
                                    temporary=True)
113

  
114
    yield
115

  
116
    driver.uninstall_addon(addon_id)
117

  
118
script_injector_script = '''\
119
/*
120
 * Selenium by default executes scripts in some weird one-time context. We want
121
 * separately-loaded scripts to be able to access global variables defined
122
 * before, including those declared with `const` or `let`. To achieve that, we
123
 * run our scripts by injecting them into the page with a <script> tag that runs
124
 * javascript served by our proxy. We use custom properties of the `window`
125
 * object to communicate with injected code.
126
 */
127
const inject = async () => {
128
    delete window.haketilo_selenium_return_value;
129
    delete window.haketilo_selenium_exception;
130
    window.returnval = val => window.haketilo_selenium_return_value = val;
131

  
132
    const injectee = document.createElement('script');
133
    injectee.src = arguments[0];
134
    injectee.type = "application/javascript";
135
    injectee.async = true;
136
    const prom = new Promise(cb => injectee.onload = cb);
137

  
138
    window.arguments = arguments[1];
139
    document.body.append(injectee);
140

  
141
    await prom;
142

  
143
    /*
144
     * To ease debugging, we want this script to signal all exceptions from the
145
     * injectee.
146
     */
147
    if (window.haketilo_selenium_exception !== false)
148
        throw ['haketilo_selenium_error',
149
               'Error in injected script! Check your geckodriver.log and ./injected_scripts/!'];
150

  
151
    return window.haketilo_selenium_return_value;
152
}
153
return inject();
154
'''
155

  
156
def _execute_in_page_context(driver, script, args):
157
    script = script + '\n;\nwindow.haketilo_selenium_exception = false;'
158
    script_url = start_serving_script(script)
159

  
160
    try:
161
        result = driver.execute_script(script_injector_script, script_url, args)
162
        if type(result) is list and len(result) == 2 and \
163
           result[0] == 'haketilo_selenium_error':
164
            raise Exception(result[1])
165
        return result
166
    except Exception as e:
167
        dump_scripts()
168
        raise e from None
169

  
170
# Some fixtures here just define functions that operate on driver. We should
171
# consider making them into webdriver wrapper class methods.
172

  
173
@pytest.fixture()
174
def execute_in_page(driver):
175
    def do_execute(script, *args):
176
        return _execute_in_page_context(driver, script, args)
177

  
178
    yield do_execute
179

  
180
@pytest.fixture()
181
def wait_elem_text(driver):
182
    def do_wait(id, text):
183
        WebDriverWait(driver, 10).until(
184
            EC.text_to_be_present_in_element((By.ID, id), text)
185
        )
186

  
187
    yield do_wait
test/data/pages/gotmyowndomain.html
1
<!DOCTYPE html>
2
<!--
3
    SPDX-License-Identifier: AGPL-3.0-or-later
4

  
5
    Sample testing page
6

  
7
    This file is part of Haketilo.
8

  
9
    Copyright (C) 2021 jahoti <jahoti@tilde.team>
10

  
11
    This program is free software: you can redistribute it and/or modify
12
    it under the terms of the GNU Affero General Public License as
13
    published by the Free Software Foundation, either version 3 of the
14
    License, or (at your option) any later version.
15

  
16
    This program is distributed in the hope that it will be useful,
17
    but WITHOUT ANY WARRANTY; without even the implied warranty of
18
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
19
    GNU Affero General Public License for more details.
20

  
21
    You should have received a copy of the GNU Affero General Public License
22
    along with this program.  If not, see <https://www.gnu.org/licenses/>.
23
  -->
24
<html>
25
	<head>
26
		<meta name=charset value="latin1">
27
		<title>Schrodinger's Document</title>
28
	</head>
29
	<body>
30
		A nice, simple page for testing.
31
		<script>
32
			document.write('<p><b>Or so you thought...</b></p>');
33
		</script>
34
	</body>
35
</html>
test/data/pages/gotmyowndomain_https.html
1
<!DOCTYPE html>
2
<!--
3
    SPDX-License-Identifier: AGPL-3.0-or-later
4

  
5
    Sample testing page to serve over HTTPS
6

  
7
    This file is part of Haketilo.
8

  
9
    Copyright (C) 2021 jahoti <jahoti@tilde.team>
10

  
11
    This program is free software: you can redistribute it and/or modify
12
    it under the terms of the GNU Affero General Public License as
13
    published by the Free Software Foundation, either version 3 of the
14
    License, or (at your option) any later version.
15

  
16
    This program is distributed in the hope that it will be useful,
17
    but WITHOUT ANY WARRANTY; without even the implied warranty of
18
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
19
    GNU Affero General Public License for more details.
20

  
21
    You should have received a copy of the GNU Affero General Public License
22
    along with this program.  If not, see <https://www.gnu.org/licenses/>.
23
  -->
24
<html>
25
	<head>
26
		<meta name="charset" value="latin1">
27
		<title>Schrodinger's Document</title>
28
	</head>
29
	<body>
30
		A nice, simple page for testing (using HTTPS).
31
		<script>
32
			document.write('<p><b>Or so you thought...</b></p>');
33
		</script>
34
	</body>
35
</html>
test/data/pages/scripts_to_block_1.html
1
<!DOCTYPE html>
2
<!--
3
    SPDX-License-Identifier: CC0-1.0
4

  
5
    A testing page with various scripts that need to get blocked.
6

  
7
    This file is part of Haketilo.
8

  
9
    Copyright (C) 2022 Wojtek Kosior <koszko@koszko.org>
10

  
11
    This program is free software: you can redistribute it and/or modify
12
    it under the terms of the CC0 1.0 Universal License as published by
13
    the Creative Commons Corporation.
14

  
15
    This program is distributed in the hope that it will be useful,
16
    but WITHOUT ANY WARRANTY; without even the implied warranty of
17
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18
    CC0 1.0 Universal License for more details.
19
  -->
20
<html>
21
  <head>
22
    <meta name="charset" value="latin1">
23
    <script>
24
      window.__run = [...(window.__run || []), 'inline'];
25
    </script>
26
    <!-- the one below shall not execute even when blocking is off... -->
27
    <script type="application/json">
28
      window.__run = [...(window.__run || []), 'json'];
29
    </script>
30
  </head>
31
  <body>
32
    <button id="clickme1"
33
	    onclick="window.__run = [...(window.__run || []), 'on'];">
34
      Click Meee!
35
    </button>
36
    <a id="clickme2"
37
       href="javascript:window.__run = [...(window.__run || []), 'href'];void(0);">
38
      Click Meee!
39
    </a>
40
    <iframe src="javascript:void(window.parent.__run = [...(window.parent.__run || []), 'src']);">
41
    </iframe>
42
    <object data="javascript:window.__run = [...(window.__run || []), 'data'];">
43
    </object>
44
  </body>
45
</html>
test/default_profiles/icecat_empty/extensions.json
1
{"schemaVersion":25,"addons":[{"id":"jid1-KtlZuoiikVfFew@jetpack","location":"app-global","userDisabled":true,"path":"/usr/lib/icecat/browser/extensions/jid1-KtlZuoiikVfFew@jetpack"},{"id":"uBlock0@raymondhill.net","location":"app-global","userDisabled":true,"path":"/usr/lib/icecat/browser/extensions/uBlock0@raymondhill.net.xpi"},{"id":"SubmitMe@0xbeef.coffee","location":"app-global","userDisabled":true,"path":"/usr/lib/icecat/browser/extensions/SubmitMe@0xbeef.coffee"},{"id":"FreeUSPS@0xbeef.coffee","location":"app-global","userDisabled":true,"path":"/usr/lib/icecat/browser/extensions/FreeUSPS@0xbeef.coffee"},{"id":"tortm-browser-button@jeremybenthum","location":"app-global","userDisabled":true,"path":"/usr/lib/icecat/browser/extensions/tortm-browser-button@jeremybenthum"},{"id":"tprb.addon@searxes.danwin1210.me","location":"app-global","userDisabled":true,"path":"/usr/lib/icecat/browser/extensions/tprb.addon@searxes.danwin1210.me"},{"id":"SimpleSumOfUs@0xbeef.coffee","location":"app-global","userDisabled":true,"path":"/usr/lib/icecat/browser/extensions/SimpleSumOfUs@0xbeef.coffee"}]}
test/extension_crafting.py
1
# SPDX-License-Identifier: GPL-3.0-or-later
2

  
3
"""
4
Making temporary WebExtensions for use in the test suite
5
"""
6

  
7
# This file is part of Haketilo.
8
#
9
# Copyright (C) 2021, 2022 Wojtek Kosior <koszko@koszko.org>
10
#
11
# This program is free software: you can redistribute it and/or modify
12
# it under the terms of the GNU General Public License as published by
13
# the Free Software Foundation, either version 3 of the License, or
14
# (at your option) any later version.
15
#
16
# This program is distributed in the hope that it will be useful,
17
# but WITHOUT ANY WARRANTY; without even the implied warranty of
18
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
19
# GNU General Public License for more details.
20
#
21
# You should have received a copy of the GNU General Public License
22
# along with this program.  If not, see <https://www.gnu.org/licenses/>.
23
#
24
# I, Wojtek Kosior, thereby promise not to sue for violation of this file's
25
# license. Although I request that you do not make use of this code in a
26
# proprietary program, I am not going to enforce this in court.
27

  
28
import json
29
import zipfile
30
import re
31
import shutil
32
import subprocess
33

  
34
from pathlib import Path
35
from uuid import uuid4
36
from tempfile import TemporaryDirectory
37

  
38
from selenium.webdriver.support.ui import WebDriverWait
39
from selenium.common.exceptions import NoSuchElementException
40

  
41
from .misc_constants import *
42

  
43
class ManifestTemplateValueToFill:
44
    pass
45

  
46
def manifest_template():
47
    return {
48
        'manifest_version': 2,
49
        'name': 'Haketilo test extension',
50
        'version': '1.0',
51
        'applications': {
52
	    'gecko': {
53
	        'id': ManifestTemplateValueToFill(),
54
	        'strict_min_version': '60.0'
55
	    }
56
        },
57
        'permissions': [
58
	    'contextMenus',
59
	    'webRequest',
60
	    'webRequestBlocking',
61
	    'activeTab',
62
	    'notifications',
63
	    'sessions',
64
	    'storage',
65
	    'tabs',
66
	    '<all_urls>',
67
	    'unlimitedStorage'
68
        ],
69
        'content_security_policy': "object-src 'none'; script-src 'self' https://serve.scrip.ts;",
70
        'web_accessible_resources': ['testpage.html'],
71
        'options_ui': {
72
	    'page': 'testpage.html',
73
	    'open_in_tab': True
74
        },
75
        'background': {
76
	    'persistent': True,
77
	    'scripts': ['__open_test_page.js', 'background.js']
78
        },
79
        'content_scripts': [
80
	    {
81
	        'run_at': 'document_start',
82
	        'matches': ['<all_urls>'],
83
	        'match_about_blank': True,
84
	        'all_frames': True,
85
	        'js': ['content.js']
86
	    }
87
        ]
88
    }
89

  
90
class ExtraHTML:
91
    def __init__(self, html_path, append={}, wrap_into_htmldoc=True):
92
        self.html_path = html_path
93
        self.append = append
94
        self.wrap_into_htmldoc = wrap_into_htmldoc
95

  
96
    def add_to_xpi(self, xpi, tmpdir=None):
97
        if tmpdir is None:
98
            with TemporaryDirectory() as tmpdir:
99
                return self.add_to_xpi(xpi, tmpdir)
100

  
101
        append_flags = []
102
        for filename, code in self.append.items():
103
            append_flags.extend(['-A', f'{filename}:{code}'])
104

  
105
        awk = subprocess.run(
106
            ['awk', '-f', awk_script_name, '--', *unit_test_defines,
107
             *append_flags, '-H', self.html_path, '--write-js-deps',
108
             '--output=files-to-copy', f'--output-dir={tmpdir}'],
109
            stdout=subprocess.PIPE, cwd=script_root, check=True
110
        )
111

  
112
        for path in filter(None, awk.stdout.decode().split('\n')):
113
            xpi.write(script_root / path, path)
114

  
115
        tmpdir = Path(tmpdir)
116
        for path in tmpdir.rglob('*'):
117
            relpath = str(path.relative_to(tmpdir))
118
            if not path.is_dir() and relpath != self.html_path:
119
                xpi.write(path, relpath)
120

  
121
        with open(tmpdir / self.html_path, 'rt') as html_file:
122
            html = html_file.read()
123
            if self.wrap_into_htmldoc:
124
                html = f'<!DOCTYPE html><html><body>{html}</body></html>'
125
            xpi.writestr(self.html_path, html)
126

  
127
default_background_script = ''
128
default_content_script = ''
129
default_test_page = '''
130
<!DOCTYPE html>
131
<html>
132
  <head>
133
    <title>Extension's options page for testing</title>
134
  </head>
135
  <body>
136
    <h1>Extension's options page for testing</h1>
137
  </body>
138
</html>
139
'''
140

  
141
open_test_page_script = '''(() => {
142
const page_url = browser.runtime.getURL("testpage.html");
143
const execute_details = {
144
    code: `window.wrappedJSObject.ext_page_url=${JSON.stringify(page_url)};`
145
};
146
browser.tabs.query({currentWindow: true, active: true})
147
    .then(t => browser.tabs.executeScript(t.id, execute_details));
148
})();'''
149

  
150
def make_extension(destination_dir,
151
                   background_script=default_background_script,
152
                   content_script=default_content_script,
153
                   test_page=default_test_page,
154
                   extra_files={}, extra_html=[]):
155
    if not hasattr(extra_html, '__iter__'):
156
        extra_html = [extra_html]
157
    manifest = manifest_template()
158
    extension_id = '{%s}' % uuid4()
159
    manifest['applications']['gecko']['id'] = extension_id
160
    files = {
161
        'manifest.json'      : json.dumps(manifest),
162
        '__open_test_page.js': open_test_page_script,
163
        'background.js'      : background_script,
164
        'content.js'         : content_script,
165
        'testpage.html'      : test_page,
166
        **extra_files
167
    }
168
    destination_path = destination_dir / f'{extension_id}.xpi'
169
    with zipfile.ZipFile(destination_path, 'x') as xpi:
170
        for filename, contents in files.items():
171
            if hasattr(contents, '__call__'):
172
                contents = contents()
173
            xpi.writestr(filename, contents)
174
        for html in extra_html:
175
            html.add_to_xpi(xpi)
176

  
177
    return destination_path
178

  
179
extract_base_url_re = re.compile(r'^(.*)manifest.json$')
180

  
181
def get_extension_base_url(driver):
182
    """
183
    Extension's internall UUID is not directly exposed in Selenium. Instead, we
184
    can navigate to about:debugging and inspect the manifest URL present there
185
    to get the base url like:
186
        moz-extension://b225c78f-d108-4caa-8406-f38b37d8dee5/
187
    which can then be used to navigate to extension-bundled pages.
188
    """
189
    # For newer Firefoxes
190
    driver.get('about:debugging#/runtime/this-firefox')
191

  
192
    def get_manifest_link_newer_ff(driver):
193
        try:
194
            return driver.find_element_by_class_name('qa-manifest-url')
195
        except NoSuchElementException:
196
            pass
197

  
198
        try:
199
            details = driver.find_element_by_class_name('error-page-details')
200
        except NoSuchElementException:
201
            return False
202

  
203
        if '#/runtime/this-firefox' in details.text:
204
            return "not_newer_ff"
205

  
206
    manifest_link = WebDriverWait(driver, 10).until(get_manifest_link_newer_ff)
207

  
208
    if manifest_link == "not_newer_ff":
209
        driver.get("about:debugging#addons")
210
        driver.implicitly_wait(10)
211
        manifest_link = driver.find_element_by_class_name('manifest-url')
212
        driver.implicitly_wait(0)
213

  
214
    manifest_url = manifest_link.get_attribute('href')
215
    return extract_base_url_re.match(manifest_url).group(1)
test/haketilo_test/__init__.py
1
# SPDX-License-Identifier: CC0-1.0
2
# Copyright (C) 2021 Wojtek Kosior
test/haketilo_test/__main__.py
1
# SPDX-License-Identifier: AGPL-3.0-or-later
2

  
3
"""
4
Run a Firefox-type browser with WebDriver attached and Python console open
5
"""
6

  
7
# This file is part of Haketilo.
8
#
9
# Copyright (C) 2021 jahoti <jahoti@tilde.team>
10
# Copyright (C) 2021 Wojtek Kosior <koszko@koszko.org>
11
#
12
# This program is free software: you can redistribute it and/or modify
13
# it under the terms of the GNU Affero General Public License as
14
# published by the Free Software Foundation, either version 3 of the
15
# License, or (at your option) any later version.
16
#
17
# This program is distributed in the hope that it will be useful,
18
# but WITHOUT ANY WARRANTY; without even the implied warranty of
19
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
20
# GNU Affero General Public License for more details.
21
#
22
# You should have received a copy of the GNU Affero General Public License
23
# along with this program.  If not, see <https://www.gnu.org/licenses/>.
24
#
25
#
26
# I, Wojtek Kosior, thereby promise not to sue for violation of this
27
# file's license. Although I request that you do not make use of this code
28
# in a proprietary program, I am not going to enforce this in court.
29

  
30
import sys
31
import time
32
import code
33
from rlcompleter import Completer
34
import readline
35

  
36
from .server import do_an_internet
37
from .misc_constants import *
38
from .profiles import firefox_safe_mode
39
from .extension_crafting import get_extension_base_url
40

  
41
def fail(msg, error_code):
42
    print('Error:', msg)
43
    print('Usage:', sys.argv[0], '[--load-haketilo]', '[certificates_directory] [proxy_port]')
44
    sys.exit(error_code)
45

  
46
load_haketilo = False
47
argv_idx = 1
48
if len(sys.argv) > argv_idx and sys.argv[argv_idx] == '--load-haketilo':
49
    load_haketilo = True
50
    argv_idx += 1
51

  
52
certdir = Path(sys.argv[argv_idx]).resolve() if len(sys.argv) > argv_idx \
53
    else default_cert_dir
54

  
55
if not certdir.is_dir():
56
    fail('selected certificate directory does not exist.', 2)
57

  
58
argv_idx += 1
59

  
60
port = sys.argv[argv_idx] if len(sys.argv) > argv_idx \
61
    else str(default_proxy_port)
62

  
63
if not port.isnumeric():
64
    fail('port must be an integer.', 3)
65

  
66
httpd = do_an_internet(certdir, int(port))
67
driver = firefox_safe_mode(proxy_port=int(port))
68

  
69
if load_haketilo:
70
    driver.install_addon(str(here.parent / 'mozilla-build.zip'), temporary=True)
71
    driver.get(get_extension_base_url(driver) + 'html/settings.html')
72

  
73
print("You can now control the browser through 'driver' object")
74

  
75
# Here we enable readline-enhanced editing:
76
# https://stackoverflow.com/questions/35115208/is-there-any-way-to-combine-readline-rlcompleter-and-interactiveconsole-in-pytho#answer-35116399
77
readline.parse_and_bind('tab: complete');
78
console_locals = globals()
79
readline.set_completer(Completer(console_locals).complete)
80
code.InteractiveConsole(locals=globals()).interact()
81

  
82
driver.quit()
83
httpd.shutdown()
test/haketilo_test/conftest.py
1
# SPDX-License-Identifier: GPL-3.0-or-later
2

  
3
"""
4
Common fixtures for Haketilo unit tests
5
"""
6

  
7
# This file is part of Haketilo.
8
#
9
# Copyright (C) 2021 Wojtek Kosior <koszko@koszko.org>
10
#
11
# This program is free software: you can redistribute it and/or modify
12
# it under the terms of the GNU General Public License as published by
13
# the Free Software Foundation, either version 3 of the License, or
14
# (at your option) any later version.
15
#
16
# This program is distributed in the hope that it will be useful,
17
# but WITHOUT ANY WARRANTY; without even the implied warranty of
18
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
19
# GNU General Public License for more details.
20
#
21
# You should have received a copy of the GNU General Public License
22
# along with this program.  If not, see <https://www.gnu.org/licenses/>.
23
#
24
# I, Wojtek Kosior, thereby promise not to sue for violation of this file's
25
# license. Although I request that you do not make use of this code in a
26
# proprietary program, I am not going to enforce this in court.
27

  
28
import pytest
29
from pathlib import Path
30
from tempfile import TemporaryDirectory
31
from selenium.webdriver.common.by import By
32
from selenium.webdriver.support.ui import WebDriverWait
33
from selenium.webdriver.support import expected_conditions as EC
34

  
35
from .profiles           import firefox_safe_mode
36
from .server             import do_an_internet
37
from .extension_crafting import make_extension
38
from .world_wide_library import start_serving_script, dump_scripts
39
from .misc_constants     import proj_root
40

  
41
@pytest.fixture(scope="session")
42
def proxy():
43
    httpd = do_an_internet()
44
    yield httpd
45
    httpd.shutdown()
46

  
47
@pytest.fixture(scope="session")
48
def _driver(proxy):
49
    with firefox_safe_mode() as driver:
50
        yield driver
51
        driver.quit()
52

  
53
def close_all_but_one_window(driver):
54
    while len(driver.window_handles) > 1:
55
        driver.switch_to.window(driver.window_handles[-1])
56
        driver.close()
57
    driver.switch_to.window(driver.window_handles[0])
58

  
59
@pytest.fixture()
60
def driver(_driver, request):
61
    nav_target = request.node.get_closest_marker('get_page')
62
    nav_target = nav_target.args[0] if nav_target else 'about:blank'
63

  
64
    second_driver = request.node.get_closest_marker('second_driver')
65

  
66
    if second_driver:
67
        with firefox_safe_mode() as _driver:
68
            _driver.get(nav_target)
69
            yield _driver
70
            _driver.quit()
71
    else:
72
        close_all_but_one_window(_driver)
73
        _driver.get(nav_target)
74
        _driver.implicitly_wait(0)
75
        yield _driver
76

  
77
@pytest.fixture()
78
def webextension(driver, request):
79
    ext_data = request.node.get_closest_marker('ext_data')
80
    if ext_data is None:
81
        raise Exception('"webextension" fixture requires "ext_data" marker to be set')
82
    ext_data = ext_data.args[0].copy()
83

  
84
    navigate_to = ext_data.get('navigate_to')
85
    if navigate_to is not None:
86
        del ext_data['navigate_to']
87

  
88
    driver.get('https://gotmyowndoma.in/')
89
    ext_path = make_extension(Path(driver.firefox_profile.path), **ext_data)
90
    addon_id = driver.install_addon(str(ext_path), temporary=True)
91
    get_url = lambda d: d.execute_script('return window.ext_page_url;')
92
    ext_page_url = WebDriverWait(driver, 10).until(get_url)
93
    driver.get(ext_page_url)
94

  
95
    if navigate_to is not None:
96
        driver.get(driver.current_url.replace('testpage.html', navigate_to))
97

  
98
    yield
99

  
100
    # Unloading an extension might cause its windows to vanish. Make sure
101
    # there's at least one window navigated to some other page before
102
    # uninstalling the addon. Otherwise, we could be left with a windowless
103
    # browser :c
104
    driver.switch_to.window(driver.window_handles[-1])
105
    driver.get('https://gotmyowndoma.in/')
106
    driver.uninstall_addon(addon_id)
107
    ext_path.unlink()
108

  
109
@pytest.fixture()
110
def haketilo(driver):
111
    addon_id = driver.install_addon(str(Path.cwd() / 'mozilla-build.zip'),
112
                                    temporary=True)
113

  
114
    yield
115

  
116
    driver.uninstall_addon(addon_id)
117

  
118
script_injector_script = '''\
119
/*
120
 * Selenium by default executes scripts in some weird one-time context. We want
121
 * separately-loaded scripts to be able to access global variables defined
122
 * before, including those declared with `const` or `let`. To achieve that, we
123
 * run our scripts by injecting them into the page with a <script> tag that runs
124
 * javascript served by our proxy. We use custom properties of the `window`
125
 * object to communicate with injected code.
126
 */
127
const inject = async () => {
128
    delete window.haketilo_selenium_return_value;
129
    delete window.haketilo_selenium_exception;
130
    window.returnval = val => window.haketilo_selenium_return_value = val;
131

  
132
    const injectee = document.createElement('script');
133
    injectee.src = arguments[0];
134
    injectee.type = "application/javascript";
135
    injectee.async = true;
136
    const prom = new Promise(cb => injectee.onload = cb);
137

  
138
    window.arguments = arguments[1];
139
    document.body.append(injectee);
140

  
141
    await prom;
142

  
143
    /*
144
     * To ease debugging, we want this script to signal all exceptions from the
145
     * injectee.
146
     */
147
    if (window.haketilo_selenium_exception !== false)
148
        throw ['haketilo_selenium_error',
149
               'Error in injected script! Check your geckodriver.log and ./injected_scripts/!'];
150

  
151
    return window.haketilo_selenium_return_value;
152
}
153
return inject();
154
'''
155

  
156
def _execute_in_page_context(driver, script, args):
157
    script = script + '\n;\nwindow.haketilo_selenium_exception = false;'
158
    script_url = start_serving_script(script)
159

  
160
    try:
161
        result = driver.execute_script(script_injector_script, script_url, args)
162
        if type(result) is list and len(result) == 2 and \
163
           result[0] == 'haketilo_selenium_error':
164
            raise Exception(result[1])
165
        return result
166
    except Exception as e:
167
        dump_scripts()
168
        raise e from None
169

  
170
# Some fixtures here just define functions that operate on driver. We should
171
# consider making them into webdriver wrapper class methods.
172

  
173
@pytest.fixture()
174
def execute_in_page(driver):
175
    def do_execute(script, *args):
176
        return _execute_in_page_context(driver, script, args)
177

  
178
    yield do_execute
179

  
180
@pytest.fixture()
181
def wait_elem_text(driver):
182
    def do_wait(id, text):
183
        WebDriverWait(driver, 10).until(
184
            EC.text_to_be_present_in_element((By.ID, id), text)
185
        )
186

  
187
    yield do_wait
test/haketilo_test/data/pages/gotmyowndomain.html
1
<!DOCTYPE html>
2
<!--
3
    SPDX-License-Identifier: AGPL-3.0-or-later
4

  
5
    Sample testing page
6

  
7
    This file is part of Haketilo.
8

  
9
    Copyright (C) 2021 jahoti <jahoti@tilde.team>
10

  
11
    This program is free software: you can redistribute it and/or modify
12
    it under the terms of the GNU Affero General Public License as
13
    published by the Free Software Foundation, either version 3 of the
14
    License, or (at your option) any later version.
15

  
16
    This program is distributed in the hope that it will be useful,
17
    but WITHOUT ANY WARRANTY; without even the implied warranty of
18
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
19
    GNU Affero General Public License for more details.
20

  
21
    You should have received a copy of the GNU Affero General Public License
22
    along with this program.  If not, see <https://www.gnu.org/licenses/>.
23
  -->
24
<html>
25
	<head>
26
		<meta name=charset value="latin1">
27
		<title>Schrodinger's Document</title>
28
	</head>
29
	<body>
30
		A nice, simple page for testing.
31
		<script>
32
			document.write('<p><b>Or so you thought...</b></p>');
33
		</script>
34
	</body>
35
</html>
test/haketilo_test/data/pages/gotmyowndomain_https.html
1
<!DOCTYPE html>
2
<!--
3
    SPDX-License-Identifier: AGPL-3.0-or-later
4

  
5
    Sample testing page to serve over HTTPS
6

  
7
    This file is part of Haketilo.
8

  
9
    Copyright (C) 2021 jahoti <jahoti@tilde.team>
10

  
11
    This program is free software: you can redistribute it and/or modify
12
    it under the terms of the GNU Affero General Public License as
13
    published by the Free Software Foundation, either version 3 of the
14
    License, or (at your option) any later version.
15

  
16
    This program is distributed in the hope that it will be useful,
17
    but WITHOUT ANY WARRANTY; without even the implied warranty of
18
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
19
    GNU Affero General Public License for more details.
20

  
21
    You should have received a copy of the GNU Affero General Public License
22
    along with this program.  If not, see <https://www.gnu.org/licenses/>.
23
  -->
24
<html>
25
	<head>
26
		<meta name="charset" value="latin1">
27
		<title>Schrodinger's Document</title>
28
	</head>
29
	<body>
30
		A nice, simple page for testing (using HTTPS).
31
		<script>
32
			document.write('<p><b>Or so you thought...</b></p>');
33
		</script>
34
	</body>
35
</html>
test/haketilo_test/data/pages/scripts_to_block_1.html
1
<!DOCTYPE html>
2
<!--
3
    SPDX-License-Identifier: CC0-1.0
4

  
5
    A testing page with various scripts that need to get blocked.
6

  
7
    This file is part of Haketilo.
8

  
9
    Copyright (C) 2022 Wojtek Kosior <koszko@koszko.org>
10

  
11
    This program is free software: you can redistribute it and/or modify
12
    it under the terms of the CC0 1.0 Universal License as published by
13
    the Creative Commons Corporation.
14

  
15
    This program is distributed in the hope that it will be useful,
16
    but WITHOUT ANY WARRANTY; without even the implied warranty of
17
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18
    CC0 1.0 Universal License for more details.
19
  -->
20
<html>
21
  <head>
22
    <meta name="charset" value="latin1">
23
    <script>
24
      window.__run = [...(window.__run || []), 'inline'];
25
    </script>
26
    <!-- the one below shall not execute even when blocking is off... -->
27
    <script type="application/json">
28
      window.__run = [...(window.__run || []), 'json'];
29
    </script>
30
  </head>
31
  <body>
32
    <button id="clickme1"
33
	    onclick="window.__run = [...(window.__run || []), 'on'];">
34
      Click Meee!
35
    </button>
36
    <a id="clickme2"
37
       href="javascript:window.__run = [...(window.__run || []), 'href'];void(0);">
38
      Click Meee!
39
    </a>
40
    <iframe src="javascript:void(window.parent.__run = [...(window.parent.__run || []), 'src']);">
41
    </iframe>
42
    <object data="javascript:window.__run = [...(window.__run || []), 'data'];">
43
    </object>
44
  </body>
45
</html>
test/haketilo_test/default_profiles/icecat_empty/extensions.json
1
{"schemaVersion":25,"addons":[{"id":"jid1-KtlZuoiikVfFew@jetpack","location":"app-global","userDisabled":true,"path":"/usr/lib/icecat/browser/extensions/jid1-KtlZuoiikVfFew@jetpack"},{"id":"uBlock0@raymondhill.net","location":"app-global","userDisabled":true,"path":"/usr/lib/icecat/browser/extensions/uBlock0@raymondhill.net.xpi"},{"id":"SubmitMe@0xbeef.coffee","location":"app-global","userDisabled":true,"path":"/usr/lib/icecat/browser/extensions/SubmitMe@0xbeef.coffee"},{"id":"FreeUSPS@0xbeef.coffee","location":"app-global","userDisabled":true,"path":"/usr/lib/icecat/browser/extensions/FreeUSPS@0xbeef.coffee"},{"id":"tortm-browser-button@jeremybenthum","location":"app-global","userDisabled":true,"path":"/usr/lib/icecat/browser/extensions/tortm-browser-button@jeremybenthum"},{"id":"tprb.addon@searxes.danwin1210.me","location":"app-global","userDisabled":true,"path":"/usr/lib/icecat/browser/extensions/tprb.addon@searxes.danwin1210.me"},{"id":"SimpleSumOfUs@0xbeef.coffee","location":"app-global","userDisabled":true,"path":"/usr/lib/icecat/browser/extensions/SimpleSumOfUs@0xbeef.coffee"}]}
test/haketilo_test/extension_crafting.py
1
# SPDX-License-Identifier: GPL-3.0-or-later
2

  
3
"""
4
Making temporary WebExtensions for use in the test suite
5
"""
6

  
7
# This file is part of Haketilo.
8
#
9
# Copyright (C) 2021, 2022 Wojtek Kosior <koszko@koszko.org>
10
#
11
# This program is free software: you can redistribute it and/or modify
12
# it under the terms of the GNU General Public License as published by
13
# the Free Software Foundation, either version 3 of the License, or
14
# (at your option) any later version.
15
#
16
# This program is distributed in the hope that it will be useful,
17
# but WITHOUT ANY WARRANTY; without even the implied warranty of
18
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
19
# GNU General Public License for more details.
20
#
21
# You should have received a copy of the GNU General Public License
22
# along with this program.  If not, see <https://www.gnu.org/licenses/>.
23
#
24
# I, Wojtek Kosior, thereby promise not to sue for violation of this file's
25
# license. Although I request that you do not make use of this code in a
26
# proprietary program, I am not going to enforce this in court.
27

  
28
import json
29
import zipfile
30
import re
31
import shutil
32
import subprocess
33

  
34
from pathlib import Path
35
from uuid import uuid4
36
from tempfile import TemporaryDirectory
37

  
38
from selenium.webdriver.support.ui import WebDriverWait
39
from selenium.common.exceptions import NoSuchElementException
40

  
41
from .misc_constants import *
42

  
43
class ManifestTemplateValueToFill:
44
    pass
45

  
46
def manifest_template():
47
    return {
48
        'manifest_version': 2,
49
        'name': 'Haketilo test extension',
50
        'version': '1.0',
51
        'applications': {
52
	    'gecko': {
53
	        'id': ManifestTemplateValueToFill(),
54
	        'strict_min_version': '60.0'
55
	    }
56
        },
57
        'permissions': [
58
	    'contextMenus',
59
	    'webRequest',
60
	    'webRequestBlocking',
61
	    'activeTab',
62
	    'notifications',
63
	    'sessions',
64
	    'storage',
65
	    'tabs',
66
	    '<all_urls>',
67
	    'unlimitedStorage'
68
        ],
69
        'content_security_policy': "object-src 'none'; script-src 'self' https://serve.scrip.ts;",
70
        'web_accessible_resources': ['testpage.html'],
71
        'options_ui': {
72
	    'page': 'testpage.html',
73
	    'open_in_tab': True
74
        },
75
        'background': {
76
	    'persistent': True,
77
	    'scripts': ['__open_test_page.js', 'background.js']
78
        },
79
        'content_scripts': [
80
	    {
81
	        'run_at': 'document_start',
82
	        'matches': ['<all_urls>'],
83
	        'match_about_blank': True,
84
	        'all_frames': True,
85
	        'js': ['content.js']
86
	    }
87
        ]
88
    }
89

  
90
class ExtraHTML:
91
    def __init__(self, html_path, append={}, wrap_into_htmldoc=True):
92
        self.html_path = html_path
93
        self.append = append
94
        self.wrap_into_htmldoc = wrap_into_htmldoc
95

  
96
    def add_to_xpi(self, xpi, tmpdir=None):
97
        if tmpdir is None:
98
            with TemporaryDirectory() as tmpdir:
99
                return self.add_to_xpi(xpi, tmpdir)
100

  
101
        append_flags = []
102
        for filename, code in self.append.items():
103
            append_flags.extend(['-A', f'{filename}:{code}'])
104

  
105
        awk = subprocess.run(
106
            ['awk', '-f', awk_script_name, '--', *unit_test_defines,
107
             *append_flags, '-H', self.html_path, '--write-js-deps',
108
             '--output=files-to-copy', f'--output-dir={tmpdir}'],
109
            stdout=subprocess.PIPE, cwd=proj_root, check=True
110
        )
111

  
112
        for path in filter(None, awk.stdout.decode().split('\n')):
113
            xpi.write(proj_root / path, path)
114

  
115
        tmpdir = Path(tmpdir)
116
        for path in tmpdir.rglob('*'):
117
            relpath = str(path.relative_to(tmpdir))
118
            if not path.is_dir() and relpath != self.html_path:
119
                xpi.write(path, relpath)
120

  
121
        with open(tmpdir / self.html_path, 'rt') as html_file:
122
            html = html_file.read()
123
            if self.wrap_into_htmldoc:
124
                html = f'<!DOCTYPE html><html><body>{html}</body></html>'
125
            xpi.writestr(self.html_path, html)
126

  
127
default_background_script = ''
128
default_content_script = ''
129
default_test_page = '''
130
<!DOCTYPE html>
131
<html>
132
  <head>
133
    <title>Extension's options page for testing</title>
134
  </head>
135
  <body>
136
    <h1>Extension's options page for testing</h1>
137
  </body>
138
</html>
139
'''
140

  
141
open_test_page_script = '''(() => {
142
const page_url = browser.runtime.getURL("testpage.html");
143
const execute_details = {
144
    code: `window.wrappedJSObject.ext_page_url=${JSON.stringify(page_url)};`
145
};
146
browser.tabs.query({currentWindow: true, active: true})
147
    .then(t => browser.tabs.executeScript(t.id, execute_details));
148
})();'''
149

  
150
def make_extension(destination_dir,
151
                   background_script=default_background_script,
152
                   content_script=default_content_script,
153
                   test_page=default_test_page,
154
                   extra_files={}, extra_html=[]):
155
    if not hasattr(extra_html, '__iter__'):
156
        extra_html = [extra_html]
157
    manifest = manifest_template()
158
    extension_id = '{%s}' % uuid4()
159
    manifest['applications']['gecko']['id'] = extension_id
160
    files = {
161
        'manifest.json'      : json.dumps(manifest),
162
        '__open_test_page.js': open_test_page_script,
163
        'background.js'      : background_script,
164
        'content.js'         : content_script,
165
        'testpage.html'      : test_page,
166
        **extra_files
167
    }
168
    destination_path = destination_dir / f'{extension_id}.xpi'
169
    with zipfile.ZipFile(destination_path, 'x') as xpi:
170
        for filename, contents in files.items():
171
            if hasattr(contents, '__call__'):
172
                contents = contents()
173
            xpi.writestr(filename, contents)
174
        for html in extra_html:
175
            html.add_to_xpi(xpi)
176

  
177
    return destination_path
178

  
179
extract_base_url_re = re.compile(r'^(.*)manifest.json$')
180

  
181
def get_extension_base_url(driver):
182
    """
183
    Extension's internall UUID is not directly exposed in Selenium. Instead, we
184
    can navigate to about:debugging and inspect the manifest URL present there
185
    to get the base url like:
186
        moz-extension://b225c78f-d108-4caa-8406-f38b37d8dee5/
187
    which can then be used to navigate to extension-bundled pages.
188
    """
189
    # For newer Firefoxes
190
    driver.get('about:debugging#/runtime/this-firefox')
191

  
192
    def get_manifest_link_newer_ff(driver):
193
        try:
194
            return driver.find_element_by_class_name('qa-manifest-url')
195
        except NoSuchElementException:
196
            pass
197

  
198
        try:
199
            details = driver.find_element_by_class_name('error-page-details')
200
        except NoSuchElementException:
201
            return False
202

  
203
        if '#/runtime/this-firefox' in details.text:
204
            return "not_newer_ff"
205

  
206
    manifest_link = WebDriverWait(driver, 10).until(get_manifest_link_newer_ff)
207

  
208
    if manifest_link == "not_newer_ff":
209
        driver.get("about:debugging#addons")
210
        driver.implicitly_wait(10)
211
        manifest_link = driver.find_element_by_class_name('manifest-url')
212
        driver.implicitly_wait(0)
213

  
214
    manifest_url = manifest_link.get_attribute('href')
215
    return extract_base_url_re.match(manifest_url).group(1)
test/haketilo_test/misc_constants.py
1
# SPDX-License-Identifier: AGPL-3.0-or-later
2

  
3
"""
4
Miscellaneous data that were found useful
5
"""
6

  
7
# This file is part of Haketilo.
8
#
9
# Copyright (C) 2021 jahoti <jahoti@tilde.team>
10
# Copyright (C) 2021 Wojtek Kosior <koszko@koszko.org>
... This diff was truncated because it exceeds the maximum size that can be displayed.

Also available in: Unified diff