Project

General

Profile

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

haketilo / html / display-panel.js @ 826b4fd8

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

    
30
let storage;
31
let tab_url;
32

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

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

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

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

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

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

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

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

    
73
    try_to_connect(tab.id);
74
}
75

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

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

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

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

    
94
    possible_patterns_ul.appendChild(li)
95

    
96
    return li.id;
97
}
98

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

    
112
    return id;
113
}
114

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

    
120
function handle_page_change(change)
121
{
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
}
128

    
129
function populate_possible_patterns_list(url)
130
{
131
    for (const pattern of each_url_pattern(url))
132
	add_pattern_to_list(pattern);
133

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

    
139
    storage.add_change_listener(handle_page_change, [TYPE_PREFIX.PAGE]);
140
}
141

    
142
const connected_chbx = by_id("connected_chbx");
143
const query_pattern_but = by_id("query_pattern");
144

    
145
var content_script_port;
146

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

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

    
157
    query_pattern_but.addEventListener("click", start_querying_repos);
158

    
159
    if (is_mozilla)
160
	setTimeout(() => monitor_connecting(tab_id), 1000);
161
}
162

    
163
const query_started_chbx = by_id("query_started_chbx");
164

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

    
173
const loading_chbx = by_id("loading_chbx");
174

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

    
180
    if (is_chrome && !browser.runtime.lastError)
181
	return;
182

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

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

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

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

    
201
    loading_chbx.checked = !loading_chbx.checked;
202
    try_to_connect(tab_id);
203
}
204

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

    
212
const queried_items = new Map();
213

    
214
function handle_activity_report(message)
215
{
216
    connected_chbx.checked = true;
217

    
218
    const [type, data] = message;
219

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

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

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

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

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

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

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

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

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

    
273
const results_lists = new Map();
274

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

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

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

    
288
    results_lists.set(url, list_object);
289

    
290
    return list_object;
291
}
292

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

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

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

    
305
    return result_item;
306
}
307

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

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

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

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

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

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

    
339
const show_install_chbx = by_id("show_install_view_chbx");
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
    show_install_chbx.checked = true;
467
    import_frame.show_loading();
468

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

    
477
    fetch_install_deps([install_target.payload]);
478

    
479
    install_check_ready();
480
}
481

    
482
var max_query_result_id = 0;
483

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

    
488
    set_appended(result_item, ul);
489

    
490
    for (const match of result) {
491
	const entry_object = clone_template("query_match_li");
492

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

    
495
	ul.appendChild(entry_object.li);
496

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

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

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

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

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

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

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

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

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

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

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