Project

General

Profile

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

haketilo / html / display-panel.js @ 014f2a2f

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 for_each_possible_pattern
24
 * IMPORT by_id
25
 * IMPORT clone_template
26
 * IMPORTS_END
27
 */
28

    
29
let storage;
30
let tab_url;
31

    
32
const tab_query = {currentWindow: true, active: true};
33

    
34
async function get_current_tab()
35
{
36
    /* Fix for fact that Chrome does not use promises here */
37
    const promise = is_chrome ?
38
	  new Promise((resolve, reject) =>
39
		      browser.tabs.query(tab_query, tab => resolve(tab))) :
40
	  browser.tabs.query(tab_query);
41

    
42
    try {
43
	return (await promise)[0];
44
    } catch(e) {
45
	console.log(e);
46
    }
47
}
48

    
49
const page_url_heading = by_id("page_url_heading");
50
const show_privileged_notice_chbx = by_id("show_privileged_notice_chbx");
51
const show_page_state_chbx = by_id("show_page_state_chbx");
52

    
53
async function show_page_activity_info()
54
{
55
    const tab = await get_current_tab();
56

    
57
    if (tab === undefined) {
58
	page_url_heading.textContent = "unknown page";
59
	return;
60
    }
61

    
62
    tab_url = /^([^?#]*)/.exec(tab.url)[1];
63
    page_url_heading.textContent = tab_url;
64
    if (is_privileged_url(tab_url)) {
65
	show_privileged_notice_chbx.checked = true;
66
	return;
67
    }
68

    
69
    populate_possible_patterns_list(tab_url);
70
    show_page_state_chbx.checked = true;
71

    
72
    try_to_connect(tab.id);
73
}
74

    
75
const possible_patterns_ul = by_id("possible_patterns");
76
const pattern_li_template = by_id("pattern_li_template");
77
pattern_li_template.removeAttribute("id");
78
const known_patterns = new Map();
79

    
80
function add_pattern_to_list(pattern)
81
{
82
    const li = pattern_li_template.cloneNode(true);
83
    li.id = `pattern_li_${known_patterns.size}`;
84
    known_patterns.set(pattern, li.id);
85

    
86
    const span = li.firstElementChild;
87
    span.textContent = pattern;
88

    
89
    const button = span.nextElementSibling;
90
    const settings_opener = () => open_in_settings(TYPE_PREFIX.PAGE, pattern);
91
    button.addEventListener("click", settings_opener);
92

    
93
    possible_patterns_ul.appendChild(li)
94

    
95
    return li.id;
96
}
97

    
98
function ensure_pattern_exists(pattern)
99
{
100
    let id = known_patterns.get(pattern);
101
    /*
102
     * As long as pattern computation works well, we should never get into this
103
     * conditional block. This is just a safety measure. To be removed as part
104
     * of a bigger rework when we start taking iframes into account.
105
     */
106
    if (id === undefined) {
107
	console.log(`unknown pattern: ${pattern}`);
108
	id = add_pattern_to_list(pattern);
109
    }
110

    
111
    return id;
112
}
113

    
114
function set_pattern_li_button_text(li_id, text)
115
{
116
    by_id(li_id).firstElementChild.nextElementSibling.textContent = text;
117
}
118

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

    
128
function populate_possible_patterns_list(url)
129
{
130
    for_each_possible_pattern(url, add_pattern_to_list);
131

    
132
    for (const [pattern, settings] of query_all(storage, url)) {
133
	set_pattern_li_button_text(ensure_pattern_exists(pattern),
134
				   "Edit in settings");
135
    }
136

    
137
    storage.add_change_listener(handle_page_change, [TYPE_PREFIX.PAGE]);
138
}
139

    
140
const connected_chbx = by_id("connected_chbx");
141
const query_pattern_but = by_id("query_pattern");
142

    
143
var content_script_port;
144

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

    
151
    const disconnect_cb = () => handle_disconnect(tab_id, start_querying_repos);
152
    content_script_port.onDisconnect.addListener(disconnect_cb);
153
    content_script_port.onMessage.addListener(handle_activity_report);
154

    
155
    query_pattern_but.addEventListener("click", start_querying_repos);
156

    
157
    if (is_mozilla)
158
	setTimeout(() => monitor_connecting(tab_id), 1000);
159
}
160

    
161
const query_started_chbx = by_id("query_started_chbx");
162

    
163
function start_querying_repos(port)
164
{
165
    const repo_urls = storage.get_all_names(TYPE_PREFIX.REPO);
166
    if (content_script_port)
167
	content_script_port.postMessage([TYPE_PREFIX.URL, tab_url, repo_urls]);
168
    query_started_chbx.checked = true;
169
}
170

    
171
const loading_chbx = by_id("loading_chbx");
172

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

    
178
    if (is_chrome && !browser.runtime.lastError)
179
	return;
180

    
181
    /* return if error was not during connection initialization */
182
    if (connected_chbx.checked)
183
	return;
184

    
185
    loading_chbx.checked = !loading_chbx.checked;
186
    setTimeout(() => try_to_connect(tab_id), 1000);
187
}
188

    
189
function monitor_connecting(tab_id)
190
{
191
    if (connected_chbx.checked)
192
	return;
193

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

    
199
    loading_chbx.checked = !loading_chbx.checked;
200
    try_to_connect(tab_id);
201
}
202

    
203
const pattern_span = by_id("pattern");
204
const view_pattern_but = by_id("view_pattern");
205
const blocked_span = by_id("blocked");
206
const payload_span = by_id("payload");
207
const view_payload_but = by_id("view_payload");
208
const container_for_injected = by_id("container_for_injected");
209

    
210
const queried_items = new Map();
211

    
212
function handle_activity_report(message)
213
{
214
    connected_chbx.checked = true;
215

    
216
    const [type, data] = message;
217

    
218
    if (type === "settings") {
219
	let [pattern, settings] = data;
220

    
221
	settings = settings || {};
222
	blocked_span.textContent = settings.allow ? "no" : "yes";
223

    
224
	if (pattern) {
225
	    pattern_span.textContent = pattern;
226
	    const settings_opener =
227
		  () => open_in_settings(TYPE_PREFIX.PAGE, pattern);
228
	    view_pattern_but.classList.remove("hide");
229
	    view_pattern_but.addEventListener("click", settings_opener);
230
	} else {
231
	    pattern_span.textContent = "none";
232
	}
233

    
234
	const components = settings.components;
235
	if (components) {
236
	    payload_span.textContent = nice_name(...components);
237
	    const settings_opener = () => open_in_settings(...components);
238
	    view_payload_but.classList.remove("hide");
239
	    view_payload_but.addEventListener("click", settings_opener);
240
	} else {
241
	    payload_span.textContent = "none";
242
	}
243
    }
244
    if (type === "script") {
245
	const h4 = document.createElement("h4");
246
	const pre = document.createElement("pre");
247
	h4.textContent = "script";
248
	pre.textContent = data;
249

    
250
	container_for_injected.appendChild(h4);
251
	container_for_injected.appendChild(pre);
252
    }
253
    if (type === "repo_query_action") {
254
	query_started_chbx.checked = true;
255

    
256
	const key = data.prefix + data.item;
257
	const results = queried_items.get(key) || {};
258
	Object.assign(results, data.results);
259
	queried_items.set(key, results);
260

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

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

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

    
271
const results_lists = new Map();
272

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

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

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

    
286
    results_lists.set(url, list_object);
287

    
288
    return list_object;
289
}
290

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

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

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

    
303
    return result_item;
304
}
305

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

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

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

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

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

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

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

    
339
let import_frame;
340
let install_target = null;
341

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

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

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

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

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

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

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

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

    
391
    import_frame.show_selection(to_install);
392
}
393

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

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

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

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

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

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

    
423
	install_target.fetched.add(key);
424

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

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

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

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

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

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

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

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

    
459
    install_check_ready();
460
}
461

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

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

    
475
    fetch_install_deps([install_target.payload]);
476

    
477
    install_check_ready();
478
}
479

    
480
var max_query_result_id = 0;
481

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

    
486
    set_appended(result_item, ul);
487

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

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

    
493
	ul.appendChild(entry_object.li);
494

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

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

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

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

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

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

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

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

    
538
by_id("settings_but")
539
    .addEventListener("click", (e) => browser.runtime.openOptionsPage());
540

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

    
549
main();
(4-4/8)