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)
|