aboutsummaryrefslogtreecommitdiff
/**
 * This file is part of Haketilo.
 *
 * Function: Driving the site payload creation form.
 *
 * Copyright (C) 2022 Wojtek Kosior
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * As additional permission under GNU GPL version 3 section 7, you
 * may distribute forms of that code without the copy of the GNU
 * GPL normally required by section 4, provided you include this
 * license notice and, in case of non-source distribution, a URL
 * through which recipients can access the Corresponding Source.
 * If you modify file(s) with this exception, you may extend this
 * exception to your version of the file(s), but you are not
 * obligated to do so. If you do not wish to do so, delete this
 * exception statement from your version.
 *
 * As a special exception to the GPL, any HTML file which merely
 * makes function calls to this code, and for that purpose
 * includes it by reference shall be deemed a separate work for
 * copyright law purposes. If you modify this code, you may extend
 * this exception to your version of the code, but you are not
 * obligated to do so. If you do not wish to do so, delete this
 * exception statement from your version.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 *
 * I, Wojtek Kosior, thereby promise not to sue for violation of this file's
 * license. Although I request that you do not make use of this code in a
 * proprietary program, I am not going to enforce this in court.
 */

#IMPORT html/dialog.js
#IMPORT common/indexeddb.js AS haketilodb

#FROM html/DOM_helpers.js IMPORT clone_template
#FROM common/sha256.js    IMPORT sha256 AS compute_sha256
#FROM common/patterns.js  IMPORT validate_normalize_url_pattern, \
                                 patterns_doc_url

/* https://stackoverflow.com/questions/105034/how-to-create-a-guid-uuid */
/* This is a helper function used by uuidv4(). */
function uuid_replace_num(num)
{
    const randbyte = crypto.getRandomValues(new Uint8Array(1))[0];
    return (num ^ randbyte & 15 >> num / 4).toString(16);
}

/* Generate a new UUID. */
function uuidv4()
{
    return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, uuid_replace_num);
}

function collect_form_data(form_ctx)
{
    const identifier_nonprepended = form_ctx.identifier.value;
    if (!identifier_nonprepended)
	throw "The 'identifier' field is required!";
    if (!/[-a-zA-Z]+/.test(identifier_nonprepended))
	throw "Identifier may only contain digits 0-9, lowercase letters a-z and hyphens '-'!"
    const identifier = `local-${identifier_nonprepended}`;

    const long_name = form_ctx.long_name.value || identifier_nonprepended;

    const description = form_ctx.description.value;

    const url_patterns = form_ctx.patterns.value.split("\n").filter(i => i);
    if (url_patterns.length === 0)
	throw "The 'URL patterns' field is required!";

    const payloads = {};

    for (let pattern of url_patterns) {
	pattern = validate_normalize_url_pattern(pattern);

	if (pattern in payloads)
	    throw `Pattern '${pattern}' specified multiple times!`;

	payloads[pattern] = {identifier};
    }

    const script = form_ctx.script.value;
    if (!script)
	throw "The 'script' field is required!";
    const sha256 = compute_sha256(script);

    const resource = {
	$schema: "https://hydrilla.koszko.org/schemas/api_resource_description-1.0.1.schema.json",
	source_name: identifier,
	source_copyright: [],
	type: "resource",
	identifier,
	long_name,
	uuid: uuidv4(),
	version: [1],
	revision: 1,
	description,
	dependencies: [],
	scripts: [{file: "payload.js", sha256}]
    };

    const mapping = {
	$schema: "https://hydrilla.koszko.org/schemas/api_mapping_description-1.0.1.schema.json",
	source_name: identifier,
	source_copyright: [],
	type: "mapping",
	identifier,
	long_name,
	uuid: uuidv4(),
	version: [1],
	description,
	payloads
    };

    return {identifier, resource, mapping, files_by_sha256: {[sha256]: script}};
}

function clear_form(form_ctx)
{
    form_ctx.identifier.value = "";
    form_ctx.long_name.value = "";
    form_ctx.description.value = "";
    form_ctx.patterns.value = "https://example.com/***";
    form_ctx.script.value = `console.log("Hello, World!");`;
}

async function save_payload(saving)
{
    const db = await haketilodb.get();
    const tx_starter = haketilodb.start_items_transaction;
    const files = {sha256: saving.files_by_sha256};
    const tx_ctx = await tx_starter(["resource", "mapping"], files);

    for (const type of ["resource", "mapping"]) {
	if (!saving[`override_${type}`] &&
	    (await haketilodb.idb_get(tx_ctx.transaction, type,
				      saving.identifier))) {
	    saving.ask_override = type;
	    return;
	}
    }

    await haketilodb.save_item(saving.resource, tx_ctx);
    await haketilodb.save_item(saving.mapping,  tx_ctx);

    return haketilodb.finalize_transaction(tx_ctx);
}

function override_question(saving)
{
    return saving.ask_override === "resource" ?
	`Resource '${saving.identifier}' already exists. Override?` :
	`Mapping '${saving.identifier}' already exists. Override?`;
}

async function create_clicked(form_ctx)
{
    if (form_ctx.dialog_ctx.shown)
	return;

    try {
	var saving = collect_form_data(form_ctx);
    } catch(e) {
	dialog.error(form_ctx.dialog_ctx, e);
	return;
    }

    dialog.loader(form_ctx.dialog_ctx, "Saving payload...");

    try {
	do {
	    if (saving.ask_override) {
		const override_prom = dialog.ask(form_ctx.dialog_ctx,
						 override_question(saving));
		dialog.loader(form_ctx.dialog_ctx, "Saving payload...");
		dialog.close(form_ctx.dialog_ctx);
		if (!(await override_prom))
		    throw "Saving would override existing data.";

		saving[`override_${saving.ask_override}`] = true;
		delete saving.ask_override;
	    }

	    await save_payload(saving);
	} while (saving.ask_override);

	dialog.info(form_ctx.dialog_ctx, "Successfully saved payload!");
	clear_form(form_ctx);
    } catch(e) {
	console.error("Haketilo:", e);
	dialog.error(form_ctx.dialog_ctx, "Failed to save payload :(");
    }

    dialog.close(form_ctx.dialog_ctx);
}

function on_show_hide(form_ctx, what_to_show)
{
    for (const item_id of ["form", "dialog"]) {
	const action = item_id === what_to_show ? "remove" : "add";
	form_ctx[`${item_id}_container`].classList[action]("hide");
    }
}

function payload_create_form()
{
    const form_ctx = clone_template("payload_create");

    form_ctx.dialog_ctx = dialog.make(() => on_show_hide(form_ctx, "dialog"),
				      () => on_show_hide(form_ctx, "form"));
    form_ctx.dialog_container.prepend(form_ctx.dialog_ctx.main_div);

    form_ctx.patterns_link.href = patterns_doc_url;
    form_ctx.create_but.addEventListener("click",
					 () => create_clicked(form_ctx));

    return form_ctx;
}
#EXPORT payload_create_form