diff --git a/bsimvis/app/static/feature/index.html b/bsimvis/app/static/feature/index.html index 4ee8b37..7919927 100644 --- a/bsimvis/app/static/feature/index.html +++ b/bsimvis/app/static/feature/index.html @@ -384,8 +384,8 @@

Global Feature Analysis

if (currentOffset === 0) { body.innerHTML = ''; const fullId = `${collection}:feature:${featureHash}`; - document.getElementById('title-text').innerHTML = `Feature occurences: ${featureHash} `; - document.getElementById('meta-info').innerHTML = `Collection: ${collection} | Total Instances: ${totalOccurrences}`; + document.getElementById('title-text').innerHTML = `Feature occurences: ${escapeHtml(featureHash)} `; + document.getElementById('meta-info').innerHTML = `Collection: ${escapeHtml(collection)} | Total Instances: ${escapeHtml(totalOccurrences)}`; } // Iterate through every representation of this feature in this page @@ -401,28 +401,28 @@

Global Feature Analysis

const funcName = (sourceData && sourceData.meta) ? sourceData.meta['function_name'] : addr; const originHtml = ` -
${funcName}
-
Address: ${addr}
-
Binary: ${md5}
+
${escapeHtml(funcName)}
+
Address: ${escapeHtml(addr)}
+
Binary: ${escapeHtml(md5)}
Lines: ${(occ['line_idx'] || []).map(l => l + 1).join(', ')}
- ${funcId} -
- - -
@@ -430,11 +430,11 @@

Global Feature Analysis

// 2. Meta Column const metaHtml = ` -
${occ.type}
+
${escapeHtml(occ.type)}
- ${occ['previous_pcode_op'] ? `PREV: ${occ['previous_pcode_op']}` : ''} - ${occ['pcode_op']} - ${occ['addr'] ? `${occ['addr']}` : ''} + ${occ['previous_pcode_op'] ? `PREV: ${escapeHtml(occ['previous_pcode_op'])}` : ''} + ${escapeHtml(occ['pcode_op'])} + ${occ['addr'] ? `${escapeHtml(occ['addr'])}` : ''}
`; @@ -445,7 +445,7 @@

Global Feature Analysis

const pcodeHtml = `
-
${occ['pcode_op_full'] || 'N/A'}
+
${occ['pcode_op_full'] ? escapeHtml(occ['pcode_op_full']) : 'N/A'}
`; @@ -459,7 +459,7 @@

Global Feature Analysis

const isPrevActive = (s === occ['previous_seq']); const cls = isActive ? 'active' : (isPrevActive ? 'prev-active' : ''); const addrPart = s.split(':')[0]; - return `
${addrPart}${op.replace(/&/g, '&').replace(/`; + return `
${addrPart}${escapeHtml(op)}
`; }).join('')}
` : '
---
'; @@ -472,7 +472,7 @@

Global Feature Analysis

let contextHtml = '
Source code mapping unavailable
'; if (sourceData && sourceData.rows) { contextHtml = `
`; + onclick="if(window.parent && window.parent.showFunctionCodeById) { window.parent.showFunctionCodeById(${jsString(funcId)}, ${jsString(funcName)}, ${jsString(lineHash)}, event); } else { window.open('/function/index.html?id=' + encodeURIComponent(${jsString(funcId)}) + ${jsString(lineHash)}, '_blank'); }">`; const contextLineSet = new Set(); (occ['line_idx'] || []).forEach(l => { @@ -507,7 +507,7 @@

Global Feature Analysis

tr.innerHTML = ` ${originHtml} ${metaHtml} - ${occ['seq'] || 'N/A'} + ${escapeHtml(occ['seq'] || 'N/A')} ${pcodeHtml} ${pcodeBlockHtml} ${tfHtml} @@ -533,7 +533,7 @@

Global Feature Analysis

} catch (err) { console.error(err); if (currentOffset === 0) { - body.innerHTML = `Error: ${err.message}`; + body.innerHTML = `Error: ${escapeHtml(err.message)}`; } } } @@ -546,7 +546,7 @@

Global Feature Analysis

return renderTokenHtml(t) .replace('class="token ', `class="token ${highlightClass} `) - .replace(' `feat-${h}`).join(' '); + const safeHashes = (t.hash_list || []).map(h => safeCssClassPart(h)); + const hashes = escapeAttr((t.hash_list || []).join(' ')); + const featClasses = safeHashes.map(h => `feat-${h}`).join(' '); - const calledAttr = t.called_func_id ? `data-called-func-id="${t.called_func_id}" data-is-external="${t.is_external || false}" data-target-name="${t.target_name || ''}"` : ''; + const calledAttr = t.called_func_id + ? `data-called-func-id="${escapeAttr(t.called_func_id)}" data-is-external="${escapeAttr(t.is_external || false)}" data-target-name="${escapeAttr(t.target_name || '')}"` + : ''; const clickClass = t.called_func_id ? (t.is_external ? 'func-call-external' : 'func-call-clickable') : ''; - const titleAttr = t.called_func_id ? `title="Click to navigate to ${t.target_name || 'called function'}"` : ''; + const titleAttr = t.called_func_id ? `title="Click to navigate to ${escapeAttr(t.target_name || 'called function')}"` : ''; - const diffClass = t.diff_class || ''; - const sideAttr = options.side ? `data-side="${options.side}"` : ''; + const diffClass = safeCssClassPart(t.diff_class || ''); + const sideAttr = options.side ? `data-side="${escapeAttr(options.side)}"` : ''; - const escapedText = t.text.replace(/&/g, '&').replace(//g, '>'); + const escapedText = escapeHtml(t.text); + const tokenType = safeCssClassPart(t.type || 'default'); + const idx = escapeAttr(t.global_idx ?? ''); const hoverHandlers = options.inlineHoverHandlers ? `onmouseenter="handleHoverMove(event, true)" onmouseleave="handleHoverMove(event, false)"` : ''; - return `${escapedText}`; + return `${escapedText}`; }; window.TOKEN_COLORS = { diff --git a/bsimvis/app/static/js/dashboard.js b/bsimvis/app/static/js/dashboard.js index 9772bf6..2ef0435 100644 --- a/bsimvis/app/static/js/dashboard.js +++ b/bsimvis/app/static/js/dashboard.js @@ -14,7 +14,7 @@ function handleFilterKey(e, searchFn) { if (e.key === 'Enter') { e.preventDefault(); if (filterDebounceTimer) clearTimeout(filterDebounceTimer); - + // Capture focus and selection before searching currentFocusId = e.target.id; try { @@ -27,7 +27,7 @@ function handleFilterKey(e, searchFn) { preservedSelection.start = 0; preservedSelection.end = 0; } - + searchFn(); } } @@ -75,12 +75,12 @@ function initColumnResize(th, path, label) { resizer.addEventListener('mousedown', (e) => { e.preventDefault(); e.stopPropagation(); - + startX = e.clientX; startWidth = th.getBoundingClientRect().width; - + document.body.classList.add('resizing'); - + // Disable pointer events on iframes during resize document.querySelectorAll('iframe').forEach(ifrm => { ifrm.style.pointerEvents = 'none'; @@ -545,7 +545,7 @@ function updateUI(path, params, route) { updateNavLink('nav-clusters', '#clusters'); updateNavLink('nav-jobs', '#jobs'); updateNavLink('nav-upload', '#upload'); - + const fileMd5 = params.get('file_md5'); const cgNav = document.getElementById('nav-file-call-graph'); if (cgNav) { @@ -571,7 +571,7 @@ function updateUI(path, params, route) { const dataTable = document.getElementById('data-table'); const dataTableHeader = document.getElementById('data-table-header'); let headHtml = ''; - + const savedForRoute = JSON.parse(localStorage.getItem('columnWidths') || '{}')[path]; const hasSavedWidths = savedForRoute && Object.keys(savedForRoute).length > 0; const hasWidths = route.headers.some(h => typeof h === 'object' && h.width) || hasSavedWidths; @@ -646,9 +646,9 @@ function updateUI(path, params, route) { settingsHtml += ` Pool Limit:
-
`; @@ -657,9 +657,9 @@ function updateUI(path, params, route) { settingsHtml += ` Limit:
-
@@ -980,7 +980,7 @@ function updateUI(path, params, route) { const p = new URLSearchParams(params); const fileMd5 = p.get('file_md5'); const callGraphBtn = fileMd5 ? `View File Call Graph 🕸️` : ''; - + searchArea.innerHTML = `
@@ -1403,7 +1403,7 @@ function createTagCard(columnId, type, value, isExclude = false) { card.innerHTML = ` NOT - ${value} + ${escapeHtml(value)} × `; @@ -1448,22 +1448,22 @@ function renderCollections(data) { if (!data.length) return 'No collections found.'; return data.map(col => ` - - ${col.name} + + ${escapeHtml(col.name)} ${col['total_files']} ${col['total_functions']} ${formatDate(col['last_updated'])} @@ -1476,11 +1476,11 @@ function renderBatches(data) { return data.map(b => { const col = b.collection || 'unknown'; return ` - +
- ${b.name || 'Unnamed'} -
@@ -1506,21 +1506,21 @@ function renderFiles(data) { const funcCount = f['function_count'] !== undefined ? f['function_count'] : 0; return ` - +
- ${f['file_name']} -
-
# ${f['file_md5']}
-
${f['language_id']}
+
# ${escapeHtml(f['file_md5'])}
+
${escapeHtml(f['language_id'])}
- - ${batchUuid.length > 8 ? batchUuid.substring(0, 8) + '...' : batchUuid} + + ${escapeHtml(batchUuid.length > 8 ? batchUuid.substring(0, 8) + '...' : batchUuid)}
@@ -1559,44 +1559,45 @@ function renderFunctions(data) { const safeName = (name || '').replace(/'/g, "\\'"); const fInfo = formatSigComponent(namespace, returnType, name, parameters); + const renderedSig = `${fInfo.ret ? `${escapeHtml(fInfo.ret)} ` : ''}${fInfo.ns ? `${escapeHtml(fInfo.ns)}::` : ''}${escapeHtml(name)}(${fInfo.params.map(t => `${escapeHtml(t)}`).join(', ')})`; const rowStyle = getRowTagColor(tags, user_tags); return ` - + -
- + - ${fInfo.ret ? `${fInfo.ret} ` : ''}${fInfo.ns ? `${fInfo.ns}::` : ''}${name}(${fInfo.params.map(t => `${t}`).join(', ')}) + onclick="showFunctionCodeById(${escapeAttr(jsString(funcId))}, ${escapeAttr(jsString(name))}, '', event)"> + ${renderedSig}
- + onclick="addToDiff(${escapeAttr(jsString(funcId))}, ${escapeAttr(jsString(name))})" title="Add to Diff" style="padding:0 5px; font-size: 0.75rem; border-radius: 3px;">±
- @ ${entry} + @ ${escapeHtml(entry)} ${renderTagEditor('function', funcId, tags, user_tags)} ${renderClusterCards(f['clusters'])}
${featCount} - +
-
${fileName}
- # ${file_md5} +
${escapeHtml(fileName)}
+ # ${escapeHtml(file_md5)} ${renderTagEditor('file', `${f.collection || 'main'}:file:${file_md5}`, f.file_tags || [], f.file_user_tags || [])} - ${language} + ${escapeHtml(language)} ${formatDate(f['entry_date'] || f['file_date'])} @@ -1614,7 +1615,7 @@ function renderGlobalFeatures(items) { let pcodeHtml = `
-
${ctx.pcode_full || 'N/A'}
+
${escapeHtml(ctx.pcode_full || 'N/A')}
`; @@ -1637,14 +1638,14 @@ function renderGlobalFeatures(items) { 'keyword': 'tok-keyword', 'comment': 'tok-comment', 'string': 'tok-string', 'number': 'tok-number' }; const cls = colorMap[t.type] || 'tok-default'; - cCodeHtml += `${t.text.replace(/&/g, '&').replace(/`; + onmousemove="moveCodePreview(event)">${escapeHtml(t.text)}`; }); cCodeHtml += `
`; } @@ -1653,15 +1654,15 @@ function renderGlobalFeatures(items) {
- ${f.hash} + ${escapeHtml(f.hash)}
-
${ctx.type}
- ${ctx.op} +
${escapeHtml(ctx.type)}
+ ${escapeHtml(ctx.op)} ${pcodeHtml} ${cCodeHtml} @@ -1697,6 +1698,7 @@ function renderTopCorrelations(items) { const f1 = formatSigComponent(p.meta1?.namespace || '', p.meta1?.return_type || '', p.name1 || '---', p.meta1?.parameters || []); const f2 = formatSigComponent(p.meta2?.namespace || '', p.meta2?.return_type || '', p.name2 || '---', p.meta2?.parameters || []); + const renderSig = (info, fallbackName) => `${info.ret ? `${escapeHtml(info.ret)} ` : ''}${info.ns ? `${escapeHtml(info.ns)}::` : ''}${escapeHtml(fallbackName || '---')}(${info.params.map(t => `${escapeHtml(t)}`).join(', ')})`; const tags = p.tags || []; const user_tags = p.user_tags || []; @@ -1704,16 +1706,16 @@ function renderTopCorrelations(items) { const pairId = p.sid || `${p.id1}|${p.id2}|${p.algo}`; return ` - +
${(p.score * 100).toFixed(1)}%
- @@ -1722,44 +1724,44 @@ function renderTopCorrelations(items) {
-
- + - ${f1.ret ? `${f1.ret} ` : ''}${f1.ns ? `${f1.ns}::` : ''}${p.name1 || '---'}(${f1.params.map(t => `${t}`).join(', ')}) + onclick="showFunctionCodeById(${escapeAttr(jsString(p.id1))}, ${escapeAttr(jsString(p.name1 || '---'))}, '', event)"> + ${renderSig(f1, p.name1)} - + onclick="addToDiff(${escapeAttr(jsString(p.id1))}, ${escapeAttr(jsString(p.name1 || '---'))})" title="Add to Diff" style="padding:0 5px; font-size: 0.75rem; margin-left: 4px; border-radius: 3px;">±
-
- + - ${f2.ret ? `${f2.ret} ` : ''}${f2.ns ? `${f2.ns}::` : ''}${p.name2 || '---'}(${f2.params.map(t => `${t}`).join(', ')}) + onclick="showFunctionCodeById(${escapeAttr(jsString(p.id2))}, ${escapeAttr(jsString(p.name2 || '---'))}, '', event)"> + ${renderSig(f2, p.name2)} - + onclick="addToDiff(${escapeAttr(jsString(p.id2))}, ${escapeAttr(jsString(p.name2 || '---'))})" title="Add to Diff" style="padding:0 5px; font-size: 0.75rem; margin-left: 4px; border-radius: 3px;">±
-
@ ${addr1}
-
@ ${addr2}
+
@ ${escapeHtml(addr1)}
+
@ ${escapeHtml(addr2)}
@@ -1778,24 +1780,24 @@ function renderTopCorrelations(items) {
${p.meta1?.bsim_features_count || 0} - +
${p.meta2?.bsim_features_count || 0} - +
-
${p.meta1?.file_name || ''}
-
${p.meta2?.file_name || ''}
+
${escapeHtml(p.meta1?.file_name || '')}
+
${escapeHtml(p.meta2?.file_name || '')}
-
# ${m1}
-
# ${m2}
+
# ${escapeHtml(m1)}
+
# ${escapeHtml(m2)}
@@ -1806,8 +1808,8 @@ function renderTopCorrelations(items) {
-
${p.meta1?.language_id || '---'}
-
${p.meta2?.language_id || '---'}
+
${escapeHtml(p.meta1?.language_id || '---')}
+
${escapeHtml(p.meta2?.language_id || '---')}
@@ -1878,14 +1880,14 @@ function showFunctionCodeById(id, name, lineHash = '', e) { function seeSimilarFromCode() { const win = windowManager.activeWindow; if (!win || !win.iframe || !win.iframe.src) return; - + let url; try { url = new URL(win.iframe.contentWindow.location.href); } catch(e) { url = new URL(win.iframe.src, window.location.origin); } - + const id = url.searchParams.get('id'); if (!id) return; @@ -2003,7 +2005,7 @@ function updateUIParams() { localStorage.setItem('cohesionThreshold', UIParams.cohesionThreshold); localStorage.setItem('colorByTag', UIParams.colorByTag); localStorage.setItem('includeHeaders', UIParams.includeHeaders); - + // Sync with sim-color-by-tag for tags.js compatibility localStorage.setItem('sim-color-by-tag', UIParams.colorByTag); @@ -2217,7 +2219,7 @@ async function populateCollectionDropdown() { if (!res.ok) return; const data = await res.json(); const collections = data.collections || (Array.isArray(data) ? data : []); - + const list = document.getElementById('collection-flyout-list'); const trigger = document.getElementById('nav-collections'); const flyout = document.getElementById('collection-flyout'); @@ -2233,9 +2235,9 @@ async function populateCollectionDropdown() { } list.innerHTML = collections.map(c => ` -
+
- ${c.name} + ${escapeHtml(c.name)}
`).join('') || '
No collections found
'; @@ -2262,7 +2264,7 @@ async function populateCollectionDropdown() { trigger.addEventListener('mouseleave', hide); flyout.addEventListener('mouseenter', () => { if (hideTimeout) clearTimeout(hideTimeout); }); flyout.addEventListener('mouseleave', hide); - + trigger.dataset.hasListener = "true"; } } catch (e) { @@ -2502,7 +2504,7 @@ window.showGraphContextMenu = function(e, type, data, isRefresh = false) { // Generate HTML content based on type let html = ''; - + if (type === 'node') { const nodeId = data.id; const nodeName = data.name; @@ -2512,7 +2514,7 @@ window.showGraphContextMenu = function(e, type, data, isRefresh = false) { const tags = latestNode.tags || []; html += `
Function: ${nodeName}
`; - + // Bookmark/Ignore/Tag actions html += renderBookmarkIgnoreTagItems('function', nodeId, tags, userTags); @@ -2543,17 +2545,17 @@ window.showGraphContextMenu = function(e, type, data, isRefresh = false) { if (selectedSim) { const simId = selectedSim.sid || `${selectedSim.id1}|${selectedSim.id2}|${selectedSim.algo}`; // Fetch latest pair from graph all_pairs to get latest tags - const latestPair = graph.all_pairs.find(p => - (p.id1 === selectedSim.id1 && p.id2 === selectedSim.id2) || + const latestPair = graph.all_pairs.find(p => + (p.id1 === selectedSim.id1 && p.id2 === selectedSim.id2) || (p.id1 === selectedSim.id2 && p.id2 === selectedSim.id1) ) || selectedSim; - + const simUserTags = latestPair.user_tags || []; const simTags = latestPair.tags || []; const percentScore = (parseFloat(selectedSim.score) * 100).toFixed(1); html += `
Similarity: ${percentScore}% Match
`; - + html += `
@@ -2568,9 +2570,9 @@ window.showGraphContextMenu = function(e, type, data, isRefresh = false) { const name1 = data.name1; const name2 = data.name2; const simId = data.sid || `${id1}|${id2}|${data.algo}`; - + // Fetch latest pair from graph - const latestPair = graph.all_pairs.find(p => + const latestPair = graph.all_pairs.find(p => (p.id1 === id1 && p.id2 === id2) || (p.id1 === id2 && p.id2 === id1) ) || data; @@ -2579,7 +2581,7 @@ window.showGraphContextMenu = function(e, type, data, isRefresh = false) { const percentScore = (parseFloat(data.score) * 100).toFixed(1); html += `
Similarity: ${percentScore}% Match
`; - + html += `
@@ -2663,7 +2665,7 @@ window.closeGraphContextMenu = function() { window.toggleContextMenuBookmark = async function(event, etype, eid) { const userTags = getEntityUserTags(etype, eid); const isBookmarked = userTags.includes('bookmark'); - + if (isBookmarked) { await removeTag(null, etype, eid, 'bookmark'); } else { @@ -2674,7 +2676,7 @@ window.toggleContextMenuBookmark = async function(event, etype, eid) { window.toggleContextMenuIgnore = async function(event, etype, eid) { const userTags = getEntityUserTags(etype, eid); const isIgnored = userTags.includes('ignore'); - + if (isIgnored) { await removeTag(null, etype, eid, 'ignore'); } else { @@ -2695,7 +2697,7 @@ window.toggleContextMenuTag = async function(event, etype, eid, tag) { window.showInlineTagInput = function(event, etype, eid) { const item = event.currentTarget; const parent = item.parentElement; - + // Create an input wrapper const wrapper = document.createElement('div'); wrapper.style.padding = '6px 12px'; @@ -2703,7 +2705,7 @@ window.showInlineTagInput = function(event, etype, eid) { wrapper.style.flexDirection = 'column'; wrapper.style.gap = '4px'; wrapper.style.position = 'relative'; - + const input = document.createElement('input'); input.type = 'text'; input.placeholder = 'Search or create...'; @@ -2714,14 +2716,14 @@ window.showInlineTagInput = function(event, etype, eid) { input.style.padding = '4px 8px'; input.style.borderRadius = '4px'; input.style.fontSize = '0.75rem'; - + wrapper.appendChild(input); parent.replaceChild(wrapper, item); input.focus(); - + // Clicking inside the input block shouldn't close the parent context menu wrapper.onmousedown = (e) => e.stopPropagation(); - + input.onkeydown = async (e) => { if (e.key === 'Enter') { e.preventDefault(); @@ -2733,7 +2735,7 @@ window.showInlineTagInput = function(event, etype, eid) { window.refreshContextMenuUI(); } }; - + if (window.attachTagAutocomplete) { window.attachTagAutocomplete(input, async (tag) => { if (tag && tag.trim()) { @@ -2756,7 +2758,7 @@ window.refreshContextMenuUI = function() { function getEntityUserTags(etype, eid) { const graph = window.graphInstance; if (!graph) return []; - + if (etype === 'function') { const latest = graph.nodes_map.get(eid); return latest ? (latest.user_tags || []) : []; @@ -2772,8 +2774,8 @@ function getEntityUserTags(etype, eid) { if (!latest) { const parts = eid.split('|'); if (parts.length >= 2) { - latest = graph.all_pairs.find(p => - (p.id1 === parts[0] && p.id2 === parts[1]) || + latest = graph.all_pairs.find(p => + (p.id1 === parts[0] && p.id2 === parts[1]) || (p.id1 === parts[1] && p.id2 === parts[0]) ); } @@ -2788,7 +2790,7 @@ function renderBookmarkIgnoreTagItems(etype, eid, tagsList, userTagsList) { const isIgnored = userTagsList.includes('ignore'); let html = ''; - + // Bookmark and Ignore side-by-side buttons html += `
@@ -2804,13 +2806,13 @@ function renderBookmarkIgnoreTagItems(etype, eid, tagsList, userTagsList) { // Generate Tags nested submenu dropdown items const allKnownTags = Object.keys(window.tagMetadata || {}).filter(t => t !== 'bookmark' && t !== 'ignore' && t && t.trim()); - + let submenuHtml = ''; allKnownTags.forEach(tag => { const isActive = userTagsList.includes(tag); const color = window.getTagMetadata ? window.getTagMetadata(tag).color : '#66d9ef'; const checkboxStyle = `color: ${isActive ? color : 'rgba(255,255,255,0.2)'}; width: 16px; text-align: center; font-size: 0.8rem;`; - + submenuHtml += `
@@ -2834,7 +2836,7 @@ function renderBookmarkIgnoreTagItems(etype, eid, tagsList, userTagsList) { Tags - + @@ -2849,7 +2851,7 @@ function renderBookmarkIgnoreTagItems(etype, eid, tagsList, userTagsList) { function renderContextMenuTagsList(etype, eid, tagsList, userTagsList) { const allTags = [...(userTagsList || [])].filter(t => t !== 'bookmark' && t !== 'ignore' && t && t.trim()); if (allTags.length === 0) return ''; - + const tagsHtml = allTags.map(tag => { let color = '#66d9ef'; if (window.getTagMetadata) { @@ -3024,7 +3026,7 @@ function getFilterSummary(path, params) { const language_id = params.get('language_id'); const min_function_count = params.get('min_function_count'); const max_function_count = params.get('max_function_count'); - + if (file_name) summary.push(`Name: "${file_name}"`); if (file_md5) summary.push(`MD5: ${file_md5.substring(0, 6)}`); if (language_id) summary.push(`Lang: ${language_id}`); @@ -3038,7 +3040,7 @@ function getFilterSummary(path, params) { const min_features = params.get('min_features'); const cluster_name = params.get('cluster_name'); const entrypoint_address = params.get('entrypoint_address'); - + if (function_name) summary.push(`Func: "${function_name}"`); if (file_name) summary.push(`File: "${file_name}"`); if (file_md5) summary.push(`MD5: ${file_md5.substring(0, 6)}`); @@ -3054,7 +3056,7 @@ function getFilterSummary(path, params) { const algo = params.get('algo'); const cross_binary = params.get('cross_binary'); const match_mode = params.get('match_mode'); - + if (name) summary.push(`Func: "${name}"`); if (md5) summary.push(`MD5: ${md5.substring(0, 6)}`); if (address) summary.push(`Addr: ${address}`); @@ -3070,7 +3072,7 @@ function getFilterSummary(path, params) { const cluster_name = params.get('cluster_name'); const min_count = params.get('min_count'); const min_cohesion = params.get('min_cohesion'); - + if (cluster_uuid) summary.push(`UUID: ${cluster_uuid.substring(0, 6)}`); if (cluster_name) summary.push(`Name: "${cluster_name}"`); if (min_count && min_count !== '0') summary.push(`Min Funcs: ${min_count}`); @@ -3191,7 +3193,7 @@ function addToHistory(path, queryString) { const last = history[0]; const isSameView = last.path === path && last.collection === col && last.view === view; const timeDiff = now - last.timestamp; - + // Typing merge (debounce within 7 seconds) if (isSameView && timeDiff < 7000) { history[0] = newItem; @@ -3396,7 +3398,7 @@ function toggleHistoryDropdown(event) { const dropdown = document.getElementById('history-dropdown'); if (!dropdown) return; const isVisible = dropdown.style.display === 'block'; - + closeAllHistoryDropdowns(); if (!isVisible) { @@ -3415,7 +3417,7 @@ function toggleViewHistoryDropdown(event) { const dropdown = document.getElementById('view-history-dropdown'); if (!dropdown) return; const isVisible = dropdown.style.display === 'block'; - + closeAllHistoryDropdowns(); if (!isVisible) { @@ -3478,7 +3480,7 @@ function restoreGraphSettings() { if (settings.colorSim !== undefined) setVal('graph-color-sim', settings.colorSim); if (settings.bundleTension !== undefined) setVal('graph-bundle-tension', settings.bundleTension); if (settings.linkWidth !== undefined) setVal('graph-link-width', settings.linkWidth); - + const chk = document.getElementById('graph-scale-width'); if (chk && settings.scaleWidth !== undefined) chk.checked = settings.scaleWidth; @@ -3508,14 +3510,14 @@ function downloadSearchResults(format) { alert("Downloads are not available for this view."); return; } - + const params = new URLSearchParams(queryString); params.set('format', format); // For downloads, we want to fetch all matches, so we set limit to a large number params.set('limit', '100000'); - + const downloadUrl = route.api + '?' + params.toString(); - + // Create a temporary anchor element to trigger the browser download const link = document.createElement('a'); link.href = downloadUrl; diff --git a/bsimvis/app/static/js/metadata.js b/bsimvis/app/static/js/metadata.js index 4cdc0df..a9e47cf 100644 --- a/bsimvis/app/static/js/metadata.js +++ b/bsimvis/app/static/js/metadata.js @@ -2,21 +2,26 @@ function renderFunctionMetadata(container, m, fullId, options = {}) { if (!m) return ""; - + // Core data extraction const label = m['function_name'] || 'unknown_func'; + const safeLabel = escapeHtml(label); const returnType = m['return_type'] || 'void'; + const safeReturnType = escapeHtml(returnType); const namespace = m['namespace'] || ''; + const safeNamespace = escapeHtml(namespace); const parameters = m['parameters'] || []; const addr = m['entrypoint_address'] || (fullId ? fullId.split(':').pop() : 'N/A'); + const safeAddr = escapeHtml(addr); const fileMd5 = m['file_md5'] || 'N/A'; + const safeFileMd5 = escapeHtml(fileMd5); const collection = fullId ? fullId.split(':')[0] : 'main'; const fileId = m['file_md5'] ? `${collection}:file:${m['file_md5']}` : null; - + // Tags and Clusters - const fileTagsHtml = (fileId && typeof renderTagEditor === 'function') - ? renderTagEditor('file', fileId, m['file_tags'] || [], m['file_user_tags'] || []) + const fileTagsHtml = (fileId && typeof renderTagEditor === 'function') + ? renderTagEditor('file', fileId, m['file_tags'] || [], m['file_user_tags'] || []) : ''; const tags = m['tags'] || []; const user_tags = m['user_tags'] || []; @@ -31,7 +36,7 @@ function renderFunctionMetadata(container, m, fullId, options = {}) {
None
`; } - + const itemsHtml = list.map(t => { if (typeof t === 'object' && t !== null && t.name) { const isExt = t.is_external; @@ -41,22 +46,22 @@ function renderFunctionMetadata(container, m, fullId, options = {}) { const ns = t.namespace || ''; const ret = t.return_type || ''; const params = t.parameters || []; - + let labelHtml = ''; if (typeof formatSigComponent === 'function') { const sig = formatSigComponent(ns, ret, name, params); - labelHtml = `${sig.ret ? `${sig.ret} ` : ''}${sig.ns ? `${sig.ns}::` : ''}${name}(${sig.params.map(p => `${p}`).join(', ')})`; + labelHtml = `${sig.ret ? `${escapeHtml(sig.ret)} ` : ''}${sig.ns ? `${escapeHtml(sig.ns)}::` : ''}${escapeHtml(name)}(${sig.params.map(p => `${escapeHtml(p)}`).join(', ')})`; } else { - labelHtml = name; + labelHtml = escapeHtml(name); } - - if (addr) labelHtml += ` @${addr}`; - + + if (addr) labelHtml += ` @${escapeHtml(addr)}`; + const color = isExt ? '#f92672' : 'var(--accent)'; const cursor = (isExt || !fid) ? 'default' : 'pointer'; const previewAttrs = (isExt || !fid) ? '' : ` onmouseenter=" - const fid='${fid}'; const n='${name.replace(/'/g, "\\'")}'; const a='${addr}'; + const fid=${jsString(fid)}; const n=${jsString(name)}; const a=${jsString(addr)}; if(window.showCodePreview) { showCodePreview(fid, n, a, '', 0, event); } else if(window.parent && window.parent.showCodePreview) { window.parent.showCodePreview(fid, n, a, '', 0, event); } " @@ -70,14 +75,14 @@ function renderFunctionMetadata(container, m, fullId, options = {}) { " `; const clickAttr = (isExt || !fid) ? '' : `onclick=" - const fid='${fid}'; - const name='${name.replace(/'/g, "\\'")}'; - if(window.showFunctionCodeById) { - showFunctionCodeById(fid, name, '', event); - } else if(window.parent && window.parent.showFunctionCodeById) { - window.parent.showFunctionCodeById(fid, name, '', event); - } else { - window.location.href='/function/index.html?id='+encodeURIComponent(fid); + const fid=${jsString(fid)}; + const name=${jsString(name)}; + if(window.showFunctionCodeById) { + showFunctionCodeById(fid, name, '', event); + } else if(window.parent && window.parent.showFunctionCodeById) { + window.parent.showFunctionCodeById(fid, name, '', event); + } else { + window.location.href='/function/index.html?id='+encodeURIComponent(fid); }"` ; return ` @@ -85,9 +90,9 @@ function renderFunctionMetadata(container, m, fullId, options = {}) { ${isExt ? 'EXT' : ''} `; } - return `${JSON.stringify(t)}`; + return `${escapeHtml(JSON.stringify(t))}`; }).join(''); - + return `
${itemsHtml}
`; @@ -98,13 +103,13 @@ function renderFunctionMetadata(container, m, fullId, options = {}) { // Action Buttons - Compact versions for header let headerActionsHtml = ''; - + if (options.showDiffBtn) { const fullTextAttr = options.diffBtnFullText ? 'data-full-text="true"' : ''; - const onhover = `onmouseenter="if(window.currentFuncId || '${fullId}') onHoverDiffButton(event, '${fullId}', '${label}')" onmouseleave="hideDiffPreview(event)" onmousemove="if(window.moveDiffPreview) moveDiffPreview(event)"`; + const onhover = `onmouseenter="if(window.currentFuncId || ${escapeAttr(jsString(fullId))}) onHoverDiffButton(event, ${escapeAttr(jsString(fullId))}, ${escapeAttr(jsString(label))})" onmouseleave="hideDiffPreview(event)" onmousemove="if(window.moveDiffPreview) moveDiffPreview(event)"`; headerActionsHtml += ` - `; @@ -112,7 +117,7 @@ function renderFunctionMetadata(container, m, fullId, options = {}) { if (options.showCodeLink) { headerActionsHtml += ` - `; @@ -120,20 +125,20 @@ function renderFunctionMetadata(container, m, fullId, options = {}) { // Grouping Metadata const funcMetaKeys = [ - 'calling_convention', 'instruction_count', 'instructions_count', 'basic_blocks_count', + 'calling_convention', 'instruction_count', 'instructions_count', 'basic_blocks_count', 'cyclomatic_complexity', 'size_bytes' ]; const bsimMetaKeys = ['bsim_features_count', 'bsim_unique_features_count', 'score', 'milvus_id']; const fileMetaKeys = ['file_name', 'language_id', 'file_size', 'file_md5']; - + const renderRow = (key, val, icon = null) => { const labelText = key.replace(/_/g, ' '); const prefix = icon ? `` : ''; - let valueDisplay = (key === 'entrypoint_address') ? `@ ${val}` : (key === 'file_md5' ? `# ${val}` : val); + let valueDisplay = (key === 'entrypoint_address') ? `@ ${escapeHtml(val)}` : (key === 'file_md5' ? `# ${escapeHtml(val)}` : escapeHtml(val)); const valueColor = (key.includes('address') || key.includes('md5') || key.includes('id') || key.includes('count') || key.includes('score')) ? 'var(--accent)' : 'inherit'; - + if (key === 'bsim_features_count') { - valueDisplay = `${val} `; + valueDisplay = `${val} `; } return `
@@ -167,10 +172,10 @@ function renderFunctionMetadata(container, m, fullId, options = {}) { bsimMetaKeys.forEach(k => { if (m[k] !== undefined) bsimHtml += renderRow(k, m[k], getIcon(k)); }); let fileHtml = ''; - fileMetaKeys.forEach(k => { + fileMetaKeys.forEach(k => { let val = m[k]; if (k === 'file_md5' && val === undefined) val = fileMd5; - if (val !== undefined) fileHtml += renderRow(k, val, getIcon(k)); + if (val !== undefined) fileHtml += renderRow(k, val, getIcon(k)); }); const cachedCollapsed = localStorage.getItem('metadata_more_collapsed'); const startCollapsed = (cachedCollapsed === null || cachedCollapsed === 'true'); @@ -180,17 +185,17 @@ function renderFunctionMetadata(container, m, fullId, options = {}) { const toggleText = startCollapsed ? 'Show More' : 'Show Less'; const cardHtml = ` -
+
- ${returnType} - ${namespace ? `${namespace}::` : ''} - ${label} - (${parameters.map(p => `${typeof p === 'object' && p !== null ? (p.name || JSON.stringify(p)) : p}`).join(', ')}) - + ${safeReturnType} + ${namespace ? `${safeNamespace}::` : ''} + ${safeLabel} + (${parameters.map(p => `${escapeHtml(typeof p === 'object' && p !== null ? (p.name || JSON.stringify(p)) : p)}`).join(', ')}) +
- @@ -198,7 +203,7 @@ function renderFunctionMetadata(container, m, fullId, options = {}) {
- @ ${addr} + @ ${safeAddr}
@@ -206,17 +211,17 @@ function renderFunctionMetadata(container, m, fullId, options = {}) {
- ${options.rightHeaderHtml || ''}
- +
- # ${fileMd5} - ${m['file_name'] || 'N/A'} + # ${safeFileMd5} + ${escapeHtml(m['file_name'] || 'N/A')}
${fileTagsHtml}
@@ -245,11 +250,11 @@ function renderFunctionMetadata(container, m, fullId, options = {}) { Clusters ${clustersHtml}
` : ''} - +
File Metadata
${fileHtml}
- +
@@ -287,15 +292,15 @@ function renderFunctionMetadata(container, m, fullId, options = {}) { function toggleMetaDetail(id, event) { const el = document.getElementById(id); if (!el) return; - + const isExpanded = el.classList.toggle('expanded'); - + // Find the toggle button that was clicked to rotate the icon let btn = event ? event.currentTarget : null; if (!btn && event) { btn = event.target.closest('.btn-details-toggle'); } - + if (btn) { btn.classList.toggle('expanded', isExpanded); const icon = btn.querySelector('.toggle-icon'); @@ -359,7 +364,7 @@ function toggleSection(headerEl) { const chevron = cCard.querySelector('.toggle-chevron'); const textSpan = cCard.querySelector('.toggle-text'); const toggleIcon = cCard.querySelector('.toggle-icon'); - + if (body) { const isCollapsed = (forceState !== undefined) ? body.classList.toggle('collapsed', forceState) : body.classList.toggle('collapsed'); if (chevron) { @@ -383,10 +388,10 @@ function toggleSection(headerEl) { toggleIcon.classList.add('fa-angles-up'); } } - + // Save state in cache localStorage.setItem('metadata_more_collapsed', isCollapsed ? 'true' : 'false'); - + return isCollapsed; } return false; @@ -394,7 +399,7 @@ function toggleSection(headerEl) { if (card) { const newState = performToggle(card); - + // Sync with other side in diff mode if (side) { const otherSide = side === 'l' ? 'r' : 'l'; diff --git a/bsimvis/app/static/js/tags.js b/bsimvis/app/static/js/tags.js index 7d8f9d2..f335e4f 100644 --- a/bsimvis/app/static/js/tags.js +++ b/bsimvis/app/static/js/tags.js @@ -1,5 +1,36 @@ let tagMetadata = {}; window.tagMetadata = tagMetadata; +if (typeof escapeHtml === 'undefined') { + window.escapeHtml = function (value) { + return String(value ?? '') + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); + }; +} +if (typeof escapeAttr === 'undefined') window.escapeAttr = window.escapeHtml; +if (typeof jsString === 'undefined') { + window.jsString = function (value) { + return JSON.stringify(String(value ?? '')) + .replace(//g, '\\u003E') + .replace(/&/g, '\\u0026') + .replace(/\u2028/g, '\\u2028') + .replace(/\u2029/g, '\\u2029'); + }; +} +if (typeof safeCssColor === 'undefined') { + window.safeCssColor = function (value, fallback = '#66d9ef') { + const color = String(value ?? '').trim(); + if (/^#[0-9a-fA-F]{3,8}$/.test(color)) return color; + if (/^rgba?\(\s*[0-9.]+%?\s*,\s*[0-9.]+%?\s*,\s*[0-9.]+%?(\s*,\s*(0|1|0?\.[0-9]+))?\s*\)$/.test(color)) return color; + if (/^hsla?\(\s*[0-9.]+(?:deg)?\s*,\s*[0-9.]+%\s*,\s*[0-9.]+%(\s*,\s*(0|1|0?\.[0-9]+))?\s*\)$/.test(color)) return color; + return fallback; + }; +} + async function fetchTagMetadata(collection) { if (!collection) return; @@ -25,16 +56,16 @@ async function fetchTagMetadata(collection) { function getRawTagColor(analysisTags, userTags = []) { const allTags = [...(analysisTags || []), ...(userTags || [])].filter(t => t && t.trim()); if (allTags.length === 0) return null; - + let bestColor = null; let maxPrio = -1; allTags.forEach(t => { let meta = tagMetadata[t]; if (t === 'bookmark') meta = { color: '#66d9ef', priority: 1000 }; if (t === 'ignore') meta = { color: '#f92672', priority: 900 }; - const color = (meta && meta.color) ? meta.color : '#66d9ef'; + const color = safeCssColor((meta && meta.color) ? meta.color : '#66d9ef'); const priority = (meta && meta.priority !== undefined) ? meta.priority : 0; - + if (priority >= maxPrio) { maxPrio = priority; bestColor = color; @@ -46,7 +77,7 @@ function getRawTagColor(analysisTags, userTags = []) { function getRowTagColor(analysisTags, userTags = []) { const colorEnabled = typeof UIParams !== 'undefined' ? UIParams.colorByTag : (localStorage.getItem('sim-color-by-tag') === 'true'); if (!colorEnabled) return ""; - + const bestColor = getRawTagColor(analysisTags, userTags); if (bestColor) { return `linear-gradient(90deg, ${bestColor}44 0%, transparent 100%)`; @@ -81,7 +112,7 @@ function refreshAllRowColors() { const analystCards = editor.querySelectorAll('.sim-tag-card'); const analysisCards = editor.querySelectorAll('.analysis-tag-badge'); - + const userTags = Array.from(analystCards).map(c => c.textContent.replace('×', '').trim()); const analysisTags = Array.from(analysisCards).map(c => c.textContent.trim()); @@ -139,10 +170,10 @@ window.showTooltip = (e, tag, coll) => { el.style.cssText = "position:fixed; z-index:20005; background:rgba(20,22,26,0.95); border:1px solid var(--border); padding:12px; border-radius:8px; box-shadow:0 10px 30px rgba(0,0,0,0.5); display:none; pointer-events:none; font-size:0.8rem; color:var(--text); backdrop-filter:blur(10px); min-width:180px;"; document.body.appendChild(el); } - - el.innerHTML = `
Loading stats for ${tag}...
`; + + el.innerHTML = `
Loading stats for ${escapeHtml(tag)}...
`; el.style.display = 'block'; - + fetch(`/api/tags/stats?collection=${coll}&tag=${encodeURIComponent(tag)}`) .then(res => res.json()) .then(stats => { @@ -151,7 +182,7 @@ window.showTooltip = (e, tag, coll) => {
- ${tag} + ${escapeHtml(tag)}
Prio: ${meta.priority || 0} @@ -165,7 +196,7 @@ window.showTooltip = (e, tag, coll) => {
Right-click tag to customize
`; }); - + const moveTooltip = (ev) => { let x = ev.clientX + 15; let y = ev.clientY + 15; @@ -181,7 +212,7 @@ window.getTagMetadata = (tag) => { if (tag === 'bookmark') return { color: '#66d9ef', priority: 1000 }; if (tag === 'ignore') return { color: '#f92672', priority: 900 }; const m = tagMetadata[tag] || (window.parent && window.parent.tagMetadata && window.parent.tagMetadata[tag]); - if (m) return m; + if (m) return { ...m, color: safeCssColor(m.color) }; const palette = ["#FF5555", "#50FA7B", "#F1FA8C", "#BD93F9", "#FF79C6", "#8BE9FD", "#FFB86C", "#A6E22E", "#66D9EF", "#FFD700", "#FF69B4", "#7B68EE", "#48D1CC", "#00FF7F", "#F4A460"]; let hash = 0; for (let i = 0; i < tag.length; i++) hash = tag.charCodeAt(i) + ((hash << 5) - hash); return { color: palette[Math.abs(hash) % palette.length], priority: 0 }; @@ -198,8 +229,9 @@ window.hideTooltip = () => { window.handleTagContextMenu = (e, tag) => { e.preventDefault(); const coll = typeof getCurrentCollection === 'function' ? getCurrentCollection() : 'main'; - const currentMeta = tagMetadata[tag] || { color: "#66d9ef", priority: 0 }; - + const currentMeta = { ...(tagMetadata[tag] || { color: "#66d9ef", priority: 0 }) }; + currentMeta.color = safeCssColor(currentMeta.color); + let menu = document.getElementById('tag-custom-context-menu'); if (!menu) { menu = document.createElement('div'); @@ -207,22 +239,22 @@ window.handleTagContextMenu = (e, tag) => { menu.style.cssText = "position:fixed; z-index:20010; background:var(--card-bg); border:1px solid var(--border); border-radius:8px; box-shadow:0 15px 35px rgba(0,0,0,0.6); display:none; overflow:hidden; width:220px; font-family:var(--font-main, inherit);"; document.body.appendChild(menu); } - + menu.innerHTML = `
- Tag: ${tag} + Tag: ${escapeHtml(tag)}
- +
- + @@ -251,7 +283,7 @@ window.handleTagContextMenu = (e, tag) => { saveBtn.innerText = "Saving..."; const newColor = colorPicker.color.hexString; const newPrio = parseInt(prioSlider.value); - + try { await Promise.all([ fetch('/api/tags/color', { @@ -265,7 +297,7 @@ window.handleTagContextMenu = (e, tag) => { body: JSON.stringify({collection: coll, tag: tag, priority: newPrio}) }) ]); - + // Force immediate metadata sync const updatedMeta = { color: newColor, priority: newPrio }; tagMetadata[tag] = updatedMeta; @@ -290,7 +322,7 @@ window.handleTagContextMenu = (e, tag) => { } menu.style.display = 'none'; - + } catch (err) { console.error("Failed to save tag metadata", err); saveBtn.disabled = false; @@ -299,16 +331,16 @@ window.handleTagContextMenu = (e, tag) => { }; menu.style.display = 'block'; - + // Position handling let x = e.clientX; let y = e.clientY; if (x + 240 > window.innerWidth) x -= 240; if (y + 350 > window.innerHeight) y -= 350; - + menu.style.left = x + 'px'; menu.style.top = y + 'px'; - + const closeMenu = (me) => { if (!menu.contains(me.target)) { menu.style.display = 'none'; @@ -323,49 +355,49 @@ const renderTagEditor = (etype, eid, tagsList, userTagsList, options = {}) => { const isIgnored = userTagsList.includes('ignore'); const editorClass = etype === 'similarity' ? 'sim-tags-editor' : 'entity-tags-editor'; const bookmarkOnClick = etype === 'similarity' - ? `toggleBookmark(event, '${eid}')` - : `toggleEntityBookmark(event, '${etype}', '${eid}')`; + ? `toggleBookmark(event, ${jsString(eid)})` + : `toggleEntityBookmark(event, ${jsString(etype)}, ${jsString(eid)})`; const ignoreOnClick = etype === 'similarity' - ? `toggleIgnore(event, '${eid}')` - : `toggleEntityIgnore(event, '${etype}', '${eid}')`; + ? `toggleIgnore(event, ${jsString(eid)})` + : `toggleEntityIgnore(event, ${jsString(etype)}, ${jsString(eid)})`; + + const addOnClick = `startAddTag(event, ${jsString(etype)}, ${jsString(eid)})`; - const addOnClick = `startAddTag(event, '${etype}', '${eid}')`; + const analysisHtml = tagsList.map(t => `${escapeHtml(t)}`).join(''); - const analysisHtml = tagsList.map(t => `${t}`).join(''); - const userHtml = userTagsList.map(t => { if (t === 'bookmark' || t === 'ignore') return ''; const meta = tagMetadata[t] || { color: '#66d9ef' }; - const color = meta.color; - const removeClick = `removeTag(event, '${etype}', '${eid}', '${t}')`; + const color = safeCssColor(meta.color); + const removeClick = `removeTag(event, ${jsString(etype)}, ${jsString(eid)}, ${jsString(t)})`; const coll = typeof getCurrentCollection === 'function' ? getCurrentCollection() : 'main'; - + return ` - - ${t} - × + oncontextmenu="handleTagContextMenu(event, ${escapeAttr(jsString(t))})"> + ${escapeHtml(t)} + × `; }).join(''); return ` -
- - ${analysisHtml} ${userHtml} - +
`; }; @@ -373,22 +405,22 @@ window.applyClusterFilter = (uuid) => { const targetWindow = (window.parent && window.parent !== window) ? window.parent : window; const hash = targetWindow.location.hash || '#collections'; const isSim = hash.startsWith('#function-similarity'); - + const targetHashPath = isSim ? '#function-similarity' : '#functions'; const inputId = isSim ? 'flt-sim-cluster' : 'flt-function-cluster'; - + let input = targetWindow.document.getElementById(inputId); if (!input) { const currentHash = targetWindow.location.hash || `#functions`; const [path, query] = currentHash.split('?'); const params = new URLSearchParams(query || ''); params.set('cluster_uuid', uuid); - + const currentParams = new URLSearchParams(targetWindow.location.hash.split('?')[1] || ''); if (currentParams.has('collection')) { params.set('collection', currentParams.get('collection')); } - + targetWindow.location.hash = `${targetHashPath}?${params.toString()}`; } else { input.value = uuid; @@ -434,7 +466,7 @@ window.moveClusterCardTooltip = function(e) { const targetWindow = (window.parent && window.parent !== window) ? window.parent : window; const tooltip = targetWindow.document.getElementById('hierarchy-tooltip'); if (!tooltip || tooltip.style.display !== 'block') return; - + if (targetWindow !== window) { let iframeId = 'code-frame'; if (window.location.pathname.includes('/diff/')) iframeId = 'diff-frame'; @@ -451,24 +483,24 @@ window.moveClusterCardTooltip = function(e) { return; } } - + const container = e.target.closest('.cluster-cards-container'); if (container) { const overflow = container.querySelector('.cluster-overflow-box'); const isOverflowVisible = overflow && window.getComputedStyle(overflow).display !== 'none'; const boxRect = (isOverflowVisible && overflow) ? overflow.getBoundingClientRect() : container.getBoundingClientRect(); const tooltipRect = tooltip.getBoundingClientRect(); - + let x = boxRect.right + 15; let y = boxRect.top; - + if (x + tooltipRect.width > window.innerWidth) { x = boxRect.left - tooltipRect.width - 15; } if (y + tooltipRect.height > window.innerHeight) { y = Math.max(10, window.innerHeight - tooltipRect.height - 15); } - + tooltip.style.left = x + 'px'; tooltip.style.top = y + 'px'; } @@ -476,11 +508,11 @@ window.moveClusterCardTooltip = function(e) { window.renderClusterCards = (clusters) => { if (!clusters || clusters.length === 0) return ''; - + const threshold = typeof UIParams !== 'undefined' ? UIParams.cohesionThreshold : 0.5; const validClusters = clusters.filter(c => (c.cohesion_score || 0) >= threshold); if (validClusters.length === 0) return ''; - + const sorted = [...validClusters].sort((a, b) => (b.cohesion_score || 0) - (a.cohesion_score || 0)); const renderCard = (c, isHidden = false) => { const name = c.cluster_name || `Cluster ${c.cluster_id}`; @@ -488,29 +520,29 @@ window.renderClusterCards = (clusters) => { const uuid = c.cluster_uuid; const hue = Math.max(0, Math.min(120, (c.cohesion_score || 0) * 120)); const color = `hsl(${hue}, 100%, 65%)`; - + const cardClass = isHidden ? 'tag-card cluster-card cluster-hidden' : 'tag-card cluster-card'; - + return ` - - ${name} - ${c.member_count || 0} + ${escapeHtml(name)} + ${Number(c.member_count || 0)} `; }; const hasMore = sorted.length > 1; const moreHtml = hasMore ? ` - +${sorted.length - 1} ` : ''; @@ -693,7 +725,7 @@ function attachAutocomplete(input, level, field, onSelect) { div.className = 'tag-suggestion-item'; div.style.display = 'flex'; div.style.justifyContent = 'space-between'; - div.innerHTML = `${item.value} ${item.count}`; + div.innerHTML = `${escapeHtml(item.value)} ${escapeHtml(item.count)}`; div.onmousedown = (e) => { e.preventDefault(); onSelect(item.value); @@ -983,7 +1015,7 @@ async function confirmAddTag(etype, eid, tag, container) { function updateUIForTagAdd(editors, tag) { const meta = tagMetadata[tag] || { color: '#66d9ef' }; - const color = meta.color; + const color = safeCssColor(meta.color); const isBookmark = (tag === 'bookmark'); const isIgnore = (tag === 'ignore'); const coll = typeof getCurrentCollection === 'function' ? getCurrentCollection() : 'main'; @@ -1005,14 +1037,14 @@ function updateUIForTagAdd(editors, tag) { card.style.color = color; card.style.background = color + '11'; card.style.cursor = 'pointer'; - + // Add event handlers for tooltip and context menu - card.setAttribute('onmouseenter', `showTooltip(event, '${tag}', '${coll}')`); + card.setAttribute('onmouseenter', `showTooltip(event, ${jsString(tag)}, ${jsString(coll)})`); card.setAttribute('onmouseleave', 'hideTooltip()'); - card.setAttribute('oncontextmenu', `handleTagContextMenu(event, '${tag}')`); + card.setAttribute('oncontextmenu', `handleTagContextMenu(event, ${jsString(tag)})`); - const removeClick = `removeTag(event, '${editor.dataset.etype}', '${editor.dataset.eid}', '${tag}')`; - card.innerHTML = `${tag} ×`; + const removeClick = `removeTag(event, ${jsString(editor.dataset.etype)}, ${jsString(editor.dataset.eid)}, ${jsString(tag)})`; + card.innerHTML = `${escapeHtml(tag)} ×`; const addBtn = editor.querySelector('.add-tag-btn'); if (addBtn) editor.insertBefore(card, addBtn); else editor.appendChild(card); diff --git a/bsimvis/app/static/js/utils.js b/bsimvis/app/static/js/utils.js index 02d187e..462cf9c 100644 --- a/bsimvis/app/static/js/utils.js +++ b/bsimvis/app/static/js/utils.js @@ -1,5 +1,39 @@ // Shared utilities for BSimVis +function escapeHtml(value) { + return String(value ?? '') + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); +} + +function escapeAttr(value) { + return escapeHtml(value); +} + +function jsString(value) { + return JSON.stringify(String(value ?? '')) + .replace(//g, '\\u003E') + .replace(/&/g, '\\u0026') + .replace(/\u2028/g, '\\u2028') + .replace(/\u2029/g, '\\u2029'); +} + +function safeCssClassPart(value) { + return String(value ?? '').replace(/[^a-zA-Z0-9_-]/g, '_'); +} + +function safeCssColor(value, fallback = '#66d9ef') { + const color = String(value ?? '').trim(); + if (/^#[0-9a-fA-F]{3,8}$/.test(color)) return color; + if (/^rgba?\(\s*[0-9.]+%?\s*,\s*[0-9.]+%?\s*,\s*[0-9.]+%?(\s*,\s*(0|1|0?\.[0-9]+))?\s*\)$/.test(color)) return color; + if (/^hsla?\(\s*[0-9.]+(?:deg)?\s*,\s*[0-9.]+%\s*,\s*[0-9.]+%(\s*,\s*(0|1|0?\.[0-9]+))?\s*\)$/.test(color)) return color; + return fallback; +} + function formatDate(iso) { if (!iso || iso === 'N/A') return '---'; if (typeof iso === 'string' && /^\d+$/.test(iso)) {