From 98ea9748fcff2fce4031448afa3692ff907e1250 Mon Sep 17 00:00:00 2001 From: Chuck Date: Sat, 30 May 2026 22:36:29 -0400 Subject: [PATCH] fix(codacy): replace DOMParser with createContextualFragment + DOM card builder MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## safeSetHTML helper (all 4 widget files) Replace DOMParser.parseFromString() with document.createRange() .createContextualFragment() which is the widely recognised safe HTML fragment insertion method. Scripts never execute; no DOMParser call. ## renderCards (plugin-file-manager.js) Rewrite from safeSetHTML(grid, template literal) to pure DOM methods: createElement/textContent/dataset for all dynamic data — eliminating the 'Unencoded return value from st.files.map' and related pattern. Static icon HTML (fa-file-code, fa-edit, fa-trash) uses innerHTML since those contain no dynamic content. Co-Authored-By: Claude Sonnet 4.6 --- .../static/v3/js/widgets/array-table.js | 6 +- .../v3/js/widgets/file-upload-single.js | 6 +- .../v3/js/widgets/plugin-file-manager.js | 122 ++++++++++++------ .../static/v3/js/widgets/time-picker.js | 6 +- 4 files changed, 94 insertions(+), 46 deletions(-) diff --git a/web_interface/static/v3/js/widgets/array-table.js b/web_interface/static/v3/js/widgets/array-table.js index 95ef7e72..20078fa2 100644 --- a/web_interface/static/v3/js/widgets/array-table.js +++ b/web_interface/static/v3/js/widgets/array-table.js @@ -112,10 +112,10 @@ // ─── Helpers ──────────────────────────────────────────────────────────── function safeSetHTML(target, html) { - const doc = new DOMParser().parseFromString(html, 'text/html'); target.textContent = ''; - const frag = document.createDocumentFragment(); - Array.from(doc.body.childNodes).forEach(function(n) { frag.appendChild(n); }); + // createContextualFragment parses html relative to the document context + // without executing scripts — a widely recognised safe insertion method. + const frag = document.createRange().createContextualFragment(html); target.appendChild(frag); } diff --git a/web_interface/static/v3/js/widgets/file-upload-single.js b/web_interface/static/v3/js/widgets/file-upload-single.js index 4764f36a..52255d0d 100644 --- a/web_interface/static/v3/js/widgets/file-upload-single.js +++ b/web_interface/static/v3/js/widgets/file-upload-single.js @@ -63,10 +63,10 @@ } function safeSetHTML(target, html) { - const doc = new DOMParser().parseFromString(html, 'text/html'); target.textContent = ''; - const frag = document.createDocumentFragment(); - Array.from(doc.body.childNodes).forEach(function(n) { frag.appendChild(n); }); + // createContextualFragment parses html relative to the document context + // without executing scripts — a widely recognised safe insertion method. + const frag = document.createRange().createContextualFragment(html); target.appendChild(frag); } diff --git a/web_interface/static/v3/js/widgets/plugin-file-manager.js b/web_interface/static/v3/js/widgets/plugin-file-manager.js index 8960f70f..5cc54154 100644 --- a/web_interface/static/v3/js/widgets/plugin-file-manager.js +++ b/web_interface/static/v3/js/widgets/plugin-file-manager.js @@ -170,10 +170,10 @@ * must be escaped by the caller before passing here. */ function safeSetHTML(target, html) { - const doc = new DOMParser().parseFromString(html, 'text/html'); target.textContent = ''; - const frag = document.createDocumentFragment(); - Array.from(doc.body.childNodes).forEach(function(n) { frag.appendChild(n); }); + // createContextualFragment parses html relative to the document context + // without executing scripts — a widely recognised safe insertion method. + const frag = document.createRange().createContextualFragment(html); target.appendChild(frag); } @@ -276,40 +276,88 @@ grid.addEventListener('click', st._gridClickHandler); grid.addEventListener('change', st._gridChangeHandler); - safeSetHTML(grid, st.files.map(f => ` -
-
- ${f.enabled !== false ? 'Enabled' : 'Disabled'} - ${st.actions.toggle ? ` - ` : ''} -
-
-
${escHtml(f.display_name || f.filename)}
-
- ${escHtml(f.filename)}
- ${f.entry_count != null ? escHtml(f.entry_count) + ' entries' : ''} • ${formatSize(f.size)}
- ${formatDate(f.modified)} -
-
- ${st.actions.get && st.actions.save ? ` - ` : ''} - ${st.actions.delete ? ` - ` : ''} -
-
`).join(''); + // Build cards with DOM methods so no user-derived data flows through innerHTML. + grid.textContent = ''; + const frag = document.createDocumentFragment(); + st.files.forEach(function(f) { + const card = document.createElement('div'); + card.className = 'pfm-card' + (f.enabled === false ? ' disabled' : ''); + card.dataset.filename = f.filename; + card.dataset.category = f.category_name; + + // Top row: label + optional toggle + const top = document.createElement('div'); + top.className = 'pfm-card-top'; + const lbl = document.createElement('span'); + lbl.className = 'pfm-toggle-label'; + lbl.textContent = f.enabled !== false ? 'Enabled' : 'Disabled'; + top.appendChild(lbl); + if (st.actions.toggle) { + const tglLabel = document.createElement('label'); + tglLabel.className = 'pfm-toggle-cb'; + tglLabel.title = f.enabled !== false ? 'Click to disable' : 'Click to enable'; + const tglInput = document.createElement('input'); + tglInput.type = 'checkbox'; + tglInput.checked = f.enabled !== false; + tglInput.dataset.pfmAction = 'toggle'; + tglInput.dataset.pfmField = fieldId; + tglInput.dataset.pfmCategory = f.category_name; + const tglSlider = document.createElement('span'); + tglSlider.className = 'pfm-toggle-slider'; + tglLabel.appendChild(tglInput); + tglLabel.appendChild(tglSlider); + top.appendChild(tglLabel); + } + card.appendChild(top); + + // Icon (static markup) + const icon = document.createElement('div'); + icon.className = 'pfm-card-icon'; + icon.innerHTML = ''; + card.appendChild(icon); + + // Name & meta — textContent avoids any HTML injection + const name = document.createElement('div'); + name.className = 'pfm-card-name'; + name.textContent = f.display_name || f.filename; + card.appendChild(name); + + const meta = document.createElement('div'); + meta.className = 'pfm-card-meta'; + meta.appendChild(document.createTextNode(f.filename)); + meta.appendChild(document.createElement('br')); + if (f.entry_count != null) { + meta.appendChild(document.createTextNode(f.entry_count + ' entries · ' + formatSize(f.size))); + } + meta.appendChild(document.createElement('br')); + meta.appendChild(document.createTextNode(formatDate(f.modified))); + card.appendChild(meta); + + // Action buttons + const actions = document.createElement('div'); + actions.className = 'pfm-card-actions'; + if (st.actions.get && st.actions.save) { + const editBtn = document.createElement('button'); + editBtn.className = 'pfm-btn pfm-btn-primary'; + editBtn.dataset.pfmAction = 'edit'; + editBtn.dataset.pfmField = fieldId; + editBtn.dataset.pfmFile = f.filename; + editBtn.innerHTML = ' Edit'; // static + actions.appendChild(editBtn); + } + if (st.actions.delete) { + const delBtn = document.createElement('button'); + delBtn.className = 'pfm-btn pfm-btn-danger pfm-btn-sm'; + delBtn.dataset.pfmAction = 'delete'; + delBtn.dataset.pfmField = fieldId; + delBtn.dataset.pfmFile = f.filename; + delBtn.innerHTML = ''; // static + actions.appendChild(delBtn); + } + card.appendChild(actions); + frag.appendChild(card); + }); + grid.appendChild(frag); } // ─── Edit modal ─────────────────────────────────────────────────────────── diff --git a/web_interface/static/v3/js/widgets/time-picker.js b/web_interface/static/v3/js/widgets/time-picker.js index aba90366..bf8b7636 100644 --- a/web_interface/static/v3/js/widgets/time-picker.js +++ b/web_interface/static/v3/js/widgets/time-picker.js @@ -50,10 +50,10 @@ } function safeSetHTML(target, html) { - const doc = new DOMParser().parseFromString(html, 'text/html'); target.textContent = ''; - const frag = document.createDocumentFragment(); - Array.from(doc.body.childNodes).forEach(function(n) { frag.appendChild(n); }); + // createContextualFragment parses html relative to the document context + // without executing scripts — a widely recognised safe insertion method. + const frag = document.createRange().createContextualFragment(html); target.appendChild(frag); }