Project

General

Profile

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

haketilo / html / display-panel.js @ 263d03d5

1
/**
2
 * This file is part of Haketilo.
3
 *
4
 * Function: Popup logic.
5
 *
6
 * Copyright (C) 2021 Wojtek Kosior
7
 *
8
 * This program is free software: you can redistribute it and/or modify
9
 * it under the terms of the GNU General Public License as published by
10
 * the Free Software Foundation, either version 3 of the License, or
11
 * (at your option) any later version.
12
 *
13
 * This program is distributed in the hope that it will be useful,
14
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16
 * GNU General Public License for more details.
17
 *
18
 * As additional permission under GNU GPL version 3 section 7, you
19
 * may distribute forms of that code without the copy of the GNU
20
 * GPL normally required by section 4, provided you include this
21
 * license notice and, in case of non-source distribution, a URL
22
 * through which recipients can access the Corresponding Source.
23
 * If you modify file(s) with this exception, you may extend this
24
 * exception to your version of the file(s), but you are not
25
 * obligated to do so. If you do not wish to do so, delete this
26
 * exception statement from your version.
27
 *
28
 * As a special exception to the GPL, any HTML file which merely
29
 * makes function calls to this code, and for that purpose
30
 * includes it by reference shall be deemed a separate work for
31
 * copyright law purposes. If you modify this code, you may extend
32
 * this exception to your version of the code, but you are not
33
 * obligated to do so. If you do not wish to do so, delete this
34
 * exception statement from your version.
35
 *
36
 * You should have received a copy of the GNU General Public License
37
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
38
 *
39
 * I, Wojtek Kosior, thereby promise not to sue for violation of this file's
40
 * license. Although I request that you do not make use this code in a
41
 * proprietary program, I am not going to enforce this in court.
42
 */
43

    
44
/*
45
 * IMPORTS_START
46
 * IMPORT browser
47
 * IMPORT is_chrome
48
 * IMPORT is_mozilla
49
 *** Using remote storage here seems inefficient, we only resort to that
50
 *** temporarily, before all storage access gets reworked.
51
 * IMPORT get_remote_storage
52
 * IMPORT get_import_frame
53
 * IMPORT init_default_policy_dialog
54
 * IMPORT query_all
55
 * IMPORT CONNECTION_TYPE
56
 * IMPORT is_privileged_url
57
 * IMPORT TYPE_PREFIX
58
 * IMPORT nice_name
59
 * IMPORT open_in_settings
60
 * IMPORT each_url_pattern
61
 * IMPORT by_id
62
 * IMPORT clone_template
63
 * IMPORTS_END
64
 */
65

    
66
let storage;
67
let tab_url;
68

    
69
/* Force popup <html>'s reflow on stupid Firefox. */
70
if (is_mozilla) {
71
    const reflow_forcer =
72
	  () => document.documentElement.style.width = "-moz-fit-content";
73
    for (const radio of document.querySelectorAll('[name="current_view"]'))
74
	radio.addEventListener("change", reflow_forcer);
75
}
76

    
77
const show_queried_view_radio = by_id("show_queried_view_radio");
78

    
79
const tab_query = {currentWindow: true, active: true};
80

    
81
async function get_current_tab()
82
{
83
    /* Fix for fact that Chrome does not use promises here */
84
    const promise = is_chrome ?
85
	  new Promise((resolve, reject) =>
86
		      browser.tabs.query(tab_query, tab => resolve(tab))) :
87
	  browser.tabs.query(tab_query);
88

    
89
    try {
90
	return (await promise)[0];
91
    } catch(e) {
92
	console.log(e);
93
    }
94
}
95

    
96
const page_url_heading = by_id("page_url_heading");
97
const privileged_notice = by_id("privileged_notice");
98
const page_state = by_id("page_state");
99

    
100
/* Helper functions to convert string into a list of one-letter <span>'s. */
101
function char_to_span(char, doc)
102
{
103
    const span = document.createElement("span");
104
    span.textContent = char;
105
    return span;
106
}
107

    
108
function to_spans(string, doc=document)
109
{
110
    return string.split("").map(c => char_to_span(c, doc));
111
}
112

    
113
async function show_page_activity_info()
114
{
115
    const tab = await get_current_tab();
116

    
117
    if (tab === undefined) {
118
	page_url_heading.textContent = "unknown page";
119
	return;
120
    }
121

    
122
    tab_url = /^([^?#]*)/.exec(tab.url)[1];
123
    to_spans(tab_url).forEach(s => page_url_heading.append(s));
124
    if (is_privileged_url(tab_url)) {
125
	privileged_notice.classList.remove("hide");
126
	return;
127
    }
128

    
129
    populate_possible_patterns_list(tab_url);
130
    page_state.classList.remove("hide");
131

    
132
    try_to_connect(tab.id);
133
}
134

    
135
const possible_patterns_list = by_id("possible_patterns");
136
const known_patterns = new Map();
137

    
138
function add_pattern_to_list(pattern)
139
{
140
    const template = clone_template("pattern_entry");
141
    template.name.textContent = pattern;
142

    
143
    const settings_opener = () => open_in_settings(TYPE_PREFIX.PAGE, pattern);
144
    template.button.addEventListener("click", settings_opener);
145

    
146
    known_patterns.set(pattern, template);
147
    possible_patterns_list.append(template.entry);
148

    
149
    return template;
150
}
151

    
152
function style_possible_pattern_entry(pattern, exists_in_settings)
153
{
154
    const [text, class_action] = exists_in_settings ?
155
	  ["Edit", "add"] : ["Add", "remove"];
156
    const entry_object = known_patterns.get(pattern);
157

    
158
    if (entry_object) {
159
	entry_object.button.textContent = `${text} setting`;
160
	entry_object.entry.classList[class_action]("matched_pattern");
161
    }
162
}
163

    
164
function handle_page_change(change)
165
{
166
    style_possible_pattern_entry(change.item, change.new_val !== undefined);
167
}
168

    
169
function populate_possible_patterns_list(url)
170
{
171
    for (const pattern of each_url_pattern(url))
172
	add_pattern_to_list(pattern);
173

    
174
    for (const [pattern, settings] of query_all(storage, url))
175
	style_possible_pattern_entry(pattern, true);
176

    
177
    storage.add_change_listener(handle_page_change, [TYPE_PREFIX.PAGE]);
178
}
179

    
180
const connected_chbx = by_id("connected_chbx");
181
const query_pattern_but = by_id("query_pattern");
182

    
183
var content_script_port;
184

    
185
function try_to_connect(tab_id)
186
{
187
    /* This won't connect to iframes. We'll add support for them later */
188
    const connect_info = {name: CONNECTION_TYPE.ACTIVITY_INFO, frameId: 0};
189
    content_script_port = browser.tabs.connect(tab_id, connect_info);
190

    
191
    const disconnect_cb = () => handle_disconnect(tab_id, start_querying_repos);
192
    content_script_port.onDisconnect.addListener(disconnect_cb);
193
    content_script_port.onMessage.addListener(handle_activity_report);
194

    
195
    query_pattern_but.addEventListener("click", start_querying_repos);
196

    
197
    if (is_mozilla)
198
	setTimeout(() => monitor_connecting(tab_id), 1000);
199
}
200

    
201
function start_querying_repos()
202
{
203
    query_pattern_but.removeEventListener("click", start_querying_repos);
204
    const repo_urls = storage.get_all_names(TYPE_PREFIX.REPO);
205
    if (content_script_port)
206
	content_script_port.postMessage([TYPE_PREFIX.URL, tab_url, repo_urls]);
207
}
208

    
209
const loading_point = by_id("loading_point");
210
const reload_notice = by_id("reload_notice");
211

    
212
function handle_disconnect(tab_id, button_cb)
213
{
214
    query_pattern_but.removeEventListener("click", button_cb);
215
    content_script_port = null;
216

    
217
    if (is_chrome && !browser.runtime.lastError)
218
	return;
219

    
220
    /* return if error was not during connection initialization */
221
    if (connected_chbx.checked)
222
	return;
223

    
224
    loading_point.classList.toggle("camouflage");
225
    reload_notice.classList.remove("hide");
226

    
227
    setTimeout(() => try_to_connect(tab_id), 1000);
228
}
229

    
230
function monitor_connecting(tab_id)
231
{
232
    if (connected_chbx.checked)
233
	return;
234

    
235
    if (content_script_port)
236
	content_script_port.disconnect();
237
    else
238
	return;
239

    
240
    loading_point.classList.toggle("camouflage");
241
    reload_notice.classList.remove("hide");
242
    try_to_connect(tab_id);
243
}
244

    
245
const pattern_span = by_id("pattern");
246
const view_pattern_but = by_id("view_pattern");
247
const blocked_span = by_id("blocked");
248
const payload_span = by_id("payload");
249
const payload_buttons_div = by_id("payload_buttons");
250
const view_payload_but = by_id("view_payload");
251
const view_injected_but = by_id("view_injected");
252
const container_for_injected = by_id("container_for_injected");
253
const content_type_cell = by_id("content_type");
254

    
255
const queried_items = new Map();
256

    
257
let max_injected_script_id = 0;
258

    
259
function handle_activity_report(message)
260
{
261
    connected_chbx.checked = true;
262

    
263
    const [type, data] = message;
264

    
265
    if (type === "settings") {
266
	let [pattern, settings] = data;
267

    
268
	blocked_span.textContent = settings.allow ? "no" : "yes";
269

    
270
	if (pattern) {
271
	    pattern_span.textContent = pattern;
272
	    const settings_opener =
273
		  () => open_in_settings(TYPE_PREFIX.PAGE, pattern);
274
	    view_pattern_but.classList.remove("hide");
275
	    view_pattern_but.addEventListener("click", settings_opener);
276
	} else {
277
	    pattern_span.textContent = "none";
278
	    blocked_span.textContent = blocked_span.textContent + " (default)";
279
	}
280

    
281
	const components = settings.components;
282
	if (components) {
283
	    payload_span.textContent = nice_name(...components);
284
	    payload_buttons_div.classList.remove("hide");
285
	    const settings_opener = () => open_in_settings(...components);
286
	    view_payload_but.addEventListener("click", settings_opener);
287
	} else {
288
	    payload_span.textContent = "none";
289
	}
290
    }
291
    if (type === "script") {
292
	const template = clone_template("injected_script");
293
	const chbx_id = `injected_script_${max_injected_script_id++}`;
294
	template.chbx.id = chbx_id;
295
	template.lbl.setAttribute("for", chbx_id);
296
	template.script_contents.textContent = data;
297
	container_for_injected.appendChild(template.div);
298
    }
299
    if (type === "is_html") {
300
	if (!data)
301
	    content_type_cell.classList.remove("hide");
302
    }
303
    if (type === "repo_query_action") {
304
	const key = data.prefix + data.item;
305
	const results = queried_items.get(key) || {};
306
	Object.assign(results, data.results);
307
	queried_items.set(key, results);
308

    
309
	const action = data.prefix === TYPE_PREFIX.URL ?
310
	      show_query_result : record_fetched_install_dep;
311

    
312
	for (const [repo_url, result] of Object.entries(data.results))
313
	    action(data.prefix, data.item, repo_url, result);
314
    }
315
}
316

    
317
const container_for_repo_responses = by_id("container_for_repo_responses");
318

    
319
const results_lists = new Map();
320

    
321
function create_results_list(url)
322
{
323
    const cloned_template = clone_template("multi_repos_query_result");
324
    cloned_template.url_span.textContent = url;
325
    container_for_repo_responses.appendChild(cloned_template.div);
326

    
327
    cloned_template.by_repo = new Map();
328
    results_lists.set(url, cloned_template);
329

    
330
    return cloned_template;
331
}
332

    
333
function create_result_item(list_object, repo_url, result)
334
{
335
    const cloned_template = clone_template("single_repo_query_result");
336
    cloned_template.repo_url.textContent = repo_url;
337
    cloned_template.appended = null;
338

    
339
    list_object.ul.appendChild(cloned_template.li);
340
    list_object.by_repo.set(repo_url, cloned_template);
341

    
342
    return cloned_template;
343
}
344

    
345
function set_appended(result_item, element)
346
{
347
    if (result_item.appended)
348
	result_item.appended.remove();
349
    result_item.appended = element;
350
    result_item.li.appendChild(element);
351
}
352

    
353
function show_message(result_item, text)
354
{
355
    const div = document.createElement("div");
356
    div.textContent = text;
357
    set_appended(result_item, div);
358
}
359

    
360
function showcb(text)
361
{
362
    return item => show_message(item, text);
363
}
364

    
365
function unroll_chbx_first_checked(entry_object)
366
{
367
    if (!entry_object.chbx.checked)
368
	return;
369

    
370
    entry_object.chbx.removeEventListener("change", entry_object.unroll_cb);
371
    delete entry_object.unroll_cb;
372

    
373
    entry_object.unroll.innerHTML = "preview not implemented...<br />(consider contributing)";
374
}
375

    
376
let import_frame;
377
let install_target = null;
378

    
379
function install_abort(error_state)
380
{
381
    import_frame.show_error(`Error: ${error_state}`);
382
    install_target = null;
383
}
384

    
385
/*
386
 * Translate objects from the format in which they are sent by Hydrilla to the
387
 * format in which they are stored in settings.
388
 */
389

    
390
function translate_script(script_object, repo_url)
391
{
392
    return {
393
	[TYPE_PREFIX.SCRIPT + script_object.name]: {
394
	    hash: script_object.sha256,
395
	    url: `${repo_url}/content/${script_object.location}`
396
	}
397
    };
398
}
399

    
400
function translate_bag(bag_object)
401
{
402
    return {
403
	[TYPE_PREFIX.BAG + bag_object.name]: bag_object.components
404
    };
405
}
406

    
407
const format_translators = {
408
    [TYPE_PREFIX.BAG]: translate_bag,
409
    [TYPE_PREFIX.SCRIPT]: translate_script
410
};
411

    
412
function install_check_ready()
413
{
414
    if (install_target.to_fetch.size > 0)
415
	return;
416

    
417
    const page_key = [TYPE_PREFIX.PAGE + install_target.pattern];
418
    const to_install = [{[page_key]: {components: install_target.payload}}];
419

    
420
    for (const key of install_target.fetched) {
421
	const old_object =
422
	      queried_items.get(key)[install_target.repo_url].response;
423
	const new_object =
424
	      format_translators[key[0]](old_object, install_target.repo_url);
425
	to_install.push(new_object);
426
    }
427

    
428
    import_frame.show_selection(to_install);
429
}
430

    
431
const possible_errors = ["connection_error", "parse_error"];
432

    
433
function fetch_install_deps(components)
434
{
435
    const needed = [...components];
436
    const processed = new Set();
437

    
438
    while (needed.length > 0) {
439
	const [prefix, item] = needed.pop();
440
	const key = prefix + item;
441
	processed.add(key);
442
	const results = queried_items.get(key);
443
	let relevant_result = null;
444

    
445
	if (results)
446
	    relevant_result = results[install_target.repo_url];
447

    
448
	if (!relevant_result) {
449
	    content_script_port.postMessage([prefix, item,
450
					     [install_target.repo_url]]);
451
	    install_target.to_fetch.add(key);
452
	    continue;
453
	}
454

    
455
	if (possible_errors.includes(relevant_result.state)) {
456
	    install_abort(relevant_result.state);
457
	    return false;
458
	}
459

    
460
	install_target.fetched.add(key);
461

    
462
	if (prefix !== TYPE_PREFIX.BAG)
463
	    continue;
464

    
465
	for (const dependency of relevant_result.response.components) {
466
	    if (processed.has(dependency.join('')))
467
		continue;
468
	    needed.push(dependency);
469
	}
470
    }
471
}
472

    
473
function record_fetched_install_dep(prefix, item, repo_url, result)
474
{
475
    const key = prefix + item;
476

    
477
    if (!install_target || repo_url !== install_target.repo_url ||
478
	!install_target.to_fetch.has(key))
479
	return;
480

    
481
    if (possible_errors.includes(result.state)) {
482
	install_abort(result.state);
483
	return;
484
    }
485

    
486
    if (result.state !== "completed")
487
	return;
488

    
489
    install_target.to_fetch.delete(key);
490
    install_target.fetched.add(key);
491

    
492
    if (prefix === TYPE_PREFIX.BAG &&
493
	fetch_install_deps(result.response.components) === false)
494
	return;
495

    
496
    install_check_ready();
497
}
498

    
499
function install_clicked(entry_object)
500
{
501
    import_frame.show_loading();
502

    
503
    install_target = {
504
	repo_url: entry_object.repo_url,
505
	pattern: entry_object.match_object.pattern,
506
	payload: entry_object.match_object.payload,
507
	fetched: new Set(),
508
	to_fetch: new Set()
509
    };
510

    
511
    fetch_install_deps([install_target.payload]);
512

    
513
    install_check_ready();
514
}
515

    
516
var max_query_result_id = 0;
517

    
518
function show_query_successful_result(result_item, repo_url, result)
519
{
520
    if (result.length === 0) {
521
	show_message(result_item, "No results :(");
522
	return;
523
    }
524

    
525
    const cloned_ul_template = clone_template("result_patterns_list");
526
    set_appended(result_item, cloned_ul_template.ul);
527

    
528
    for (const match of result) {
529
	const entry_object = clone_template("query_match_li");
530

    
531
	entry_object.pattern.textContent = match.pattern;
532

    
533
	cloned_ul_template.ul.appendChild(entry_object.li);
534

    
535
	if (!match.payload) {
536
	    entry_object.payload.textContent = "(none)";
537
	    for (const key of ["chbx", "triangle", "unroll"])
538
		entry_object[key].remove();
539
	    continue;
540
	}
541

    
542
	entry_object.payload.textContent = nice_name(...match.payload);
543

    
544
	const install_cb = () => install_clicked(entry_object);
545
	entry_object.btn.addEventListener("click", install_cb);
546

    
547
	const chbx_id = `query_result_${max_query_result_id++}`;
548
	entry_object.chbx.id = chbx_id;
549
	entry_object.lbl.setAttribute("for", chbx_id);
550

    
551
	entry_object.unroll_cb = () => unroll_chbx_first_checked(entry_object);
552
	entry_object.chbx.addEventListener("change", entry_object.unroll_cb);
553

    
554
	entry_object.component_object = match.payload;
555
	entry_object.match_object = match;
556
	entry_object.repo_url = repo_url;
557
    }
558
}
559

    
560
function show_query_result(url_prefix, url, repo_url, result)
561
{
562
    const results_list_object = results_lists.get(url) ||
563
	  create_results_list(url);
564
    const result_item = results_list_object.by_repo.get(repo_url) ||
565
	  create_result_item(results_list_object, repo_url, result);
566

    
567
    const completed_cb =
568
	  item => show_query_successful_result(item, repo_url, result.response);
569
    const possible_actions = {
570
	completed: completed_cb,
571
	started: showcb("loading..."),
572
	connection_error: showcb("Error when querying repository."),
573
	parse_error: showcb("Bad data format received.")
574
    };
575
    possible_actions[result.state](result_item, repo_url);
576
}
577

    
578
by_id("settings_but")
579
    .addEventListener("click", (e) => browser.runtime.openOptionsPage());
580

    
581
async function main()
582
{
583
    init_default_policy_dialog();
584

    
585
    storage = await get_remote_storage();
586
    import_frame = await get_import_frame();
587
    import_frame.onclose = () => show_queried_view_radio.checked = true;
588
    show_page_activity_info();
589
}
590

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