Project

General

Profile

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

haketilo / html / display-panel.js @ 53837634

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 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 (const pattern of each_url_pattern(url))
131
	add_pattern_to_list(pattern);
132

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

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

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

    
144
var content_script_port;
145

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
211
const queried_items = new Map();
212

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

    
217
    const [type, data] = message;
218

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

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

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

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

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

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

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

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

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

    
272
const results_lists = new Map();
273

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

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

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

    
287
    results_lists.set(url, list_object);
288

    
289
    return list_object;
290
}
291

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

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

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

    
304
    return result_item;
305
}
306

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

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

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

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

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

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

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

    
340
let import_frame;
341
let install_target = null;
342

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

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

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

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

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

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

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

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

    
392
    import_frame.show_selection(to_install);
393
}
394

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

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

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

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

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

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

    
424
	install_target.fetched.add(key);
425

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

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

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

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

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

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

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

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

    
460
    install_check_ready();
461
}
462

    
463
function install_clicked(entry_object)
464
{
465
    show_install_chbx.checked = true;
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
    const ul = document.createElement("ul");
486

    
487
    set_appended(result_item, ul);
488

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

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

    
494
	ul.appendChild(entry_object.li);
495

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

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

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

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

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

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

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

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

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

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

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