Project

General

Profile

Download (6.89 KB) Statistics
| Branch: | Tag: | Revision:

haketilo / test / extension_crafting.py @ 3611dd6a

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)
(4-4/11)