From 93610213e393e8c8fcc121fc8fb572b9e84c3a88 Mon Sep 17 00:00:00 2001 From: Max Berger Date: Sat, 7 Mar 2026 23:57:44 +0100 Subject: [PATCH] Allow only valid hrefs in newShare for map --- .../CreateEditCollectionScene.js | 17 +- radicale/web/internal_data/NewShareScene.js | 3 + .../internal_data/UploadCollectionScene.js | 337 +++++++++--------- radicale/web/internal_data/utils.js | 54 +-- 4 files changed, 212 insertions(+), 199 deletions(-) diff --git a/radicale/web/internal_data/CreateEditCollectionScene.js b/radicale/web/internal_data/CreateEditCollectionScene.js index 903c7e13..62d268ce 100644 --- a/radicale/web/internal_data/CreateEditCollectionScene.js +++ b/radicale/web/internal_data/CreateEditCollectionScene.js @@ -23,18 +23,19 @@ import { create_collection, edit_collection } from "./api.js"; import { COLOR_RE } from "./constants.js"; import { Collection, CollectionType } from "./models.js"; import { Scene, pop_scene, push_scene, scene_stack } from "./scene_manager.js"; -import { cleanHREFinput, isValidHREF, random_hex, random_uuid } from "./utils.js"; +import { cleanHREFinput, isValidHREF, onCleanHREFinput, random_hex, random_uuid } from "./utils.js"; /** - * @constructor * @implements {Scene} - * @param {string} user - * @param {string} password - * @param {Collection} collection if it's a principal collection, a new - * collection will be created inside of it. - * Otherwise the collection will be edited. */ export class CreateEditCollectionScene { + /** + * @param {string} user + * @param {string} password + * @param {Collection} collection if it's a principal collection, a new + * collection will be created inside of it. + * Otherwise the collection will be edited. + */ constructor(user, password, collection) { let edit = collection.type !== CollectionType.PRINCIPAL; let html_scene = document.getElementById(edit ? "editcollectionscene" : "createcollectionscene"); @@ -69,7 +70,7 @@ export class CreateEditCollectionScene { let color = edit && collection.color ? collection.color : "#" + random_hex(6); if (!edit) { - href_form.addEventListener("keydown", cleanHREFinput); + href_form.addEventListener("input", onCleanHREFinput); } function remove_invalid_types() { diff --git a/radicale/web/internal_data/NewShareScene.js b/radicale/web/internal_data/NewShareScene.js index b9819a21..7672885f 100644 --- a/radicale/web/internal_data/NewShareScene.js +++ b/radicale/web/internal_data/NewShareScene.js @@ -20,6 +20,7 @@ import { add_share_by_map, add_share_by_token } from "./api.js"; import { Scene, pop_scene, scene_stack } from "./scene_manager.js"; +import { onCleanHREFinput } from "./utils.js"; /** * @implements {Scene} @@ -45,6 +46,8 @@ export class NewShareScene { /** @type {HTMLInputElement} */ let properties_input = html_scene.querySelector("[data-name=properties]"); /** @type {HTMLElement} */ let cancel_btn = html_scene.querySelector("[data-name=cancel]"); + sharehref_input.addEventListener("input", onCleanHREFinput); + /** @type {?number} */ let scene_index = null; function oncancel() { diff --git a/radicale/web/internal_data/UploadCollectionScene.js b/radicale/web/internal_data/UploadCollectionScene.js index 2704f461..fcaf612f 100644 --- a/radicale/web/internal_data/UploadCollectionScene.js +++ b/radicale/web/internal_data/UploadCollectionScene.js @@ -20,195 +20,198 @@ import { Scene, pop_scene, scene_stack } from "./scene_manager.js"; import { Collection } from "./models.js"; -import { cleanHREFinput, isValidHREF, random_uuid } from "./utils.js"; +import { cleanHREFinput, isValidHREF, onCleanHREFinput, random_uuid } from "./utils.js"; import { upload_collection } from "./api.js"; /** - * @constructor * @implements {Scene} - * @param {string} user - * @param {string} password - * @param {Collection} collection parent collection */ -export function UploadCollectionScene(user, password, collection) { - /** @type {HTMLElement} */ let html_scene = document.getElementById("uploadcollectionscene"); - /** @type {HTMLElement} */ let template = html_scene.querySelector("[data-name=filetemplate]"); - /** @type {HTMLElement} */ let upload_btn = html_scene.querySelector("[data-name=submit]"); - /** @type {HTMLElement} */ let close_btn = html_scene.querySelector("[data-name=close]"); - /** @type {HTMLInputElement} */ let uploadfile_form = html_scene.querySelector("[data-name=uploadfile]"); - /** @type {HTMLElement} */ let uploadfile_lbl = html_scene.querySelector("label[for=uploadfile]"); - /** @type {HTMLInputElement} */ let href_form = html_scene.querySelector("[data-name=href]"); - /** @type {HTMLElement} */ let href_label = html_scene.querySelector("label[for=href]"); - /** @type {HTMLElement} */ let hreflimitmsg_html = html_scene.querySelector("[data-name=hreflimitmsg]"); - /** @type {HTMLElement} */ let pending_html = html_scene.querySelector("[data-name=pending]"); +export class UploadCollectionScene { + /** + * @param {string} user + * @param {string} password + * @param {Collection} collection parent collection + */ + constructor(user, password, collection) { + /** @type {HTMLElement} */ let html_scene = document.getElementById("uploadcollectionscene"); + /** @type {HTMLElement} */ let template = html_scene.querySelector("[data-name=filetemplate]"); + /** @type {HTMLElement} */ let upload_btn = html_scene.querySelector("[data-name=submit]"); + /** @type {HTMLElement} */ let close_btn = html_scene.querySelector("[data-name=close]"); + /** @type {HTMLInputElement} */ let uploadfile_form = html_scene.querySelector("[data-name=uploadfile]"); + /** @type {HTMLElement} */ let uploadfile_lbl = html_scene.querySelector("label[for=uploadfile]"); + /** @type {HTMLInputElement} */ let href_form = html_scene.querySelector("[data-name=href]"); + /** @type {HTMLElement} */ let href_label = html_scene.querySelector("label[for=href]"); + /** @type {HTMLElement} */ let hreflimitmsg_html = html_scene.querySelector("[data-name=hreflimitmsg]"); + /** @type {HTMLElement} */ let pending_html = html_scene.querySelector("[data-name=pending]"); - let files = uploadfile_form.files; - href_form.addEventListener("keydown", cleanHREFinput); - upload_btn.onclick = upload_start; - uploadfile_form.onchange = onfileschange; + let files = uploadfile_form.files; + href_form.addEventListener("input", onCleanHREFinput); + upload_btn.onclick = upload_start; + uploadfile_form.onchange = onfileschange; - href_form.value = ""; - let href = ""; + href_form.value = ""; + let href = ""; - /** @type {?number} */ let scene_index = null; - /** @type {?XMLHttpRequest} */ let upload_req = null; - /** @type {Array} */ let results = []; - /** @type {?Array} */ let nodes = null; + /** @type {?number} */ let scene_index = null; + /** @type {?XMLHttpRequest} */ let upload_req = null; + /** @type {Array} */ let results = []; + /** @type {?Array} */ let nodes = null; - function upload_start() { - try { - if(!read_form()){ + function upload_start() { + try { + if (!read_form()) { + return false; + } + uploadfile_form.classList.add("hidden"); + uploadfile_lbl.classList.add("hidden"); + href_form.classList.add("hidden"); + href_label.classList.add("hidden"); + hreflimitmsg_html.classList.add("hidden"); + upload_btn.classList.add("hidden"); + close_btn.classList.add("hidden"); + + pending_html.classList.remove("hidden"); + + nodes = []; + for (let i = 0; i < files.length; i++) { + let file = files[i]; + let node = /** @type {HTMLElement} */ (template.cloneNode(true)); + node.classList.remove("hidden"); + let name_form = node.querySelector("[data-name=name]"); + name_form.textContent = file.name; + node.classList.remove("hidden"); + nodes.push(node); + updateFileStatus(i); + template.parentNode.insertBefore(node, template); + } + upload_next(); + } catch (err) { + console.error(err); + } + return false; + } + + function upload_next() { + try { + if (files.length === results.length) { + pending_html.classList.add("hidden"); + close_btn.classList.remove("hidden"); + return; + } else { + let file = files[results.length]; + if (files.length > 1 || href.length == 0) { + href = random_uuid(); + } + let upload_href = collection.href + href + "/"; + upload_req = upload_collection(user, password, upload_href, file, function (result) { + upload_req = null; + results.push(result); + updateFileStatus(results.length - 1); + upload_next(); + }); + } + } catch (err) { + console.error(err); + } + } + + function onclose() { + try { + pop_scene(scene_index - 1); + } catch (err) { + console.error(err); + } + return false; + } + + function updateFileStatus(i) { + if (nodes === null) { + return; + } + let success_form = nodes[i].querySelector("[data-name=success]"); + let error_form = nodes[i].querySelector("[data-name=error]"); + if (results.length > i) { + if (results[i]) { + success_form.classList.add("hidden"); + error_form.textContent = "Error: " + results[i]; + error_form.classList.remove("hidden"); + } else { + success_form.classList.remove("hidden"); + error_form.classList.add("hidden"); + } + } else { + success_form.classList.add("hidden"); + error_form.classList.add("hidden"); + } + } + + function read_form() { + cleanHREFinput(href_form); + let newhreftxtvalue = href_form.value.trim().toLowerCase(); + if (!isValidHREF(newhreftxtvalue)) { + alert("You must enter a valid HREF"); return false; } - uploadfile_form.classList.add("hidden"); - uploadfile_lbl.classList.add("hidden"); - href_form.classList.add("hidden"); - href_label.classList.add("hidden"); - hreflimitmsg_html.classList.add("hidden"); - upload_btn.classList.add("hidden"); - close_btn.classList.add("hidden"); + href = newhreftxtvalue; - pending_html.classList.remove("hidden"); - - nodes = []; - for (let i = 0; i < files.length; i++) { - let file = files[i]; - /** @type {HTMLElement} */ let node = template.cloneNode(true); - node.classList.remove("hidden"); - let name_form = node.querySelector("[data-name=name]"); - name_form.textContent = file.name; - node.classList.remove("hidden"); - nodes.push(node); - updateFileStatus(i); - template.parentNode.insertBefore(node, template); + if (uploadfile_form.files.length == 0) { + alert("You must select at least one file to upload"); + return false; } - upload_next(); - } catch(err) { - console.error(err); + files = uploadfile_form.files; + return true; } - return false; - } - function upload_next(){ - try{ - if (files.length === results.length) { - pending_html.classList.add("hidden"); - close_btn.classList.remove("hidden"); - return; + function onfileschange() { + files = uploadfile_form.files; + if (files.length > 1) { + hreflimitmsg_html.classList.remove("hidden"); + href_form.classList.add("hidden"); + href_label.classList.add("hidden"); + href_form.value = random_uuid(); // dummy, will be replaced on upload } else { - let file = files[results.length]; - if(files.length > 1 || href.length == 0){ - href = random_uuid(); - } - let upload_href = collection.href + href + "/"; - upload_req = upload_collection(user, password, upload_href, file, function(result) { - upload_req = null; - results.push(result); - updateFileStatus(results.length - 1); - upload_next(); - }); + hreflimitmsg_html.classList.add("hidden"); + href_form.classList.remove("hidden"); + href_label.classList.remove("hidden"); + href_form.value = files[0].name.replace(/\.(ics|vcf)$/, ''); } - }catch(err){ - console.error(err); - } - } - - function onclose() { - try { - pop_scene(scene_index - 1); - } catch(err) { - console.error(err); - } - return false; - } - - function updateFileStatus(i) { - if (nodes === null) { - return; - } - let success_form = nodes[i].querySelector("[data-name=success]"); - let error_form = nodes[i].querySelector("[data-name=error]"); - if (results.length > i) { - if (results[i]) { - success_form.classList.add("hidden"); - error_form.textContent = "Error: " + results[i]; - error_form.classList.remove("hidden"); - } else { - success_form.classList.remove("hidden"); - error_form.classList.add("hidden"); - } - } else { - success_form.classList.add("hidden"); - error_form.classList.add("hidden"); - } - } - - function read_form() { - cleanHREFinput(href_form); - let newhreftxtvalue = href_form.value.trim().toLowerCase(); - if(!isValidHREF(newhreftxtvalue)){ - alert("You must enter a valid HREF"); return false; } - href = newhreftxtvalue; - if(uploadfile_form.files.length == 0){ - alert("You must select at least one file to upload"); - return false; - } - files = uploadfile_form.files; - return true; - } + this.show = function () { + scene_index = scene_stack.length - 1; + html_scene.classList.remove("hidden"); + close_btn.onclick = onclose; + }; - function onfileschange() { - files = uploadfile_form.files; - if(files.length > 1){ - hreflimitmsg_html.classList.remove("hidden"); - href_form.classList.add("hidden"); - href_label.classList.add("hidden"); - href_form.value = random_uuid(); // dummy, will be replaced on upload - }else{ - hreflimitmsg_html.classList.add("hidden"); + this.hide = function () { + html_scene.classList.add("hidden"); + close_btn.classList.remove("hidden"); + upload_btn.classList.remove("hidden"); + uploadfile_form.classList.remove("hidden"); + uploadfile_lbl.classList.remove("hidden"); href_form.classList.remove("hidden"); href_label.classList.remove("hidden"); - href_form.value = files[0].name.replace(/\.(ics|vcf)$/, ''); - } - return false; + hreflimitmsg_html.classList.add("hidden"); + pending_html.classList.add("hidden"); + close_btn.onclick = null; + upload_btn.onclick = null; + href_form.value = ""; + uploadfile_form.value = ""; + if (nodes == null) { + return; + } + nodes.forEach(function (node) { + node.parentNode.removeChild(node); + }); + nodes = null; + }; + this.release = function () { + scene_index = null; + if (upload_req !== null) { + upload_req.abort(); + upload_req = null; + } + }; } - - this.show = function() { - scene_index = scene_stack.length - 1; - html_scene.classList.remove("hidden"); - close_btn.onclick = onclose; - }; - - this.hide = function() { - html_scene.classList.add("hidden"); - close_btn.classList.remove("hidden"); - upload_btn.classList.remove("hidden"); - uploadfile_form.classList.remove("hidden"); - uploadfile_lbl.classList.remove("hidden"); - href_form.classList.remove("hidden"); - href_label.classList.remove("hidden"); - hreflimitmsg_html.classList.add("hidden"); - pending_html.classList.add("hidden"); - close_btn.onclick = null; - upload_btn.onclick = null; - href_form.value = ""; - uploadfile_form.value = ""; - if(nodes == null){ - return; - } - nodes.forEach(function(node) { - node.parentNode.removeChild(node); - }); - nodes = null; - }; - this.release = function() { - scene_index = null; - if (upload_req !== null) { - upload_req.abort(); - upload_req = null; - } - }; } \ No newline at end of file diff --git a/radicale/web/internal_data/utils.js b/radicale/web/internal_data/utils.js index ad709f55..0e5ed015 100644 --- a/radicale/web/internal_data/utils.js +++ b/radicale/web/internal_data/utils.js @@ -25,11 +25,11 @@ */ export function escape_xml(s) { return (s - .replace(/&/g, "&") - .replace(/"/g, """) - .replace(/'/g, "'") - .replace(//g, ">")); + .replace(/&/g, "&") + .replace(/"/g, """) + .replace(/'/g, "'") + .replace(//g, ">")); } /** @@ -47,19 +47,18 @@ export function random_uuid() { export function random_hex(length) { let bytes = new Uint8Array(Math.ceil(length / 2)); window.crypto.getRandomValues(bytes); - return bytes.reduce((s, b) => s + b.toString(16).padStart(2, "0"), "").substring(0, length); + // Fallback for compatibility with older browsers which may not have padStart + return bytes.reduce((s, b) => { + let hex = b.toString(16); + return s + (String.prototype["padStart"] ? hex["padStart"](2, "0") : ("0" + hex).slice(-2)); + }, "").substring(0, length); } /** * Removed invalid HREF characters for a collection HREF. - * - * @param a A valid Input element or an onchange Event of an Input element. + * @param {HTMLInputElement} href_form A valid Input element or an onchange Event of an Input element. */ -export function cleanHREFinput(a) { - let href_form = a; - if (a.target) { - href_form = a.target; - } +export function cleanHREFinput(href_form) { let currentTxtVal = href_form.value.trim().toLowerCase(); //Clean the HREF to remove not permitted chars currentTxtVal = currentTxtVal.replace(/(?![0-9a-z\-\_\.])./g, ''); @@ -68,11 +67,19 @@ export function cleanHREFinput(a) { href_form.value = currentTxtVal; } +/** + * Event listener for cleaning HREF input. + * @param {Event} event + */ +export function onCleanHREFinput(event) { + if (event.target instanceof HTMLInputElement) { + cleanHREFinput(event.target); + } +} + /** * Checks if a proposed HREF for a collection has a valid format and syntax. - * - * @param href String of the porposed HREF. - * + * @param {string} href String of the proposed HREF. * @return Boolean results if the HREF is valid. */ export function isValidHREF(href) { @@ -88,16 +95,15 @@ export function isValidHREF(href) { /** * Format bytes to human-readable text. - * - * @param bytes Number of bytes. - * + * @param {number} bytes Number of bytes. * @return Formatted string. */ -export function bytesToHumanReadable(bytes, dp=1) { - let isNumber = !isNaN(parseFloat(bytes)) && !isNaN(bytes - 0); - if(!isNumber){ +export function bytesToHumanReadable(bytes) { + if (isNaN(bytes - 0)) { return ""; } - var i = bytes == 0 ? 0 : Math.floor(Math.log(bytes) / Math.log(1024)); - return (bytes / Math.pow(1024, i)).toFixed(dp) * 1 + ' ' + ['b', 'kb', 'mb', 'gb', 'tb'][i]; + const units = ['b', 'kb', 'mb', 'gb', 'tb']; + let i = bytes == 0 ? 0 : Math.floor(Math.log(bytes) / Math.log(1024)); + i = Math.min(i, units.length - 1); + return (bytes / Math.pow(1024, i)) + ' ' + units[i]; } \ No newline at end of file