Project

General

Profile

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

haketilo / html / display-panel.js @ 4b59dced

1
/**
2
 * Hachette display panel logic
3
 *
4
 * Copyright (C) 2021 Wojtek Kosior
5
 * Redistribution terms are gathered in the `copyright' file.
6
 */
7

    
8
/*
9
 * IMPORTS_START
10
 * IMPORT browser
11
 * IMPORT is_chrome
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
18
 * IMPORT CONNECTION_TYPE
19
 * IMPORT is_privileged_url
20
 * IMPORT TYPE_PREFIX
21
 * IMPORT nice_name
22
 * IMPORT open_in_settings
23
 * IMPORT url_matches
24
 * IMPORT each_url_pattern
25
 * IMPORT by_id
26
 * IMPORT clone_template
27
 * IMPORTS_END
28
 */
29

    
30
let storage;
31
let tab_url;
32

    
33
/* Force popup <html>'s reflow on stupid Firefox. */
34
if (is_mozilla) {
35
    const reflow_forcer =
36
	  () => document.documentElement.style.width = "-moz-fit-content";
37
    for (const radio of document.querySelectorAll('[name="current_view"]'))
38
	radio.addEventListener("change", reflow_forcer);
39
}
40

    
41
const show_queried_view_radio = by_id("show_queried_view_radio");
42

    
43
const tab_query = {currentWindow: true, active: true};
44

    
45
async function get_current_tab()
46
{
47
    /* Fix for fact that Chrome does not use promises here */
48
    const promise = is_chrome ?
49
	  new Promise((resolve, reject) =>
50
		      browser.tabs.query(tab_query, tab => resolve(tab))) :
51
	  browser.tabs.query(tab_query);
52

    
53
    try {
54
	return (await promise)[0];
55
    } catch(e) {
56
	console.log(e);
57
    }
58
}
59

    
60
const page_url_heading = by_id("page_url_heading");
61
const privileged_notice = by_id("privileged_notice");
62
const page_state = by_id("page_state");
63

    
64
/* Helper functions to convert string into a list of one-letter <span>'s. */
65
function char_to_span(char, doc)
66
{
67
    const span = document.createElement("span");
68
    span.textContent = char;
69
    return span;
70
}
71

    
72
function to_spans(string, doc=document)
73
{
74
    return string.split("").map(c => char_to_span(c, doc));
75
}
76

    
77
async function show_page_activity_info()
78
{
79
    const tab = await get_current_tab();
80

    
81
    if (tab === undefined) {
82
	page_url_heading.textContent = "unknown page";
83
	return;
84
    }
85

    
86
    tab_url = /^([^?#]*)/.exec(tab.url)[1];
87
    to_spans(tab_url).forEach(s => page_url_heading.append(s));
88
    if (is_privileged_url(tab_url)) {
89
	privileged_notice.classList.remove("hide");
90
	return;
91
    }
92

    
93
    populate_possible_patterns_list(tab_url);
94
    page_state.classList.remove("hide");
95

    
96
    try_to_connect(tab.id);
97
}
98

    
99
const possible_patterns_list = by_id("possible_patterns");
100
const known_patterns = new Map();
101

    
102
function add_pattern_to_list(pattern)
103
{
104
    const template = clone_template("pattern_entry");
105
    template.name.textContent = pattern;
106

    
107
    const settings_opener = () => open_in_settings(TYPE_PREFIX.PAGE, pattern);
108
    template.button.addEventListener("click", settings_opener);
109

    
110
    known_patterns.set(pattern, template);
111
    possible_patterns_list.append(template.entry);
112

    
113
    return template;
114
}
115

    
116
function ensure_pattern_exists(pattern)
117
{
118
    let entry_object = known_patterns.get(pattern);
119
    /*
120
     * As long as pattern computation works well, we should never get into this
121
     * conditional block. This is just a safety measure. To be removed as part
122
     * of a bigger rework when we start taking iframes into account.
123
     */
124
    if (entry_object === undefined) {
125
	console.log(`unknown pattern: ${pattern}`);
126
	entry_object = add_pattern_to_list(pattern);
127
    }
128

    
129
    return entry_object;
130
}
131

    
132
function style_possible_pattern_entry(pattern, exists_in_settings)
133
{
134
    const [text, class_action] = exists_in_settings ?
135
	  ["Edit", "add"] : ["Add", "remove"];
136
    const entry_object = ensure_pattern_exists(pattern);
137

    
138
    entry_object.button.textContent = `${text} setting`;
139
    entry_object.entry.classList[class_action]("matched_pattern");
140
}
141

    
142
function handle_page_change(change)
143
{
144
    if (url_matches(tab_url, change.item))
145
        style_possible_pattern_entry(change.item, change.new_val !== undefined);
146
}
147

    
148
function populate_possible_patterns_list(url)
149
{
150
    for (const pattern of each_url_pattern(url))
151
	add_pattern_to_list(pattern);
152

    
153
    for (const [pattern, settings] of query_all(storage, url))
154
	style_possible_pattern_entry(pattern, true);
155

    
156
    storage.add_change_listener(handle_page_change, [TYPE_PREFIX.PAGE]);
157
}
158

    
159
const connected_chbx = by_id("connected_chbx");
160
const query_pattern_but = by_id("query_pattern");
161

    
162
var content_script_port;
163

    
164
function try_to_connect(tab_id)
165
{
166
    /* This won't connect to iframes. We'll add support for them later */
167
    const connect_info = {name: CONNECTION_TYPE.ACTIVITY_INFO, frameId: 0};
168
    content_script_port = browser.tabs.connect(tab_id, connect_info);
169

    
170
    const disconnect_cb = () => handle_disconnect(tab_id, start_querying_repos);
171
    content_script_port.onDisconnect.addListener(disconnect_cb);
172
    content_script_port.onMessage.addListener(handle_activity_report);
173

    
174
    query_pattern_but.addEventListener("click", start_querying_repos);
175

    
176
    if (is_mozilla)
177
	setTimeout(() => monitor_connecting(tab_id), 1000);
178
}
179

    
180
function start_querying_repos()
181
{
182
    query_pattern_but.removeEventListener("click", start_querying_repos);
183
    const repo_urls = storage.get_all_names(TYPE_PREFIX.REPO);
184
    if (content_script_port)
185
	content_script_port.postMessage([TYPE_PREFIX.URL, tab_url, repo_urls]);
186
}
187

    
188
const loading_point = by_id("loading_point");
189
const reload_notice = by_id("reload_notice");
190

    
191
function handle_disconnect(tab_id, button_cb)
192
{
193
    query_pattern_but.removeEventListener("click", button_cb);
194
    content_script_port = null;
195

    
196
    if (is_chrome && !browser.runtime.lastError)
197
	return;
198

    
199
    /* return if error was not during connection initialization */
200
    if (connected_chbx.checked)
201
	return;
202

    
203
    loading_point.classList.toggle("camouflage");
204
    reload_notice.classList.remove("hide");
205

    
206
    setTimeout(() => try_to_connect(tab_id), 1000);
207
}
208

    
209
function monitor_connecting(tab_id)
210
{
211
    if (connected_chbx.checked)
212
	return;
213

    
214
    if (content_script_port)
215
	content_script_port.disconnect();
216
    else
217
	return;
218

    
219
    loading_point.classList.toggle("camouflage");
220
    reload_notice.classList.remove("hide");
221
    try_to_connect(tab_id);
222
}
223

    
224
const pattern_span = by_id("pattern");
225
const view_pattern_but = by_id("view_pattern");
226
const blocked_span = by_id("blocked");
227
const payload_span = by_id("payload");
228
const payload_buttons_div = by_id("payload_buttons");
229
const view_payload_but = by_id("view_payload");
230
const view_injected_but = by_id("view_injected");
231
const container_for_injected = by_id("container_for_injected");
232

    
233
const queried_items = new Map();
234

    
235
let max_injected_script_id = 0;
236

    
237
function handle_activity_report(message)
238
{
239
    connected_chbx.checked = true;
240

    
241
    const [type, data] = message;
242

    
243
    if (type === "settings") {
244
	let [pattern, settings] = data;
245

    
246
	settings = settings || {};
247
	blocked_span.textContent = settings.allow ? "no" : "yes";
248

    
249
	if (pattern) {
250
	    pattern_span.textContent = pattern;
251
	    const settings_opener =
252
		  () => open_in_settings(TYPE_PREFIX.PAGE, pattern);
253
	    view_pattern_but.classList.remove("hide");
254
	    view_pattern_but.addEventListener("click", settings_opener);
255
	} else {
256
	    pattern_span.textContent = "none";
257
	}
258

    
259
	const components = settings.components;
260
	if (components) {
261
	    payload_span.textContent = nice_name(...components);
262
	    payload_buttons_div.classList.remove("hide");
263
	    const settings_opener = () => open_in_settings(...components);
264
	    view_payload_but.addEventListener("click", settings_opener);
265
	} else {
266
	    payload_span.textContent = "none";
267
	}
268
    }
269
    if (type === "script") {
270
	const template = clone_template("injected_script");
271
	const chbx_id = `injected_script_${max_injected_script_id++}`;
272
	template.chbx.id = chbx_id;
273
	template.lbl.setAttribute("for", chbx_id);
274
	template.script_contents.textContent = data;
275
	container_for_injected.appendChild(template.div);
276
    }
277
    if (type === "repo_query_action") {
278
	const key = data.prefix + data.item;
279
	const results = queried_items.get(key) || {};
280
	Object.assign(results, data.results);
281
	queried_items.set(key, results);
282

    
283
	const action = data.prefix === TYPE_PREFIX.URL ?
284
	      show_query_result : record_fetched_install_dep;
285

    
286
	for (const [repo_url, result] of Object.entries(data.results))
287
	    action(data.prefix, data.item, repo_url, result);
288
    }
289
}
290

    
291
const container_for_repo_responses = by_id("container_for_repo_responses");
292

    
293
const results_lists = new Map();
294

    
295
function create_results_list(url)
296
{
297
    const cloned_template = clone_template("multi_repos_query_result");
298
    cloned_template.url_span.textContent = url;
299
    container_for_repo_responses.appendChild(cloned_template.div);
300

    
301
    cloned_template.by_repo = new Map();
302
    results_lists.set(url, cloned_template);
303

    
304
    return cloned_template;
305
}
306

    
307
function create_result_item(list_object, repo_url, result)
308
{
309
    const cloned_template = clone_template("single_repo_query_result");
310
    cloned_template.repo_url.textContent = repo_url;
311
    cloned_template.appended = null;
312

    
313
    list_object.ul.appendChild(cloned_template.li);
314
    list_object.by_repo.set(repo_url, cloned_template);
315

    
316
    return cloned_template;
317
}
318

    
319
function set_appended(result_item, element)
320
{
321
    if (result_item.appended)
322
	result_item.appended.remove();
323
    result_item.appended = element;
324
    result_item.li.appendChild(element);
325
}
326

    
327
function show_message(result_item, text)
328
{
329
    const div = document.createElement("div");
330
    div.textContent = text;
331
    set_appended(result_item, div);
332
}
333

    
334
function showcb(text)
335
{
336
    return item => show_message(item, text);
337
}
338

    
339
function unroll_chbx_first_checked(entry_object)
340
{
341
    if (!entry_object.chbx.checked)
342
	return;
343

    
344
    entry_object.chbx.removeEventListener("change", entry_object.unroll_cb);
345
    delete entry_object.unroll_cb;
346

    
347
    entry_object.unroll.innerHTML = "preview not implemented...<br />(consider contributing)";
348
}
349

    
350
let import_frame;
351
let install_target = null;
352

    
353
function install_abort(error_state)
354
{
355
    import_frame.show_error(`Error: ${error_state}`);
356
    install_target = null;
357
}
358

    
359
/*
360
 * Translate objects from the format in which they are sent by Hydrilla to the
361
 * format in which they are stored in settings.
362
 */
363

    
364
function translate_script(script_object, repo_url)
365
{
366
    return {
367
	[TYPE_PREFIX.SCRIPT + script_object.name]: {
368
	    hash: script_object.sha256,
369
	    url: `${repo_url}/content/${script_object.location}`
370
	}
371
    };
372
}
373

    
374
function translate_bag(bag_object)
375
{
376
    return {
377
	[TYPE_PREFIX.BAG + bag_object.name]: bag_object.components
378
    };
379
}
380

    
381
const format_translators = {
382
    [TYPE_PREFIX.BAG]: translate_bag,
383
    [TYPE_PREFIX.SCRIPT]: translate_script
384
};
385

    
386
function install_check_ready()
387
{
388
    if (install_target.to_fetch.size > 0)
389
	return;
390

    
391
    const page_key = [TYPE_PREFIX.PAGE + install_target.pattern];
392
    const to_install = [{[page_key]: {components: install_target.payload}}];
393

    
394
    for (const key of install_target.fetched) {
395
	const old_object =
396
	      queried_items.get(key)[install_target.repo_url].response;
397
	const new_object =
398
	      format_translators[key[0]](old_object, install_target.repo_url);
399
	to_install.push(new_object);
400
    }
401

    
402
    import_frame.show_selection(to_install);
403
}
404

    
405
const possible_errors = ["connection_error", "parse_error"];
406

    
407
function fetch_install_deps(components)
408
{
409
    const needed = [...components];
410
    const processed = new Set();
411

    
412
    while (needed.length > 0) {
413
	const [prefix, item] = needed.pop();
414
	const key = prefix + item;
415
	processed.add(key);
416
	const results = queried_items.get(key);
417
	let relevant_result = null;
418

    
419
	if (results)
420
	    relevant_result = results[install_target.repo_url];
421

    
422
	if (!relevant_result) {
423
	    content_script_port.postMessage([prefix, item,
424
					     [install_target.repo_url]]);
425
	    install_target.to_fetch.add(key);
426
	    continue;
427
	}
428

    
429
	if (possible_errors.includes(relevant_result.state)) {
430
	    install_abort(relevant_result.state);
431
	    return false;
432
	}
433

    
434
	install_target.fetched.add(key);
435

    
436
	if (prefix !== TYPE_PREFIX.BAG)
437
	    continue;
438

    
439
	for (const dependency of relevant_result.response.components) {
440
	    if (processed.has(dependency.join('')))
441
		continue;
442
	    needed.push(dependency);
443
	}
444
    }
445
}
446

    
447
function record_fetched_install_dep(prefix, item, repo_url, result)
448
{
449
    const key = prefix + item;
450

    
451
    if (!install_target || repo_url !== install_target.repo_url ||
452
	!install_target.to_fetch.has(key))
453
	return;
454

    
455
    if (possible_errors.includes(result.state)) {
456
	install_abort(result.state);
457
	return;
458
    }
459

    
460
    if (result.state !== "completed")
461
	return;
462

    
463
    install_target.to_fetch.delete(key);
464
    install_target.fetched.add(key);
465

    
466
    if (prefix === TYPE_PREFIX.BAG &&
467
	fetch_install_deps(result.response.components) === false)
468
	return;
469

    
470
    install_check_ready();
471
}
472

    
473
function install_clicked(entry_object)
474
{
475
    import_frame.show_loading();
476

    
477
    install_target = {
478
	repo_url: entry_object.repo_url,
479
	pattern: entry_object.match_object.pattern,
480
	payload: entry_object.match_object.payload,
481
	fetched: new Set(),
482
	to_fetch: new Set()
483
    };
484

    
485
    fetch_install_deps([install_target.payload]);
486

    
487
    install_check_ready();
488
}
489

    
490
var max_query_result_id = 0;
491

    
492
function show_query_successful_result(result_item, repo_url, result)
493
{
494
    const cloned_ul_template = clone_template("result_patterns_list");
495
    set_appended(result_item, cloned_ul_template.ul);
496

    
497
    for (const match of result) {
498
	const entry_object = clone_template("query_match_li");
499

    
500
	entry_object.pattern.textContent = match.pattern;
501

    
502
	cloned_ul_template.ul.appendChild(entry_object.li);
503

    
504
	if (!match.payload) {
505
	    entry_object.payload.textContent = "(none)";
506
	    for (const key of ["chbx", "triangle", "unroll"])
507
		entry_object[key].remove();
508
	    continue;
509
	}
510

    
511
	entry_object.payload.textContent = nice_name(...match.payload);
512

    
513
	const install_cb = () => install_clicked(entry_object);
514
	entry_object.btn.addEventListener("click", install_cb);
515

    
516
	const chbx_id = `query_result_${max_query_result_id++}`;
517
	entry_object.chbx.id = chbx_id;
518
	entry_object.lbl.setAttribute("for", chbx_id);
519

    
520
	entry_object.unroll_cb = () => unroll_chbx_first_checked(entry_object);
521
	entry_object.chbx.addEventListener("change", entry_object.unroll_cb);
522

    
523
	entry_object.component_object = match.payload;
524
	entry_object.match_object = match;
525
	entry_object.repo_url = repo_url;
526
    }
527
}
528

    
529
function show_query_result(url_prefix, url, repo_url, result)
530
{
531
    const results_list_object = results_lists.get(url) ||
532
	  create_results_list(url);
533
    const result_item = results_list_object.by_repo.get(repo_url) ||
534
	  create_result_item(results_list_object, repo_url, result);
535

    
536
    const completed_cb =
537
	  item => show_query_successful_result(item, repo_url, result.response);
538
    const possible_actions = {
539
	completed: completed_cb,
540
	started: showcb("loading..."),
541
	connection_error: showcb("Error when querying repository."),
542
	parse_error: showcb("Bad data format received.")
543
    };
544
    possible_actions[result.state](result_item, repo_url);
545
}
546

    
547
by_id("settings_but")
548
    .addEventListener("click", (e) => browser.runtime.openOptionsPage());
549

    
550
async function main()
551
{
552
    storage = await get_remote_storage();
553
    import_frame = await get_import_frame();
554
    import_frame.onclose = () => show_queried_view_radio.checked = true;
555
    show_page_activity_info();
556
}
557

    
558
main();
(5-5/11)