Revision b7e2870f
Added by koszko about 2 years ago
background/main.js | ||
---|---|---|
12 | 12 |
* IMPORT start_storage_server |
13 | 13 |
* IMPORT start_page_actions_server |
14 | 14 |
* IMPORT start_policy_injector |
15 |
* IMPORT start_page_info_server |
|
15 | 16 |
* IMPORT browser |
16 | 17 |
* IMPORTS_END |
17 | 18 |
*/ |
... | ... | |
19 | 20 |
start_storage_server(); |
20 | 21 |
start_page_actions_server(); |
21 | 22 |
start_policy_injector(); |
23 |
start_page_info_server(); |
|
22 | 24 |
|
23 | 25 |
async function init_myext(install_details) |
24 | 26 |
{ |
background/page_actions_server.js | ||
---|---|---|
21 | 21 |
var query_best; |
22 | 22 |
var handler; |
23 | 23 |
|
24 |
function send_scripts(url, port)
|
|
24 |
function send_actions(url, port)
|
|
25 | 25 |
{ |
26 | 26 |
let [pattern, settings] = query_best(url); |
27 |
|
|
28 |
port.postMessage(["settings", [pattern, settings]]); |
|
29 |
|
|
27 | 30 |
if (settings === undefined) |
28 | 31 |
return; |
29 | 32 |
|
... | ... | |
31 | 34 |
let processed_bags = new Set(); |
32 | 35 |
|
33 | 36 |
if (components !== undefined) |
34 |
send_scripts_rec([components], port, processed_bags);
|
|
37 |
send_scripts([components], port, processed_bags); |
|
35 | 38 |
} |
36 | 39 |
|
37 | 40 |
// TODO: parallelize script fetching |
38 |
async function send_scripts_rec(components, port, processed_bags)
|
|
41 |
async function send_scripts(components, port, processed_bags) |
|
39 | 42 |
{ |
40 | 43 |
for (let [prefix, name] of components) { |
41 | 44 |
if (prefix === TYPE_PREFIX.BAG) { |
... | ... | |
52 | 55 |
} |
53 | 56 |
|
54 | 57 |
processed_bags.add(name); |
55 |
await send_scripts_rec(bag, port, processed_bags); |
|
58 |
await send_scripts(bag, port, processed_bags); |
|
59 |
|
|
56 | 60 |
processed_bags.delete(name); |
57 | 61 |
} else { |
58 | 62 |
let script_text = await get_script_text(name); |
59 | 63 |
if (script_text === undefined) |
60 | 64 |
continue; |
61 | 65 |
|
62 |
port.postMessage({inject : [script_text]});
|
|
66 |
port.postMessage(["inject", [script_text]]);
|
|
63 | 67 |
} |
64 | 68 |
} |
65 | 69 |
} |
... | ... | |
127 | 131 |
port.onMessage.removeListener(handler[0]); |
128 | 132 |
let url = message.url; |
129 | 133 |
console.log({url}); |
130 |
send_scripts(url, port);
|
|
134 |
send_actions(url, port);
|
|
131 | 135 |
} |
132 | 136 |
|
133 | 137 |
function new_connection(port) |
background/page_info_server.js | ||
---|---|---|
1 |
/** |
|
2 |
* part of Hachette |
|
3 |
* Serving of storage data corresponding to requested urls (server side). |
|
4 |
* |
|
5 |
* Copyright (C) 2021 Wojtek Kosior |
|
6 |
* Redistribution terms are gathered in the `copyright' file. |
|
7 |
*/ |
|
8 |
|
|
9 |
/* |
|
10 |
* IMPORTS_START |
|
11 |
* IMPORT listen_for_connection |
|
12 |
* IMPORT get_storage |
|
13 |
* IMPORT get_query_all |
|
14 |
* IMPORT TYPE_PREFIX |
|
15 |
* IMPORT CONNECTION_TYPE |
|
16 |
* IMPORT url_matches |
|
17 |
* IMPORTS_END |
|
18 |
*/ |
|
19 |
|
|
20 |
var storage; |
|
21 |
var query_all; |
|
22 |
|
|
23 |
function handle_change(connection_data, change) |
|
24 |
{ |
|
25 |
if (change.prefix !== TYPE_PREFIX.PAGE) |
|
26 |
return; |
|
27 |
|
|
28 |
connection_data.port.postMessage(["change", change]); |
|
29 |
} |
|
30 |
|
|
31 |
async function handle_subscription(connection_data, message) |
|
32 |
{ |
|
33 |
const [action, url] = message; |
|
34 |
if (action === "unsubscribe") { |
|
35 |
connection_data.subscribed.delete(url); |
|
36 |
return; |
|
37 |
} |
|
38 |
|
|
39 |
connection_data.subscribed.add(url); |
|
40 |
connection_data.port.postMessage(["new_url", query_all(url)]); |
|
41 |
} |
|
42 |
|
|
43 |
function remove_storage_listener(cb) |
|
44 |
{ |
|
45 |
storage.remove_change_listener(cb); |
|
46 |
} |
|
47 |
|
|
48 |
function new_connection(port) |
|
49 |
{ |
|
50 |
console.log("new page info connection!"); |
|
51 |
|
|
52 |
const connection_data = { |
|
53 |
subscribed : new Set(), |
|
54 |
port |
|
55 |
}; |
|
56 |
|
|
57 |
let _handle_change = change => handle_change(connection_data, change); |
|
58 |
|
|
59 |
storage.add_change_listener(_handle_change); |
|
60 |
|
|
61 |
port.onMessage.addListener(m => handle_subscription(connection_data, m)); |
|
62 |
port.onDisconnect.addListener(() => remove_storage_listener(handle_change)); |
|
63 |
} |
|
64 |
|
|
65 |
async function start_page_info_server() |
|
66 |
{ |
|
67 |
storage = await get_storage(); |
|
68 |
query_all = await get_query_all(); |
|
69 |
|
|
70 |
listen_for_connection(CONNECTION_TYPE.PAGE_INFO, new_connection); |
|
71 |
} |
|
72 |
|
|
73 |
/* |
|
74 |
* EXPORTS_START |
|
75 |
* EXPORT start_page_info_server |
|
76 |
* EXPORTS_END |
|
77 |
*/ |
background/settings_query.js | ||
---|---|---|
10 | 10 |
* IMPORT make_once |
11 | 11 |
* IMPORT get_storage |
12 | 12 |
* IMPORT TYPE_PREFIX |
13 |
* IMPORT for_each_possible_pattern |
|
13 | 14 |
* IMPORTS_END |
14 | 15 |
*/ |
15 | 16 |
|
16 | 17 |
var storage; |
17 | 18 |
|
18 |
var exports = {}; |
|
19 |
|
|
20 | 19 |
async function init(fun) |
21 | 20 |
{ |
22 | 21 |
storage = await get_storage(); |
... | ... | |
24 | 23 |
return fun; |
25 | 24 |
} |
26 | 25 |
|
27 |
// TODO: also support urls with specified ports |
|
28 |
function query(url, multiple) |
|
26 |
function check_pattern(pattern, multiple, matched) |
|
29 | 27 |
{ |
30 |
let proto_re = "[a-zA-Z]*:\/\/"; |
|
31 |
let domain_re = "[^/?#]+"; |
|
32 |
let segments_re = "/[^?#]*"; |
|
33 |
let query_re = "\\?[^#]*"; |
|
34 |
|
|
35 |
let url_regex = new RegExp(`\ |
|
36 |
^\ |
|
37 |
(${proto_re})\ |
|
38 |
(${domain_re})\ |
|
39 |
(${segments_re})?\ |
|
40 |
(${query_re})?\ |
|
41 |
#?.*\$\ |
|
42 |
`); |
|
43 |
|
|
44 |
let regex_match = url_regex.exec(url); |
|
45 |
if (regex_match === null) { |
|
46 |
console.log("bad url format", url); |
|
47 |
return multiple ? [] : [undefined, undefined]; |
|
48 |
} |
|
49 |
|
|
50 |
let [_, proto, domain, segments, query] = regex_match; |
|
51 |
|
|
52 |
domain = domain.split("."); |
|
53 |
let segments_trailing_dash = |
|
54 |
segments && segments[segments.length - 1] === "/"; |
|
55 |
segments = (segments || "").split("/").filter(s => s !== ""); |
|
56 |
segments.unshift(""); |
|
57 |
|
|
58 |
let matched = []; |
|
28 |
const settings = storage.get(TYPE_PREFIX.PAGE, pattern); |
|
59 | 29 |
|
60 |
for (let d_slice = 0; d_slice < domain.length; d_slice++) { |
|
61 |
let domain_part = domain.slice(d_slice).join("."); |
|
62 |
let domain_wildcards = []; |
|
63 |
if (d_slice === 0) |
|
64 |
domain_wildcards.push(""); |
|
65 |
if (d_slice === 1) |
|
66 |
domain_wildcards.push("*."); |
|
67 |
if (d_slice > 0) |
|
68 |
domain_wildcards.push("**."); |
|
69 |
domain_wildcards.push("***."); |
|
30 |
if (settings === undefined) |
|
31 |
return; |
|
70 | 32 |
|
71 |
for (let domain_wildcard of domain_wildcards) { |
|
72 |
let domain_pattern = domain_wildcard + domain_part; |
|
33 |
matched.push([pattern, settings]); |
|
73 | 34 |
|
74 |
for (let s_slice = segments.length; s_slice > 0; s_slice--) { |
|
75 |
let segments_part = segments.slice(0, s_slice).join("/"); |
|
76 |
let segments_wildcards = []; |
|
77 |
if (s_slice === segments.length) { |
|
78 |
if (segments_trailing_dash) |
|
79 |
segments_wildcards.push("/"); |
|
80 |
segments_wildcards.push(""); |
|
81 |
} |
|
82 |
if (s_slice === segments.length - 1) { |
|
83 |
if (segments[s_slice] !== "*") |
|
84 |
segments_wildcards.push("/*"); |
|
85 |
} |
|
86 |
if (s_slice < segments.length && |
|
87 |
(segments[s_slice] !== "**" || |
|
88 |
s_slice < segments.length - 1)) |
|
89 |
segments_wildcards.push("/**"); |
|
90 |
if (segments[s_slice] !== "***" || |
|
91 |
s_slice < segments.length) |
|
92 |
segments_wildcards.push("/***"); |
|
93 |
|
|
94 |
for (let segments_wildcard of segments_wildcards) { |
|
95 |
let segments_pattern = |
|
96 |
segments_part + segments_wildcard; |
|
97 |
|
|
98 |
let pattern = proto + domain_pattern + segments_pattern; |
|
99 |
console.log("trying", pattern); |
|
100 |
let settings = storage.get(TYPE_PREFIX.PAGE, pattern); |
|
101 |
|
|
102 |
if (settings === undefined) |
|
103 |
continue; |
|
104 |
|
|
105 |
if (!multiple) |
|
106 |
return [pattern, settings]; |
|
35 |
if (!multiple) |
|
36 |
return false; |
|
37 |
} |
|
107 | 38 |
|
108 |
matched.push([pattern, settings]); |
|
109 |
} |
|
110 |
} |
|
111 |
} |
|
112 |
} |
|
39 |
function query(url, multiple) |
|
40 |
{ |
|
41 |
const matched = []; |
|
42 |
for_each_possible_pattern(url, p => check_pattern(p, multiple, matched)); |
|
113 | 43 |
|
114 |
return multiple ? matched : [undefined, undefined];
|
|
44 |
return multiple ? matched : (matched[0] || [undefined, undefined]);
|
|
115 | 45 |
} |
116 | 46 |
|
117 | 47 |
function query_best(url) |
common/connection_types.js | ||
---|---|---|
12 | 12 |
|
13 | 13 |
const CONNECTION_TYPE = { |
14 | 14 |
REMOTE_STORAGE : "0", |
15 |
PAGE_ACTIONS : "1" |
|
15 |
PAGE_ACTIONS : "1", |
|
16 |
PAGE_INFO : "2", |
|
17 |
ACTIVITY_INFO : "3" |
|
16 | 18 |
}; |
17 | 19 |
|
18 | 20 |
/* |
common/misc.js | ||
---|---|---|
10 | 10 |
* IMPORT sha256 |
11 | 11 |
* IMPORT browser |
12 | 12 |
* IMPORT is_chrome |
13 |
* IMPORT TYPE_NAME |
|
13 | 14 |
* IMPORTS_END |
14 | 15 |
*/ |
15 | 16 |
|
... | ... | |
71 | 72 |
return rule; |
72 | 73 |
} |
73 | 74 |
|
75 |
/* |
|
76 |
* Print item together with type, e.g. |
|
77 |
* nice_name("s", "hello") → "hello (script)" |
|
78 |
*/ |
|
79 |
function nice_name(prefix, name) |
|
80 |
{ |
|
81 |
return `${name} (${TYPE_NAME[prefix]})`; |
|
82 |
} |
|
83 |
|
|
84 |
/* Open settings tab with given item's editing already on. */ |
|
85 |
function open_in_settings(prefix, name) |
|
86 |
{ |
|
87 |
name = encodeURIComponent(name); |
|
88 |
const url = browser.runtime.getURL("html/options.html#" + prefix + name); |
|
89 |
window.open(url, "_blank"); |
|
90 |
} |
|
91 |
|
|
92 |
/* Check if url corresponds to a browser's special page */ |
|
93 |
function is_privileged_url(url) |
|
94 |
{ |
|
95 |
return !!/^(chrome(-extension)?|moz-extension):\/\/|^about:/i.exec(url); |
|
96 |
} |
|
97 |
|
|
74 | 98 |
/* |
75 | 99 |
* EXPORTS_START |
76 | 100 |
* EXPORT gen_unique |
77 | 101 |
* EXPORT url_item |
78 | 102 |
* EXPORT url_extract_target |
79 | 103 |
* EXPORT csp_rule |
104 |
* EXPORT nice_name |
|
105 |
* EXPORT open_in_settings |
|
106 |
* EXPORT is_privileged_url |
|
80 | 107 |
* EXPORTS_END |
81 | 108 |
*/ |
common/patterns.js | ||
---|---|---|
1 |
/** |
|
2 |
* Hydrilla/Lernette operations on page url patterns |
|
3 |
* |
|
4 |
* Copyright (C) 2021 Wojtek Kosior |
|
5 |
* Redistribution terms are gathered in the `copyright' file. |
|
6 |
*/ |
|
7 |
|
|
8 |
const proto_re = "[a-zA-Z]*:\/\/"; |
|
9 |
const domain_re = "[^/?#]+"; |
|
10 |
const segments_re = "/[^?#]*"; |
|
11 |
const query_re = "\\?[^#]*"; |
|
12 |
|
|
13 |
const url_regex = new RegExp(`\ |
|
14 |
^\ |
|
15 |
(${proto_re})\ |
|
16 |
(${domain_re})\ |
|
17 |
(${segments_re})?\ |
|
18 |
(${query_re})?\ |
|
19 |
#?.*\$\ |
|
20 |
`); |
|
21 |
|
|
22 |
function deconstruct_url(url) |
|
23 |
{ |
|
24 |
const regex_match = url_regex.exec(url); |
|
25 |
if (regex_match === null) |
|
26 |
return undefined; |
|
27 |
|
|
28 |
let [_, proto, domain, path, query] = regex_match; |
|
29 |
|
|
30 |
domain = domain.split("."); |
|
31 |
let path_trailing_dash = |
|
32 |
path && path[path.length - 1] === "/"; |
|
33 |
path = (path || "").split("/").filter(s => s !== ""); |
|
34 |
path.unshift(""); |
|
35 |
|
|
36 |
return {proto, domain, path, query, path_trailing_dash}; |
|
37 |
} |
|
38 |
|
|
39 |
/* Be sane: both arguments should be arrays of length >= 2 */ |
|
40 |
function domain_matches(url_domain, pattern_domain) |
|
41 |
{ |
|
42 |
const length_difference = url_domain.length - pattern_domain.length; |
|
43 |
|
|
44 |
for (let i = 1; i <= url_domain.length; i++) { |
|
45 |
const url_part = url_domain[url_domain.length - i]; |
|
46 |
const pattern_part = pattern_domain[pattern_domain.length - i]; |
|
47 |
|
|
48 |
if (pattern_domain.length === i) { |
|
49 |
if (pattern_part === "*") |
|
50 |
return length_difference === 0; |
|
51 |
if (pattern_part === "**") |
|
52 |
return length_difference > 0; |
|
53 |
if (pattern_part === "***") |
|
54 |
return true; |
|
55 |
return length_difference === 0 && pattern_part === url_part; |
|
56 |
} |
|
57 |
|
|
58 |
if (pattern_part !== url_part) |
|
59 |
return false; |
|
60 |
} |
|
61 |
|
|
62 |
return pattern_domain.length === url_domain.length + 1 && |
|
63 |
pattern_domain[0] === "***"; |
|
64 |
} |
|
65 |
|
|
66 |
function path_matches(url_path, url_trailing_dash, |
|
67 |
pattern_path, pattern_trailing_dash) |
|
68 |
{ |
|
69 |
const dashes_ok = !(pattern_trailing_dash && !url_trailing_dash); |
|
70 |
|
|
71 |
if (pattern_path.length === 0) |
|
72 |
return url_path.length === 0 && dashes_ok; |
|
73 |
|
|
74 |
const length_difference = url_path.length - pattern_path.length; |
|
75 |
|
|
76 |
for (let i = 0; i < url_path.length; i++) { |
|
77 |
if (pattern_path.length === i + 1) { |
|
78 |
if (pattern_path[i] === "*") |
|
79 |
return length_difference === 0; |
|
80 |
if (pattern_path[i] === "**") { |
|
81 |
return length_difference > 0 || |
|
82 |
(url_path[i] === "**" && dashes_ok); |
|
83 |
} |
|
84 |
if (pattern_path[i] === "***") |
|
85 |
return length_difference >= 0; |
|
86 |
return length_difference === 0 && |
|
87 |
pattern_path[i] === url_path[i] && dashes_ok; |
|
88 |
} |
|
89 |
|
|
90 |
if (pattern_path[i] !== url_path[i]) |
|
91 |
return false; |
|
92 |
} |
|
93 |
|
|
94 |
return false; |
|
95 |
} |
|
96 |
|
|
97 |
function url_matches(url, pattern) |
|
98 |
{ |
|
99 |
const url_deco = deconstruct_url(url); |
|
100 |
const pattern_deco = deconstruct_url(pattern); |
|
101 |
|
|
102 |
if (url_deco === undefined || pattern_deco === undefined) { |
|
103 |
console.log(`bad comparison: ${url} and ${pattern}`); |
|
104 |
return false |
|
105 |
} |
|
106 |
|
|
107 |
if (pattern_deco.proto !== url_deco.proto) |
|
108 |
return false; |
|
109 |
|
|
110 |
return domain_matches(url_deco.domain, pattern_deco.domain) && |
|
111 |
path_matches(url_deco.path, url_deco.path_trailing_dash, |
|
112 |
pattern_deco.path, pattern_deco.path_trailing_dash); |
|
113 |
} |
|
114 |
|
|
115 |
/* |
|
116 |
* Call callback for every possible pattern that matches url. Return when there |
|
117 |
* are no more patterns or callback returns false. |
|
118 |
*/ |
|
119 |
function for_each_possible_pattern(url, callback) |
|
120 |
{ |
|
121 |
const deco = deconstruct_url(url); |
|
122 |
|
|
123 |
if (deco === undefined) { |
|
124 |
console.log("bad url format", url); |
|
125 |
return; |
|
126 |
} |
|
127 |
|
|
128 |
for (let d_slice = 0; d_slice < deco.domain.length; d_slice++) { |
|
129 |
const domain_part = deco.domain.slice(d_slice).join("."); |
|
130 |
const domain_wildcards = []; |
|
131 |
if (d_slice === 0) |
|
132 |
domain_wildcards.push(""); |
|
133 |
if (d_slice === 1) |
|
134 |
domain_wildcards.push("*."); |
|
135 |
if (d_slice > 0) |
|
136 |
domain_wildcards.push("**."); |
|
137 |
domain_wildcards.push("***."); |
|
138 |
|
|
139 |
for (const domain_wildcard of domain_wildcards) { |
|
140 |
const domain_pattern = domain_wildcard + domain_part; |
|
141 |
|
|
142 |
for (let s_slice = deco.path.length; s_slice > 0; s_slice--) { |
|
143 |
const path_part = deco.path.slice(0, s_slice).join("/"); |
|
144 |
const path_wildcards = []; |
|
145 |
if (s_slice === deco.path.length) { |
|
146 |
if (deco.path_trailing_dash) |
|
147 |
path_wildcards.push("/"); |
|
148 |
path_wildcards.push(""); |
|
149 |
} |
|
150 |
if (s_slice === deco.path.length - 1 && |
|
151 |
deco.path[s_slice] !== "*") |
|
152 |
path_wildcards.push("/*"); |
|
153 |
if (s_slice < deco.path.length && |
|
154 |
(deco.path[s_slice] !== "**" || |
|
155 |
s_slice < deco.path.length - 1)) |
|
156 |
path_wildcards.push("/**"); |
|
157 |
if (deco.path[s_slice] !== "***" || s_slice < deco.path.length) |
|
158 |
path_wildcards.push("/***"); |
|
159 |
|
|
160 |
for (const path_wildcard of path_wildcards) { |
|
161 |
const path_pattern = path_part + path_wildcard; |
|
162 |
|
|
163 |
const pattern = deco.proto + domain_pattern + path_pattern; |
|
164 |
|
|
165 |
if (callback(pattern) === false) |
|
166 |
return; |
|
167 |
} |
|
168 |
} |
|
169 |
} |
|
170 |
} |
|
171 |
} |
|
172 |
|
|
173 |
function possible_patterns(url) |
|
174 |
{ |
|
175 |
const patterns = []; |
|
176 |
for_each_possible_pattern(url, patterns.push); |
|
177 |
|
|
178 |
return patterns; |
|
179 |
} |
|
180 |
|
|
181 |
/* |
|
182 |
* EXPORTS_START |
|
183 |
* EXPORT url_matches |
|
184 |
* EXPORT for_each_possible_pattern |
|
185 |
* EXPORT possible_patterns |
|
186 |
* EXPORTS_END |
|
187 |
*/ |
content/activity_info_server.js | ||
---|---|---|
1 |
/** |
|
2 |
* part of Hachette |
|
3 |
* Informing about activities performed by content script (script injection, |
|
4 |
* script blocking). |
|
5 |
* |
|
6 |
* Copyright (C) 2021 Wojtek Kosior |
|
7 |
* Redistribution terms are gathered in the `copyright' file. |
|
8 |
*/ |
|
9 |
|
|
10 |
/* |
|
11 |
* IMPORTS_START |
|
12 |
* IMPORT listen_for_connection |
|
13 |
* IMPORT CONNECTION_TYPE |
|
14 |
* IMPORTS_END |
|
15 |
*/ |
|
16 |
|
|
17 |
var activities = []; |
|
18 |
var ports = new Set(); |
|
19 |
|
|
20 |
function report_activity(name, data) |
|
21 |
{ |
|
22 |
const activity = [name, data]; |
|
23 |
activities.push(activity); |
|
24 |
|
|
25 |
for (const port of ports) |
|
26 |
port.postMessage(activity); |
|
27 |
} |
|
28 |
|
|
29 |
function report_script(script_data) |
|
30 |
{ |
|
31 |
report_activity("script", script_data); |
|
32 |
} |
|
33 |
|
|
34 |
function report_settings(settings) |
|
35 |
{ |
|
36 |
report_activity("settings", settings); |
|
37 |
} |
|
38 |
|
|
39 |
function new_connection(port) |
|
40 |
{ |
|
41 |
console.log("new activity info connection!"); |
|
42 |
|
|
43 |
ports.add(port); |
|
44 |
|
|
45 |
for (const activity of activities) |
|
46 |
port.postMessage(activity); |
|
47 |
} |
|
48 |
|
|
49 |
function start_activity_info_server() |
|
50 |
{ |
|
51 |
listen_for_connection(CONNECTION_TYPE.ACTIVITY_INFO, new_connection); |
|
52 |
} |
|
53 |
|
|
54 |
/* |
|
55 |
* EXPORTS_START |
|
56 |
* EXPORT start_activity_info_server |
|
57 |
* EXPORT report_script |
|
58 |
* EXPORT report_settings |
|
59 |
* EXPORTS_END |
|
60 |
*/ |
content/main.js | ||
---|---|---|
12 | 12 |
* IMPORT url_extract_target |
13 | 13 |
* IMPORT gen_unique |
14 | 14 |
* IMPORT csp_rule |
15 |
* IMPORT is_privileged_url |
|
15 | 16 |
* IMPORT sanitize_attributes |
16 | 17 |
* IMPORT script_suppressor |
17 | 18 |
* IMPORT is_chrome |
18 | 19 |
* IMPORT is_mozilla |
20 |
* IMPORT start_activity_info_server |
|
19 | 21 |
* IMPORTS_END |
20 | 22 |
*/ |
21 | 23 |
|
... | ... | |
35 | 37 |
|
36 | 38 |
const suppressor = script_suppressor(unique); |
37 | 39 |
|
38 |
function needs_blocking() |
|
40 |
|
|
41 |
function is_http() |
|
39 | 42 |
{ |
40 |
if (url.startsWith("https://") || url.startsWith("http://"))
|
|
41 |
return false;
|
|
43 |
return !!/^https?:\/\//i.exec(document.URL);
|
|
44 |
}
|
|
42 | 45 |
|
46 |
function is_whitelisted() |
|
47 |
{ |
|
43 | 48 |
const parsed_url = url_extract_target(document.URL); |
44 | 49 |
|
45 | 50 |
if (parsed_url.target !== undefined && |
... | ... | |
49 | 54 |
else |
50 | 55 |
history.replaceState(null, "", parsed_url.base_url); |
51 | 56 |
|
52 |
console.log(["allowing whitelisted", document.URL]); |
|
53 |
return false; |
|
57 |
return true; |
|
54 | 58 |
} |
55 | 59 |
|
56 |
console.log(["disallowing", document.URL]); |
|
57 |
return true; |
|
60 |
return false; |
|
58 | 61 |
} |
59 | 62 |
|
60 | 63 |
function handle_mutation(mutations, observer) |
... | ... | |
120 | 123 |
head.insertBefore(meta, head.firstElementChild); |
121 | 124 |
} |
122 | 125 |
|
123 |
if (needs_blocking()) { |
|
124 |
block_nodes_recursively(document.documentElement); |
|
125 |
|
|
126 |
if (is_chrome) { |
|
127 |
var observer = new MutationObserver(handle_mutation); |
|
128 |
observer.observe(document.documentElement, { |
|
129 |
attributes: true, |
|
130 |
childList: true, |
|
131 |
subtree: true |
|
132 |
}); |
|
126 |
if (!is_privileged_url(document.URL)) { |
|
127 |
start_activity_info_server(); |
|
128 |
handle_page_actions(unique); |
|
129 |
|
|
130 |
if (is_http()) { |
|
131 |
/* rely on CSP injected through webRequest */ |
|
132 |
} else if (is_whitelisted()) { |
|
133 |
/* do not block scripts at all */ |
|
134 |
} else { |
|
135 |
block_nodes_recursively(document.documentElement); |
|
136 |
|
|
137 |
if (is_chrome) { |
|
138 |
var observer = new MutationObserver(handle_mutation); |
|
139 |
observer.observe(document.documentElement, { |
|
140 |
attributes: true, |
|
141 |
childList: true, |
|
142 |
subtree: true |
|
143 |
}); |
|
144 |
} |
|
145 |
|
|
146 |
if (is_mozilla) |
|
147 |
addEventListener('beforescriptexecute', suppressor, true); |
|
133 | 148 |
} |
134 |
|
|
135 |
if (is_mozilla) |
|
136 |
addEventListener('beforescriptexecute', suppressor, true); |
|
137 | 149 |
} |
138 |
|
|
139 |
handle_page_actions(unique); |
content/page_actions.js | ||
---|---|---|
9 | 9 |
* IMPORTS_START |
10 | 10 |
* IMPORT CONNECTION_TYPE |
11 | 11 |
* IMPORT browser |
12 |
* IMPORT report_script |
|
13 |
* IMPORT report_settings |
|
12 | 14 |
* IMPORTS_END |
13 | 15 |
*/ |
14 | 16 |
|
... | ... | |
19 | 21 |
|
20 | 22 |
function handle_message(message) |
21 | 23 |
{ |
22 |
if (message.inject === undefined) |
|
23 |
return; |
|
24 |
const [action, data] = message; |
|
24 | 25 |
|
25 |
for (let script_text of message.inject) { |
|
26 |
if (loaded) |
|
27 |
add_script(script_text); |
|
28 |
else |
|
29 |
scripts_awaiting.push(script_text); |
|
26 |
if (action === "inject") { |
|
27 |
for (let script_text of data) { |
|
28 |
if (loaded) |
|
29 |
add_script(script_text); |
|
30 |
else |
|
31 |
scripts_awaiting.push(script_text); |
|
32 |
} |
|
30 | 33 |
} |
34 |
if (action === "settings") |
|
35 |
report_settings(data); |
|
31 | 36 |
} |
32 | 37 |
|
33 | 38 |
function document_loaded(event) |
... | ... | |
46 | 51 |
script.textContent = script_text; |
47 | 52 |
script.setAttribute("nonce", nonce); |
48 | 53 |
document.body.appendChild(script); |
54 |
|
|
55 |
report_script(script_text); |
|
49 | 56 |
} |
50 | 57 |
|
51 | 58 |
function handle_page_actions(script_nonce) { |
html/display-panel.html | ||
---|---|---|
6 | 6 |
<html> |
7 | 7 |
<head> |
8 | 8 |
<meta charset="utf-8"/> |
9 |
<title>Myext popup</title> |
|
9 |
<title>Hachette - page settings</title> |
|
10 |
<style> |
|
11 |
input[type="radio"], input[type="checkbox"] { |
|
12 |
display: none; |
|
13 |
} |
|
14 |
|
|
15 |
body { |
|
16 |
width: 300px; |
|
17 |
height: 300px; |
|
18 |
} |
|
19 |
|
|
20 |
.show_next:not(:checked)+* { |
|
21 |
display: none; |
|
22 |
} |
|
23 |
|
|
24 |
.hide { |
|
25 |
display: none; |
|
26 |
} |
|
27 |
|
|
28 |
#possible_patterns_chbx:not(:checked)+label span#triangle:first-child+span, |
|
29 |
#possible_patterns_chbx:not(:checked)+label+*, |
|
30 |
#possible_patterns_chbx:checked+label span#triangle:first-child { |
|
31 |
display: none; |
|
32 |
} |
|
33 |
|
|
34 |
#container_for_injected>#none_injected:not(:last-child) { |
|
35 |
display: none; |
|
36 |
} |
|
37 |
|
|
38 |
input#connected_chbx:checked+div+h3 { |
|
39 |
display: none; |
|
40 |
} |
|
41 |
</style> |
|
10 | 42 |
</head> |
11 | 43 |
<body> |
12 |
<button id="settings_but" type="button">Settings</button>_POPUPSCRIPTS_ |
|
44 |
<!-- The invisible div below is for elements that will be cloned. --> |
|
45 |
<div class="hide"> |
|
46 |
<li id="pattern_li_template"> |
|
47 |
<span></span> |
|
48 |
<button>View in settings</button> |
|
49 |
</li> |
|
50 |
</div> |
|
51 |
|
|
52 |
<h2 id="page_url_heading"></h2> |
|
53 |
|
|
54 |
<input id="show_privileged_notice_chbx" type="checkbox" class="show_next"></input> |
|
55 |
<h3>Privileged page</h3> |
|
56 |
|
|
57 |
<input id="show_page_state_chbx" type="checkbox" class="show_next"></input> |
|
58 |
<div> |
|
59 |
<input id="possible_patterns_chbx" type="checkbox"></input> |
|
60 |
<label for="possible_patterns_chbx"> |
|
61 |
<h3> |
|
62 |
<span id="triangle">⏵</span><span>⏷</span> |
|
63 |
Possible patterns |
|
64 |
</h3> |
|
65 |
</label> |
|
66 |
<ul id="possible_patterns"></ul> |
|
67 |
|
|
68 |
<input id="connected_chbx" type="checkbox" class="show_next"></input> |
|
69 |
<div> |
|
70 |
<h3> |
|
71 |
Matched pattern: <span id="pattern">...</span> |
|
72 |
<button id="view_pattern" class="hide"> |
|
73 |
View in settings |
|
74 |
</button> |
|
75 |
</h3> |
|
76 |
<h3> |
|
77 |
Blocked: <span id="blocked">...</span> |
|
78 |
</h3> |
|
79 |
<h3> |
|
80 |
Payload: <span id="payload">...</span> |
|
81 |
<button id="view_payload" class="hide"> |
|
82 |
View in settings |
|
83 |
</button> |
|
84 |
</h3> |
|
85 |
<h3>Injected</h3> |
|
86 |
<div id="container_for_injected"> |
|
87 |
<span id="none_injected">None</span> |
|
88 |
</div> |
|
89 |
</div> |
|
90 |
<h3>Trying to connect..<input id="loading_chbx" type="checkbox" class="show_next"></input><span>.</span></h3> |
|
91 |
</div> |
|
92 |
|
|
93 |
<button id="settings_but" type="button" style="margin-top: 20px;">Settings</button>_POPUPSCRIPTS_ |
|
13 | 94 |
</body> |
14 | 95 |
</html> |
html/display-panel.js | ||
---|---|---|
8 | 8 |
/* |
9 | 9 |
* IMPORTS_START |
10 | 10 |
* IMPORT browser |
11 |
* IMPORT is_chrome |
|
12 |
* IMPORT is_mozilla |
|
13 |
* IMPORT CONNECTION_TYPE |
|
14 |
* IMPORT url_item |
|
15 |
* IMPORT is_privileged_url |
|
16 |
* IMPORT TYPE_PREFIX |
|
17 |
* IMPORT nice_name |
|
18 |
* IMPORT open_in_settings |
|
19 |
* IMPORT for_each_possible_pattern |
|
11 | 20 |
* IMPORTS_END |
12 | 21 |
*/ |
13 | 22 |
|
14 |
document.getElementById("settings_but") |
|
23 |
function by_id(id) |
|
24 |
{ |
|
25 |
return document.getElementById(id); |
|
26 |
} |
|
27 |
|
|
28 |
const tab_query = {currentWindow: true, active: true}; |
|
29 |
|
|
30 |
async function get_current_tab() |
|
31 |
{ |
|
32 |
/* Fix for fact that Chrome does not use promises here */ |
|
33 |
const promise = is_chrome ? |
|
34 |
new Promise((resolve, reject) => |
|
35 |
browser.tabs.query(tab_query, tab => resolve(tab))) : |
|
36 |
browser.tabs.query(tab_query); |
|
37 |
|
|
38 |
try { |
|
39 |
return (await promise)[0]; |
|
40 |
} catch(e) { |
|
41 |
console.log(e); |
|
42 |
} |
|
43 |
} |
|
44 |
|
|
45 |
const page_url_heading = by_id("page_url_heading"); |
|
46 |
const show_privileged_notice_chbx = by_id("show_privileged_notice_chbx"); |
|
47 |
const show_page_state_chbx = by_id("show_page_state_chbx"); |
|
48 |
|
|
49 |
async function show_page_activity_info() |
|
50 |
{ |
|
51 |
const tab = await get_current_tab(); |
|
52 |
|
|
53 |
if (tab === undefined) { |
|
54 |
page_url_heading.textContent = "unknown page"; |
|
55 |
return; |
|
56 |
} |
|
57 |
|
|
58 |
const url = url_item(tab.url); |
|
59 |
page_url_heading.textContent = url; |
|
60 |
if (is_privileged_url(url)) { |
|
61 |
show_privileged_notice_chbx.checked = true; |
|
62 |
return; |
|
63 |
} |
|
64 |
|
|
65 |
populate_possible_patterns_list(url); |
|
66 |
show_page_state_chbx.checked = true; |
|
67 |
|
|
68 |
try_to_connect(tab.id); |
|
69 |
} |
|
70 |
|
|
71 |
function populate_possible_patterns_list(url) |
|
72 |
{ |
|
73 |
for_each_possible_pattern(url, add_pattern_to_list); |
|
74 |
|
|
75 |
const port = browser.runtime.connect({name: CONNECTION_TYPE.PAGE_INFO}); |
|
76 |
port.onMessage.addListener(handle_page_info); |
|
77 |
port.postMessage(["subscribe", url]); |
|
78 |
} |
|
79 |
|
|
80 |
const possible_patterns_ul = by_id("possible_patterns"); |
|
81 |
const pattern_li_template = by_id("pattern_li_template"); |
|
82 |
pattern_li_template.removeAttribute("id"); |
|
83 |
const known_patterns = new Map(); |
|
84 |
|
|
85 |
function add_pattern_to_list(pattern) |
|
86 |
{ |
|
87 |
const li = pattern_li_template.cloneNode(true); |
|
88 |
li.id = `pattern_li_${known_patterns.size}`; |
|
89 |
known_patterns.set(pattern, li.id); |
|
90 |
|
|
91 |
const span = li.firstElementChild; |
|
92 |
span.textContent = pattern; |
|
93 |
|
|
94 |
const button = span.nextElementSibling; |
|
95 |
const settings_opener = () => open_in_settings(TYPE_PREFIX.PAGE, pattern); |
|
96 |
button.addEventListener("click", settings_opener); |
|
97 |
|
|
98 |
possible_patterns_ul.appendChild(li) |
|
99 |
|
|
100 |
return li.id; |
|
101 |
} |
|
102 |
|
|
103 |
function ensure_pattern_exists(pattern) |
|
104 |
{ |
|
105 |
let id = known_patterns.get(pattern); |
|
106 |
/* |
|
107 |
* As long as pattern computation works well, we should never get into this |
|
108 |
* conditional block. This is just a safety measure. To be removed as part |
|
109 |
* of a bigger rework when we start taking iframes into account. |
|
110 |
*/ |
|
111 |
if (id === undefined) { |
|
112 |
console.log(`unknown pattern: ${pattern}`); |
|
113 |
id = add_pattern_to_list(pattern); |
|
114 |
} |
|
115 |
|
|
116 |
return id; |
|
117 |
} |
|
118 |
|
|
119 |
function set_pattern_li_button_text(li_id, text) |
|
120 |
{ |
|
121 |
by_id(li_id).firstElementChild.nextElementSibling.textContent = text; |
|
122 |
} |
|
123 |
|
|
124 |
function handle_page_info(message) |
|
125 |
{ |
|
126 |
const [type, data] = message; |
|
127 |
|
|
128 |
if (type === "change") { |
|
129 |
const li_id = ensure_pattern_exists(data.item); |
|
130 |
if (data.old_val === undefined) |
|
131 |
set_pattern_li_button_text(li_id, "Edit in settings"); |
|
132 |
if (data.new_val === undefined) |
|
133 |
set_pattern_li_button_text(li_id, "Add setting"); |
|
134 |
} |
|
135 |
|
|
136 |
if (type === "new_url") { |
|
137 |
for (const li_id of known_patterns.values()) |
|
138 |
set_pattern_li_button_text(li_id, "Add setting"); |
|
139 |
for (const [pattern, settings] of data) { |
|
140 |
set_pattern_li_button_text(ensure_pattern_exists(pattern), |
|
141 |
"Edit in settings") |
|
142 |
} |
|
143 |
} |
|
144 |
} |
|
145 |
|
|
146 |
const connected_chbx = by_id("connected_chbx"); |
|
147 |
|
|
148 |
function try_to_connect(tab_id) |
|
149 |
{ |
|
150 |
/* This won't connect to iframes. We'll add support for them later */ |
|
151 |
const connect_info = {name: CONNECTION_TYPE.ACTIVITY_INFO, frameId: 0}; |
|
152 |
const port = browser.tabs.connect(tab_id, connect_info); |
|
153 |
|
|
154 |
port.onDisconnect.addListener(port => handle_disconnect(tab_id)); |
|
155 |
port.onMessage.addListener(handle_activity_report); |
|
156 |
|
|
157 |
if (is_mozilla) |
|
158 |
setTimeout(() => monitor_connecting(port, tab_id), 1000); |
|
159 |
} |
|
160 |
|
|
161 |
const loading_chbx = by_id("loading_chbx"); |
|
162 |
|
|
163 |
function handle_disconnect(tab_id) |
|
164 |
{ |
|
165 |
if (is_chrome && !browser.runtime.lastError) |
|
166 |
return; |
|
167 |
|
|
168 |
/* return if there was no connection initialization failure */ |
|
169 |
if (connected_chbx.checked) |
|
170 |
return; |
|
171 |
|
|
172 |
loading_chbx.checked = !loading_chbx.checked; |
|
173 |
setTimeout(() => try_to_connect(tab_id), 1000); |
|
174 |
} |
|
175 |
|
|
176 |
function monitor_connecting(port, tab_id) |
|
177 |
{ |
|
178 |
if (connected_chbx.checked) |
|
179 |
return; |
|
180 |
|
|
181 |
port.disconnect(); |
|
182 |
loading_chbx.checked = !loading_chbx.checked; |
|
183 |
try_to_connect(tab_id); |
|
184 |
} |
|
185 |
|
|
186 |
const pattern_span = by_id("pattern"); |
|
187 |
const view_pattern_but = by_id("view_pattern"); |
|
188 |
const blocked_span = by_id("blocked"); |
|
189 |
const payload_span = by_id("payload"); |
|
190 |
const view_payload_but = by_id("view_payload"); |
|
191 |
const container_for_injected = by_id("container_for_injected"); |
|
192 |
|
|
193 |
function handle_activity_report(message) |
|
194 |
{ |
|
195 |
connected_chbx.checked = true; |
|
196 |
|
|
197 |
const [type, data] = message; |
|
198 |
|
|
199 |
if (type === "settings") { |
|
200 |
let [pattern, settings] = data; |
|
201 |
|
|
202 |
settings = settings || {}; |
|
203 |
blocked_span.textContent = settings.allow ? "no" : "yes"; |
|
204 |
|
|
205 |
if (pattern) { |
|
206 |
pattern_span.textContent = pattern; |
|
207 |
const settings_opener = |
|
208 |
() => open_in_settings(TYPE_PREFIX.PAGE, pattern); |
|
209 |
view_pattern_but.classList.remove("hide"); |
|
210 |
view_pattern_but.addEventListener("click", settings_opener); |
|
211 |
} else { |
|
212 |
pattern_span.textContent = "none"; |
|
213 |
} |
|
214 |
|
|
215 |
const components = settings.components; |
|
216 |
if (components) { |
|
217 |
payload_span.textContent = nice_name(...components); |
|
218 |
const settings_opener = () => open_in_settings(...components); |
|
219 |
view_payload_but.classList.remove("hide"); |
|
220 |
view_payload_but.addEventListener("click", settings_opener); |
|
221 |
} else { |
|
222 |
payload_span.textContent = "none"; |
|
223 |
} |
|
224 |
} |
|
225 |
if (type === "script") { |
|
226 |
const h4 = document.createElement("h4"); |
|
227 |
const pre = document.createElement("pre"); |
|
228 |
h4.textContent = "script"; |
|
229 |
pre.textContent = data; |
|
230 |
|
|
231 |
container_for_injected.appendChild(h4); |
|
232 |
container_for_injected.appendChild(pre); |
|
233 |
} |
|
234 |
} |
|
235 |
|
|
236 |
by_id("settings_but") |
|
15 | 237 |
.addEventListener("click", (e) => browser.runtime.openOptionsPage()); |
238 |
|
|
239 |
show_page_activity_info(); |
html/options_main.js | ||
---|---|---|
12 | 12 |
* IMPORT TYPE_NAME |
13 | 13 |
* IMPORT list_prefixes |
14 | 14 |
* IMPORT url_extract_target |
15 |
* IMPORT nice_name |
|
15 | 16 |
* IMPORTS_END |
16 | 17 |
*/ |
17 | 18 |
|
... | ... | |
21 | 22 |
return document.getElementById(id); |
22 | 23 |
} |
23 | 24 |
|
24 |
function nice_name(prefix, name) |
|
25 |
{ |
|
26 |
return `${name} (${TYPE_NAME[prefix]})`; |
|
27 |
} |
|
28 |
|
|
29 | 25 |
const item_li_template = by_id("item_li_template"); |
30 | 26 |
const bag_component_li_template = by_id("bag_component_li_template"); |
31 | 27 |
const chbx_component_li_template = by_id("chbx_component_li_template"); |
Also available in: Unified diff
show some settings of the current page in the popup