fix(csp): replace inline on* handlers with data-on* + JS wiring
Lint & Test / test (push) Successful in 38s

The strict `script-src 'self'` CSP blocks inline onclick/onchange/oninput/
onsubmit attribute evaluation, breaking every button and form in the UI.

- Rename all 53 inline handler attributes in index.html to data-on*
- Add wireInlineHandlers() in app.js that parses each data-on* expression
  on DOMContentLoaded and attaches a proper addEventListener calling the
  matching window-global function. Supports no-arg, string/number/bool/null
  literals, and the `event` token.

CSP stays strict; no unsafe-inline or unsafe-hashes needed.
This commit is contained in:
2026-05-16 18:35:51 +03:00
parent bcc6d40ed7
commit eaeebb64cd
2 changed files with 115 additions and 53 deletions
+62
View File
@@ -159,10 +159,72 @@ HTMLDialogElement.prototype.showModal = function (...args) {
return result;
};
// CSP-safe replacement for inline on* handlers. HTML uses data-onclick,
// data-onchange, data-oninput, data-onsubmit with simple call expressions
// like "fn()", "fn('arg')", "fn(event)". We parse those at startup and
// attach proper addEventListener calls so script-src 'self' stays strict.
const INLINE_HANDLER_EVENTS = {
'data-onclick': 'click',
'data-onchange': 'change',
'data-oninput': 'input',
'data-onsubmit': 'submit',
};
function parseInlineHandlerArg(token) {
const t = token.trim();
if (t === '') return { kind: 'empty' };
if (t === 'event') return { kind: 'event' };
if (t === 'true') return { kind: 'literal', value: true };
if (t === 'false') return { kind: 'literal', value: false };
if (t === 'null') return { kind: 'literal', value: null };
if (/^-?\d+(\.\d+)?$/.test(t)) return { kind: 'literal', value: Number(t) };
if ((t.startsWith("'") && t.endsWith("'")) || (t.startsWith('"') && t.endsWith('"'))) {
return { kind: 'literal', value: t.slice(1, -1) };
}
console.warn('inline-handler: unsupported arg token', token);
return { kind: 'literal', value: undefined };
}
function compileInlineHandler(expr) {
const m = expr.match(/^\s*([A-Za-z_$][\w$]*)\s*\((.*)\)\s*;?\s*$/s);
if (!m) {
console.warn('inline-handler: unparsable expression', expr);
return null;
}
const fnName = m[1];
const argsRaw = m[2].trim();
const argTokens = argsRaw === '' ? [] : argsRaw.split(',').map(s => s.trim());
const parsedArgs = argTokens.map(parseInlineHandlerArg);
return function (event) {
const fn = window[fnName];
if (typeof fn !== 'function') {
console.error('inline-handler: missing global function', fnName);
return;
}
const args = parsedArgs.map(a => a.kind === 'event' ? event : a.value);
return fn.apply(this, args);
};
}
function wireInlineHandlers(root) {
for (const [attr, eventName] of Object.entries(INLINE_HANDLER_EVENTS)) {
const nodes = root.querySelectorAll(`[${attr}]`);
for (const el of nodes) {
const expr = el.getAttribute(attr);
const handler = compileInlineHandler(expr);
if (handler) el.addEventListener(eventName, handler);
el.removeAttribute(attr);
}
}
}
window.addEventListener('DOMContentLoaded', async () => {
// Cache DOM references
cacheDom();
// Wire CSP-safe inline-handler stand-ins from index.html
wireInlineHandlers(document);
// Initialize theme and accent color
initTheme();
initAccentColor();