Project

General

Profile

« Previous | Next » 

Revision 792fbe18

Added by koszko about 2 years ago

Facilitate installation of scripts from the repository

This commit includes:

  • removal of page_info_server
  • running of storage client in popup context
  • extraction of some common CSS to a separate file
  • extraction of scripts import view to a separate file
  • addition of a facility to conveniently clone complex structures from DOM (in DOM_helpers.js)
  • addition of hydrilla repo url to default settings
  • other minor changes and of course changes related to the actual installation of scripts from the repo

View differences:

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
16 15
 * IMPORT browser
17 16
 * IMPORTS_END
18 17
 */
......
20 19
start_storage_server();
21 20
start_page_actions_server();
22 21
start_policy_injector();
23
start_page_info_server();
24 22

  
25 23
async function init_ext(install_details)
26 24
{
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 query_all
14
 * IMPORT TYPE_PREFIX
15
 * IMPORT CONNECTION_TYPE
16
 * IMPORT url_matches
17
 * IMPORTS_END
18
 */
19

  
20
var storage;
21

  
22
function handle_change(connection_data, change)
23
{
24
    if (change.prefix !== TYPE_PREFIX.PAGE)
25
	return;
26

  
27
    connection_data.port.postMessage(["change", change]);
28
}
29

  
30
async function handle_subscription(connection_data, message)
31
{
32
    const [action, url] = message;
33
    if (action === "unsubscribe") {
34
	connection_data.subscribed.delete(url);
35
	return;
36
    }
37

  
38
    connection_data.subscribed.add(url);
39
    connection_data.port.postMessage(["new_url", query_all(storage, url)]);
40
}
41

  
42
function new_connection(port)
43
{
44
    console.log("new page info connection!");
45

  
46
    const connection_data = {
47
	subscribed : new Set(),
48
	port
49
    };
50

  
51
    let _handle_change = change => handle_change(connection_data, change);
52

  
53
    storage.add_change_listener(_handle_change);
54

  
55
    port.onMessage.addListener(m => handle_subscription(connection_data, m));
56
    port.onDisconnect.addListener(
57
	() => storage.remove_change_listener(_handle_change)
58
    );
59
}
60

  
61
async function start_page_info_server()
62
{
63
    storage = await get_storage();
64

  
65
    listen_for_connection(CONNECTION_TYPE.PAGE_INFO, new_connection);
66
}
67

  
68
/*
69
 * EXPORTS_START
70
 * EXPORT start_page_info_server
71
 * EXPORTS_END
72
 */
build.sh
273 273
    fi
274 274

  
275 275
    cp -r copyright licenses/ $BUILDDIR
276
    cp html/*.css $BUILDDIR/html
276 277
    mkdir $BUILDDIR/icons
277 278
    cp icons/*.png $BUILDDIR/icons
278 279
}
common/connection_types.js
13 13
const CONNECTION_TYPE = {
14 14
    REMOTE_STORAGE : "0",
15 15
    PAGE_ACTIONS : "1",
16
    PAGE_INFO : "2",
17
    ACTIVITY_INFO : "3"
16
    ACTIVITY_INFO : "2"
18 17
};
19 18

  
20 19
/*
common/misc.js
12 12
 * IMPORT browser
13 13
 * IMPORT is_chrome
14 14
 * IMPORT TYPE_NAME
15
 * IMPORT TYPE_PREFIX
15 16
 * IMPORTS_END
16 17
 */
17 18

  
......
154 155
    return gen_unique(time + policy);
155 156
}
156 157

  
158
/* Regexes and objest to use as/in schemas for parse_json_with_schema(). */
159
const nonempty_string_matcher = /.+/;
160

  
161
const matchers = {
162
    sha256: /^[0-9a-f]{64}$/,
163
    nonempty_string: nonempty_string_matcher,
164
    component: [
165
	new RegExp(`^[${TYPE_PREFIX.SCRIPT}${TYPE_PREFIX.BAG}]$`),
166
	nonempty_string_matcher
167
    ]
168
};
169

  
157 170
/*
158 171
 * EXPORTS_START
159 172
 * EXPORT gen_nonce
......
165 178
 * EXPORT nice_name
166 179
 * EXPORT open_in_settings
167 180
 * EXPORT is_privileged_url
181
 * EXPORT matchers
168 182
 * EXPORTS_END
169 183
 */
common/sanitize_JSON.js
37 37
    let _default = undefined;
38 38

  
39 39
    if (!Array.isArray(schema) || schema[1] === "matchentry" ||
40
	schema.length < 2 || !["ordefault", "or"].includes(schema))
40
	schema.length < 2 || !["ordefault", "or"].includes(schema[1]))
41 41
	return sanitize_unknown_no_alternatives(schema, item);
42 42

  
43 43
    if ((schema.length & 1) !== 1) {
common/stored_types.js
18 18
    PAGE : "p",
19 19
    BAG : "b",
20 20
    SCRIPT : "s",
21
    VAR : "_"
21
    VAR : "_",
22
    /* Url prefix is not used in stored settings. */
23
    URL : "u"
22 24
};
23 25

  
24 26
const TYPE_NAME = {
content/activity_info_server.js
11 11
 * IMPORTS_START
12 12
 * IMPORT listen_for_connection
13 13
 * IMPORT CONNECTION_TYPE
14
 * IMPORT set_repo_query_repos
15
 * IMPORT set_repo_query_callback
14
 * IMPORT repo_query
15
 * IMPORT subscribe_repo_query_results
16
 * IMPORT unsubscribe_repo_query_results
16 17
 * IMPORTS_END
17 18
 */
18 19

  
19 20
var activities = [];
20 21
var ports = new Set();
21 22

  
23
function report_activity_oneshot(name, data, port)
24
{
25
    port.postMessage([name, data]);
26
}
27

  
22 28
function report_activity(name, data)
23 29
{
24 30
    const activity = [name, data];
......
35 41

  
36 42
function report_settings(settings)
37 43
{
38
    const [pattern, settings_data, repos] = settings;
39
    set_repo_query_repos(repos);
40

  
41 44
    report_activity("settings", settings);
42 45
}
43 46

  
44
function report_repo_query_result(result)
47
function report_repo_query_action(update, port)
45 48
{
46
    report_activity("repo_query_result", result);
49
    report_activity_oneshot("repo_query_action", update, port);
47 50
}
48 51

  
49
function trigger_repo_query()
52
function trigger_repo_query(query_specifier)
50 53
{
51
    set_repo_query_callback(report_repo_query_result);
54
    repo_query(...query_specifier);
55
}
56

  
57
function handle_disconnect(port, report_action)
58
{
59
    ports.delete(port)
60
    unsubscribe_repo_query_results(report_action);
52 61
}
53 62

  
54 63
function new_connection(port)
......
60 69
    for (const activity of activities)
61 70
	port.postMessage(activity);
62 71

  
72
    const report_action = u => report_repo_query_action(u, port);
73
    subscribe_repo_query_results(report_action);
74

  
63 75
    /*
64 76
     * So far the only thing we expect to receive is repo query order. Once more
65 77
     * possibilities arrive, we will need to complicate this listener.
66 78
     */
67 79
    port.onMessage.addListener(trigger_repo_query);
68 80

  
69
    port.onDisconnect.addListener(() => ports.delete(port));
81
    port.onDisconnect.addListener(() => handle_disconnect(port, report_action));
70 82
}
71 83

  
72 84
function start_activity_info_server()
content/main.js
20 20
 * IMPORT is_chrome
21 21
 * IMPORT is_mozilla
22 22
 * IMPORT start_activity_info_server
23
 * IMPORT set_repo_query_url
24 23
 * IMPORTS_END
25 24
 */
26 25

  
......
130 129
    }
131 130

  
132 131
    start_activity_info_server();
133

  
134
    set_repo_query_url(document.URL);
135 132
}
content/repo_query.js
9 9
/*
10 10
 * IMPORTS_START
11 11
 * IMPORT make_ajax_request
12
 * IMPORT observables
13
 * IMPORT TYPE_PREFIX
14
 * IMPORT parse_json_with_schema
15
 * IMPORT matchers
12 16
 * IMPORTS_END
13 17
 */
14 18

  
15
var query_started = false;
19
const paths = {
20
    [TYPE_PREFIX.PAGE]: "/pattern",
21
    [TYPE_PREFIX.BAG]: "/bag",
22
    [TYPE_PREFIX.SCRIPT]: "/script",
23
    [TYPE_PREFIX.URL]: "/query"
24
};
16 25

  
17
var url = undefined;
18
var repos = undefined;
19
var callback = undefined;
26
const queried_items = new Map();
27
const observable = observables.make();
20 28

  
21
async function query(repo)
29
function repo_query(prefix, item, repo_urls)
22 30
{
23
    const [repo_url, data] = repo;
31
    const key = prefix + item;
24 32

  
25
    let response = "Query failed";
26
    const query_url = `${repo_url}/query?n=${encodeURIComponent(url)}`;
33
    const results = queried_items.get(key) || {};
34
    queried_items.set(key, results);
27 35

  
28
    try {
29
	let xhttp = await make_ajax_request("GET", query_url);
30
	if (xhttp.status === 200)
31
	    response = xhttp.responseText;
32
	console.log(xhttp);
33
    } catch (e) {
34
	console.log(e);
35
    }
36
    for (const repo_url of repo_urls)
37
	perform_query_against(key, repo_url, results);
38
}
36 39

  
37
    callback([repo_url, response]);
40
const page_schema = {
41
    pattern: matchers.nonempty_string,
42
    payload: ["optional", matchers.component, "default", undefined]
43
};
44
const bag_schema = {
45
    name: matchers.nonempty_string,
46
    components: ["optional", [matchers.component, "repeat"], "default", []]
47
};
48
const script_schema = {
49
    name: matchers.nonempty_string,
50
    location: matchers.nonempty_string,
51
    sha256: matchers.sha256,
52
};
53
const search_result_schema = [page_schema, "repeat"];
54

  
55
const schemas = {
56
    [TYPE_PREFIX.PAGE]: page_schema,
57
    [TYPE_PREFIX.BAG]: bag_schema,
58
    [TYPE_PREFIX.SCRIPT]: script_schema,
59
    [TYPE_PREFIX.URL]: search_result_schema
38 60
}
39 61

  
40
function start_query()
62
async function perform_query_against(key, repo_url, results)
41 63
{
42
    if (query_started || !url || !repos || !callback)
64
    if (results[repo_url] !== undefined)
43 65
	return;
44 66

  
45
    query_started = true;
67
    const prefix = key[0];
68
    const item = key.substring(1);
69
    const result = {state: "started"};
70
    results[repo_url] = result;
46 71

  
47
    console.log(`about to query ${url} from ${repos}`);
72
    const broadcast_msg = {prefix, item, results: {[repo_url]: result}};
73
    observables.broadcast(observable, broadcast_msg);
48 74

  
49
    for (const repo of repos)
50
	query(repo);
51
}
75
    let state = "connection_error";
76
    const query_url =
77
	  `${repo_url}${paths[prefix]}?n=${encodeURIComponent(item)}`;
52 78

  
53
function set_repo_query_url(_url)
54
{
55
    url = _url;
79
    try {
80
	let xhttp = await make_ajax_request("GET", query_url);
81
	if (xhttp.status === 200) {
82
	    state = "parse_error";
83
	    result.response =
84
		parse_json_with_schema(schemas[prefix], xhttp.responseText);
85
	    state = "completed";
86
	}
87
    } catch (e) {
88
	console.log(e);
89
    }
56 90

  
57
    start_query();
91
    result.state = state;
92
    observables.broadcast(observable, broadcast_msg);
58 93
}
59 94

  
60
function set_repo_query_repos(_repos)
95
function subscribe_repo_query_results(cb)
61 96
{
62
    repos = _repos;
63

  
64
    start_query();
97
    observables.subscribe(observable, cb);
98
    for (const [key, results] of queried_items.entries())
99
	cb({prefix: key[0], item: key.substring(1), results});
65 100
}
66 101

  
67
function set_repo_query_callback(_callback)
102
function unsubscribe_repo_query_results(cb)
68 103
{
69
    callback = _callback;
70

  
71
    start_query();
104
    observables.unsubscribe(observable, cb);
72 105
}
73 106

  
74 107
/*
75 108
 * EXPORTS_START
76
 * EXPORT set_repo_query_url
77
 * EXPORT set_repo_query_repos
78
 * EXPORT set_repo_query_callback
109
 * EXPORT repo_query
110
 * EXPORT subscribe_repo_query_results
111
 * EXPORT unsubscribe_repo_query_results
79 112
 * EXPORTS_END
80 113
 */
copyright
20 20
   2021 jahoti <jahoti@tilde.team>
21 21
License: GPL-3+-javascript or Alicense-1.0
22 22

  
23
Files: README.txt copyright
23
Files: *.html README.txt copyright
24 24
Copyright: 2021 Wojtek Kosior <koszko@koszko.org>
25 25
License: GPL-3+ or Alicense-1.0 or CC-BY-SA-4.0
26 26

  
27
Files: *.html
27
Files: html/base.css
28 28
Copyright: 2021 Wojtek Kosior <koszko@koszko.org>
29 29
   2021 Nicholas Johnson <nicholasjohnson@posteo.org>
30 30
License: GPL-3+ or Alicense-1.0 or CC-BY-SA-4.0
default_settings.json
43 43
	"phttps://www.worldcat.org/title/**": {
44 44
	    "components": ["s", "worldcat (library holdings)"]
45 45
	}
46
    },
47
    {
48
	"rhttps://hydrilla.koszko.org": {}
46 49
    }
47 50
]
html/DOM_helpers.js
1
/**
2
 * Hachette operations on DOM elements
3
 *
4
 * Copyright (C) 2021 Wojtek Kosior
5
 * Redistribution terms are gathered in the `copyright' file.
6
 */
7

  
8
function by_id(id)
9
{
10
    return document.getElementById(id);
11
}
12

  
13
function clone_template(template_id)
14
{
15
    const clone = document.getElementById(template_id).cloneNode(true);
16
    const result_object = {};
17
    const to_process = [clone];
18

  
19
    while (to_process.length > 0) {
20
	const element = to_process.pop();
21
	const template_key = element.getAttribute("data-template");
22

  
23
	if (template_key)
24
	    result_object[template_key] = element;
25

  
26
	element.removeAttribute("id");
27
	element.removeAttribute("template_key");
28

  
29
	for (const child of element.children)
30
	    to_process.push(child);
31
    }
32

  
33
    return result_object;
34
}
35

  
36
/*
37
 * EXPORTS_START
38
 * EXPORT by_id
39
 * EXPORT clone_template
40
 * EXPORTS_END
41
 */
html/base.css
1
/**
2
 * Hachette base styles
3
 *
4
 * Copyright (C) 2021 Wojtek Kosior
5
 * Copyright (C) 2021 Nicholas Johnson
6
 * Redistribution terms are gathered in the `copyright' file.
7
 */
8

  
9
input[type="checkbox"], input[type="radio"], .hide {
10
    display: none;
11
}
12

  
13
.show_next:not(:checked)+* {
14
    display: none;
15
}
16

  
17
.show_hide_next2:not(:checked)+* {
18
    display: none;
19
}
20

  
21
.show_hide_next2:checked+*+* {
22
    display: none;
23
}
24

  
25
button, .button {
26
    background-color: #4CAF50;
27
    border: none;
28
    border-radius: 8px;
29
    color: white;
30
    text-align: center;
31
    text-decoration: none;
32
    display: inline-block;
33
    padding: 6px 12px;
34
    margin: 2px 0px;
35
}
36

  
37
button.slimbutton, .button.slimbutton {
38
    padding: 2px 4px;
39
    margin: 0;
40
}
41

  
42
button:hover, .button:hover {
43
    box-shadow: 0 6px 8px 0 rgba(0,0,0,0.24), 0 17px 50px 0 rgba(0,0,0,0.19);
44
}
html/display-panel.html
7 7
  <head>
8 8
    <meta charset="utf-8"/>
9 9
    <title>Hachette - page settings</title>
10
    <link type="text/css" rel="stylesheet" href="base.css" />
10 11
    <style>
11
      input[type="radio"], input[type="checkbox"] {
12
	  display: none;
13
      }
14

  
15 12
      body {
16 13
	  width: 300px;
17 14
	  height: 300px;
18 15
      }
19 16

  
20
      .show_next:not(:checked)+* {
21
	  display: none;
17
      ul {
18
	  padding-inline-start: 15px;
22 19
      }
23 20

  
24
      .show_hide_next2:not(:checked)+* {
25
	  display: none;
21
      .bold {
22
	  font-weight: bold;
26 23
      }
27 24

  
28
      .show_hide_next2:checked+*+* {
29
	  display: none;
30
      }
31

  
32
      .hide {
33
	  display: none;
34
      }
35

  
36
      #possible_patterns_chbx:not(:checked)+label span#triangle:first-child+span,
37
      #possible_patterns_chbx:not(:checked)+label+*,
38
      #possible_patterns_chbx:checked+label span#triangle:first-child {
25
      .unroll_chbx:not(:checked)+*+label span.triangle:first-child+span.triangle,
26
      .unroll_chbx:checked+*+label span.triangle:first-child,
27
      .unroll_chbx:not(:checked)+*,
28
      .unroll_chbx:not(:checked)+*+label+* {
39 29
	  display: none;
40 30
      }
41 31

  
......
51 41
	<span></span>
52 42
	<button>View in settings</button>
53 43
      </li>
44
      <li id="query_match_li_template" class="queried_pattern_match" data-template="li">
45
	<div>
46
	  <span>pattern:</span>
47
	  <span class="bold" data-template="pattern"></span>
48
	  <button data-template="btn">Install</button>
49
	</div>
50
	<div id="unrollable_component_template" data-template="unroll_container">
51
	  <span data-template="component_label">payload:</span>
52
	  <input type="checkbox" class="unroll_chbx" data-template="chbx"></input>
53
	  <br data-template="br"/>
54
	  <label class="bold" data-template="lbl">
55
	    <span data-template="triangle">
56
	      <span class="triangle">&#x23F5;</span>
57
	      <span class="triangle">&#x23F7;</span>
58
	    </span>
59
	    <span data-template="component"></span>
60
	  </label>
61
	  <div data-template="unroll"></div>
62
	</div>
63
      </li>
54 64
    </div>
55 65

  
56
    <h2 id="page_url_heading"></h2>
57

  
58
    <input id="show_privileged_notice_chbx" type="checkbox" class="show_next"></input>
59
    <h3>Privileged page</h3>
66
    <input id="show_install_view_chbx" type="checkbox" class="show_hide_next2"></input>
67
    <div id="install_view">
68
      <IMPORT html/import_frame.html />
69
      <!--
70
	  <div id="install_status"></div>
71
	  <label for="show_install_chbx" class="bold">Cancel install</label>
72
	  <button id="commit_install_but">Commit install</button>
73
      -->
74
    </div>
75
    <div id="main_view">
76
      <h2 id="page_url_heading"></h2>
60 77

  
61
    <input id="show_page_state_chbx" type="checkbox" class="show_next"></input>
62
    <div>
63
      <input id="possible_patterns_chbx" type="checkbox"></input>
64
      <label for="possible_patterns_chbx">
65
	<h3>
66
	  <span id="triangle">&#x23F5;</span><span>&#x23F7;</span>
67
	  Possible patterns
68
	</h3>
69
      </label>
70
      <ul id="possible_patterns"></ul>
78
      <input id="show_privileged_notice_chbx" type="checkbox" class="show_next"></input>
79
      <h3>Privileged page</h3>
71 80

  
72
      <input id="connected_chbx" type="checkbox" class="show_hide_next2"></input>
81
      <input id="show_page_state_chbx" type="checkbox" class="show_next"></input>
73 82
      <div>
74
	<h3>
75
	  Matched pattern: <span id="pattern">...</span>
83
	<input id="possible_patterns_chbx" type="checkbox" class="unroll_chbx"></input>
84
	<span></span>
85
	<label for="possible_patterns_chbx">
86
	  <h3>
87
	    <span class="triangle">&#x23F5;</span>
88
	    <span class="triangle">&#x23F7;</span>
89
	    Possible patterns
90
	  </h3>
91
	</label>
92
	<ul id="possible_patterns"></ul>
93

  
94
	<input id="connected_chbx" type="checkbox" class="show_hide_next2"></input>
95
	<div>
96
	  Matched pattern: <span id="pattern" class="bold">...</span>
76 97
	  <button id="view_pattern" class="hide">
77 98
	    View in settings
78 99
	  </button>
100
	  <br/>
101
	  Blocked: <span id="blocked" class="bold">...</span>
102
	  <br/>
103
	  Payload: <span id="payload" class="bold">...</span>
104
	  <button id="view_payload" class="hide">
105
	    View in settings
106
	  </button>
107
	  <h3>Injected</h3>
108
	  <div id="container_for_injected">
109
	    <span id="none_injected">None</span>
110
	  </div>
79 111
	  <input id="query_started_chbx" type="checkbox" class="show_hide_next2"></input>
80 112
	  <div id="container_for_repo_responses">
81 113
	    <h3>Queried from repositories</h3>
......
83 115
	  <button id="query_pattern">
84 116
	    Search for matching patterns
85 117
	  </button>
86
	</h3>
87
	<h3>
88
	  Blocked: <span id="blocked">...</span>
89
	</h3>
90
	<h3>
91
	  Payload: <span id="payload">...</span>
92
	  <button id="view_payload" class="hide">
93
	    View in settings
94
	  </button>
95
	</h3>
96
	<h3>Injected</h3>
97
	<div id="container_for_injected">
98
	  <span id="none_injected">None</span>
99 118
	</div>
119
	<h3>Trying to connect..<input id="loading_chbx" type="checkbox" class="show_next"></input><span>.</span></h3>
100 120
      </div>
101
      <h3>Trying to connect..<input id="loading_chbx" type="checkbox" class="show_next"></input><span>.</span></h3>
102
    </div>
103 121

  
104
    <button id="settings_but" type="button" style="margin-top: 20px;">Settings</button>_POPUPSCRIPTS_
122
      <button id="settings_but" type="button" style="margin-top: 20px;">Settings</button>
123
    </div>_POPUPSCRIPTS_
105 124
  </body>
106 125
</html>
html/display-panel.js
10 10
 * IMPORT browser
11 11
 * IMPORT is_chrome
12 12
 * IMPORT is_mozilla
13
 *** Using remote storage here seems inefficient, we only resort to that
14
 *** temporarily, before all storage access gets reworked.
15
 * IMPORT get_remote_storage
16
 * IMPORT get_import_frame
17
 * IMPORT query_all
13 18
 * IMPORT CONNECTION_TYPE
14 19
 * IMPORT url_item
15 20
 * IMPORT is_privileged_url
......
17 22
 * IMPORT nice_name
18 23
 * IMPORT open_in_settings
19 24
 * IMPORT for_each_possible_pattern
25
 * IMPORT by_id
26
 * IMPORT clone_template
20 27
 * IMPORTS_END
21 28
 */
22 29

  
23
function by_id(id)
24
{
25
    return document.getElementById(id);
26
}
30
let storage;
31
let tab_url;
27 32

  
28 33
const tab_query = {currentWindow: true, active: true};
29 34

  
......
55 60
	return;
56 61
    }
57 62

  
58
    const url = url_item(tab.url);
59
    page_url_heading.textContent = url;
60
    if (is_privileged_url(url)) {
63
    tab_url = url_item(tab.url);
64
    page_url_heading.textContent = tab_url;
65
    if (is_privileged_url(tab_url)) {
61 66
	show_privileged_notice_chbx.checked = true;
62 67
	return;
63 68
    }
64 69

  
65
    populate_possible_patterns_list(url);
70
    populate_possible_patterns_list(tab_url);
66 71
    show_page_state_chbx.checked = true;
67 72

  
68 73
    try_to_connect(tab.id);
69 74
}
70 75

  
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 76
const possible_patterns_ul = by_id("possible_patterns");
81 77
const pattern_li_template = by_id("pattern_li_template");
82 78
pattern_li_template.removeAttribute("id");
......
121 117
    by_id(li_id).firstElementChild.nextElementSibling.textContent = text;
122 118
}
123 119

  
124
function handle_page_info(message)
120
function handle_page_change(change)
125 121
{
126
    const [type, data] = message;
122
    const li_id = ensure_pattern_exists(change.item);
123
    if (change.old_val === undefined)
124
	set_pattern_li_button_text(li_id, "Edit in settings");
125
    if (change.new_val === undefined)
126
	set_pattern_li_button_text(li_id, "Add setting");
127
}
127 128

  
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
    }
129
function populate_possible_patterns_list(url)
130
{
131
    for_each_possible_pattern(url, add_pattern_to_list);
135 132

  
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
	}
133
    for (const [pattern, settings] of query_all(storage, url)) {
134
	set_pattern_li_button_text(ensure_pattern_exists(pattern),
135
				   "Edit in settings");
143 136
    }
137

  
138
    storage.add_change_listener(handle_page_change, [TYPE_PREFIX.PAGE]);
144 139
}
145 140

  
146 141
const connected_chbx = by_id("connected_chbx");
147 142
const query_pattern_but = by_id("query_pattern");
148 143

  
144
var content_script_port;
145

  
149 146
function try_to_connect(tab_id)
150 147
{
151 148
    /* This won't connect to iframes. We'll add support for them later */
152 149
    const connect_info = {name: CONNECTION_TYPE.ACTIVITY_INFO, frameId: 0};
153
    const port = browser.tabs.connect(tab_id, connect_info);
150
    content_script_port = browser.tabs.connect(tab_id, connect_info);
154 151

  
155
    const button_cb = (e) => start_querying_repos(port);
152
    const disconnect_cb = () => handle_disconnect(tab_id, start_querying_repos);
153
    content_script_port.onDisconnect.addListener(disconnect_cb);
154
    content_script_port.onMessage.addListener(handle_activity_report);
156 155

  
157
    port.onDisconnect.addListener(port => handle_disconnect(tab_id, button_cb));
158
    port.onMessage.addListener(handle_activity_report);
159

  
160
    query_pattern_but.addEventListener("click", button_cb);
156
    query_pattern_but.addEventListener("click", start_querying_repos);
161 157

  
162 158
    if (is_mozilla)
163
	setTimeout(() => monitor_connecting(port, tab_id), 1000);
159
	setTimeout(() => monitor_connecting(tab_id), 1000);
164 160
}
165 161

  
166 162
const query_started_chbx = by_id("query_started_chbx");
167 163

  
168 164
function start_querying_repos(port)
169 165
{
170
    port.postMessage("dummy (trigger repo querying)");
166
    const repo_urls = storage.get_all_names(TYPE_PREFIX.REPO);
167
    if (content_script_port)
168
	content_script_port.postMessage([TYPE_PREFIX.URL, tab_url, repo_urls]);
171 169
    query_started_chbx.checked = true;
172 170
}
173 171

  
......
176 174
function handle_disconnect(tab_id, button_cb)
177 175
{
178 176
    query_pattern_but.removeEventListener("click", button_cb);
177
    content_script_port = null;
179 178

  
180 179
    if (is_chrome && !browser.runtime.lastError)
181 180
	return;
......
188 187
    setTimeout(() => try_to_connect(tab_id), 1000);
189 188
}
190 189

  
191
function monitor_connecting(port, tab_id)
190
function monitor_connecting(tab_id)
192 191
{
193 192
    if (connected_chbx.checked)
194 193
	return;
195 194

  
196
    port.disconnect();
195
    if (content_script_port)
196
	content_script_port.disconnect();
197
    else
198
	return;
199

  
197 200
    loading_chbx.checked = !loading_chbx.checked;
198 201
    try_to_connect(tab_id);
199 202
}
......
204 207
const payload_span = by_id("payload");
205 208
const view_payload_but = by_id("view_payload");
206 209
const container_for_injected = by_id("container_for_injected");
207
const container_for_repo_responses = by_id("container_for_repo_responses");
210

  
211
const queried_items = new Map();
208 212

  
209 213
function handle_activity_report(message)
210 214
{
......
213 217
    const [type, data] = message;
214 218

  
215 219
    if (type === "settings") {
216
	let [pattern, settings, repos] = data;
220
	let [pattern, settings] = data;
217 221

  
218 222
	settings = settings || {};
219 223
	blocked_span.textContent = settings.allow ? "no" : "yes";
......
247 251
	container_for_injected.appendChild(h4);
248 252
	container_for_injected.appendChild(pre);
249 253
    }
250
    if (type === "repo_query_result") {
251
	const [repo_url, response_text] = data;
254
    if (type === "repo_query_action") {
255
	query_started_chbx.checked = true;
252 256

  
253
	const h4 = document.createElement("h4");
254
	const pre = document.createElement("pre");
255
	h4.textContent = repo_url;
256
	pre.textContent = response_text;
257
	const key = data.prefix + data.item;
258
	const results = queried_items.get(key) || {};
259
	Object.assign(results, data.results);
260
	queried_items.set(key, results);
261

  
262
	const action = data.prefix === TYPE_PREFIX.URL ?
263
	      show_query_result : record_fetched_install_dep;
264

  
265
	for (const [repo_url, result] of Object.entries(data.results))
266
	    action(data.prefix, data.item, repo_url, result);
267
    }
268
}
269

  
270
const container_for_repo_responses = by_id("container_for_repo_responses");
271

  
272
const results_lists = new Map();
273

  
274
function create_results_list(url)
275
{
276
    const list_div = document.createElement("div");
277
    const list_head = document.createElement("h4");
278
    const list = document.createElement("ul");
279

  
280
    list_head.textContent = url;
281
    list_div.appendChild(list_head);
282
    list_div.appendChild(list);
283
    container_for_repo_responses.appendChild(list_div);
284

  
285
    const list_object = {list, by_repo: new Map()};
286

  
287
    results_lists.set(url, list_object);
288

  
289
    return list_object;
290
}
291

  
292
function create_result_item(list_object, repo_url, result)
293
{
294
    const result_li = document.createElement("li");
295
    const repo_url_span = document.createElement("span");
296
    const result_item = {result_li, appended: null};
297

  
298
    repo_url_span.textContent = repo_url;
299
    result_li.appendChild(repo_url_span);
300

  
301
    list_object.list.appendChild(result_li);
302
    list_object.by_repo.set(repo_url, result_item);
303

  
304
    return result_item;
305
}
306

  
307
function set_appended(result_item, element)
308
{
309
    if (result_item.appended)
310
	result_item.appended.remove();
311
    result_item.appended = element;
312
    result_item.result_li.appendChild(element);
313
}
314

  
315
function show_message(result_item, text)
316
{
317
    const div = document.createElement("div");
318
    div.textContent = text;
319
    set_appended(result_item, div);
320
}
321

  
322
function showcb(text)
323
{
324
    return item => show_message(item, text);
325
}
326

  
327
function unroll_chbx_first_checked(entry_object)
328
{
329
    if (!entry_object.chbx.checked)
330
	return;
331

  
332
    entry_object.chbx.removeEventListener("change", entry_object.unroll_cb);
333
    delete entry_object.unroll_cb;
334

  
335
    entry_object.unroll.textContent = "preview not implemented...";
336
}
337

  
338
const show_install_chbx = by_id("show_install_view_chbx");
339

  
340
let import_frame;
341
let install_target = null;
342

  
343
function install_abort(error_state)
344
{
345
    import_frame.show_error(`Error: ${error_state}`);
346
    install_target = null;
347
}
348

  
349
/*
350
 * Translate objects from the format in which they are sent by Hydrilla to the
351
 * format in which they are stored in settings.
352
 */
257 353

  
258
	container_for_repo_responses.appendChild(h4);
259
	container_for_repo_responses.appendChild(pre);
354
function translate_script(script_object, repo_url)
355
{
356
    return {
357
	[TYPE_PREFIX.SCRIPT + script_object.name]: {
358
	    hash: script_object.sha256,
359
	    url: `${repo_url}/content/${script_object.location}`
360
	}
361
    };
362
}
363

  
364
function translate_bag(bag_object)
365
{
366
    return {
367
	[TYPE_PREFIX.BAG + bag_object.name]: bag_object.components
368
    };
369
}
370

  
371
const format_translators = {
372
    [TYPE_PREFIX.BAG]: translate_bag,
373
    [TYPE_PREFIX.SCRIPT]: translate_script
374
};
375

  
376
function install_check_ready()
377
{
378
    if (install_target.to_fetch.size > 0)
379
	return;
380

  
381
    const page_key = [TYPE_PREFIX.PAGE + install_target.pattern];
382
    const to_install = [{[page_key]: {components: install_target.payload}}];
383

  
384
    for (const key of install_target.fetched) {
385
	const old_object =
386
	      queried_items.get(key)[install_target.repo_url].response;
387
	const new_object =
388
	      format_translators[key[0]](old_object, install_target.repo_url);
389
	to_install.push(new_object);
390
    }
391

  
392
    import_frame.show_selection(to_install);
393
}
394

  
395
const possible_errors = ["connection_error", "parse_error"];
396

  
397
function fetch_install_deps(components)
398
{
399
    const needed = [...components];
400
    const processed = new Set();
401

  
402
    while (needed.length > 0) {
403
	const [prefix, item] = needed.pop();
404
	const key = prefix + item;
405
	processed.add(key);
406
	const results = queried_items.get(key);
407
	let relevant_result = null;
408

  
409
	if (results)
410
	    relevant_result = results[install_target.repo_url];
411

  
412
	if (!relevant_result) {
413
	    content_script_port.postMessage([prefix, item,
414
					     [install_target.repo_url]]);
415
	    install_target.to_fetch.add(key);
416
	    continue;
417
	}
418

  
419
	if (possible_errors.includes(relevant_result.state)) {
420
	    install_abort(relevant_result.state);
421
	    return false;
422
	}
423

  
424
	install_target.fetched.add(key);
425

  
426
	if (prefix !== TYPE_PREFIX.BAG)
427
	    continue;
428

  
429
	for (const dependency of relevant_result.response.components) {
430
	    if (processed.has(dependency.join('')))
431
		continue;
432
	    needed.push(dependency);
433
	}
260 434
    }
261 435
}
262 436

  
437
function record_fetched_install_dep(prefix, item, repo_url, result)
438
{
439
    const key = prefix + item;
440

  
441
    if (!install_target || repo_url !== install_target.repo_url ||
442
	!install_target.to_fetch.has(key))
443
	return;
444

  
445
    if (possible_errors.includes(result.state)) {
446
	install_abort(result.state);
447
	return;
448
    }
449

  
450
    if (result.state !== "completed")
451
	return;
452

  
453
    install_target.to_fetch.delete(key);
454
    install_target.fetched.add(key);
455

  
456
    if (prefix === TYPE_PREFIX.BAG &&
457
	fetch_install_deps(result.response.components) === false)
458
	return;
459

  
460
    install_check_ready();
461
}
462

  
463
function install_clicked(entry_object)
464
{
465
    show_install_chbx.checked = true;
466
    import_frame.show_loading();
467

  
468
    install_target = {
469
	repo_url: entry_object.repo_url,
470
	pattern: entry_object.match_object.pattern,
471
	payload: entry_object.match_object.payload,
472
	fetched: new Set(),
473
	to_fetch: new Set()
474
    };
475

  
476
    fetch_install_deps([install_target.payload]);
477

  
478
    install_check_ready();
479
}
480

  
481
var max_query_result_id = 0;
482

  
483
function show_query_successful_result(result_item, repo_url, result)
484
{
485
    const ul = document.createElement("ul");
486

  
487
    set_appended(result_item, ul);
488

  
489
    for (const match of result) {
490
	const entry_object = clone_template("query_match_li_template");
491

  
492
	entry_object.pattern.textContent = match.pattern;
493

  
494
	ul.appendChild(entry_object.li);
495

  
496
	if (!match.payload) {
497
	    entry_object.payload.textContent = "(none)";
498
	    for (const key of ["chbx", "br", "triangle", "unroll"])
499
		entry_object[key].remove();
500
	    continue;
501
	}
502

  
503
	entry_object.component.textContent = nice_name(...match.payload);
504

  
505
	const install_cb = () => install_clicked(entry_object);
506
	entry_object.btn.addEventListener("click", install_cb);
507

  
508
	const chbx_id = `query_result_${max_query_result_id++}`;
509
	entry_object.chbx.id = chbx_id;
510
	entry_object.lbl.setAttribute("for", chbx_id);
511

  
512
	entry_object.unroll_cb = () => unroll_chbx_first_checked(entry_object);
513
	entry_object.chbx.addEventListener("change", entry_object.unroll_cb);
514

  
515
	entry_object.component_object = match.payload;
516
	entry_object.match_object = match;
517
	entry_object.repo_url = repo_url;
518
    }
519
}
520

  
521
function show_query_result(url_prefix, url, repo_url, result)
522
{
523
    const results_list_object = results_lists.get(url) ||
524
	  create_results_list(url);
525
    const result_item = results_list_object.by_repo.get(repo_url) ||
526
	  create_result_item(results_list_object, repo_url, result);
527

  
528
    const completed_cb =
529
	  item => show_query_successful_result(item, repo_url, result.response);
530
    const possible_actions = {
531
	completed: completed_cb,
532
	started: showcb("loading..."),
533
	connection_error: showcb("Error when querying repository."),
534
	parse_error: showcb("Bad data format received.")
535
    };
536
    possible_actions[result.state](result_item, repo_url);
537
}
538

  
263 539
by_id("settings_but")
264 540
    .addEventListener("click", (e) => browser.runtime.openOptionsPage());
265 541

  
266
show_page_activity_info();
542
async function main()
543
{
544
    storage = await get_remote_storage();
545
    import_frame = await get_import_frame();
546
    import_frame.onclose = () => show_install_chbx.checked = false;
547
    show_page_activity_info();
548
}
549

  
550
main();
html/import_frame.html
1
<div style="display: none;">
2
  <li id="import_li_template">
3
    <span></span>
4
    <input type="checkbox" style="display: inline;" checked></input>
5
    <span></span>
6
  </li>
7
</div>
8
<h2> Settings import </h2>
9
<input id="import_loading_radio" type="radio" name="import_window_content" class="show_next"></input>
10
<span> Loading... </span>
11
<input id="import_failed_radio" type="radio" name="import_window_content" class="show_next"></input>
12
<div>
13
  <span id="import_errormsg"></span>
14
  <input id="import_errordetail_chbx" type="checkbox" class="show_next"></input>
15
  <pre id="import_errordetail"></pre>
16
  <button id="import_failok_but"> OK </button>
17
</div>
18
<input id="import_selection_radio" type="radio" name="import_window_content" class="show_next"></input>
19
<div>
20
  <button id="check_all_import_but"> Check all </button>
21
  <button id="uncheck_all_import_but"> Uncheck all </button>
22
  <button id="uncheck_colliding_import_but"> Uncheck existing </button>
23
  <ul id="import_ul">
24
  </ul>
25
  <button id="commit_import_but"> OK </button>
26
  <button id="cancel_import_but"> Cancel </button>
27
</div>
html/import_frame.js
1
/**
2
 * Hachette HTML import frame script
3
 *
4
 * Copyright (C) 2021 Wojtek Kosior
5
 * Redistribution terms are gathered in the `copyright' file.
6
 */
7

  
8
/*
9
 * IMPORTS_START
10
 * IMPORT get_remote_storage
11
 * IMPORT by_id
12
 * IMPORT nice_name
13
 * IMPORT make_once
14
 * IMPORTS_END
15
 */
16

  
17
let storage;
18

  
19
const import_li_template = by_id("import_li_template");
20
import_li_template.removeAttribute("id");
21

  
22
function import_li_id(prefix, item)
23
{
24
    return `ili_${prefix}_${item}`;
25
}
26

  
27
let import_ul = by_id("import_ul");
28
let import_chbxs_colliding = undefined;
29
let settings_import_map = undefined;
30

  
31
function add_import_li(prefix, name)
32
{
33
    let li = import_li_template.cloneNode(true);
34
    let name_span = li.firstElementChild;
35
    let chbx = name_span.nextElementSibling;
36
    let warning_span = chbx.nextElementSibling;
37

  
38
    li.setAttribute("data-prefix", prefix);
39
    li.setAttribute("data-name", name);
40
    li.id = import_li_id(prefix, name);
41
    name_span.textContent = nice_name(prefix, name);
42

  
43
    if (storage.get(prefix, name) !== undefined) {
44
	import_chbxs_colliding.push(chbx);
45
	warning_span.textContent = "(will overwrite existing setting!)";
46
    }
47

  
48
    import_ul.appendChild(li);
49
}
50

  
51
function check_all_imports()
52
{
53
    for (let li of import_ul.children)
54
	li.firstElementChild.nextElementSibling.checked = true;
55
}
56

  
57
function uncheck_all_imports()
58
{
59
    for (let li of import_ul.children)
60
	li.firstElementChild.nextElementSibling.checked = false;
61
}
62

  
63
function uncheck_colliding_imports()
64
{
65
    for (let chbx of import_chbxs_colliding)
66
	chbx.checked = false;
67
}
68

  
69
function commit_import()
70
{
71
    for (let li of import_ul.children) {
72
	let chbx = li.firstElementChild.nextElementSibling;
73

  
74
	if (!chbx.checked)
75
	    continue;
76

  
77
	let prefix = li.getAttribute("data-prefix");
78
	let name = li.getAttribute("data-name");
79
	let key = prefix + name;
80
	let value = settings_import_map.get(key);
81
	storage.set(prefix, name, value);
82
    }
83

  
84
    deactivate();
85
}
86

  
87
const import_loading_radio = by_id("import_loading_radio");
88

  
89
function show_loading()
90
{
91
    import_loading_radio.checked = true;
92
}
93

  
94
const import_failed_radio = by_id("import_failed_radio");
95
const import_errormsg = by_id("import_errormsg");
96
const import_errordetail_chbx = by_id("import_errordetail_chbx");
97
const import_errordetail = by_id("import_errordetail");
98

  
99
function show_error(errormsg, errordetail)
100
{
101
    import_failed_radio.checked = true;
102
    import_errormsg.textContent = errormsg;
103
    import_errordetail_chbx.checked = errordetail;
104
    import_errordetail.textContent = errordetail;
105
}
106

  
107
const import_selection_radio = by_id("import_selection_radio");
108

  
109
function show_selection(settings)
110
{
111
    import_selection_radio.checked = true;
112

  
113
    let old_children = import_ul.children;
114
    while (old_children[0] !== undefined)
115
	import_ul.removeChild(old_children[0]);
116

  
117
    import_chbxs_colliding = [];
118
    settings_import_map = new Map();
119

  
120
    for (let setting of settings) {
121
	let [key, value] = Object.entries(setting)[0];
122
	let prefix = key[0];
123
	let name = key.substring(1);
124
	add_import_li(prefix, name);
125
	settings_import_map.set(key, value);
126
    }
127
}
128

  
129
function deactivate()
130
{
131
    /* Let GC free some memory */
132
    import_chbxs_colliding = undefined;
133
    settings_import_map = undefined;
134

  
135
    if (exports.onclose)
136
	exports.onclose();
137
}
138

  
139
const exports = {show_loading, show_error, show_selection, deactivate};
140

  
141
async function init()
142
{
143
    storage = await get_remote_storage();
144

  
145
    by_id("commit_import_but").addEventListener("click", commit_import);
146
    by_id("check_all_import_but").addEventListener("click", check_all_imports);
147
    by_id("uncheck_all_import_but")
148
	.addEventListener("click", uncheck_all_imports);
149
    by_id("uncheck_colliding_import_but")
150
	.addEventListener("click", uncheck_colliding_imports);
151
    by_id("cancel_import_but").addEventListener("click", deactivate);
152
    by_id("import_failok_but").addEventListener("click", deactivate);
153

  
154
    return exports;
155
}
156

  
157
const get_import_frame = make_once(init);
158

  
159
/*
160
 * EXPORTS_START
161
 * EXPORT get_import_frame
162
 * EXPORTS_END
163
 */
html/options.html
8 8
  <head>
9 9
    <meta charset="utf-8"/>
10 10
    <title>Hachette options</title>
11
    <link type="text/css" rel="stylesheet" href="base.css" />
11 12
    <style>
12
      input[type="checkbox"], input[type="radio"], .hide, .popup.hide {
13
	  display: none;
14
      }
15

  
16 13
      /* pages list */
17 14
      #page_components_ul {
18 15
	  max-height: 80vh;
......
78 75
      input[type="radio"]:not(:checked)+.import_window_content {
79 76
	  display: none;
80 77
      }
81

  
82
      /* buttons */
83
      button {
84
          background-color: #4CAF50;
85
          border: none;
86
          border-radius: 8px;
87
          color: white;
88
          padding: 6px 12px;
89
          text-align: center;
90
          text-decoration: none;
91
          display: inline-block;
92
          margin: 2px 0px;
93
      }
94

  
95
    button:hover {
96
        box-shadow: 0 6px 8px 0 rgba(0,0,0,0.24), 0 17px 50px 0 rgba(0,0,0,0.19);
97
    }
98 78
    </style>
99 79
  </head>
100 80
  <body>
......
118 98
	<input type="radio" style="display: inline;" name="page_components"></input>
119 99
	<span></span>
120 100
      </li>
121
      <li id="import_li_template">
122
	<span></span>
123
	<input type="checkbox" style="display: inline;" checked></input>
124
	<span></span>
125
      </li>
... This diff was truncated because it exceeds the maximum size that can be displayed.

Also available in: Unified diff