/** * @fileoverview Shared utility functions: HTML escaping, HTML sanitization, and debounce. */ (function() { 'use strict'; /** * Escape a string for safe insertion into HTML. * Uses a detached DOM element to leverage the browser's own escaping. * @param {string} text - Raw text to escape. * @returns {string} HTML-safe string. */ function escapeHtml(text) { if (!text) return ''; const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } /** Set of element tag names that are stripped during sanitization. */ const DANGEROUS_ELEMENTS = new Set([ 'script', 'iframe', 'object', 'embed', 'form', 'style', 'base', 'svg', 'math', 'meta', 'link', ]); /** * Sanitize an HTML string for safe rendering of rich content. * * Allows common formatting tags (p, a, img, strong, em, etc.) while * stripping dangerous elements (script, iframe, form, ...) and dangerous * attributes (on* event handlers, javascript: URLs). * * Uses the browser's own HTML parser via a detached DOM element so the * result is always well-formed HTML. * * @param {string} html - Raw HTML string (e.g. from an RSS body field). * @returns {string} Sanitized HTML safe for innerHTML insertion. */ function sanitizeHtml(html) { if (!html) return ''; const container = document.createElement('div'); container.innerHTML = html; /** Recursively walk the DOM tree and strip dangerous nodes/attributes. */ function clean(node) { // Iterate children in reverse so removals don't shift indices. const children = Array.from(node.children); for (let i = children.length - 1; i >= 0; i--) { const child = children[i]; if (DANGEROUS_ELEMENTS.has(child.tagName.toLowerCase())) { child.remove(); } else { cleanAttributes(child); clean(child); } } } /** Remove dangerous attributes from a single element. */ function cleanAttributes(el) { // Collect names first; removing during iteration can skip entries. const attrNames = Array.from(el.attributes).map(a => a.name); for (const name of attrNames) { const lower = name.toLowerCase(); // Strip all on* event-handler attributes. if (lower.startsWith('on')) { el.removeAttribute(name); continue; } // Strip formaction (can override form action URL on buttons/inputs). if (lower === 'formaction') { el.removeAttribute(name); continue; } // Strip dangerous URL schemes in href and src. // Normalize by removing whitespace/control chars to defeat obfuscation. if (lower === 'href' || lower === 'src') { const value = (el.getAttribute(name) || '').replace(/[\s\x00-\x1f]/g, '').toLowerCase(); if (value.startsWith('javascript:') || value.startsWith('data:') || value.startsWith('vbscript:')) { el.removeAttribute(name); } } } } clean(container); return container.innerHTML; } /** * Escape a string for safe insertion into an HTML attribute. * Handles backslashes, single quotes, and double quotes. * @param {string} str - Raw string to escape. * @returns {string} Attribute-safe string. */ function escapeAttr(str) { if (!str) return ''; return String(str).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"').replace(/'/g, '''); } /** * Debounce a function: delays execution until `delay` ms after the last call. * @param {function} fn - Function to debounce. * @param {number} delay - Delay in milliseconds. * @returns {function} Debounced wrapper. */ function debounce(fn, delay) { let timeout; return (...args) => { clearTimeout(timeout); timeout = setTimeout(() => fn(...args), delay); }; } /** * Extract a human-readable message from a Tauri API error. * Tauri commands that return Result reject with the serialized * ApiError object ({code, message}), not a plain string. * @param {any} err - Error from a rejected invoke() promise. * @returns {string} The error message string. */ function getErrorMessage(err) { if (!err) return 'Unknown error'; if (typeof err === 'string') return err; if (err.message) return err.message; if (typeof err === 'object') { try { return JSON.stringify(err); } catch (_) { /* fall through */ } } return String(err); } BB.utils = { escapeHtml, escapeAttr, sanitizeHtml, debounce, getErrorMessage }; })();