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