fix(ui): jsAttrEsc for inline onclick handlers — apostrophe in product_name broke SKU mapping modal
HTML parser decodes ' back to ' inside onclick="..." before the JS parser
runs, so esc() left inline handlers vulnerable: product names containing an
apostrophe terminated the JS string literal ("missing ) after argument list").
New jsAttrEsc() escapes for JS-string-inside-HTML-attribute (\\, ', \n first;
then &, ", <, >). Applied to all inline onclick sites that interpolate
user-controlled sku/product_name/codmat: shared.js detail modal (lines
879/939), missing_skus.html (4 sites), mappings.js (3 sites).
Cache-bust: shared.js v49→50, mappings.js v17→18.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -33,6 +33,23 @@ function esc(s) {
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
// JS-string-inside-HTML-attribute escape.
|
||||
// HTML parser decodes entities (incl. ') BEFORE the JS parser runs the inline
|
||||
// handler, so esc() is unsafe for `onclick="fn('${...}')"`. This escapes for the
|
||||
// JS string layer (\\, \') first, then HTML-escapes the dangerous attribute chars
|
||||
// (&, ", <, >) — leaving apostrophes literal so JS sees \' after attribute decoding.
|
||||
function jsAttrEsc(s) {
|
||||
if (s == null) return '';
|
||||
return String(s)
|
||||
.replace(/\\/g, '\\\\')
|
||||
.replace(/'/g, "\\'")
|
||||
.replace(/\r?\n/g, '\\n')
|
||||
.replace(/&/g, '&')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>');
|
||||
}
|
||||
|
||||
// ── Date formatting ───────────────────────────────
|
||||
function fmtDate(dateStr, includeSeconds) {
|
||||
if (!dateStr) return '-';
|
||||
@@ -876,7 +893,7 @@ async function renderOrderDetailModal(orderNumber, opts) {
|
||||
? item.codmat_details.map(d => `<code>${esc(d.codmat)}</code>${d.direct ? ' <span class="badge bg-secondary" style="font-size:0.55rem">direct</span>' : ''}`).join(' ')
|
||||
: `<code>${esc(item.codmat || '–')}</code>`;
|
||||
const valoare = (Number(item.price || 0) * Number(item.quantity || 0));
|
||||
const clickAttr = opts.onQuickMap ? `onclick="_sharedModalQuickMap('${esc(item.sku)}','${esc(item.product_name||'')}','${esc(orderNumber)}',${idx})"` : '';
|
||||
const clickAttr = opts.onQuickMap ? `onclick="_sharedModalQuickMap('${jsAttrEsc(item.sku)}','${jsAttrEsc(item.product_name||'')}','${jsAttrEsc(orderNumber)}',${idx})"` : '';
|
||||
return `<div class="dif-item">
|
||||
<div class="dif-row">
|
||||
<span class="dif-sku${opts.onQuickMap ? ' dif-codmat-link' : ''}" ${clickAttr}>${esc(item.sku)}</span>
|
||||
@@ -936,7 +953,7 @@ async function renderOrderDetailModal(orderNumber, opts) {
|
||||
|
||||
// Desktop items table
|
||||
const clickAttrFn = (item, idx) => opts.onQuickMap
|
||||
? `onclick="_sharedModalQuickMap('${esc(item.sku)}', '${esc(item.product_name || '')}', '${esc(orderNumber)}', ${idx})" title="Click pentru mapare"`
|
||||
? `onclick="_sharedModalQuickMap('${jsAttrEsc(item.sku)}', '${jsAttrEsc(item.product_name || '')}', '${jsAttrEsc(orderNumber)}', ${idx})" title="Click pentru mapare"`
|
||||
: '';
|
||||
|
||||
let tableHtml = items.map((item, idx) => {
|
||||
|
||||
Reference in New Issue
Block a user