From 22d3d8c1ffbf4338a8c7291484a5693e6cd5fe62 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 8 Jul 2026 15:13:26 +0000 Subject: [PATCH] Address Codacy static-analysis findings in settings JS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refactor the two new modules to clear the flagged patterns without any behavior change: - Build the search dropdown with DOM nodes + textContent instead of innerHTML string concatenation, removing the XSS sinks and the manual escapeHtml helper it needed. - Replace numeric index access (index[i], terms[j], opts[idx], currentResults[i]) with array iteration methods, NodeList.item(), and Array.prototype.at() to clear detect-object-injection. - Use === via a shared termsMatch() helper, optional-catch binding, and drop a useless initial assignment. Verified with ESLint (eslint:recommended + eslint-plugin-security) at zero findings and re-ran the headless-Chromium behavior test (tooltip, per-tab filter, global search navigation) — all green. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_014gZxznuxw8L92FUMBN3Nqz --- web_interface/static/v3/js/settings-search.js | 75 ++++++++++--------- web_interface/static/v3/js/tooltips.js | 6 +- 2 files changed, 42 insertions(+), 39 deletions(-) diff --git a/web_interface/static/v3/js/settings-search.js b/web_interface/static/v3/js/settings-search.js index 914b680f..4b0ff04c 100644 --- a/web_interface/static/v3/js/settings-search.js +++ b/web_interface/static/v3/js/settings-search.js @@ -44,10 +44,9 @@ }; } - function escapeHtml(s) { - return String(s == null ? '' : s) - .replace(/&/g, '&').replace(//g, '>') - .replace(/"/g, '"').replace(/'/g, '''); + // True when every search term is present in the haystack. + function termsMatch(hay, terms) { + return terms.every(function (t) { return hay.indexOf(t) !== -1; }); } function textOf(el) { @@ -130,43 +129,54 @@ q = q.trim().toLowerCase(); if (!q) return []; var terms = q.split(/\s+/); - var index = window._settingsIndex || []; var out = []; - for (var i = 0; i < index.length && out.length < MAX_RESULTS; i++) { - var hay = index[i].hay; - var ok = true; - for (var j = 0; j < terms.length; j++) { - if (hay.indexOf(terms[j]) === -1) { ok = false; break; } - } - if (ok) out.push(index[i]); - } + (window._settingsIndex || []).some(function (entry) { + if (termsMatch(entry.hay, terms)) out.push(entry); + return out.length >= MAX_RESULTS; // stop once we have enough + }); return out; } + function span(cls, text) { + var s = document.createElement('span'); + s.className = cls; + s.textContent = text; + return s; + } + + // Build the dropdown with DOM nodes + textContent (never innerHTML) so + // setting labels/help can never be interpreted as markup. function renderResults(results) { currentResults = results; activeIndex = -1; + resultsBox.textContent = ''; if (!results.length) { - resultsBox.innerHTML = '
No settings found.
'; + resultsBox.appendChild(span('ssr-empty', 'No settings found.')); openResults(); return; } - var html = ''; var lastTab = null; results.forEach(function (r, i) { if (r.tabLabel !== lastTab) { - html += '
' + escapeHtml(r.tabLabel) + '
'; + var group = document.createElement('div'); + group.className = 'ssr-group'; + group.textContent = r.tabLabel; + resultsBox.appendChild(group); lastTab = r.tabLabel; } var sub = r.section ? (r.section + ' · ') : ''; var snippet = r.help ? r.help.split('\n')[0] : ''; - html += ''; + var opt = document.createElement('button'); + opt.type = 'button'; + opt.className = 'ssr-option'; + opt.setAttribute('role', 'option'); + opt.id = 'ssr-' + i; + opt.setAttribute('data-idx', String(i)); + opt.appendChild(span('ssr-label', r.label)); + var helpText = snippet ? (sub + snippet) : (sub ? r.section : ''); + if (helpText) opt.appendChild(span('ssr-help', helpText)); + resultsBox.appendChild(opt); }); - resultsBox.innerHTML = html; openResults(); } @@ -188,7 +198,7 @@ opts.forEach(function (o) { o.classList.remove('active'); }); if (idx < 0 || idx >= opts.length) { activeIndex = -1; return; } activeIndex = idx; - var el = opts[idx]; + var el = opts.item(idx); el.classList.add('active'); el.scrollIntoView({ block: 'nearest' }); input.setAttribute('aria-activedescendant', el.id); @@ -314,12 +324,10 @@ e.preventDefault(); highlight(Math.max(activeIndex - 1, 0)); } else if (e.key === 'Enter') { - if (activeIndex >= 0 && currentResults[activeIndex]) { + var chosen = currentResults.at(activeIndex >= 0 ? activeIndex : 0); + if (chosen) { e.preventDefault(); - navigateToSetting(currentResults[activeIndex]); - } else if (currentResults.length) { - e.preventDefault(); - navigateToSetting(currentResults[0]); + navigateToSetting(chosen); } } else if (e.key === 'Escape') { closeResults(); @@ -333,7 +341,8 @@ if (!opt) return; e.preventDefault(); var idx = parseInt(opt.getAttribute('data-idx'), 10); - if (currentResults[idx]) navigateToSetting(currentResults[idx]); + var chosen = currentResults.at(idx); + if (chosen) navigateToSetting(chosen); }); document.addEventListener('click', function (e) { @@ -367,13 +376,7 @@ var anyVisible = false; fields.forEach(function (fg) { - var show = true; - if (terms.length) { - var hay = fieldHay(fg); - for (var i = 0; i < terms.length; i++) { - if (hay.indexOf(terms[i]) === -1) { show = false; break; } - } - } + var show = !terms.length || termsMatch(fieldHay(fg), terms); fg.style.display = show ? '' : 'none'; if (show) anyVisible = true; }); diff --git a/web_interface/static/v3/js/tooltips.js b/web_interface/static/v3/js/tooltips.js index bcebbb3c..fd81bdd9 100644 --- a/web_interface/static/v3/js/tooltips.js +++ b/web_interface/static/v3/js/tooltips.js @@ -109,11 +109,11 @@ if (!t) return; // Only auto-show on keyboard focus, so a mouse/touch focus does not // fight the click handler below. - var focusVisible = true; + var focusVisible; try { focusVisible = t.matches(':focus-visible'); - } catch (err) { - focusVisible = true; // older browsers: fall back to always show + } catch { // older browsers without :focus-visible + focusVisible = true; } if (focusVisible) show(t); });