/** * 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 . * * 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