Project

General

Profile

« Previous | Next » 

Revision 7218849a

Added by koszko over 1 year ago

add a mapping/resources installation dialog

View differences:

background/CORS_bypass_server.js
48 48
    try {
49 49
	result_object[prop] = call_prop ? (await object[prop]()) : object[prop];
50 50
    } catch(e) {
51
	result_object[`error-${prop}`] = "" + e;
51
	result_object[`error_${prop}`] = "" + e;
52 52
    }
53 53
}
54 54

  
background/patterns_query_manager.js
144 144
    secret = secret_;
145 145

  
146 146
    const [mapping_tracking, initial_mappings] =
147
	  await haketilodb.track.mappings(ch => changed("mappings", ch));
147
	  await haketilodb.track.mapping(ch => changed("mappings", ch));
148 148
    const [blocking_tracking, initial_blocking] =
149 149
	  await haketilodb.track.blocking(ch => changed("blocking", ch));
150 150

  
common/entities.js
54 54
 * No version normalization is performed.
55 55
 */
56 56
const version_string = (ver, rev=0) => ver.join(".") + (rev ? `-${rev}` : "");
57
#EXPORT version_string
58

  
59
/*
60
 * This function overloads on the number of arguments. If one argument is
61
 * passed, it is an item definition (it need not be complete, only identifier,
62
 * version and, if applicable, revision properties are relevant). If two or
63
 * three arguments are given, they are in order: item identifier, item version
64
 * and item revision.
65
 * Returned is a string identifying this version of item.
66
 */
67
function item_id_string(...args) {
68
    let def = args[0]
69
    if (args.length > 1)
70
	def = {identifier: args[0], version: args[1], revision: args[2]};
71
    return !Array.isArray(def.version) ? def.identifier :
72
	`${def.identifier}-${version_string(def.version, def.revision)}`;
73
}
74
#EXPORT item_id_string
57 75

  
58 76
/* vers should be an array of comparable values. Return the greatest one. */
59 77
const max = vals => Array.reduce(vals, (v1, v2) => v1 > v2 ? v1 : v2);
common/indexeddb.js
61 61
const stores = 	[
62 62
    ["files",     {keyPath: "hash_key"}],
63 63
    ["file_uses", {keyPath: "hash_key"}],
64
    ["resources", {keyPath: "identifier"}],
65
    ["mappings",  {keyPath: "identifier"}],
64
    ["resource",  {keyPath: "identifier"}],
65
    ["mapping",   {keyPath: "identifier"}],
66 66
    ["settings",  {keyPath: "name"}],
67 67
    ["blocking",  {keyPath: "pattern"}],
68 68
    ["repos",     {keyPath: "url"}]
......
175 175
}
176 176

  
177 177
/*
178
 * item_store_names should be an array with either string "mappings", string
179
 * "resources" or both. files should be a dict with values being contents of
178
 * item_store_names should be an array with either string "mapping", string
179
 * "resource" or both. files should be an object with values being contents of
180 180
 * files that are to be possibly saved in this transaction and keys of the form
181 181
 * `sha256-<file's-sha256-sum>`.
182 182
 *
......
292 292
 */
293 293
async function save_items(data)
294 294
{
295
    const item_store_names = ["resources", "mappings"];
295
    const item_store_names = ["resource", "mapping"];
296 296
    const context = await start_items_transaction(item_store_names, data.files);
297 297

  
298 298
    return _save_items(data.resources, data.mappings, context);
......
323 323
 */
324 324
async function save_item(item, context)
325 325
{
326
    const store_name = {resource: "resources", mapping: "mappings"}[item.type];
327

  
328 326
    for (const file_ref of entities.get_files(item))
329 327
	await incr_file_uses(context, file_ref);
330 328

  
331
    broadcast.prepare(context.sender, `idb_changes_${store_name}`,
329
    broadcast.prepare(context.sender, `idb_changes_${item.type}`,
332 330
		      item.identifier);
333
    await _remove_item(store_name, item.identifier, context, false);
334
    await idb_put(context.transaction, store_name, item);
331
    await _remove_item(item.type, item.identifier, context, false);
332
    await idb_put(context.transaction, item.type, item);
335 333
}
336 334
#EXPORT save_item
337 335

  
......
360 358
    await idb_del(context.transaction, store_name, identifier);
361 359
}
362 360

  
363
const remove_resource = (id, ctx) => remove_item("resources", id, ctx);
361
const remove_resource = (id, ctx) => remove_item("resource", id, ctx);
364 362
#EXPORT remove_resource
365 363

  
366
const remove_mapping = (id, ctx) => remove_item("mappings",  id, ctx);
364
const remove_mapping = (id, ctx) => remove_item("mapping",  id, ctx);
367 365
#EXPORT remove_mapping
368 366

  
369 367
/* Function to retrieve all items from a given store. */
......
460 458
/*
461 459
 * Monitor changes to `store_name` IndexedDB object store.
462 460
 *
463
 * `store_name` should be either "resources", "mappings" or "settings".
461
 * `store_name` should be either "resource", "mapping", "settings", "blocking"
462
 * or "repos".
464 463
 *
465 464
 * `onchange` should be a callback that will be called when an item is added,
466 465
 * modified or removed from the store. The callback will be passed an object
......
491 490
}
492 491

  
493 492
const track = {};
494
const trackable = ["resources", "mappings", "settings", "blocking", "repos"];
493
const trackable = ["resource", "mapping", "settings", "blocking", "repos"];
495 494
for (const store_name of trackable)
496 495
    track[store_name] = onchange => start_tracking(store_name, onchange);
497 496
#EXPORT track
common/misc.js
45 45
#FROM common/browser.js      IMPORT browser
46 46
#FROM common/stored_types.js IMPORT TYPE_NAME, TYPE_PREFIX
47 47

  
48
/* uint8_to_hex is a separate function used in cryptographic functions. */
49
const uint8_to_hex =
50
      array => [...array].map(b => ("0" + b.toString(16)).slice(-2)).join("");
51

  
48 52
/*
49
 * generating unique, per-site value that can be computed synchronously
50
 * and is impossible to guess for a malicious website
53
 * Asynchronously compute hex string representation of a sha256 digest of a
54
 * UTF-8 string.
51 55
 */
52

  
53
/* Uint8toHex is a separate function not exported as (a) it's useful and (b) it will be used in crypto.subtle-based digests */
54
function Uint8toHex(data)
55
{
56
    let returnValue = '';
57
    for (let byte of data)
58
	returnValue += ('00' + byte.toString(16)).slice(-2);
59
    return returnValue;
56
async function sha256_async(string) {
57
    const input_ab = new TextEncoder("utf-8").encode(string);
58
    const digest_ab = await crypto.subtle.digest("SHA-256", input_ab);
59
    return uint8_to_hex(new Uint8Array(digest_ab));
60 60
}
61
#EXPORT sha256_async
61 62

  
63
/*
64
 * Generate a unique value that can be computed synchronously and is impossible
65
 * to guess for a malicious website.
66
 */
62 67
function gen_nonce(length=16)
63 68
{
64
    let randomData = new Uint8Array(length);
65
    crypto.getRandomValues(randomData);
66
    return Uint8toHex(randomData);
69
    const random_data = new Uint8Array(length);
70
    crypto.getRandomValues(random_data);
71
    return uint8_to_hex(random_data);
67 72
}
68 73
#EXPORT gen_nonce
69 74

  
html/base.css
96 96
    height: var(--line-height);
97 97
    background: linear-gradient(transparent, #555);
98 98
}
99

  
100
.text_center {
101
    text-align: center;
102
}
103
.text_right {
104
    text-align: right;
105
}
html/dialog.js
131 131
#EXPORT loader
132 132

  
133 133
/*
134
 * Wrapper around target.addEventListener() that makes the requested callback
135
 * only execute if dialog is not shown.
134
 * Wrapper the requested callback into one that only executes it if dialog is
135
 * not shown.
136 136
 */
137
function onevent(ctx, target, event, cb)
137
function when_hidden(ctx, cb)
138 138
{
139
    target.addEventListener(event, e => !ctx.shown && cb(e));
139
    function wrapped_cb(...args) {
140
	if (!ctx.shown)
141
	    return cb(...args);
142
    }
143
    return wrapped_cb;
140 144
}
141
#EXPORT onevent
145
#EXPORT when_hidden
html/install.html
1
#IF !INSTALL_LOADED
2
#DEFINE INSTALL_LOADED
3
<!--
4
    SPDX-License-Identifier: GPL-3.0-or-later OR CC-BY-SA-4.0
5

  
6
    Install mappings/resources in Haketilo.
7

  
8
    This file is part of Haketilo.
9

  
10
    Copyright (C) 2022 Wojtek Kosior <koszko@koszko.org>
11

  
12
    File is dual-licensed. You can choose either GPLv3+, CC BY-SA or both.
13

  
14
    This program is free software: you can redistribute it and/or modify
15
    it under the terms of the GNU General Public License as published by
16
    the Free Software Foundation, either version 3 of the License, or
17
    (at your option) any later version.
18

  
19
    This program is distributed in the hope that it will be useful,
20
    but WITHOUT ANY WARRANTY; without even the implied warranty of
21
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
22
    GNU General Public License for more details.
23

  
24
    You should have received a copy of the GNU General Public License
25
    along with this program.  If not, see <https://www.gnu.org/licenses/>.
26

  
27
    I, Wojtek Kosior, thereby promise not to sue for violation of this file's
28
    licenses. Although I request that you do not make use of this code in a
29
    proprietary program, I am not going to enforce this in court.
30
  -->
31

  
32
<!--
33
    This is not a standalone page. This file is meant to be imported into other
34
    HTML code.
35
  -->
36

  
37
#INCLUDE html/dialog.html
38
#INCLUDE html/item_preview.html
39

  
40
#LOADCSS html/reset.css
41
#LOADCSS html/base.css
42
<style>
43
  .install_entry_li {
44
      display: flex;
45
      align-items: center;
46
      margin: 0;
47
      padding: 0.2em;
48
  }
49
  .install_entry_li:nth-child(2n) {
50
      background-color:#dadada;
51
  }
52

  
53
  .install_item_info {
54
      display: grid;
55
      grid-template-columns: auto;
56
      flex: 1 1 auto;
57
      min-width: 0;
58
  }
59
  .install_item_info > span {
60
      white-space: nowrap;
61
      overflow: hidden;
62
      text-overflow: ellipsis;
63
  }
64
  .install_item_more_info {
65
      font-size: 80%;
66
      font-style: italic;
67
  }
68

  
69
  .install_bottom_buttons {
70
      margin: 1em auto;
71
      text-align: center;
72
  }
73
</style>
74
<template>
75
  <div id="install_view" data-template="main_div">
76
    <div data-template="install_preview">
77
      <ul data-template="to_install_list"></ul>
78
      <div class="install_bottom_buttons">
79
	<button data-template="install_but">Install</button>
80
	<button data-template="cancel_but">Cancel</button>
81
      </div>
82
    </div>
83
    <div data-template="dialog_container">
84
      <!-- dialog shall be dynamically inserted here -->
85
    </div>
86
    <div data-template="mapping_preview_container">
87
      <!-- item preview shall be dynamically inserted here -->
88
      <div class="install_bottom_buttons">
89
	<button data-template="mapping_back_but">Back</button>
90
      </div>
91
    </div>
92
    <div data-template="resource_preview_container">
93
      <div class="install_bottom_buttons">
94
	<button data-template="resource_back_but">Back</button>
95
      </div>
96
    </div>
97
  </div>
98
  <li id="install_list_entry" data-template="main_li"
99
      class="install_entry_li">
100
    <div class="install_item_info">
101
      <span data-template="item_name"></span>
102
      <span data-template="item_id" class="install_item_more_info"></span>
103
      <span data-template="update_info"
104
	    class="install_item_more_info hide">
105
	(update from <span data-template="old_ver"></span>)
106
      </span>
107
    </div>
108
    <span class="text_right">
109
      <button data-template="details_but">Details</button>
110
    </span>
111
  </li>
112
</template>
113
#ENDIF
html/install.js
1
/**
2
 * This file is part of Haketilo.
3
 *
4
 * Function: Install mappings/resources in Haketilo.
5
 *
6
 * Copyright (C) 2022 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 of this code in a
41
 * proprietary program, I am not going to enforce this in court.
42
 */
43

  
44
#IMPORT common/indexeddb.js AS haketilodb
45
#IMPORT html/dialog.js
46
#IMPORT html/item_preview.js AS ip
47

  
48
#FROM common/browser.js   IMPORT browser
49
#FROM html/DOM_helpers.js IMPORT clone_template
50
#FROM common/entities.js  IMPORT item_id_string, version_string, get_files
51
#FROM common/misc.js      IMPORT sha256_async AS sha256
52

  
53
const coll = new Intl.Collator();
54

  
55
/*
56
 * Comparator used to sort items in the order we want them to appear in
57
 * install dialog: first mappings alphabetically, then resources alphabetically.
58
 */
59
function compare_items(def1, def2) {
60
    if (def1.type !== def2.type)
61
	return def1.type === "mapping" ? -1 : 1;
62

  
63
    const name_comparison = coll.compare(def1.long_name, def2.long_name);
64
    return name_comparison === 0 ?
65
	coll.compare(def1.identifier, def2.identifier) : name_comparison;
66
}
67

  
68
function ItemEntry(install_view, item) {
69
    Object.assign(this, clone_template("install_list_entry"));
70
    this.item_def = item.def;
71

  
72
    this.item_name.innerText = item.def.long_name;
73
    this.item_id.innerText = item_id_string(item.def);
74
    if (item.db_def) {
75
	this.old_ver.innerText =
76
	    version_string(item.db_def.version, item.db_def.revision);
77
	this.update_info.classList.remove("hide");
78
    }
79

  
80
    let preview_cb = () => install_view.preview_item(item.def);
81
    preview_cb = dialog.when_hidden(install_view.dialog_ctx, preview_cb);
82
    this.details_but.addEventListener("click", preview_cb);
83
}
84

  
85
const container_ids = [
86
    "install_preview",
87
    "dialog_container",
88
    "mapping_preview_container",
89
    "resource_preview_container"
90
];
91

  
92
/*
93
 * Work object is used to communicate between asynchronously executing
94
 * functions when computing dependencies tree of an item and when fetching
95
 * files for installation.
96
 */
97
async function init_work() {
98
    const work = {
99
	waiting: 0,
100
	is_ok: true,
101
	db: (await haketilodb.get()),
102
	result: []
103
    };
104

  
105
    work.err = function (error, user_message) {
106
	if (error)
107
    	    console.error(error);
108
	work.is_ok = false;
109
	work.reject_cb(user_message);
110
    }
111

  
112
    return [work,
113
	    new Promise((...cbs) => [work.resolve_cb, work.reject_cb] = cbs)];
114
}
115

  
116
function InstallView(tab_id, on_view_show, on_view_hide) {
117
    Object.assign(this, clone_template("install_view"));
118
    this.shown = false;
119

  
120
    const show_container = name => {
121
	for (const cid of container_ids) {
122
	    if (cid !== name)
123
		this[cid].classList.add("hide");
124
	}
125
	this[name].classList.remove("hide");
126
    }
127

  
128
    this.dialog_ctx = dialog.make(() => show_container("dialog_container"),
129
				  () => show_container("install_preview"));
130
    this.dialog_container.prepend(this.dialog_ctx.main_div);
131

  
132
    /* Make a link to view a file from the repository. */
133
    const make_file_link = (preview_ctx, file_ref) => {
134
	const a = document.createElement("a");
135
	a.href = `${this.repo_url}file/${file_ref.hash_key}`;
136
	a.innerText = file_ref.file;
137

  
138
	return a;
139
    }
140

  
141
    this.previews_ctx = {};
142

  
143
    this.preview_item = item_def => {
144
	if (!this.shown)
145
	    return;
146

  
147
	const fun = ip[`${item_def.type}_preview`];
148
	const preview_ctx = fun(item_def, this.previews_ctx[item_def.type],
149
				make_file_link);
150
	this.previews_ctx[item_def.type] = preview_ctx;
151

  
152
	const container_name = `${item_def.type}_preview_container`;
153
	show_container(container_name);
154
	this[container_name].prepend(preview_ctx.main_div);
155
    }
156

  
157
    const back_cb = dialog.when_hidden(this.dialog_ctx,
158
				       () => show_container("install_preview"));
159
    for (const type of ["resource", "mapping"])
160
	this[`${type}_back_but`].addEventListener("click", back_cb);
161

  
162
    const process_item = async (work, item_type, id, ver) => {
163
	if (!work.is_ok || work.processed_by_type[item_type].has(id))
164
	    return;
165

  
166
	work.processed_by_type[item_type].add(id);
167
	work.waiting++;
168

  
169
	const url = ver ?
170
	      `${this.repo_url}${item_type}/${id}/${ver.join(".")}.json` :
171
	      `${this.repo_url}${item_type}/${id}.json`;
172
	const response =
173
	      await browser.tabs.sendMessage(tab_id, ["repo_query", url]);
174
	if (!work.is_ok)
175
	    return;
176

  
177
	if ("error" in response) {
178
	    return work.err(response.error,
179
			    "Failure to communicate with repository :(");
180
	}
181

  
182
	if (!response.ok) {
183
	    return work.err(null,
184
			    `Repository sent HTTP code ${response.status} :(`);
185
	}
186

  
187
	if ("error_json" in response) {
188
	    return work.err(response.error_json,
189
			    "Repository's response is not valid JSON :(");
190
	}
191

  
192
	if (response.json.api_schema_version > [1]) {
193
	    let api_ver = "";
194
	    try {
195
		api_ver =
196
		    ` (${version_string(response.json.api_schema_version)})`;
197
	    } catch(e) {
198
		console.warn(e);
199
	    }
200

  
201
	    const captype = item_type[0].toUpperCase() + item_type.substring(1);
202
	    const msg = `${captype} ${item_id_string(id, ver)} was served using unsupported Hydrilla API version${api_ver}. You might need to update Haketilo.`;
203
	    return work.err(null, msg);
204
	}
205

  
206
	/* TODO: JSON schema validation should be added here. */
207

  
208
	delete response.json.api_schema_version;
209
	delete response.json.api_schema_revision;
210

  
211
	const files = response.json.source_copyright
212
	      .concat(item_type === "resource" ? response.json.scripts : []);
213
	for (const file of files) {
214
	    file.hash_key = `sha256-${file.sha256}`;
215
	    delete file.sha256;
216
	}
217

  
218
	if (item_type === "mapping") {
219
	    for (const res_ref of Object.values(response.json.payloads))
220
		process_item(work, "resource", res_ref.identifier);
221
	} else {
222
	    for (const res_id of (response.json.dependencies || []))
223
		process_item(work, "resource", res_id);
224
	}
225

  
226
	/*
227
	 * At this point we already have JSON definition of the item and we
228
	 * triggered processing of its dependencies. We now have to verify if
229
	 * the same or newer version of the item is already present in the
230
	 * database and if so - omit this item.
231
	 */
232
	const transaction = work.db.transaction(item_type);
233
	try {
234
	    var db_def = await haketilodb.idb_get(transaction, item_type, id);
235
	    if (!work.is_ok)
236
		return;
237
	} catch(e) {
238
	    const msg = "Error accessing Haketilo's internal database :(";
239
	    return work.err(e, msg);
240
	}
241
	if (!db_def || db_def.version < response.json.version)
242
	    work.result.push({def: response.json, db_def});
243

  
244
	if (--work.waiting === 0)
245
	    work.resolve_cb(work.result);
246
    }
247

  
248
    async function compute_deps(item_type, item_id, item_ver) {
249
	const [work, work_prom] = await init_work();
250
	work.processed_by_type = {"mapping" : new Set(), "resource": new Set()};
251

  
252
	process_item(work, item_type, item_id, item_ver);
253

  
254
	const items = await work_prom;
255
	items.sort((i1, i2) => compare_items(i1.def, i2.def));
256
	return items;
257
    }
258

  
259
    this.show = async (repo_url, item_type, item_id, item_ver) => {
260
	if (this.shown)
261
	    return;
262

  
263
	this.shown = true;
264

  
265
	this.repo_url = repo_url;
266

  
267
	dialog.loader(this.dialog_ctx, "Fetching data from repository...");
268

  
269
	try {
270
	    on_view_show();
271
	} catch(e) {
272
	    console.error(e);
273
	}
274

  
275
	try {
276
	    var items = await compute_deps(item_type, item_id, item_ver);
277
	} catch(e) {
278
	    var dialog_prom = dialog.error(this.dialog_ctx, e);
279
	}
280

  
281
	if (!dialog_prom && items.length === 0) {
282
	    const msg = "Nothing to do - packages already installed.";
283
	    var dialog_prom = dialog.info(this.dialog_ctx, msg);
284
	}
285

  
286
	if (dialog_prom) {
287
	    dialog.close(this.dialog_ctx);
288

  
289
	    await dialog_prom;
290

  
291
	    hide();
292
	    return;
293
	}
294

  
295
	this.item_entries = items.map(i => new ItemEntry(this, i));
296
	this.to_install_list.append(...this.item_entries.map(ie => ie.main_li));
297

  
298
	dialog.close(this.dialog_ctx);
299
    }
300

  
301
    const process_file = async (work, hash_key) => {
302
	if (!work.is_ok)
303
	    return;
304

  
305
	work.waiting++;
306

  
307
	try {
308
	    var file_uses = await haketilodb.idb_get(work.file_uses_transaction,
309
						     "file_uses", hash_key);
310
	    if (!work.is_ok)
311
		return;
312
	} catch(e) {
313
	    const msg = "Error accessing Haketilo's internal database :(";
314
	    return work.err(e, msg);
315
	}
316

  
317
	if (!file_uses) {
318
	    const url = `${this.repo_url}file/${hash_key}`;
319

  
320
	    try {
321
		var response = await fetch(url);
322
		if (!work.is_ok)
323
		    return;
324
	    } catch(e) {
325
		const msg = "Failure to communicate with repository :(";
326
		return work.err(e, msg);
327
	    }
328

  
329
	    if (!response.ok) {
330
		const msg = `Repository sent HTTP code ${response.status} :(`;
331
		return work.err(null, msg);
332
	    }
333

  
334
	    try {
335
		var text = await response.text();
336
		if (!work.is_ok)
337
		    return;
338
	    } catch(e) {
339
		const msg = "Repository's response is not valid text :(";
340
		return work.err(e, msg);
341
	    }
342

  
343
	    const digest = await sha256(text);
344
	    if (!work.is_ok)
345
		return;
346
	    if (`sha256-${digest}` !== hash_key) {
347
		const msg = `${url} served a file with different SHA256 cryptographic sum :(`;
348
		return work.err(null, msg);
349
	    }
350

  
351
	    work.result.push([hash_key, text]);
352
	}
353

  
354
	if (--work.waiting === 0)
355
	    work.resolve_cb(work.result);
356
    }
357

  
358
    const get_missing_files = async item_defs => {
359
	const [work, work_prom] = await init_work();
360
	work.file_uses_transaction = work.db.transaction("file_uses");
361

  
362
	const processed_files = new Set();
363

  
364
	for (const item_def of item_defs) {
365
	    for (const file of get_files(item_def)) {
366
		if (!processed_files.has(file.hash_key)) {
367
		    processed_files.add(file.hash_key);
368
		    process_file(work, file.hash_key);
369
		}
370
	    }
371
	}
372

  
373
	return processed_files.size > 0 ? work_prom : [];
374
    }
375

  
376
    const perform_install = async () => {
377
	if (!this.show || !this.item_entries)
378
	    return;
379

  
380
	dialog.loader(this.dialog_ctx, "Installing...");
381

  
382
	const item_defs = this.item_entries.map(ie => ie.item_def);
383

  
384
	try {
385
	    var files = (await get_missing_files(item_defs))
386
		.reduce((ac, [hk, txt]) => Object.assign(ac, {[hk]: txt}), {});
387
	} catch(e) {
388
	    var dialog_prom = dialog.error(this.dialog_ctx, e);
389
	}
390

  
391
	if (files !== undefined) {
392
	    const data = {files};
393
	    const names = [["mappings", "mapping"], ["resources", "resource"]];
394

  
395
	    for (const [set_name, type] of names) {
396
		const set = {};
397

  
398
		for (const def of item_defs.filter(def => def.type === type))
399
		    set[def.identifier] = {[version_string(def.version)]: def};
400

  
401
		data[set_name] = set;
402
	    }
403

  
404
	    try {
405
		await haketilodb.save_items(data);
406
	    } catch(e) {
407
		console.error(e);
408
		const msg = "Error writing to Haketilo's internal database :(";
409
		var dialog_prom = dialog.error(this.dialog_ctx, msg);
410
	    }
411
	}
412

  
413
	if (!dialog_prom) {
414
	    const msg = "Successfully installed!";
415
	    var dialog_prom = dialog.info(this.dialog_ctx, msg);
416
	}
417

  
418
	dialog.close(this.dialog_ctx);
419

  
420
	await dialog_prom;
421

  
422
	hide();
423
    }
424

  
425
    const hide = () => {
426
	if (!this.shown)
427
	    return;
428

  
429
	this.shown = false;
430
	delete this.item_entries;
431
	[...this.to_install_list.children].forEach(n => n.remove());
432

  
433
	try {
434
	    on_view_hide();
435
	} catch(e) {
436
	    console.error(e);
437
	}
438
    }
439

  
440
    this.when_hidden = cb => {
441
	const wrapped_cb = (...args) => {
442
	    if (!this.shown)
443
		return cb(...args);
444
	}
445
	return wrapped_cb;
446
    }
447

  
448
    const hide_cb = dialog.when_hidden(this.dialog_ctx, hide);
449
    this.cancel_but.addEventListener("click", hide_cb);
450

  
451
    const install_cb = dialog.when_hidden(this.dialog_ctx, perform_install);
452
    this.install_but.addEventListener("click", install_cb);
453
}
html/item_list.js
200 200

  
201 201
async function remove_single_item(item_type, identifier)
202 202
{
203
    const store = ({resource: "resources", mapping: "mappings"})[item_type];
204 203
    const transaction_ctx =
205
	  await haketilodb.start_items_transaction([store], {});
204
	  await haketilodb.start_items_transaction([item_type], {});
206 205
    await haketilodb[`remove_${item_type}`](identifier, transaction_ctx);
207 206
    await haketilodb.finalize_transaction(transaction_ctx);
208 207
}
209 208

  
210 209
function resource_list()
211 210
{
212
      return item_list(resource_preview, haketilodb.track.resources,
211
      return item_list(resource_preview, haketilodb.track.resource,
213 212
		       id => remove_single_item("resource", id));
214 213
}
215 214
#EXPORT resource_list
216 215

  
217 216
function mapping_list()
218 217
{
219
      return item_list(mapping_preview, haketilodb.track.mappings,
218
      return item_list(mapping_preview, haketilodb.track.mapping,
220 219
		       id => remove_single_item("mapping", id));
221 220
}
222 221
#EXPORT mapping_list
html/item_preview.js
55 55
    }
56 56
}
57 57

  
58
/* Link click handler used in make_file_link(). */
58 59
async function file_link_clicked(preview_object, file_ref, event)
59 60
{
60 61
    event.preventDefault();
......
71 72
    }
72 73
}
73 74

  
75
/*
76
 * The default function to use to create file preview link. Links it creates can
77
 * be used to view files from IndexedDB.
78
 */
74 79
function make_file_link(preview_object, file_ref)
75 80
{
76 81
    const a = document.createElement("a");
......
82 87
    return a;
83 88
}
84 89

  
85
function resource_preview(resource, preview_object, dialog_context)
90
function resource_preview(resource, preview_object, dialog_context,
91
			  make_link_cb=make_file_link)
86 92
{
87 93
    if (preview_object === undefined)
88 94
	preview_object = clone_template("resource_preview");
......
98 104
    [...preview_object.dependencies.childNodes].forEach(n => n.remove());
99 105
    populate_list(preview_object.dependencies, resource.dependencies);
100 106

  
101
    const link_maker = file_ref => make_file_link(preview_object, file_ref);
107
    const link_maker = file_ref => make_link_cb(preview_object, file_ref);
102 108

  
103 109
    [...preview_object.scripts.childNodes].forEach(n => n.remove());
104 110
    populate_list(preview_object.scripts, resource.scripts.map(link_maker));
......
113 119
}
114 120
#EXPORT resource_preview
115 121

  
116
function mapping_preview(mapping, preview_object, dialog_context)
122
function mapping_preview(mapping, preview_object, dialog_context,
123
			 make_link_cb=make_file_link)
117 124
{
118 125
    if (preview_object === undefined)
119 126
	preview_object = clone_template("mapping_preview");
......
138 145
	}
139 146
    }
140 147

  
141
    const link_maker = file_ref => make_file_link(preview_object, file_ref);
148
    const link_maker = file_ref => make_link_cb(preview_object, file_ref);
142 149

  
143 150
    [...preview_object.copyright.childNodes].forEach(n => n.remove());
144 151
    populate_list(preview_object.copyright,
html/payload_create.js
137 137
{
138 138
    const db = await haketilodb.get();
139 139
    const tx_starter = haketilodb.start_items_transaction;
140
    const tx_ctx = await tx_starter(["resources", "mappings"], saving.files);
140
    const tx_ctx = await tx_starter(["resource", "mapping"], saving.files);
141 141

  
142
    for (const [type, store_name] of
143
	 [["resource", "resources"], ["mapping", "mappings"]]) {
142
    for (const type of ["resource", "mapping"]) {
144 143
	if (!saving[`override_${type}`] &&
145
	    (await haketilodb.idb_get(tx_ctx.transaction, store_name,
144
	    (await haketilodb.idb_get(tx_ctx.transaction, type,
146 145
				      saving.identifier))) {
147 146
	    saving.ask_override = type;
148 147
	    return;
html/settings.html
92 92
      #repos_list_container, #repos_dialog_container {
93 93
	  padding: 0.8em 0.4em 0.4em 0.4em;
94 94
      }
95
      #default_policy_dialog {
96
	  text-align: center;
97
      }
98 95
      #blocking_editable_container, #blocking_dialog_container,
99 96
      #repos_list_container, #repos_dialog_container {
100 97
	  max-width: var(--content-max-width);
......
134 131
	<div id="allowing_list_container">
135 132
	  <h3>Allow scripts on</h3>
136 133
	</div>
137
	<div id="default_policy_dialog" class="grid_col_both">
134
	<div id="default_policy_dialog" class="grid_col_both text_center">
138 135
#INCLUDE html/default_blocking_policy.html
139 136
	</div>
140 137
      </div>
html/text_entry_list.js
162 162
	[this.cancel_but,       this.make_noneditable],
163 163
	[this.noneditable_view, this.make_editable],
164 164
    ])
165
	dialog.onevent(list.dialog_ctx, node, "click", cb);
165
	node.addEventListener("click", dialog.when_hidden(list.dialog_ctx, cb));
166 166

  
167
    dialog.onevent(list.dialog_ctx, this.input, "keypress",
168
		   e => (e.key === 'Enter') && enter_hit());
167
    const enter_cb = e => (e.key === 'Enter') && enter_hit();
168
    this.input.addEventListener("keypress",
169
				dialog.when_hidden(list.dialog_ctx, enter_cb));
169 170

  
170 171
    if (entry_idx > 0) {
171 172
	const prev_text = list.shown_texts[entry_idx - 1];
......
226 227

  
227 228
    const add_new = () => new Entry(null, this, 0);
228 229

  
229
    dialog.onevent(dialog_ctx, this.new_but, "click", add_new);
230
    this.new_but.addEventListener("click",
231
				  dialog.when_hidden(dialog_ctx, add_new));
230 232
}
231 233

  
232 234
async function repo_list(dialog_ctx) {
test/unit/test_CORS_bypass_server.py
97 97
    assert set(results['invalid'].keys()) == {'error'}
98 98

  
99 99
    assert set(results['nonexistent'].keys()) == \
100
        {'ok', 'status', 'text', 'error-json'}
100
        {'ok', 'status', 'text', 'error_json'}
101 101
    assert results['nonexistent']['ok'] == False
102 102
    assert results['nonexistent']['status'] == 404
103 103
    assert results['nonexistent']['text'] == 'Handler for this URL not found.'
test/unit/test_indexeddb.py
78 78

  
79 79
    execute_in_page(
80 80
        '''{
81
        const promise = start_items_transaction(["resources"], arguments[1])
81
        const promise = start_items_transaction(["resource"], arguments[1])
82 82
            .then(ctx => save_item(arguments[0], ctx).then(() => ctx))
83 83
            .then(finalize_transaction);
84 84
        returnval(promise);
......
97 97
    assert set([uses['hash_key'] for uses in database_contents['file_uses']]) \
98 98
        == set([file['hash_key'] for file in database_contents['files']])
99 99

  
100
    assert database_contents['mappings'] == []
101
    assert database_contents['resources'] == [sample_item]
100
    assert database_contents['mapping'] == []
101
    assert database_contents['resource'] == [sample_item]
102 102

  
103 103
    # See if trying to add an item without providing all its files ends in an
104 104
    # exception and aborts the transaction as it should.
......
111 111
        async function try_add_item()
112 112
        {
113 113
            const context =
114
                await start_items_transaction(["resources"], args[1]);
114
                await start_items_transaction(["resource"], args[1]);
115 115
            try {
116 116
                await save_item(args[0], context);
117 117
                await finalize_transaction(context);
......
137 137
    sample_item = make_sample_mapping()
138 138
    database_contents = execute_in_page(
139 139
        '''{
140
        const promise = start_items_transaction(["mappings"], arguments[1])
140
        const promise = start_items_transaction(["mapping"], arguments[1])
141 141
            .then(ctx => save_item(arguments[0], ctx).then(() => ctx))
142 142
            .then(finalize_transaction);
143 143
        returnval(promise);
......
161 161
    assert files == dict([(file['hash_key'], file['contents'])
162 162
                          for file in sample_files_list])
163 163

  
164
    del database_contents['resources'][0]['source_copyright'][0]['extra_prop']
165
    assert database_contents['resources'] == [make_sample_resource()]
166
    assert database_contents['mappings']  == [sample_item]
164
    del database_contents['resource'][0]['source_copyright'][0]['extra_prop']
165
    assert database_contents['resource'] == [make_sample_resource()]
166
    assert database_contents['mapping']  == [sample_item]
167 167

  
168 168
    # Try removing the items to get an empty database again.
169 169
    results = [None, None]
......
172 172
            f'''{{
173 173
            const remover = remove_{item_type};
174 174
            const promise =
175
                start_items_transaction(["{item_type}s"], {{}})
175
                start_items_transaction(["{item_type}"], {{}})
176 176
                .then(ctx => remover('helloapple', ctx).then(() => ctx))
177 177
                .then(finalize_transaction);
178 178
            returnval(promise);
......
193 193
    assert files == dict([(file['hash_key'], file['contents'])
194 194
                          for file in sample_files_list])
195 195

  
196
    assert results[0]['resources'] == []
197
    assert results[0]['mappings'] == [sample_item]
196
    assert results[0]['resource'] == []
197
    assert results[0]['mapping'] == [sample_item]
198 198

  
199 199
    assert results[1] == dict([(key, []) for key in  results[0].keys()])
200 200

  
......
223 223
    execute_in_page('initial_data = arguments[0];', initial_data)
224 224
    database_contents = get_db_contents(execute_in_page)
225 225

  
226
    assert database_contents['resources'] == [sample_resource]
227
    assert database_contents['mappings']  == [sample_mapping]
226
    assert database_contents['resource'] == [sample_resource]
227
    assert database_contents['mapping']  == [sample_mapping]
228 228

  
229 229
@pytest.mark.get_page('https://gotmyowndoma.in')
230 230
def test_haketilodb_settings(driver, execute_in_page):
......
407 407
        }''')
408 408
    assert item_counts == [1 for _ in item_counts]
409 409
    for elem_id, json_value in [
410
            ('resources_helloapple', sample_resource),
411
            ('mappings_helloapple', sample_mapping),
410
            ('resource_helloapple', sample_resource),
411
            ('mapping_helloapple', sample_mapping),
412 412
            ('settings_option15', {'name': 'option15', 'value': '123'}),
413 413
            ('repos_https://hydril.la', {'url': 'https://hydril.la'}),
414 414
            ('blocking_file:///*', {'pattern': 'file:///*', 'allow': False})
......
442 442
    driver.switch_to.window(windows[0])
443 443
    driver.implicitly_wait(10)
444 444
    for elem_id, json_value in [
445
            ('resources_helloapple-copy', sample_resource2),
446
            ('mappings_helloapple-copy', sample_mapping2),
445
            ('resource_helloapple-copy', sample_resource2),
446
            ('mapping_helloapple-copy', sample_mapping2),
447 447
            ('settings_option22', {'name': 'option22', 'value': 'abc'}),
448 448
            ('repos_https://hydril2.la', {'url': 'https://hydril2.la'}),
449 449
            ('blocking_ftp://a.bc/', {'pattern': 'ftp://a.bc/', 'allow': True})
......
457 457
        '''{
458 458
        async function change_remove_items()
459 459
        {
460
            const store_names = ["resources", "mappings"];
460
            const store_names = ["resource", "mapping"];
461 461
            const ctx = await start_items_transaction(store_names, {});
462 462
            await remove_resource("helloapple", ctx);
463 463
            await remove_mapping("helloapple-copy", ctx);
......
470 470
        returnval(change_remove_items());
471 471
        }''')
472 472

  
473
    removed_ids = ['mappings_helloapple-copy', 'resources_helloapple',
473
    removed_ids = ['mapping_helloapple-copy', 'resource_helloapple',
474 474
                   'repos_https://hydril.la', 'blocking_file:///*']
475 475
    def condition_items_absent_and_changed(driver):
476 476
        for id in removed_ids:
test/unit/test_install.py
1
# SPDX-License-Identifier: CC0-1.0
2

  
3
"""
4
Haketilo unit tests - item installation dialog
5
"""
6

  
7
# This file is part of Haketilo
8
#
9
# Copyright (C) 2022 Wojtek Kosior <koszko@koszko.org>
10
#
11
# This program is free software: you can redistribute it and/or modify
12
# it under the terms of the CC0 1.0 Universal License as published by
13
# the Creative Commons Corporation.
14
#
15
# This program is distributed in the hope that it will be useful,
16
# but WITHOUT ANY WARRANTY; without even the implied warranty of
17
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18
# CC0 1.0 Universal License for more details.
19

  
20
import pytest
21
import json
22
from selenium.webdriver.support.ui import WebDriverWait
23

  
24
from ..extension_crafting import ExtraHTML
25
from ..script_loader import load_script
26
from .utils import *
27

  
28
def content_script():
29
    script = load_script('content/repo_query_cacher.js')
30
    return f'{script}; {tab_id_asker}; start();'
31

  
32
def background_script():
33
    script = load_script('background/broadcast_broker.js',
34
                         '#IMPORT background/CORS_bypass_server.js')
35
    return f'{script}; {tab_id_responder}; start(); CORS_bypass_server.start();'
36

  
37
def setup_view(driver, execute_in_page):
38
    tab_id = run_content_script_in_new_window(driver, 'https://gotmyowndoma.in')
39

  
40
    execute_in_page(load_script('html/install.js'))
41
    container_ids, containers_objects = execute_in_page(
42
        '''
43
        const cb_calls = [];
44
        const install_view = new InstallView(arguments[0],
45
                                             () => cb_calls.push("show"),
46
                                             () => cb_calls.push("hide"));
47
        document.body.append(install_view.main_div);
48
        const ets = () => install_view.item_entries;
49
        const shw = slice => [cb_calls.slice(slice || 0), install_view.shown];
50
        returnval([container_ids, container_ids.map(cid => install_view[cid])]);
51
        ''',
52
        tab_id)
53

  
54
    containers = dict(zip(container_ids, containers_objects))
55

  
56
    def assert_container_displayed(container_id):
57
        for cid, cobj in zip(container_ids, containers_objects):
58
            assert (cid == container_id) == cobj.is_displayed()
59

  
60
    return containers, assert_container_displayed
61

  
62
install_ext_data = {
63
    'content_script': content_script,
64
    'background_script': background_script,
65
    'extra_html': ExtraHTML('html/install.html', {}),
66
    'navigate_to': 'html/install.html'
67
}
68

  
69
@pytest.mark.ext_data(install_ext_data)
70
@pytest.mark.usefixtures('webextension')
71
@pytest.mark.parametrize('complex_variant', [False, True])
72
def test_install_normal_usage(driver, execute_in_page, complex_variant):
73
    """
74
    Test of the normal package installation procedure with one mapping and,
75
    depending on parameter, one or many resources.
76
    """
77
    containers, assert_container_displayed = setup_view(driver, execute_in_page)
78

  
79
    assert execute_in_page('returnval(shw());') == [[], False]
80

  
81
    if complex_variant:
82
        # The resource/mapping others depend on.
83
        root_id = 'abcd-defg-ghij'
84
        root_resource_id = f'resource_{root_id}'
85
        root_mapping_id = f'mapping_{root_id}'
86
        # Those ids are used to check the alphabetical ordering.
87
        resource_ids = [f'resource_{letters}' for letters in (
88
            'a', 'abcd', root_id, 'b', 'c',
89
            'd', 'defg', 'e', 'f',
90
            'g', 'ghij', 'h', 'i', 'j'
91
        )]
92
        files_count = 9
93
    else:
94
        root_resource_id = f'resource_a'
95
        root_mapping_id = f'mapping_a'
96
        resource_ids = [root_resource_id]
97
        files_count = 0
98

  
99
    # Preview the installation of a resource, show resource's details, close
100
    # the details and cancel installation.
101
    execute_in_page('returnval(install_view.show(...arguments));',
102
                    'https://hydril.la/', 'resource', root_resource_id)
103

  
104
    assert execute_in_page('returnval(shw());') == [['show'], True]
105
    assert f'{root_resource_id}-2021.11.11-1'\
106
        in containers['install_preview'].text
107
    assert_container_displayed('install_preview')
108

  
109
    entries = execute_in_page('returnval(ets().map(e => e.main_li.innerText));')
110
    assert len(entries) == len(resource_ids)
111
    # Verify alphabetical ordering.
112
    assert all([id in text for id, text in zip(resource_ids, entries)])
113

  
114
    assert not execute_in_page('returnval(ets()[0].old_ver);').is_displayed()
115
    execute_in_page('returnval(ets()[0].details_but);').click()
116
    assert 'resource_a' in containers['resource_preview_container'].text
117
    assert_container_displayed('resource_preview_container')
118

  
119
    execute_in_page('returnval(install_view.resource_back_but);').click()
120
    assert_container_displayed('install_preview')
121

  
122
    assert execute_in_page('returnval(shw());') == [['show'], True]
123
    execute_in_page('returnval(install_view.cancel_but);').click()
124
    assert execute_in_page('returnval(shw());') == [['show', 'hide'], False]
125

  
126
    # Preview the installation of a mapping and a resource, show mapping's
127
    # details, close the details and commit the installation.
128
    execute_in_page('returnval(install_view.show(...arguments));',
129
                    'https://hydril.la/', 'mapping',
130
                    root_mapping_id, [2022, 5, 10])
131

  
132
    assert execute_in_page('returnval(shw(2));') == [['show'], True]
133
    assert_container_displayed('install_preview')
134

  
135
    entries = execute_in_page('returnval(ets().map(e => e.main_li.innerText));')
136
    assert len(entries) == len(resource_ids) + 1
137
    assert f'{root_mapping_id}-2022.5.10' in entries[0]
138
    # Verify alphabetical ordering.
139
    assert all([id in text for id, text in zip(resource_ids, entries[1:])])
140

  
141
    assert not execute_in_page('returnval(ets()[0].old_ver);').is_displayed()
142
    execute_in_page('returnval(ets()[0].details_but);').click()
143
    assert root_mapping_id in containers['mapping_preview_container'].text
144
    assert_container_displayed('mapping_preview_container')
145

  
146
    execute_in_page('returnval(install_view.mapping_back_but);').click()
147
    assert_container_displayed('install_preview')
148

  
149
    execute_in_page('returnval(install_view.install_but);').click()
150
    installed = lambda d: 'ly installed!' in containers['dialog_container'].text
151
    WebDriverWait(driver, 10000).until(installed)
152

  
153
    assert execute_in_page('returnval(shw(2));') == [['show'], True]
154
    execute_in_page('returnval(install_view.dialog_ctx.ok_but);').click()
155
    assert execute_in_page('returnval(shw(2));') == [['show', 'hide'], False]
156

  
157
    # Verify the install
158
    db_contents = get_db_contents(execute_in_page)
159
    for item_type, ids in \
160
        [('mapping', {root_mapping_id}), ('resource', set(resource_ids))]:
161
        assert set([it['identifier'] for it in db_contents[item_type]]) == ids
162

  
163
    assert all([len(db_contents[store]) == files_count
164
                for store in ('files', 'file_uses')])
165

  
166
    # Update the installed mapping to a newer version.
167
    execute_in_page('returnval(install_view.show(...arguments));',
168
                    'https://hydril.la/', 'mapping', root_mapping_id)
169
    assert execute_in_page('returnval(shw(4));') == [['show'], True]
170
    # resources are already in the newest versions, hence they should not appear
171
    # in the install preview list.
172
    assert execute_in_page('returnval(ets().length);') == 1
173
    # Mapping's version update information should be displayed.
174
    assert execute_in_page('returnval(ets()[0].old_ver);').is_displayed()
175
    execute_in_page('returnval(install_view.install_but);').click()
176

  
177
    WebDriverWait(driver, 10).until(installed)
178

  
179
    assert execute_in_page('returnval(shw(4));') == [['show'], True]
180
    execute_in_page('returnval(install_view.dialog_ctx.ok_but);').click()
181
    assert execute_in_page('returnval(shw(4));') == [['show', 'hide'], False]
182

  
183
    # Verify the newer version install.
184
    old_db_contents, db_contents = db_contents, get_db_contents(execute_in_page)
185
    old_db_contents['mapping'][0]['version'][-1] += 1
186
    assert db_contents['mapping'] == old_db_contents['mapping']
187

  
188
    # All items are up to date - verify dialog is instead shown in this case.
189
    execute_in_page('install_view.show(...arguments);',
190
                    'https://hydril.la/', 'mapping', root_mapping_id)
191
    assert execute_in_page('returnval(shw(6));') == [['show'], True]
192
    assert_container_displayed('dialog_container')
193
    assert 'Nothing to do - packages already installed.' \
194
        in containers['dialog_container'].text
195
    execute_in_page('returnval(install_view.dialog_ctx.ok_but);').click()
196
    assert execute_in_page('returnval(shw(6));') == [['show', 'hide'], False]
197

  
198
@pytest.mark.ext_data(install_ext_data)
199
@pytest.mark.usefixtures('webextension')
200
@pytest.mark.parametrize('message', [
201
    'fetching_data',
202
    'failure_to_communicate_sendmessage',
203
    'HTTP_code_item',
204
    'invalid_JSON',
205
    'newer_API_version',
206
    'invalid_API_version',
207
    'indexeddb_error_item',
208
    'installing',
209
    'indexeddb_error_file_uses',
210
    'failure_to_communicate_fetch',
211
    'HTTP_code_file',
212
    'not_valid_text',
213
    'sha256_mismatch',
214
    'indexeddb_error_write'
215
])
216
def test_install_dialogs(driver, execute_in_page, message):
217
    """
218
    Test of various error and loading messages used in install view.
219
    """
220
    containers, assert_container_displayed = setup_view(driver, execute_in_page)
221

  
222
    def dlg_buts():
223
        return execute_in_page(
224
            '''{
225
            const dlg = install_view.dialog_ctx;
226
            const ids = ['ask_buts', 'conf_buts'];
227
            returnval(ids.filter(id => !dlg[id].classList.contains("hide")));
228
            }''')
229

  
230
    def dialog_txt():
231
        return execute_in_page(
232
            'returnval(install_view.dialog_ctx.msg.textContent);'
233
        )
234

  
235
    def assert_dlg(awaited_buttons, expected_msg, hides_install_view=True,
236
                   button_to_click='ok_but'):
237
        WebDriverWait(driver, 10).until(lambda d: dlg_buts() == awaited_buttons)
238

  
239
        assert expected_msg == dialog_txt()
240

  
241
        execute_in_page(
242
            f'returnval(install_view.dialog_ctx.{button_to_click});'
243
        ).click()
244

  
245
        if hides_install_view:
246
            assert execute_in_page('returnval(shw());') == \
247
                [['show', 'hide'], False]
248

  
249
    if message == 'fetching_data':
250
        execute_in_page(
251
            '''
252
            browser.tabs.sendMessage = () => new Promise(cb => {});
253
            install_view.show(...arguments);
254
            ''',
255
            'https://hydril.la/', 'mapping', 'mapping_a')
256

  
257
        assert dlg_buts() == []
258
        assert dialog_txt() == 'Fetching data from repository...'
259
    elif message == 'failure_to_communicate_sendmessage':
260
        execute_in_page(
261
            '''
262
            browser.tabs.sendMessage = () => Promise.resolve({error: "sth"});
263
            install_view.show(...arguments);
264
            ''',
265
            'https://hydril.la/', 'mapping', 'mapping_a')
266

  
267
        assert_dlg(['conf_buts'], 'Failure to communicate with repository :(')
268
    elif message == 'HTTP_code_item':
269
        execute_in_page(
270
            '''
271
            const response = {ok: false, status: 404};
272
            browser.tabs.sendMessage = () => Promise.resolve(response);
273
            install_view.show(...arguments);
274
            ''',
275
            'https://hydril.la/', 'mapping', 'mapping_a')
276

  
277
        assert_dlg(['conf_buts'], 'Repository sent HTTP code 404 :(')
278
    elif message == 'invalid_JSON':
279
        execute_in_page(
280
            '''
281
            const response = {ok: true, status: 200, error_json: "sth"};
282
            browser.tabs.sendMessage = () => Promise.resolve(response);
283
            install_view.show(...arguments);
284
            ''',
285
            'https://hydril.la/', 'mapping', 'mapping_a')
286

  
287
        assert_dlg(['conf_buts'], "Repository's response is not valid JSON :(")
288
    elif message == 'newer_API_version':
289
        execute_in_page(
290
            '''
291
            const response = {
292
                ok: true,
293
                status: 200,
294
                json: {api_schema_version: [99, 99]}
295
            };
296
            browser.tabs.sendMessage = () => Promise.resolve(response);
297
            install_view.show(...arguments);
298
            ''',
299
            'https://hydril.la/', 'mapping', 'somemapping', [2, 1])
300

  
301
        assert_dlg(['conf_buts'],
302
                   'Mapping somemapping-2.1 was served using unsupported Hydrilla API version (99.99). You might need to update Haketilo.')
303
    elif message == 'invalid_API_version':
304
        execute_in_page(
305
            '''
306
            const response = {
307
                ok: true,
308
                status: 200,
309
                /* API version here is not an array as it should be. */
310
                json: {api_schema_version: 123}
311
            };
312
            browser.tabs.sendMessage = () => Promise.resolve(response);
313
            install_view.show(...arguments);
314
            ''',
315
            'https://hydril.la/', 'resource', 'someresource')
316

  
317
        assert_dlg(['conf_buts'],
318
                   'Resource someresource was served using unsupported Hydrilla API version. You might need to update Haketilo.')
319
    elif message == 'indexeddb_error_item':
320
        execute_in_page(
321
            '''
322
            haketilodb.idb_get = () => {throw "some error";};
323
            install_view.show(...arguments);
324
            ''',
325
            'https://hydril.la/', 'mapping', 'mapping_a')
326

  
327
        assert_dlg(['conf_buts'],
328
                   "Error accessing Haketilo's internal database :(")
329
    elif message == 'installing':
330
        execute_in_page(
331
            '''
332
            haketilodb.save_items = () => new Promise(() => {});
333
            returnval(install_view.show(...arguments));
334
            ''',
335
            'https://hydril.la/', 'mapping', 'mapping_b')
336

  
337
        execute_in_page('returnval(install_view.install_but);').click()
338

  
339
        assert dlg_buts() == []
340
        assert dialog_txt() == 'Installing...'
341
    elif message == 'indexeddb_error_file_uses':
342
        execute_in_page(
343
            '''
344
            const old_idb_get = haketilodb.idb_get;
345
            haketilodb.idb_get = function(transaction, store_name, identifier) {
346
                if (store_name === "file_uses")
347
                    throw "some error";
348
                return old_idb_get(...arguments);
349
            }
350
            returnval(install_view.show(...arguments));
351
            ''',
352
            'https://hydril.la/', 'mapping', 'mapping_b')
353

  
354
        execute_in_page('returnval(install_view.install_but);').click()
355

  
356
        assert_dlg(['conf_buts'],
357
                   "Error accessing Haketilo's internal database :(")
358
    elif message == 'failure_to_communicate_fetch':
359
        execute_in_page(
360
            '''
361
            fetch = () => {throw "some error";};
362
            returnval(install_view.show(...arguments));
363
            ''',
364
            'https://hydril.la/', 'mapping', 'mapping_b')
365

  
366
        execute_in_page('returnval(install_view.install_but);').click()
367

  
368
        assert_dlg(['conf_buts'],
369
                   'Failure to communicate with repository :(')
370
    elif message == 'HTTP_code_file':
371
        execute_in_page(
372
            '''
373
            fetch = () => Promise.resolve({ok: false, status: 400});
374
            returnval(install_view.show(...arguments));
375
            ''',
376
            'https://hydril.la/', 'mapping', 'mapping_b')
377

  
378
        execute_in_page('returnval(install_view.install_but);').click()
379

  
... This diff was truncated because it exceeds the maximum size that can be displayed.

Also available in: Unified diff