Project

General

Profile

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

haketilo / html / display-panel.js @ c12b9ee3

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 init_default_policy_dialog
18
 * IMPORT query_all
19
 * IMPORT CONNECTION_TYPE
20
 * IMPORT is_privileged_url
21
 * IMPORT TYPE_PREFIX
22
 * IMPORT nice_name
23
 * IMPORT open_in_settings
24
 * IMPORT url_matches
25
 * IMPORT each_url_pattern
26
 * IMPORT by_id
27
 * IMPORT clone_template
28
 * IMPORTS_END
29
 */
30

    
31
let storage;
32
let tab_url;
33

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

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

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

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

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

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

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

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

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

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

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

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

    
97
    try_to_connect(tab.id);
98
}
99

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

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

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

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

    
114
    return template;
115
}
116

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

    
130
    return entry_object;
131
}
132

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

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

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

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

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

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

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

    
163
var content_script_port;
164

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
235
const queried_items = new Map();
236

    
237
let max_injected_script_id = 0;
238

    
239
function handle_activity_report(message)
240
{
241
    connected_chbx.checked = true;
242

    
243
    const [type, data] = message;
244

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

    
248
	blocked_span.textContent = settings.allow ? "no" : "yes";
249

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

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

    
289
	const action = data.prefix === TYPE_PREFIX.URL ?
290
	      show_query_result : record_fetched_install_dep;
291

    
292
	for (const [repo_url, result] of Object.entries(data.results))
293
	    action(data.prefix, data.item, repo_url, result);
294
    }
295
}
296

    
297
const container_for_repo_responses = by_id("container_for_repo_responses");
298

    
299
const results_lists = new Map();
300

    
301
function create_results_list(url)
302
{
303
    const cloned_template = clone_template("multi_repos_query_result");
304
    cloned_template.url_span.textContent = url;
305
    container_for_repo_responses.appendChild(cloned_template.div);
306

    
307
    cloned_template.by_repo = new Map();
308
    results_lists.set(url, cloned_template);
309

    
310
    return cloned_template;
311
}
312

    
313
function create_result_item(list_object, repo_url, result)
314
{
315
    const cloned_template = clone_template("single_repo_query_result");
316
    cloned_template.repo_url.textContent = repo_url;
317
    cloned_template.appended = null;
318

    
319
    list_object.ul.appendChild(cloned_template.li);
320
    list_object.by_repo.set(repo_url, cloned_template);
321

    
322
    return cloned_template;
323
}
324

    
325
function set_appended(result_item, element)
326
{
327
    if (result_item.appended)
328
	result_item.appended.remove();
329
    result_item.appended = element;
330
    result_item.li.appendChild(element);
331
}
332

    
333
function show_message(result_item, text)
334
{
335
    const div = document.createElement("div");
336
    div.textContent = text;
337
    set_appended(result_item, div);
338
}
339

    
340
function showcb(text)
341
{
342
    return item => show_message(item, text);
343
}
344

    
345
function unroll_chbx_first_checked(entry_object)
346
{
347
    if (!entry_object.chbx.checked)
348
	return;
349

    
350
    entry_object.chbx.removeEventListener("change", entry_object.unroll_cb);
351
    delete entry_object.unroll_cb;
352

    
353
    entry_object.unroll.innerHTML = "preview not implemented...<br />(consider contributing)";
354
}
355

    
356
let import_frame;
357
let install_target = null;
358

    
359
function install_abort(error_state)
360
{
361
    import_frame.show_error(`Error: ${error_state}`);
362
    install_target = null;
363
}
364

    
365
/*
366
 * Translate objects from the format in which they are sent by Hydrilla to the
367
 * format in which they are stored in settings.
368
 */
369

    
370
function translate_script(script_object, repo_url)
371
{
372
    return {
373
	[TYPE_PREFIX.SCRIPT + script_object.name]: {
374
	    hash: script_object.sha256,
375
	    url: `${repo_url}/content/${script_object.location}`
376
	}
377
    };
378
}
379

    
380
function translate_bag(bag_object)
381
{
382
    return {
383
	[TYPE_PREFIX.BAG + bag_object.name]: bag_object.components
384
    };
385
}
386

    
387
const format_translators = {
388
    [TYPE_PREFIX.BAG]: translate_bag,
389
    [TYPE_PREFIX.SCRIPT]: translate_script
390
};
391

    
392
function install_check_ready()
393
{
394
    if (install_target.to_fetch.size > 0)
395
	return;
396

    
397
    const page_key = [TYPE_PREFIX.PAGE + install_target.pattern];
398
    const to_install = [{[page_key]: {components: install_target.payload}}];
399

    
400
    for (const key of install_target.fetched) {
401
	const old_object =
402
	      queried_items.get(key)[install_target.repo_url].response;
403
	const new_object =
404
	      format_translators[key[0]](old_object, install_target.repo_url);
405
	to_install.push(new_object);
406
    }
407

    
408
    import_frame.show_selection(to_install);
409
}
410

    
411
const possible_errors = ["connection_error", "parse_error"];
412

    
413
function fetch_install_deps(components)
414
{
415
    const needed = [...components];
416
    const processed = new Set();
417

    
418
    while (needed.length > 0) {
419
	const [prefix, item] = needed.pop();
420
	const key = prefix + item;
421
	processed.add(key);
422
	const results = queried_items.get(key);
423
	let relevant_result = null;
424

    
425
	if (results)
426
	    relevant_result = results[install_target.repo_url];
427

    
428
	if (!relevant_result) {
429
	    content_script_port.postMessage([prefix, item,
430
					     [install_target.repo_url]]);
431
	    install_target.to_fetch.add(key);
432
	    continue;
433
	}
434

    
435
	if (possible_errors.includes(relevant_result.state)) {
436
	    install_abort(relevant_result.state);
437
	    return false;
438
	}
439

    
440
	install_target.fetched.add(key);
441

    
442
	if (prefix !== TYPE_PREFIX.BAG)
443
	    continue;
444

    
445
	for (const dependency of relevant_result.response.components) {
446
	    if (processed.has(dependency.join('')))
447
		continue;
448
	    needed.push(dependency);
449
	}
450
    }
451
}
452

    
453
function record_fetched_install_dep(prefix, item, repo_url, result)
454
{
455
    const key = prefix + item;
456

    
457
    if (!install_target || repo_url !== install_target.repo_url ||
458
	!install_target.to_fetch.has(key))
459
	return;
460

    
461
    if (possible_errors.includes(result.state)) {
462
	install_abort(result.state);
463
	return;
464
    }
465

    
466
    if (result.state !== "completed")
467
	return;
468

    
469
    install_target.to_fetch.delete(key);
470
    install_target.fetched.add(key);
471

    
472
    if (prefix === TYPE_PREFIX.BAG &&
473
	fetch_install_deps(result.response.components) === false)
474
	return;
475

    
476
    install_check_ready();
477
}
478

    
479
function install_clicked(entry_object)
480
{
481
    import_frame.show_loading();
482

    
483
    install_target = {
484
	repo_url: entry_object.repo_url,
485
	pattern: entry_object.match_object.pattern,
486
	payload: entry_object.match_object.payload,
487
	fetched: new Set(),
488
	to_fetch: new Set()
489
    };
490

    
491
    fetch_install_deps([install_target.payload]);
492

    
493
    install_check_ready();
494
}
495

    
496
var max_query_result_id = 0;
497

    
498
function show_query_successful_result(result_item, repo_url, result)
499
{
500
    const cloned_ul_template = clone_template("result_patterns_list");
501
    set_appended(result_item, cloned_ul_template.ul);
502

    
503
    for (const match of result) {
504
	const entry_object = clone_template("query_match_li");
505

    
506
	entry_object.pattern.textContent = match.pattern;
507

    
508
	cloned_ul_template.ul.appendChild(entry_object.li);
509

    
510
	if (!match.payload) {
511
	    entry_object.payload.textContent = "(none)";
512
	    for (const key of ["chbx", "triangle", "unroll"])
513
		entry_object[key].remove();
514
	    continue;
515
	}
516

    
517
	entry_object.payload.textContent = nice_name(...match.payload);
518

    
519
	const install_cb = () => install_clicked(entry_object);
520
	entry_object.btn.addEventListener("click", install_cb);
521

    
522
	const chbx_id = `query_result_${max_query_result_id++}`;
523
	entry_object.chbx.id = chbx_id;
524
	entry_object.lbl.setAttribute("for", chbx_id);
525

    
526
	entry_object.unroll_cb = () => unroll_chbx_first_checked(entry_object);
527
	entry_object.chbx.addEventListener("change", entry_object.unroll_cb);
528

    
529
	entry_object.component_object = match.payload;
530
	entry_object.match_object = match;
531
	entry_object.repo_url = repo_url;
532
    }
533
}
534

    
535
function show_query_result(url_prefix, url, repo_url, result)
536
{
537
    const results_list_object = results_lists.get(url) ||
538
	  create_results_list(url);
539
    const result_item = results_list_object.by_repo.get(repo_url) ||
540
	  create_result_item(results_list_object, repo_url, result);
541

    
542
    const completed_cb =
543
	  item => show_query_successful_result(item, repo_url, result.response);
544
    const possible_actions = {
545
	completed: completed_cb,
546
	started: showcb("loading..."),
547
	connection_error: showcb("Error when querying repository."),
548
	parse_error: showcb("Bad data format received.")
549
    };
550
    possible_actions[result.state](result_item, repo_url);
551
}
552

    
553
by_id("settings_but")
554
    .addEventListener("click", (e) => browser.runtime.openOptionsPage());
555

    
556
async function main()
557
{
558
    init_default_policy_dialog();
559

    
560
    storage = await get_remote_storage();
561
    import_frame = await get_import_frame();
562
    import_frame.onclose = () => show_queried_view_radio.checked = true;
563
    show_page_activity_info();
564
}
565

    
566
main();
(8-8/14)