fix(csp): replace inline on* handlers with data-on* + JS wiring
Lint & Test / test (push) Successful in 38s
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:
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user