Project

General

Profile

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

haketilo / html / display-panel.js @ 96068ada

1
/**
2
 * This file is part of Haketilo.
3
 *
4
 * Function: Popup logic.
5
 *
6
 * Copyright (C) 2021 Wojtek Kosior
7
 * Redistribution terms are gathered in the `copyright' file.
8
 */
9

    
10
/*
11
 * IMPORTS_START
12
 * IMPORT browser
13
 * IMPORT is_chrome
14
 * IMPORT is_mozilla
15
 *** Using remote storage here seems inefficient, we only resort to that
16
 *** temporarily, before all storage access gets reworked.
17
 * IMPORT get_remote_storage
18
 * IMPORT get_import_frame
19
 * IMPORT init_default_policy_dialog
20
 * IMPORT query_all
21
 * IMPORT CONNECTION_TYPE
22
 * IMPORT is_privileged_url
23
 * IMPORT TYPE_PREFIX
24
 * IMPORT nice_name
25
 * IMPORT open_in_settings
26
 * IMPORT each_url_pattern
27
 * IMPORT by_id
28
 * IMPORT clone_template
29
 * IMPORTS_END
30
 */
31

    
32
let storage;
33
let tab_url;
34

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

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

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

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

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

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

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

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

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

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

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

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

    
98
    try_to_connect(tab.id);
99
}
100

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

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

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

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

    
115
    return template;
116
}
117

    
118
function style_possible_pattern_entry(pattern, exists_in_settings)
119
{
120
    const [text, class_action] = exists_in_settings ?
121
	  ["Edit", "add"] : ["Add", "remove"];
122
    const entry_object = known_patterns.get(pattern);
123

    
124
    if (entry_object) {
125
	entry_object.button.textContent = `${text} setting`;
126
	entry_object.entry.classList[class_action]("matched_pattern");
127
    }
128
}
129

    
130
function handle_page_change(change)
131
{
132
    style_possible_pattern_entry(change.item, change.new_val !== undefined);
133
}
134

    
135
function populate_possible_patterns_list(url)
136
{
137
    for (const pattern of each_url_pattern(url))
138
	add_pattern_to_list(pattern);
139

    
140
    for (const [pattern, settings] of query_all(storage, url))
141
	style_possible_pattern_entry(pattern, true);
142

    
143
    storage.add_change_listener(handle_page_change, [TYPE_PREFIX.PAGE]);
144
}
145

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

    
149
var content_script_port;
150

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

    
157
    const disconnect_cb = () => handle_disconnect(tab_id, start_querying_repos);
158
    content_script_port.onDisconnect.addListener(disconnect_cb);
159
    content_script_port.onMessage.addListener(handle_activity_report);
160

    
161
    query_pattern_but.addEventListener("click", start_querying_repos);
162

    
163
    if (is_mozilla)
164
	setTimeout(() => monitor_connecting(tab_id), 1000);
165
}
166

    
167
function start_querying_repos()
168
{
169
    query_pattern_but.removeEventListener("click", start_querying_repos);
170
    const repo_urls = storage.get_all_names(TYPE_PREFIX.REPO);
171
    if (content_script_port)
172
	content_script_port.postMessage([TYPE_PREFIX.URL, tab_url, repo_urls]);
173
}
174

    
175
const loading_point = by_id("loading_point");
176
const reload_notice = by_id("reload_notice");
177

    
178
function handle_disconnect(tab_id, button_cb)
179
{
180
    query_pattern_but.removeEventListener("click", button_cb);
181
    content_script_port = null;
182

    
183
    if (is_chrome && !browser.runtime.lastError)
184
	return;
185

    
186
    /* return if error was not during connection initialization */
187
    if (connected_chbx.checked)
188
	return;
189

    
190
    loading_point.classList.toggle("camouflage");
191
    reload_notice.classList.remove("hide");
192

    
193
    setTimeout(() => try_to_connect(tab_id), 1000);
194
}
195

    
196
function monitor_connecting(tab_id)
197
{
198
    if (connected_chbx.checked)
199
	return;
200

    
201
    if (content_script_port)
202
	content_script_port.disconnect();
203
    else
204
	return;
205

    
206
    loading_point.classList.toggle("camouflage");
207
    reload_notice.classList.remove("hide");
208
    try_to_connect(tab_id);
209
}
210

    
211
const pattern_span = by_id("pattern");
212
const view_pattern_but = by_id("view_pattern");
213
const blocked_span = by_id("blocked");
214
const payload_span = by_id("payload");
215
const payload_buttons_div = by_id("payload_buttons");
216
const view_payload_but = by_id("view_payload");
217
const view_injected_but = by_id("view_injected");
218
const container_for_injected = by_id("container_for_injected");
219
const content_type_cell = by_id("content_type");
220

    
221
const queried_items = new Map();
222

    
223
let max_injected_script_id = 0;
224

    
225
function handle_activity_report(message)
226
{
227
    connected_chbx.checked = true;
228

    
229
    const [type, data] = message;
230

    
231
    if (type === "settings") {
232
	const settings = data;
233

    
234
	blocked_span.textContent = settings.allow ? "no" : "yes";
235

    
236
	if (settings.pattern) {
237
	    pattern_span.textContent = pattern;
238
	    const settings_opener =
239
		  () => open_in_settings(TYPE_PREFIX.PAGE, settings.pattern);
240
	    view_pattern_but.classList.remove("hide");
241
	    view_pattern_but.addEventListener("click", settings_opener);
242
	} else {
243
	    pattern_span.textContent = "none";
244
	    blocked_span.textContent = blocked_span.textContent + " (default)";
245
	}
246

    
247
	if (settings.payload) {
248
	    payload_span.textContent = nice_name(...settings.payload);
249
	    payload_buttons_div.classList.remove("hide");
250
	    const settings_opener = () => open_in_settings(...settings.payload);
251
	    view_payload_but.addEventListener("click", settings_opener);
252
	} else {
253
	    payload_span.textContent = "none";
254
	}
255
    }
256
    if (type === "script") {
257
	const template = clone_template("injected_script");
258
	const chbx_id = `injected_script_${max_injected_script_id++}`;
259
	template.chbx.id = chbx_id;
260
	template.lbl.setAttribute("for", chbx_id);
261
	template.script_contents.textContent = data;
262
	container_for_injected.appendChild(template.div);
263
    }
264
    if (type === "is_html") {
265
	if (!data)
266
	    content_type_cell.classList.remove("hide");
267
    }
268
    if (type === "repo_query_action") {
269
	const key = data.prefix + data.item;
270
	const results = queried_items.get(key) || {};
271
	Object.assign(results, data.results);
272
	queried_items.set(key, results);
273

    
274
	const action = data.prefix === TYPE_PREFIX.URL ?
275
	      show_query_result : record_fetched_install_dep;
276

    
277
	for (const [repo_url, result] of Object.entries(data.results))
278
	    action(data.prefix, data.item, repo_url, result);
279
    }
280
}
281

    
282
const container_for_repo_responses = by_id("container_for_repo_responses");
283

    
284
const results_lists = new Map();
285

    
286
function create_results_list(url)
287
{
288
    const cloned_template = clone_template("multi_repos_query_result");
289
    cloned_template.url_span.textContent = url;
290
    container_for_repo_responses.appendChild(cloned_template.div);
291

    
292
    cloned_template.by_repo = new Map();
293
    results_lists.set(url, cloned_template);
294

    
295
    return cloned_template;
296
}
297

    
298
function create_result_item(list_object, repo_url, result)
299
{
300
    const cloned_template = clone_template("single_repo_query_result");
301
    cloned_template.repo_url.textContent = repo_url;
302
    cloned_template.appended = null;
303

    
304
    list_object.ul.appendChild(cloned_template.li);
305
    list_object.by_repo.set(repo_url, cloned_template);
306

    
307
    return cloned_template;
308
}
309

    
310
function set_appended(result_item, element)
311
{
312
    if (result_item.appended)
313
	result_item.appended.remove();
314
    result_item.appended = element;
315
    result_item.li.appendChild(element);
316
}
317

    
318
function show_message(result_item, text)
319
{
320
    const div = document.createElement("div");
321
    div.textContent = text;
322
    set_appended(result_item, div);
323
}
324

    
325
function showcb(text)
326
{
327
    return item => show_message(item, text);
328
}
329

    
330
function unroll_chbx_first_checked(entry_object)
331
{
332
    if (!entry_object.chbx.checked)
333
	return;
334

    
335
    entry_object.chbx.removeEventListener("change", entry_object.unroll_cb);
336
    delete entry_object.unroll_cb;
337

    
338
    entry_object.unroll.innerHTML = "preview not implemented...<br />(consider contributing)";
339
}
340

    
341
let import_frame;
342
let install_target = null;
343

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

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

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

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

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

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

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

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

    
393
    import_frame.show_selection(to_install);
394
}
395

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

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

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

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

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

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

    
425
	install_target.fetched.add(key);
426

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

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

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

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

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

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

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

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

    
461
    install_check_ready();
462
}
463

    
464
function install_clicked(entry_object)
465
{
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
    if (result.length === 0) {
486
	show_message(result_item, "No results :(");
487
	return;
488
    }
489

    
490
    const cloned_ul_template = clone_template("result_patterns_list");
491
    set_appended(result_item, cloned_ul_template.ul);
492

    
493
    for (const match of result) {
494
	const entry_object = clone_template("query_match_li");
495

    
496
	entry_object.pattern.textContent = match.pattern;
497

    
498
	cloned_ul_template.ul.appendChild(entry_object.li);
499

    
500
	if (!match.payload) {
501
	    entry_object.payload.textContent = "(none)";
502
	    for (const key of ["chbx", "triangle", "unroll"])
503
		entry_object[key].remove();
504
	    continue;
505
	}
506

    
507
	entry_object.payload.textContent = nice_name(...match.payload);
508

    
509
	const install_cb = () => install_clicked(entry_object);
510
	entry_object.btn.addEventListener("click", install_cb);
511

    
512
	const chbx_id = `query_result_${max_query_result_id++}`;
513
	entry_object.chbx.id = chbx_id;
514
	entry_object.lbl.setAttribute("for", chbx_id);
515

    
516
	entry_object.unroll_cb = () => unroll_chbx_first_checked(entry_object);
517
	entry_object.chbx.addEventListener("change", entry_object.unroll_cb);
518

    
519
	entry_object.component_object = match.payload;
520
	entry_object.match_object = match;
521
	entry_object.repo_url = repo_url;
522
    }
523
}
524

    
525
function show_query_result(url_prefix, url, repo_url, result)
526
{
527
    const results_list_object = results_lists.get(url) ||
528
	  create_results_list(url);
529
    const result_item = results_list_object.by_repo.get(repo_url) ||
530
	  create_result_item(results_list_object, repo_url, result);
531

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

    
543
by_id("settings_but")
544
    .addEventListener("click", (e) => browser.runtime.openOptionsPage());
545

    
546
async function main()
547
{
548
    init_default_policy_dialog();
549

    
550
    storage = await get_remote_storage();
551
    import_frame = await get_import_frame();
552
    import_frame.onclose = () => show_queried_view_radio.checked = true;
553
    show_page_activity_info();
554
}
555

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