| 1 | /**
 | 
  
    | 2 |  * This file is part of Haketilo.
 | 
  
    | 3 |  *
 | 
  
    | 4 |  * Function: Driving the site payload creation form.
 | 
  
    | 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 html/dialog.js
 | 
  
    | 45 | #IMPORT common/indexeddb.js AS haketilodb
 | 
  
    | 46 | 
 | 
  
    | 47 | #FROM html/DOM_helpers.js IMPORT clone_template
 | 
  
    | 48 | #FROM common/sha256.js    IMPORT sha256 AS compute_sha256
 | 
  
    | 49 | #FROM common/patterns.js  IMPORT validate_normalize_url_pattern, \
 | 
  
    | 50 |                                  patterns_doc_url
 | 
  
    | 51 | 
 | 
  
    | 52 | /* https://stackoverflow.com/questions/105034/how-to-create-a-guid-uuid */
 | 
  
    | 53 | /* This is a helper function used by uuidv4(). */
 | 
  
    | 54 | function uuid_replace_num(num)
 | 
  
    | 55 | {
 | 
  
    | 56 |     const randbyte = crypto.getRandomValues(new Uint8Array(1))[0];
 | 
  
    | 57 |     return (num ^ randbyte & 15 >> num / 4).toString(16);
 | 
  
    | 58 | }
 | 
  
    | 59 | 
 | 
  
    | 60 | /* Generate a new UUID. */
 | 
  
    | 61 | function uuidv4()
 | 
  
    | 62 | {
 | 
  
    | 63 |     return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, uuid_replace_num);
 | 
  
    | 64 | }
 | 
  
    | 65 | 
 | 
  
    | 66 | function collect_form_data(form_ctx)
 | 
  
    | 67 | {
 | 
  
    | 68 |     const identifier_nonprepended = form_ctx.identifier.value;
 | 
  
    | 69 |     if (!identifier_nonprepended)
 | 
  
    | 70 | 	throw "The 'identifier' field is required!";
 | 
  
    | 71 |     if (!/[-a-zA-Z]+/.test(identifier_nonprepended))
 | 
  
    | 72 | 	throw "Identifier may only contain digits 0-9, lowercase letters a-z and hyphens '-'!"
 | 
  
    | 73 |     const identifier = `local-${identifier_nonprepended}`;
 | 
  
    | 74 | 
 | 
  
    | 75 |     const long_name = form_ctx.long_name.value || identifier_nonprepended;
 | 
  
    | 76 | 
 | 
  
    | 77 |     const description = form_ctx.description.value;
 | 
  
    | 78 | 
 | 
  
    | 79 |     const url_patterns = form_ctx.patterns.value.split("\n").filter(i => i);
 | 
  
    | 80 |     if (url_patterns.length === 0)
 | 
  
    | 81 | 	throw "The 'URL patterns' field is required!";
 | 
  
    | 82 | 
 | 
  
    | 83 |     const payloads = {};
 | 
  
    | 84 | 
 | 
  
    | 85 |     for (let pattern of url_patterns) {
 | 
  
    | 86 | 	pattern = validate_normalize_url_pattern(pattern);
 | 
  
    | 87 | 
 | 
  
    | 88 | 	if (pattern in payloads)
 | 
  
    | 89 | 	    throw `Pattern '${pattern}' specified multiple times!`;
 | 
  
    | 90 | 
 | 
  
    | 91 | 	payloads[pattern] = {identifier};
 | 
  
    | 92 |     }
 | 
  
    | 93 | 
 | 
  
    | 94 |     const script = form_ctx.script.value;
 | 
  
    | 95 |     if (!script)
 | 
  
    | 96 | 	throw "The 'script' field is required!";
 | 
  
    | 97 |     const sha256 = compute_sha256(script);
 | 
  
    | 98 | 
 | 
  
    | 99 |     const resource = {
 | 
  
    | 100 | 	$schema: "https://hydrilla.koszko.org/schemas/api_resource_description-1.0.1.schema.json",
 | 
  
    | 101 | 	source_name: identifier,
 | 
  
    | 102 | 	source_copyright: [],
 | 
  
    | 103 | 	type: "resource",
 | 
  
    | 104 | 	identifier,
 | 
  
    | 105 | 	long_name,
 | 
  
    | 106 | 	uuid: uuidv4(),
 | 
  
    | 107 | 	version: [1],
 | 
  
    | 108 | 	revision: 1,
 | 
  
    | 109 | 	description,
 | 
  
    | 110 | 	dependencies: [],
 | 
  
    | 111 | 	scripts: [{file: "payload.js", sha256}]
 | 
  
    | 112 |     };
 | 
  
    | 113 | 
 | 
  
    | 114 |     const mapping = {
 | 
  
    | 115 | 	$schema: "https://hydrilla.koszko.org/schemas/api_mapping_description-1.0.1.schema.json",
 | 
  
    | 116 | 	source_name: identifier,
 | 
  
    | 117 | 	source_copyright: [],
 | 
  
    | 118 | 	type: "mapping",
 | 
  
    | 119 | 	identifier,
 | 
  
    | 120 | 	long_name,
 | 
  
    | 121 | 	uuid: uuidv4(),
 | 
  
    | 122 | 	version: [1],
 | 
  
    | 123 | 	description,
 | 
  
    | 124 | 	payloads
 | 
  
    | 125 |     };
 | 
  
    | 126 | 
 | 
  
    | 127 |     return {identifier, resource, mapping, files_by_sha256: {[sha256]: script}};
 | 
  
    | 128 | }
 | 
  
    | 129 | 
 | 
  
    | 130 | function clear_form(form_ctx)
 | 
  
    | 131 | {
 | 
  
    | 132 |     form_ctx.identifier.value = "";
 | 
  
    | 133 |     form_ctx.long_name.value = "";
 | 
  
    | 134 |     form_ctx.description.value = "";
 | 
  
    | 135 |     form_ctx.patterns.value = "https://example.com/***";
 | 
  
    | 136 |     form_ctx.script.value = `console.log("Hello, World!");`;
 | 
  
    | 137 | }
 | 
  
    | 138 | 
 | 
  
    | 139 | async function save_payload(saving)
 | 
  
    | 140 | {
 | 
  
    | 141 |     const db = await haketilodb.get();
 | 
  
    | 142 |     const tx_starter = haketilodb.start_items_transaction;
 | 
  
    | 143 |     const files = {sha256: saving.files_by_sha256};
 | 
  
    | 144 |     const tx_ctx = await tx_starter(["resource", "mapping"], files);
 | 
  
    | 145 | 
 | 
  
    | 146 |     for (const type of ["resource", "mapping"]) {
 | 
  
    | 147 | 	if (!saving[`override_${type}`] &&
 | 
  
    | 148 | 	    (await haketilodb.idb_get(tx_ctx.transaction, type,
 | 
  
    | 149 | 				      saving.identifier))) {
 | 
  
    | 150 | 	    saving.ask_override = type;
 | 
  
    | 151 | 	    return;
 | 
  
    | 152 | 	}
 | 
  
    | 153 |     }
 | 
  
    | 154 | 
 | 
  
    | 155 |     await haketilodb.save_item(saving.resource, tx_ctx);
 | 
  
    | 156 |     await haketilodb.save_item(saving.mapping,  tx_ctx);
 | 
  
    | 157 | 
 | 
  
    | 158 |     return haketilodb.finalize_transaction(tx_ctx);
 | 
  
    | 159 | }
 | 
  
    | 160 | 
 | 
  
    | 161 | function override_question(saving)
 | 
  
    | 162 | {
 | 
  
    | 163 |     return saving.ask_override === "resource" ?
 | 
  
    | 164 | 	`Resource '${saving.identifier}' already exists. Override?` :
 | 
  
    | 165 | 	`Mapping '${saving.identifier}' already exists. Override?`;
 | 
  
    | 166 | }
 | 
  
    | 167 | 
 | 
  
    | 168 | async function create_clicked(form_ctx)
 | 
  
    | 169 | {
 | 
  
    | 170 |     if (form_ctx.dialog_ctx.shown)
 | 
  
    | 171 | 	return;
 | 
  
    | 172 | 
 | 
  
    | 173 |     try {
 | 
  
    | 174 | 	var saving = collect_form_data(form_ctx);
 | 
  
    | 175 |     } catch(e) {
 | 
  
    | 176 | 	dialog.error(form_ctx.dialog_ctx, e);
 | 
  
    | 177 | 	return;
 | 
  
    | 178 |     }
 | 
  
    | 179 | 
 | 
  
    | 180 |     dialog.loader(form_ctx.dialog_ctx, "Saving payload...");
 | 
  
    | 181 | 
 | 
  
    | 182 |     try {
 | 
  
    | 183 | 	do {
 | 
  
    | 184 | 	    if (saving.ask_override) {
 | 
  
    | 185 | 		const override_prom = dialog.ask(form_ctx.dialog_ctx,
 | 
  
    | 186 | 						 override_question(saving));
 | 
  
    | 187 | 		dialog.loader(form_ctx.dialog_ctx, "Saving payload...");
 | 
  
    | 188 | 		dialog.close(form_ctx.dialog_ctx);
 | 
  
    | 189 | 		if (!(await override_prom))
 | 
  
    | 190 | 		    throw "Saving would override existing data.";
 | 
  
    | 191 | 
 | 
  
    | 192 | 		saving[`override_${saving.ask_override}`] = true;
 | 
  
    | 193 | 		delete saving.ask_override;
 | 
  
    | 194 | 	    }
 | 
  
    | 195 | 
 | 
  
    | 196 | 	    await save_payload(saving);
 | 
  
    | 197 | 	} while (saving.ask_override);
 | 
  
    | 198 | 
 | 
  
    | 199 | 	dialog.info(form_ctx.dialog_ctx, "Successfully saved payload!");
 | 
  
    | 200 | 	clear_form(form_ctx);
 | 
  
    | 201 |     } catch(e) {
 | 
  
    | 202 | 	console.error(e);
 | 
  
    | 203 | 	dialog.error(form_ctx.dialog_ctx, "Failed to save payload :(");
 | 
  
    | 204 |     }
 | 
  
    | 205 | 
 | 
  
    | 206 |     dialog.close(form_ctx.dialog_ctx);
 | 
  
    | 207 | }
 | 
  
    | 208 | 
 | 
  
    | 209 | function on_show_hide(form_ctx, what_to_show)
 | 
  
    | 210 | {
 | 
  
    | 211 |     for (const item_id of ["form", "dialog"]) {
 | 
  
    | 212 | 	const action = item_id === what_to_show ? "remove" : "add";
 | 
  
    | 213 | 	form_ctx[`${item_id}_container`].classList[action]("hide");
 | 
  
    | 214 |     }
 | 
  
    | 215 | }
 | 
  
    | 216 | 
 | 
  
    | 217 | function payload_create_form()
 | 
  
    | 218 | {
 | 
  
    | 219 |     const form_ctx = clone_template("payload_create");
 | 
  
    | 220 | 
 | 
  
    | 221 |     form_ctx.dialog_ctx = dialog.make(() => on_show_hide(form_ctx, "dialog"),
 | 
  
    | 222 | 				      () => on_show_hide(form_ctx, "form"));
 | 
  
    | 223 |     form_ctx.dialog_container.prepend(form_ctx.dialog_ctx.main_div);
 | 
  
    | 224 | 
 | 
  
    | 225 |     form_ctx.patterns_link.href = patterns_doc_url;
 | 
  
    | 226 |     form_ctx.create_but.addEventListener("click",
 | 
  
    | 227 | 					 () => create_clicked(form_ctx));
 | 
  
    | 228 | 
 | 
  
    | 229 |     return form_ctx;
 | 
  
    | 230 | }
 | 
  
    | 231 | #EXPORT payload_create_form
 |