Project

General

Profile

« Previous | Next » 

Revision 4c6a2323

Added by koszko over 1 year ago

make Haketilo buildable again (for Mozilla)

How cool it is to throw away 5755 lines of code...

View differences:

Makefile.in
69 69
	openssl req -x509 -new -nodes -key $< -days 1024 -out $@ \
70 70
		 -subj "/CN=Haketilo Test"
71 71

  
72
test: test/certs/rootCA.pem test/certs/site.key
72
test: test/certs/rootCA.pem test/certs/site.key $(default_target)-build.zip
73 73
	MOZ_HEADLESS=whatever pytest
74 74

  
75 75
test-environment: test/certs/rootCA.pem test/certs/site.key
background/background.js
1
/**
2
 * This file is part of Haketilo.
3
 *
4
 * Function: Background scripts - main script.
5
 *
6
 * Copyright (C) 2022 Wojtek Kosior
7
 *
8
 * This program is free software: you can redistribute it and/or modify
9
 * it under the terms of the GNU General Public License as published by
10
 * the Free Software Foundation, either version 3 of the License, or
11
 * (at your option) any later version.
12
 *
13
 * This program is distributed in the hope that it will be useful,
14
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16
 * GNU General Public License for more details.
17
 *
18
 * As additional permission under GNU GPL version 3 section 7, you
19
 * may distribute forms of that code without the copy of the GNU
20
 * GPL normally required by section 4, provided you include this
21
 * license notice and, in case of non-source distribution, a URL
22
 * through which recipients can access the Corresponding Source.
23
 * If you modify file(s) with this exception, you may extend this
24
 * exception to your version of the file(s), but you are not
25
 * obligated to do so. If you do not wish to do so, delete this
26
 * exception statement from your version.
27
 *
28
 * As a special exception to the GPL, any HTML file which merely
29
 * makes function calls to this code, and for that purpose
30
 * includes it by reference shall be deemed a separate work for
31
 * copyright law purposes. If you modify this code, you may extend
32
 * this exception to your version of the code, but you are not
33
 * obligated to do so. If you do not wish to do so, delete this
34
 * exception statement from your version.
35
 *
36
 * You should have received a copy of the GNU General Public License
37
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
38
 *
39
 * I, Wojtek Kosior, thereby promise not to sue for violation of this file's
40
 * license. Although I request that you do not make use of this code in a
41
 * proprietary program, I am not going to enforce this in court.
42
 */
43

  
44
#IMPORT background/patterns_query_manager.js
45
#IMPORT background/webrequest.js
46
#IMPORT background/CORS_bypass_server.js
47
#IMPORT background/broadcast_broker.js
48
#IMPORT background/indexeddb_files_server.js
49

  
50
#FROM common/misc.js IMPORT gen_nonce
51

  
52
function main() {
53
    const secret = gen_nonce();
54

  
55
    /*
56
     * Some other services depend on IndexedDB which depende on broadcast
57
     * broker, hence we start it first.
58
     */
59
    broadcast_broker.start();
60
    CORS_bypass_server.start();
61
    indexeddb_files_server.start();
62

  
63
    patterns_query_manager.start(secret);
64
    webrequest.start(secret);
65
}
66

  
67
main();
background/broadcast_broker.js
42 42
 * proprietary program, I am not going to enforce this in court.
43 43
 */
44 44

  
45
#IMPORT common/connection_types.js AS CONNECTION_TYPE
46

  
47 45
#FROM common/message_server.js IMPORT listen_for_connection
48 46

  
49 47
let next_id = 1;
......
169 167

  
170 168
function start()
171 169
{
172
    listen_for_connection(CONNECTION_TYPE.BROADCAST_SEND, new_broadcast_sender);
173
    listen_for_connection(CONNECTION_TYPE.BROADCAST_LISTEN,
174
			  new_broadcast_listener);
170
    listen_for_connection("broadcast_send", new_broadcast_sender);
171
    listen_for_connection("broadcast_listen", new_broadcast_listener);
175 172
}
176 173
#EXPORT start
background/main.js
1
/**
2
 * This file is part of Haketilo.
3
 *
4
 * Function: Main background script.
5
 *
6
 * Copyright (C) 2021 Wojtek Kosior <koszko@koszko.org>
7
 * Copyright (C) 2021 Jahoti <jahoti@envs.net>
8
 *
9
 * This program is free software: you can redistribute it and/or modify
10
 * it under the terms of the GNU General Public License as published by
11
 * the Free Software Foundation, either version 3 of the License, or
12
 * (at your option) any later version.
13
 *
14
 * This program is distributed in the hope that it will be useful,
15
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17
 * GNU General Public License for more details.
18
 *
19
 * As additional permission under GNU GPL version 3 section 7, you
20
 * may distribute forms of that code without the copy of the GNU
21
 * GPL normally required by section 4, provided you include this
22
 * license notice and, in case of non-source distribution, a URL
23
 * through which recipients can access the Corresponding Source.
24
 * If you modify file(s) with this exception, you may extend this
25
 * exception to your version of the file(s), but you are not
26
 * obligated to do so. If you do not wish to do so, delete this
27
 * exception statement from your version.
28
 *
29
 * As a special exception to the GPL, any HTML file which merely
30
 * makes function calls to this code, and for that purpose
31
 * includes it by reference shall be deemed a separate work for
32
 * copyright law purposes. If you modify this code, you may extend
33
 * this exception to your version of the code, but you are not
34
 * obligated to do so. If you do not wish to do so, delete this
35
 * exception statement from your version.
36
 *
37
 * You should have received a copy of the GNU General Public License
38
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
39
 *
40
 * I, Wojtek Kosior, thereby promise not to sue for violation of this file's
41
 * license. Although I request that you do not make use of this code in a
42
 * proprietary program, I am not going to enforce this in court.
43
 */
44

  
45
#IMPORT common/storage_light.js AS light_storage
46

  
47
#IMPORT background/storage_server.js
48
#IMPORT background/page_actions_server.js
49
#IMPORT background/stream_filter.js
50

  
51
#FROM common/browser.js             IMPORT browser
52
#FROM common/stored_types.js        IMPORT TYPE_PREFIX
53
#FROM background/storage.js         IMPORT get_storage
54
#FROM common/misc.js                IMPORT is_privileged_url
55
#FROM common/settings_query.js      IMPORT query_best
56
#FROM background/policy_injector.js IMPORT inject_csp_headers
57

  
58
const initial_data = (
59
#INCLUDE_VERBATIM default_settings.json
60
);
61

  
62
storage_server.start();
63
page_actions_server.start();
64

  
65
async function init_ext(install_details)
66
{
67
    if (install_details.reason != "install")
68
	return;
69

  
70
    let storage = await get_storage();
71

  
72
    await storage.clear();
73

  
74
    /* Below we add sample settings to the extension. */
75
    for (let setting of initial_data) {
76
	let [key, value] = Object.entries(setting)[0];
77
	storage.set(key[0], key.substring(1), value);
78
    }
79
}
80

  
81
browser.runtime.onInstalled.addListener(init_ext);
82

  
83
/*
84
 * The function below implements a more practical interface for what it does by
85
 * wrapping the old query_best() function.
86
 */
87
function decide_policy_for_url(storage, policy_observable, url)
88
{
89
    if (storage === undefined)
90
	return {allow: false};
91

  
92
    const settings =
93
	{allow: policy_observable !== undefined && policy_observable.value};
94

  
95
    const [pattern, queried_settings] = query_best(storage, url);
96

  
97
    if (queried_settings) {
98
	settings.payload = queried_settings.components;
99
	settings.allow = !!queried_settings.allow && !settings.payload;
100
	settings.pattern = pattern;
101
    }
102

  
103
    return settings;
104
}
105

  
106
let storage;
107
let policy_observable = {};
108

  
109
function sanitize_web_page(details)
110
{
111
    const url = details.url;
112
    if (is_privileged_url(details.url))
113
	return;
114

  
115
    const policy =
116
	  decide_policy_for_url(storage, policy_observable, details.url);
117

  
118
    let headers = details.responseHeaders;
119

  
120
    headers = inject_csp_headers(headers, policy);
121

  
122
    let skip = false;
123
    for (const header of headers) {
124
	if ((header.name.toLowerCase().trim() === "content-disposition" &&
125
	     /^\s*attachment\s*(;.*)$/i.test(header.value)))
126
	    skip = true;
127
    }
128
    skip = skip || (details.statusCode >= 300 && details.statusCode < 400);
129

  
130
    if (!skip) {
131
	/* Check for API availability. */
132
	if (browser.webRequest.filterResponseData)
133
	    headers = stream_filter.apply(details, headers, policy);
134
    }
135

  
136
    return {responseHeaders: headers};
137
}
138

  
139
const request_url_regex = /^[^?]*\?url=(.*)$/;
140
const redirect_url_template = browser.runtime.getURL("dummy") + "?settings=";
141

  
142
function synchronously_smuggle_policy(details)
143
{
144
    /*
145
     * Content script will make a synchronous XmlHttpRequest to extension's
146
     * `dummy` file to query settings for given URL. We smuggle that
147
     * information in query parameter of the URL we redirect to.
148
     * A risk of fingerprinting arises if a page with script execution allowed
149
     * guesses the dummy file URL and makes an AJAX call to it. It is currently
150
     * a problem in ManifestV2 Chromium-family port of Haketilo because Chromium
151
     * uses predictable URLs for web-accessible resources. We plan to fix it in
152
     * the future ManifestV3 port.
153
     */
154
    if (details.type !== "xmlhttprequest")
155
	return {cancel: true};
156

  
157
    console.debug(`Settings queried using XHR for '${details.url}'.`);
158

  
159
    let policy = {allow: false};
160

  
161
    try {
162
	/*
163
	 * request_url should be of the following format:
164
	 *     <url_for_extension's_dummy_file>?url=<valid_urlencoded_url>
165
	 */
166
	const match = request_url_regex.exec(details.url);
167
	const queried_url = decodeURIComponent(match[1]);
168

  
169
	if (details.initiator && !queried_url.startsWith(details.initiator)) {
170
	    console.warn(`Blocked suspicious query of '${url}' by '${details.initiator}'. This might be the result of page fingerprinting the browser.`);
171
	    return {cancel: true};
172
	}
173

  
174
	policy = decide_policy_for_url(storage, policy_observable, queried_url);
175
    } catch (e) {
176
	console.warn(`Bad request! Expected ${browser.runtime.getURL("dummy")}?url=<valid_urlencoded_url>. Got ${request_url}. This might be the result of page fingerprinting the browser.`);
177
    }
178

  
179
    const encoded_policy = encodeURIComponent(JSON.stringify(policy));
180

  
181
    return {redirectUrl: redirect_url_template + encoded_policy};
182
}
183

  
184
const all_types = [
185
    "main_frame", "sub_frame", "stylesheet", "script", "image", "font",
186
    "object", "xmlhttprequest", "ping", "csp_report", "media", "websocket",
187
    "other", "main_frame", "sub_frame"
188
];
189

  
190
async function start_webRequest_operations()
191
{
192
    storage = await get_storage();
193

  
194
#IF CHROMIUM
195
    const extra_opts = ["blocking", "extraHeaders"];
196
#ELSE
197
    const extra_opts = ["blocking"];
198
#ENDIF
199

  
200
    browser.webRequest.onHeadersReceived.addListener(
201
	sanitize_web_page,
202
	{urls: ["<all_urls>"], types: ["main_frame", "sub_frame"]},
203
	extra_opts.concat("responseHeaders")
204
    );
205

  
206
    const dummy_url_pattern = browser.runtime.getURL("dummy") + "?url=*";
207
    browser.webRequest.onBeforeRequest.addListener(
208
	synchronously_smuggle_policy,
209
	{urls: [dummy_url_pattern], types: ["xmlhttprequest"]},
210
	extra_opts
211
    );
212

  
213
    policy_observable = await light_storage.observe_var("default_allow");
214
}
215

  
216
start_webRequest_operations();
217

  
218
#IF MOZILLA
219
const code = `\
220
console.warn("Hi, I'm Mr Dynamic!");
221

  
222
console.debug("let's see how window.haketilo_exports looks like now");
223

  
224
console.log("haketilo_exports", window.haketilo_exports);
225
`
226

  
227
async function test_dynamic_content_scripts()
228
{
229
    browser.contentScripts.register({
230
	"js": [{code}],
231
	"matches": ["<all_urls>"],
232
	"allFrames": true,
233
	"runAt": "document_start"
234
});
235
}
236

  
237
test_dynamic_content_scripts();
238
#ENDIF
background/page_actions_server.js
1
/**
2
 * This file is part of Haketilo.
3
 *
4
 * Function: Serving page actions to content scripts.
5
 *
6
 * Copyright (C) 2021 Wojtek Kosior
7
 *
8
 * This program is free software: you can redistribute it and/or modify
9
 * it under the terms of the GNU General Public License as published by
10
 * the Free Software Foundation, either version 3 of the License, or
11
 * (at your option) any later version.
12
 *
13
 * This program is distributed in the hope that it will be useful,
14
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16
 * GNU General Public License for more details.
17
 *
18
 * As additional permission under GNU GPL version 3 section 7, you
19
 * may distribute forms of that code without the copy of the GNU
20
 * GPL normally required by section 4, provided you include this
21
 * license notice and, in case of non-source distribution, a URL
22
 * through which recipients can access the Corresponding Source.
23
 * If you modify file(s) with this exception, you may extend this
24
 * exception to your version of the file(s), but you are not
25
 * obligated to do so. If you do not wish to do so, delete this
26
 * exception statement from your version.
27
 *
28
 * As a special exception to the GPL, any HTML file which merely
29
 * makes function calls to this code, and for that purpose
30
 * includes it by reference shall be deemed a separate work for
31
 * copyright law purposes. If you modify this code, you may extend
32
 * this exception to your version of the code, but you are not
33
 * obligated to do so. If you do not wish to do so, delete this
34
 * exception statement from your version.
35
 *
36
 * You should have received a copy of the GNU General Public License
37
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
38
 *
39
 * I, Wojtek Kosior, thereby promise not to sue for violation of this file's
40
 * license. Although I request that you do not make use of this code in a
41
 * proprietary program, I am not going to enforce this in court.
42
 */
43

  
44
#IMPORT common/storage_light.js      AS light_storage
45
#IMPORT common/connection_types.js   AS CONNECTION_TYPE
46

  
47
#FROM common/browser.js        IMPORT browser
48
#FROM common/message_server.js IMPORT listen_for_connection
49
#FROM background/storage.js    IMPORT get_storage
50
#FROM common/stored_types.js   IMPORT TYPE_PREFIX
51
#FROM common/sha256.js         IMPORT sha256
52
#FROM common/ajax.js           IMPORT make_ajax_request
53

  
54
var storage;
55
var handler;
56

  
57
// TODO: parallelize script fetching
58
async function send_scripts(components, port, processed_bags)
59
{
60
    for (let [prefix, name] of components) {
61
	if (prefix === TYPE_PREFIX.BAG) {
62
	    if (processed_bags.has(name)) {
63
		console.log(`preventing recursive inclusion of bag ${name}`);
64
		continue;
65
	    }
66

  
67
	    var bag = storage.get(TYPE_PREFIX.BAG, name);
68

  
69
	    if (bag === undefined) {
70
		console.log(`no bag in storage for key ${name}`);
71
		continue;
72
	    }
73

  
74
	    processed_bags.add(name);
75
	    await send_scripts(bag, port, processed_bags);
76

  
77
	    processed_bags.delete(name);
78
	} else {
79
	    let script_text = await get_script_text(name);
80
	    if (script_text === undefined)
81
		continue;
82

  
83
	    port.postMessage(["inject", [script_text]]);
84
	}
85
    }
86
}
87

  
88
async function get_script_text(script_name)
89
{
90
    try {
91
	let script_data = storage.get(TYPE_PREFIX.SCRIPT, script_name);
92
	if (script_data === undefined) {
93
	    console.log(`missing data for ${script_name}`);
94
	    return;
95
	}
96
	let script_text = script_data.text;
97
	if (!script_text)
98
	    script_text = await fetch_remote_script(script_data);
99
	return script_text;
100
    } catch (e) {
101
	console.log(e);
102
    }
103
}
104

  
105
async function fetch_remote_script(script_data)
106
{
107
    try {
108
	let xhttp = await make_ajax_request("GET", script_data.url);
109
	if (xhttp.status === 200) {
110
	    let computed_hash = sha256(xhttp.responseText);
111
	    if (computed_hash !== script_data.hash) {
112
		console.log(`Bad hash for ${script_data.url}\n    got ${computed_hash} instead of ${script_data.hash}`);
113
		return;
114
	    }
115
	    return xhttp.responseText;
116
	} else {
117
	    console.log("script not fetched: " + script_data.url);
118
	    return;
119
	}
120
    } catch (e) {
121
	console.log(e);
122
    }
123
}
124

  
125
function handle_message(port, message, handler)
126
{
127
    port.onMessage.removeListener(handler[0]);
128
    console.debug(`Loading payload '${message.payload}'.`);
129

  
130
    const processed_bags = new Set();
131

  
132
    send_scripts([message.payload], port, processed_bags);
133
}
134

  
135
function new_connection(port)
136
{
137
    console.log("new page actions connection!");
138
    let handler = [];
139
    handler.push(m => handle_message(port, m, handler));
140
    port.onMessage.addListener(handler[0]);
141
}
142

  
143
async function start()
144
{
145
    storage = await get_storage();
146

  
147
    listen_for_connection(CONNECTION_TYPE.PAGE_ACTIONS, new_connection);
148
}
149
#EXPORT start
background/patterns_query_manager.js
64 64
let script_update_occuring = false;
65 65
let script_update_needed;
66 66

  
67
async function update_content_script()
68
{
67
async function update_content_script() {
69 68
    if (script_update_occuring)
70 69
	return;
71 70

  
......
98 97
}
99 98
#ENDIF
100 99

  
101
function register(kind, object)
102
{
100
function register(kind, object) {
103 101
    if (kind === "mappings") {
104 102
	for (const [pattern, resource] of Object.entries(object.payloads))
105 103
	    pqt.register(tree, pattern, object.identifier, resource);
......
118 116
#ENDIF
119 117
}
120 118

  
121
function changed(kind, change)
122
{
119
function changed(kind, change) {
123 120
    const old_version = currently_registered.get(change.key);
124 121
    if (old_version !== undefined) {
125 122
	if (kind === "mappings") {
......
143 140
#ENDIF
144 141
}
145 142

  
146
async function start(secret_)
147
{
143
function setting_changed(change) {
144
    if (change.key !== "default_allow")
145
	return;
146

  
147
    default_allow.value = (change.new_val || {}).value;
148

  
149
#IF MOZILLA || MV3
150
    script_update_needed = true;
151
    setTimeout(update_content_script, 0);
152
#ENDIF
153
}
154

  
155
async function start(secret_) {
148 156
    secret = secret_;
149 157

  
150 158
    const [mapping_tracking, initial_mappings] =
......
155 163
    initial_mappings.forEach(m => register("mappings", m));
156 164
    initial_blocking.forEach(b => register("blocking", b));
157 165

  
158
    const set_allow_val = ch => default_allow.value = (ch.new_val || {}).value;
159 166
    const [setting_tracking, initial_settings] =
160
	  await haketilodb.track.settings(set_allow_val);
167
	  await haketilodb.track.settings(setting_changed);
168

  
161 169
    for (const setting of initial_settings) {
162 170
	if (setting.name === "default_allow")
163 171
	    Object.assign(default_allow, setting);
background/policy_injector.js
1
/**
2
 * This file is part of Haketilo.
3
 *
4
 * Function: Injecting policy to page by modifying HTTP headers.
5
 *
6
 * Copyright (C) 2021, Wojtek Kosior
7
 * Copyright (C) 2021, jahoti
8
 *
9
 * This program is free software: you can redistribute it and/or modify
10
 * it under the terms of the GNU General Public License as published by
11
 * the Free Software Foundation, either version 3 of the License, or
12
 * (at your option) any later version.
13
 *
14
 * This program is distributed in the hope that it will be useful,
15
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17
 * GNU General Public License for more details.
18
 *
19
 * As additional permission under GNU GPL version 3 section 7, you
20
 * may distribute forms of that code without the copy of the GNU
21
 * GPL normally required by section 4, provided you include this
22
 * license notice and, in case of non-source distribution, a URL
23
 * through which recipients can access the Corresponding Source.
24
 * If you modify file(s) with this exception, you may extend this
25
 * exception to your version of the file(s), but you are not
26
 * obligated to do so. If you do not wish to do so, delete this
27
 * exception statement from your version.
28
 *
29
 * As a special exception to the GPL, any HTML file which merely
30
 * makes function calls to this code, and for that purpose
31
 * includes it by reference shall be deemed a separate work for
32
 * copyright law purposes. If you modify this code, you may extend
33
 * this exception to your version of the code, but you are not
34
 * obligated to do so. If you do not wish to do so, delete this
35
 * exception statement from your version.
36
 *
37
 * You should have received a copy of the GNU General Public License
38
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
39
 *
40
 *
41
 * I, Wojtek Kosior, thereby promise not to sue for violation of this file's
42
 * license. Although I request that you do not make use of this code in a
43
 * proprietary program, I am not going to enforce this in court.
44
 */
45

  
46
#FROM common/misc.js IMPORT csp_header_regex
47

  
48
/* Re-enable the import below once nonce stuff here is ready */
49
#IF NEVER
50
#FROM common/misc.js IMPORT gen_nonce
51
#ENDIF
52

  
53
/* CSP rule that blocks scripts according to policy's needs. */
54
function make_csp_rule(policy)
55
{
56
    let rule = "prefetch-src 'none'; script-src-attr 'none';";
57
    const script_src = policy.nonce !== undefined ?
58
         `'nonce-${policy.nonce}'` : "'none'";
59
    rule += ` script-src ${script_src}; script-src-elem ${script_src};`;
60
    return rule;
61
}
62

  
63
function inject_csp_headers(headers, policy)
64
{
65
    let csp_headers;
66

  
67
    if (policy.payload) {
68
	headers = headers.filter(h => !csp_header_regex.test(h.name));
69

  
70
	// TODO: make CSP rules with nonces and facilitate passing them to
71
	// content scripts via dynamic content script registration or
72
	// synchronous XHRs
73

  
74
	// policy.nonce = gen_nonce();
75
    }
76

  
77
    if (!policy.allow && (policy.nonce || !policy.payload)) {
78
	headers.push({
79
	    name: "content-security-policy",
80
	    value: make_csp_rule(policy)
81
	});
82
    }
83

  
84
    return headers;
85
}
86

  
87
#EXPORT inject_csp_headers
background/storage.js
1
/**
2
 * This file is part of Haketilo.
3
 *
4
 * Function: Storage manager.
5
 *
6
 * Copyright (C) 2021 Wojtek Kosior
7
 *
8
 * This program is free software: you can redistribute it and/or modify
9
 * it under the terms of the GNU General Public License as published by
10
 * the Free Software Foundation, either version 3 of the License, or
11
 * (at your option) any later version.
12
 *
13
 * This program is distributed in the hope that it will be useful,
14
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16
 * GNU General Public License for more details.
17
 *
18
 * As additional permission under GNU GPL version 3 section 7, you
19
 * may distribute forms of that code without the copy of the GNU
20
 * GPL normally required by section 4, provided you include this
21
 * license notice and, in case of non-source distribution, a URL
22
 * through which recipients can access the Corresponding Source.
23
 * If you modify file(s) with this exception, you may extend this
24
 * exception to your version of the file(s), but you are not
25
 * obligated to do so. If you do not wish to do so, delete this
26
 * exception statement from your version.
27
 *
28
 * As a special exception to the GPL, any HTML file which merely
29
 * makes function calls to this code, and for that purpose
30
 * includes it by reference shall be deemed a separate work for
31
 * copyright law purposes. If you modify this code, you may extend
32
 * this exception to your version of the code, but you are not
33
 * obligated to do so. If you do not wish to do so, delete this
34
 * exception statement from your version.
35
 *
36
 * You should have received a copy of the GNU General Public License
37
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
38
 *
39
 * I, Wojtek Kosior, thereby promise not to sue for violation of this file's
40
 * license. Although I request that you do not make use of this code in a
41
 * proprietary program, I am not going to enforce this in court.
42
 */
43

  
44
#IMPORT common/storage_raw.js AS raw_storage
45
#IMPORT common/observables.js
46

  
47
#FROM common/stored_types.js IMPORT list_prefixes, TYPE_NAME
48
#FROM common/lock.js         IMPORT lock, unlock, make_lock
49
#FROM common/once.js         IMPORT make_once
50
#FROM common/browser.js      IMPORT browser
51

  
52
var exports = {};
53

  
54
/* A special case of persisted variable is one that contains list of items. */
55

  
56
async function get_list_var(name)
57
{
58
    let list = await raw_storage.get_var(name);
59

  
60
    return list === undefined ? [] : list;
61
}
62

  
63
/* We maintain in-memory copies of some stored lists. */
64

  
65
async function list(prefix)
66
{
67
    let name = TYPE_NAME[prefix] + "s"; /* Make plural. */
68
    let map = new Map();
69

  
70
    for (let item of await get_list_var(name))
71
	map.set(item, await raw_storage.get(prefix + item));
72

  
73
    return {map, prefix, name, observable: observables.make(),
74
	    lock: make_lock()};
75
}
76

  
77
var list_by_prefix = {};
78

  
79
async function init()
80
{
81
    for (let prefix of list_prefixes)
82
	list_by_prefix[prefix] = await list(prefix);
83

  
84
    return exports;
85
}
86

  
87
/*
88
 * Facilitate listening to changes
89
 */
90

  
91
exports.add_change_listener = function (cb, prefixes=list_prefixes)
92
{
93
    if (typeof(prefixes) === "string")
94
	prefixes = [prefixes];
95

  
96
    for (let prefix of prefixes)
97
	observables.subscribe(list_by_prefix[prefix].observable, cb);
98
}
99

  
100
exports.remove_change_listener = function (cb, prefixes=list_prefixes)
101
{
102
    if (typeof(prefixes) === "string")
103
	prefixes = [prefixes];
104

  
105
    for (let prefix of prefixes)
106
	observables.unsubscribe(list_by_prefix[prefix].observable, cb);
107
}
108

  
109
/* Prepare some hepler functions to get elements of a list */
110

  
111
function list_items_it(list, with_values=false)
112
{
113
    return with_values ? list.map.entries() : list.map.keys();
114
}
115

  
116
function list_entries_it(list)
117
{
118
    return list_items_it(list, true);
119
}
120

  
121
function list_items(list, with_values=false)
122
{
123
    let array = [];
124

  
125
    for (let item of list_items_it(list, with_values))
126
	array.push(item);
127

  
128
    return array;
129
}
130

  
131
function list_entries(list)
132
{
133
    return list_items(list, true);
134
}
135

  
136
/*
137
 * Below we make additional effort to update map of given kind of items
138
 * every time an item is added/removed to keep everything coherent.
139
 */
140
async function set_item(item, value, list)
141
{
142
    await lock(list.lock);
143
    let result = await _set_item(...arguments);
144
    unlock(list.lock)
145
    return result;
146
}
147
async function _set_item(item, value, list)
148
{
149
    const key = list.prefix + item;
150
    const old_val = list.map.get(item);
151
    const set_obj = {[key]: value};
152
    if (old_val === undefined) {
153
	const items = list_items(list);
154
	items.push(item);
155
	set_obj["_" + list.name] = items;
156
    }
157

  
158
    await raw_storage.set(set_obj);
159
    list.map.set(item, value);
160

  
161
    const change = {
162
	prefix : list.prefix,
163
	item,
164
	old_val,
165
	new_val : value
166
    };
167

  
168
    observables.broadcast(list.observable, change);
169

  
170
    return old_val;
171
}
172

  
173
// TODO: The actual idea to set value to undefined is good - this way we can
174
//       also set a new list of items in the same API call. But such key
175
//       is still stored in the storage. We need to somehow remove it later.
176
//       For that, we're going to have to store 1 more list of each kind.
177
async function remove_item(item, list)
178
{
179
    await lock(list.lock);
180
    let result = await _remove_item(...arguments);
181
    unlock(list.lock)
182
    return result;
183
}
184
async function _remove_item(item, list)
185
{
186
    const old_val = list.map.get(item);
187
    if (old_val === undefined)
188
	return;
189

  
190
    const items = list_items(list);
191
    const index = items.indexOf(item);
192
    items.splice(index, 1);
193

  
194
    await raw_storage.set({
195
	[list.prefix + item]: undefined,
196
	["_" + list.name]: items
197
    });
198
    list.map.delete(item);
199

  
200
    const change = {
201
	prefix : list.prefix,
202
	item,
203
	old_val,
204
	new_val : undefined
205
    };
206

  
207
    observables.broadcast(list.observable, change);
208

  
209
    return old_val;
210
}
211

  
212
// TODO: same as above applies here
213
async function replace_item(old_item, new_item, list, new_val=undefined)
214
{
215
    await lock(list.lock);
216
    let result = await _replace_item(...arguments);
217
    unlock(list.lock)
218
    return result;
219
}
220
async function _replace_item(old_item, new_item, list, new_val=undefined)
221
{
222
    const old_val = list.map.get(old_item);
223
    if (new_val === undefined) {
224
	if (old_val === undefined)
225
	    return;
226
	new_val = old_val;
227
    } else if (new_val === old_val && new_item === old_item) {
228
	return old_val;
229
    }
230

  
231
    if (old_item === new_item || old_val === undefined) {
232
	await _set_item(new_item, new_val, list);
233
	return old_val;
234
    }
235

  
236
    const items = list_items(list);
237
    const index = items.indexOf(old_item);
238
    items[index] = new_item;
239

  
240
    await raw_storage.set({
241
	[list.prefix + old_item]: undefined,
242
	[list.prefix + new_item]: new_val,
243
	["_" + list.name]: items
244
    });
245
    list.map.delete(old_item);
246

  
247
    const change = {
248
	prefix : list.prefix,
249
	item : old_item,
250
	old_val,
251
	new_val : undefined
252
    };
253

  
254
    observables.broadcast(list.observable, change);
255

  
256
    list.map.set(new_item, new_val);
257

  
258
    change.item = new_item;
259
    change.old_val = undefined;
260
    change.new_val = new_val;
261

  
262
    observables.broadcast(list.observable, change);
263

  
264
    return old_val;
265
}
266

  
267
/*
268
 * For scripts, item name is chosen by user, data should be
269
 * an object containing:
270
 * - script's url and hash or
271
 * - script's text or
272
 * - all three
273
 */
274

  
275
/*
276
 * For bags, item name is chosen by user, data is an array of 2-element
277
 * arrays with type prefix and script/bag names.
278
 */
279

  
280
/*
281
 * For pages data argument is an object with properties `allow'
282
 * and `components'. Item name is url.
283
 */
284

  
285
exports.set = async function (prefix, item, data)
286
{
287
    return set_item(item, data, list_by_prefix[prefix]);
288
}
289

  
290
exports.get = function (prefix, item)
291
{
292
    return list_by_prefix[prefix].map.get(item);
293
}
294

  
295
exports.remove = async function (prefix, item)
296
{
297
    return remove_item(item, list_by_prefix[prefix]);
298
}
299

  
300
exports.replace = async function (prefix, old_item, new_item,
301
				  new_data=undefined)
302
{
303
    return replace_item(old_item, new_item, list_by_prefix[prefix],
304
			new_data);
305
}
306

  
307
exports.get_all_names = function (prefix)
308
{
309
    return list_items(list_by_prefix[prefix]);
310
}
311

  
312
exports.get_all_names_it = function (prefix)
313
{
314
    return list_items_it(list_by_prefix[prefix]);
315
}
316

  
317
exports.get_all = function (prefix)
318
{
319
    return list_entries(list_by_prefix[prefix]);
320
}
321

  
322
exports.get_all_it = function (prefix)
323
{
324
    return list_entries_it(list_by_prefix[prefix]);
325
}
326

  
327
/* Finally, a quick way to wipe all the data. */
328
// TODO: maybe delete items in such order that none of them ever references
329
// an already-deleted one?
330
exports.clear = async function ()
331
{
332
    let lists = list_prefixes.map((p) => list_by_prefix[p]);
333

  
334
    for (let list of lists)
335
	await lock(list.lock);
336

  
337
    for (let list of lists) {
338

  
339
	let change = {
340
	    prefix : list.prefix,
341
	    new_val : undefined
342
	};
343

  
344
	for (let [item, val] of list_entries_it(list)) {
345
	    change.item = item;
346
	    change.old_val = val;
347
	    observables.broadcast(list.observable, change);
348
	}
349

  
350
	list.map = new Map();
351
    }
352

  
353
    await browser.storage.local.clear();
354

  
355
    for (let list of lists)
356
	unlock(list.lock);
357
}
358

  
359
#EXPORT make_once(init) AS get_storage
background/storage_server.js
1
/**
2
 * This file is part of Haketilo.
3
 *
4
 * Function: Storage through messages (server side).
5
 *
6
 * Copyright (C) 2021 Wojtek Kosior
7
 *
8
 * This program is free software: you can redistribute it and/or modify
9
 * it under the terms of the GNU General Public License as published by
10
 * the Free Software Foundation, either version 3 of the License, or
11
 * (at your option) any later version.
12
 *
13
 * This program is distributed in the hope that it will be useful,
14
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16
 * GNU General Public License for more details.
17
 *
18
 * As additional permission under GNU GPL version 3 section 7, you
19
 * may distribute forms of that code without the copy of the GNU
20
 * GPL normally required by section 4, provided you include this
21
 * license notice and, in case of non-source distribution, a URL
22
 * through which recipients can access the Corresponding Source.
23
 * If you modify file(s) with this exception, you may extend this
24
 * exception to your version of the file(s), but you are not
25
 * obligated to do so. If you do not wish to do so, delete this
26
 * exception statement from your version.
27
 *
28
 * As a special exception to the GPL, any HTML file which merely
29
 * makes function calls to this code, and for that purpose
30
 * includes it by reference shall be deemed a separate work for
31
 * copyright law purposes. If you modify this code, you may extend
32
 * this exception to your version of the code, but you are not
33
 * obligated to do so. If you do not wish to do so, delete this
34
 * exception statement from your version.
35
 *
36
 * You should have received a copy of the GNU General Public License
37
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
38
 *
39
 * I, Wojtek Kosior, thereby promise not to sue for violation of this file's
40
 * license. Although I request that you do not make use of this code in a
41
 * proprietary program, I am not going to enforce this in court.
42
 */
43

  
44
#IMPORT common/connection_types.js AS CONNECTION_TYPE
45

  
46
#FROM common/message_server.js IMPORT listen_for_connection
47
#FROM background/storage.js    IMPORT get_storage
48
#FROM common/stored_types.js   IMPORT list_prefixes
49

  
50
var storage;
51

  
52
async function handle_remote_call(port, message)
53
{
54
    let [call_id, func, args] = message;
55

  
56
    try {
57
	let result = await Promise.resolve(storage[func](...args));
58
	port.postMessage({call_id, result});
59
    } catch (error) {
60
	error = error + '';
61
	port.postMessage({call_id, error});
62
    }
63
}
64

  
65
function remove_storage_listener(cb)
66
{
67
    storage.remove_change_listener(cb);
68
}
69

  
70
function new_connection(port)
71
{
72
    console.log("new remote storage connection!");
73

  
74
    const message = {};
75
    for (const prefix of list_prefixes)
76
	message[prefix] = storage.get_all(prefix);
77

  
78
    port.postMessage(message);
79

  
80
    let handle_change = change => port.postMessage(change);
81

  
82
    storage.add_change_listener(handle_change);
83

  
84
    port.onMessage.addListener(m => handle_remote_call(port, m));
85
    port.onDisconnect.addListener(() =>
86
				  remove_storage_listener(handle_change));
87
}
88

  
89
async function start()
90
{
91
    storage = await get_storage();
92

  
93
    listen_for_connection(CONNECTION_TYPE.REMOTE_STORAGE, new_connection);
94
}
95
#EXPORT start
background/webrequest.js
172 172
	extra_opts
173 173
    );
174 174
#ENDIF
175

  
176
    await track_default_allow();
177 175
}
178 176
#EXPORT start
common/ajax.js
1
/**
2
 * This file is part of Haketilo.
3
 *
4
 * Function: Wrapping XMLHttpRequest into a Promise.
5
 *
6
 * Copyright (C) 2021 Wojtek Kosior
7
 *
8
 * This program is free software: you can redistribute it and/or modify
9
 * it under the terms of the GNU General Public License as published by
10
 * the Free Software Foundation, either version 3 of the License, or
11
 * (at your option) any later version.
12
 *
13
 * This program is distributed in the hope that it will be useful,
14
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16
 * GNU General Public License for more details.
17
 *
18
 * As additional permission under GNU GPL version 3 section 7, you
19
 * may distribute forms of that code without the copy of the GNU
20
 * GPL normally required by section 4, provided you include this
21
 * license notice and, in case of non-source distribution, a URL
22
 * through which recipients can access the Corresponding Source.
23
 * If you modify file(s) with this exception, you may extend this
24
 * exception to your version of the file(s), but you are not
25
 * obligated to do so. If you do not wish to do so, delete this
26
 * exception statement from your version.
27
 *
28
 * As a special exception to the GPL, any HTML file which merely
29
 * makes function calls to this code, and for that purpose
30
 * includes it by reference shall be deemed a separate work for
31
 * copyright law purposes. If you modify this code, you may extend
32
 * this exception to your version of the code, but you are not
33
 * obligated to do so. If you do not wish to do so, delete this
34
 * exception statement from your version.
35
 *
36
 * You should have received a copy of the GNU General Public License
37
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
38
 *
39
 * I, Wojtek Kosior, thereby promise not to sue for violation of this file's
40
 * license. Although I request that you do not make use of this code in a
41
 * proprietary program, I am not going to enforce this in court.
42
 */
43

  
44
function ajax_callback()
45
{
46
    if (this.readyState == 4)
47
	this.resolve_callback(this);
48
}
49

  
50
function initiate_ajax_request(resolve, reject, method, url)
51
{
52
    const xhttp = new XMLHttpRequest();
53
    xhttp.resolve_callback = resolve;
54
    xhttp.onreadystatechange = ajax_callback;
55
    xhttp.open(method, url, true);
56
    try {
57
	xhttp.send();
58
    } catch(e) {
59
	console.log(e);
60
	setTimeout(reject, 0);
61
    }
62
}
63

  
64
function make_ajax_request(method, url)
65
{
66
    return new Promise((resolve, reject) =>
67
		       initiate_ajax_request(resolve, reject, method, url));
68
}
69

  
70
#EXPORT make_ajax_request
common/broadcast.js
41 41
 * proprietary program, I am not going to enforce this in court.
42 42
 */
43 43

  
44
#IMPORT common/connection_types.js AS CONNECTION_TYPE
45

  
46 44
#FROM common/message_server.js IMPORT connect_to_background
47 45

  
48 46
function sender_connection()
49 47
{
50 48
    return {
51
	port: connect_to_background(CONNECTION_TYPE.BROADCAST_SEND)
49
	port: connect_to_background("broadcast_send")
52 50
    };
53 51
}
54 52
#EXPORT sender_connection
......
94 92
function listener_connection(cb)
95 93
{
96 94
    const conn = {
97
	port: connect_to_background(CONNECTION_TYPE.BROADCAST_LISTEN)
95
	port: connect_to_background("broadcast_listen")
98 96
    };
99 97

  
100 98
    conn.port.onMessage.addListener(cb);
common/connection_types.js
1
/**
2
 * This file is part of Haketilo.
3
 *
4
 * Function: Define an "enum" of message connection types.
5
 *
6
 * Copyright (C) 2021 Wojtek Kosior
7
 *
8
 * This program is free software: you can redistribute it and/or modify
9
 * it under the terms of the GNU General Public License as published by
10
 * the Free Software Foundation, either version 3 of the License, or
11
 * (at your option) any later version.
12
 *
13
 * This program is distributed in the hope that it will be useful,
14
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16
 * GNU General Public License for more details.
17
 *
18
 * As additional permission under GNU GPL version 3 section 7, you
19
 * may distribute forms of that code without the copy of the GNU
20
 * GPL normally required by section 4, provided you include this
21
 * license notice and, in case of non-source distribution, a URL
22
 * through which recipients can access the Corresponding Source.
23
 * If you modify file(s) with this exception, you may extend this
24
 * exception to your version of the file(s), but you are not
25
 * obligated to do so. If you do not wish to do so, delete this
26
 * exception statement from your version.
27
 *
28
 * As a special exception to the GPL, any HTML file which merely
29
 * makes function calls to this code, and for that purpose
30
 * includes it by reference shall be deemed a separate work for
31
 * copyright law purposes. If you modify this code, you may extend
32
 * this exception to your version of the code, but you are not
33
 * obligated to do so. If you do not wish to do so, delete this
34
 * exception statement from your version.
35
 *
36
 * You should have received a copy of the GNU General Public License
37
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
38
 *
39
 * I, Wojtek Kosior, thereby promise not to sue for violation of this file's
40
 * license. Although I request that you do not make use of this code in a
41
 * proprietary program, I am not going to enforce this in court.
42
 */
43

  
44
/*
45
 * Those need to be strings so they can be used as 'name' parameter
46
 * to browser.runtime.connect()
47
 */
48

  
49
#EXPORT  "0"  AS REMOTE_STORAGE
50
#EXPORT  "1"  AS PAGE_ACTIONS
51
#EXPORT  "2"  AS ACTIVITY_INFO
52
#EXPORT  "3"  AS BROADCAST_SEND
53
#EXPORT  "4"  AS BROADCAST_LISTEN
common/indexeddb.js
48 48
#IF UNIT_TEST
49 49
    {}
50 50
#ELSE
51
#INCLUDE_VERBATIM default_settings.json
51
#INCLUDE default_settings.json
52 52
#ENDIF
53 53
);
54 54

  
common/lock.js
1
/**
2
 * This file is part of Haketilo.
3
 *
4
 * Function: Implement a lock (aka binary semaphore aka mutex).
5
 *
6
 * Copyright (C) 2021 Wojtek Kosior
7
 *
8
 * This program is free software: you can redistribute it and/or modify
9
 * it under the terms of the GNU General Public License as published by
10
 * the Free Software Foundation, either version 3 of the License, or
11
 * (at your option) any later version.
12
 *
13
 * This program is distributed in the hope that it will be useful,
14
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16
 * GNU General Public License for more details.
17
 *
18
 * As additional permission under GNU GPL version 3 section 7, you
19
 * may distribute forms of that code without the copy of the GNU
20
 * GPL normally required by section 4, provided you include this
21
 * license notice and, in case of non-source distribution, a URL
22
 * through which recipients can access the Corresponding Source.
23
 * If you modify file(s) with this exception, you may extend this
24
 * exception to your version of the file(s), but you are not
25
 * obligated to do so. If you do not wish to do so, delete this
26
 * exception statement from your version.
27
 *
28
 * As a special exception to the GPL, any HTML file which merely
29
 * makes function calls to this code, and for that purpose
30
 * includes it by reference shall be deemed a separate work for
31
 * copyright law purposes. If you modify this code, you may extend
32
 * this exception to your version of the code, but you are not
33
 * obligated to do so. If you do not wish to do so, delete this
34
 * exception statement from your version.
35
 *
36
 * You should have received a copy of the GNU General Public License
37
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
38
 *
39
 * I, Wojtek Kosior, thereby promise not to sue for violation of this file's
40
 * license. Although I request that you do not make use of this code in a
41
 * proprietary program, I am not going to enforce this in court.
42
 */
43

  
44
/*
45
 * Javascript runs single-threaded, with an event loop. Because of that,
46
 * explicit synchronization is often not needed. An exception is when we use
47
 * an API function that must wait. Ajax is an example. Callback passed to ajax
48
 * call doesn't get called immediately, but after some time. In the meantime
49
 * some other piece of code might get to execute and modify some variables.
50
 * Access to WebExtension local storage is another situation where this problem
51
 * can occur.
52
 *
53
 * This is a solution. A lock object, that can be used to delay execution of
54
 * some code until other code finishes its critical work. Locking is wrapped
55
 * in a promise.
56
 */
57

  
58
#EXPORT  () => ({free: true, queue: []})  AS make_lock
59

  
60
function _lock(lock, cb) {
61
    if (lock.free) {
62
	lock.free = false;
63
	setTimeout(cb);
64
    } else {
65
	lock.queue.push(cb);
66
    }
67
}
68

  
69
#EXPORT  lock => new Promise(resolve => _lock(lock, resolve))  AS lock
70

  
71
function try_lock(lock)
72
{
73
    if (lock.free) {
74
	lock.free = false;
75
	return true;
76
    }
77

  
78
    return false;
79
}
80
#EXPORT try_lock
81

  
82
function unlock(lock) {
83
    if (lock.free)
84
	throw new Exception("Attempting to release a free lock");
85

  
86
    if (lock.queue.length === 0) {
87
	lock.free = true;
88
    } else {
89
	let cb = lock.queue[0];
90
	lock.queue.splice(0, 1);
91
	setTimeout(cb);
92
    }
93
}
94
#EXPORT unlock
common/message_server.js
97 97
	return browser.runtime.connect({name: magic});
98 98

  
99 99
    if (!(magic in listeners))
100
	throw `no listener for '${magic}'`
100
	throw `no listener for '${magic}'`;
101 101

  
102 102
    const ports = [new Port(magic), new Port(magic)];
103 103
    ports[0].other = ports[1];
common/misc.js
42 42
 * proprietary program, I am not going to enforce this in court.
43 43
 */
44 44

  
45
#FROM common/browser.js      IMPORT browser
46
#FROM common/stored_types.js IMPORT TYPE_NAME, TYPE_PREFIX
47

  
48 45
/* uint8_to_hex is a separate function used in cryptographic functions. */
49 46
const uint8_to_hex =
50 47
      array => [...array].map(b => ("0" + b.toString(16)).slice(-2)).join("");
......
83 80
 */
84 81
#EXPORT  (prefix, name) => `${name} (${TYPE_NAME[prefix]})`  AS nice_name
85 82

  
86
/* Open settings tab with given item's editing already on. */
87
function open_in_settings(prefix, name)
88
{
89
    name = encodeURIComponent(name);
90
    const url = browser.runtime.getURL("html/options.html#" + prefix + name);
91
    window.open(url, "_blank");
92
}
93
#EXPORT open_in_settings
94

  
95 83
/*
96 84
 * Check if url corresponds to a browser's special page (or a directory index in
97 85
 * case of `file://' protocol).
......
102 90
const priv_reg = /^chrome(-extension)?:\/\/|^about:|^file:\/\/[^?#]*\/([?#]|$)/;
103 91
#ENDIF
104 92
#EXPORT  url => priv_reg.test(url)  AS is_privileged_url
105

  
106
/* Parse a CSP header */
107
function parse_csp(csp) {
108
    let directive, directive_array;
109
    let directives = {};
110
    for (directive of csp.split(';')) {
111
	directive = directive.trim();
112
	if (directive === '')
113
	    continue;
114

  
115
	directive_array = directive.split(/\s+/);
116
	directive = directive_array.shift();
117
	/* The "true" case should never occur; nevertheless... */
118
	directives[directive] = directive in directives ?
119
	    directives[directive].concat(directive_array) :
120
	    directive_array;
121
    }
122
    return directives;
123
}
124

  
125
/* Regexes and objects to use as/in schemas for parse_json_with_schema(). */
... This diff was truncated because it exceeds the maximum size that can be displayed.

Also available in: Unified diff