Escape what the monitor page renders, and put the helpers where every page can reach them
The page interpolated media titles, partner hostnames read over the mesh, and docker folder names straight into innerHTML — its own header claimed otherwise, and the helpers that would have fixed it were defined in a page it never loads. Container WebUI values now get a scheme check before they reach window.open().
This commit is contained in:
@@ -31,6 +31,16 @@
|
||||
// against real inventory.
|
||||
//
|
||||
// All remote and container-supplied strings render escaped.
|
||||
// Through vvEscHtml()/vvEscAttr() from Varaverk.page — media titles and usernames from
|
||||
// Emby/Jellyfin/Plex, partner hostnames and versions read over the mesh, docker folder
|
||||
// names, VM names, UPS and GPU model strings. This line claimed to be true before any of
|
||||
// it was: the page had no escaping at all, and the helpers it needed lived in a page it
|
||||
// never loads.
|
||||
//
|
||||
// A container WebUI value is scheme-checked before it is a link.
|
||||
// include/docker_folders.php allows only http/https out of the template XML, and
|
||||
// vvSafeUrl() filters again at window.open(), where a javascript: URL would run with
|
||||
// this page's origin.
|
||||
//
|
||||
// RENDERS
|
||||
// System header, CPU per core, memory breakdown, GPU cards, storage pools and array disks,
|
||||
@@ -477,7 +487,7 @@ function vvRenderMemory(mem) {
|
||||
|
||||
const procs = mem.top_procs ?? [];
|
||||
const procStrip = procs.map(p =>
|
||||
`<span style="white-space:nowrap;">${p.name} <span style="color:#aaa;font-weight:600;">${vvFmtGib(p.kb)}</span></span>`
|
||||
`<span style="white-space:nowrap;">${vvEscHtml(p.name)} <span style="color:#aaa;font-weight:600;">${vvFmtGib(p.kb)}</span></span>`
|
||||
).join('<span style="color:#333;margin:0 5px;">·</span>');
|
||||
|
||||
let html = `<div style="display:flex;justify-content:space-between;align-items:baseline;margin-bottom:10px;">
|
||||
@@ -664,7 +674,7 @@ function vvDiskRow(disk) {
|
||||
: `<span style="color:#555;font-size:10px;">${vvFmt(disk.used_gb)} / ${vvFmt(disk.size_gb)}</span>`;
|
||||
return `<div style="margin-bottom:7px;">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;font-size:11px;margin-bottom:3px;">
|
||||
<span style="color:${nameColor};display:flex;align-items:center;">${disk.name}${spinLabel}${failLabel}${vvIoChip(disk.device)}</span>
|
||||
<span style="color:${nameColor};display:flex;align-items:center;">${vvEscHtml(disk.name)}${spinLabel}${failLabel}${vvIoChip(disk.device)}</span>
|
||||
${right}
|
||||
<span style="color:${tempColor};font-size:10px;margin-left:6px;flex-shrink:0;">${tempStr}</span>
|
||||
</div>
|
||||
@@ -741,7 +751,7 @@ function vvPollMonitor() {
|
||||
document.getElementById('vv-system-body').innerHTML =
|
||||
`<div style="display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:8px;">
|
||||
<div style="min-width:0;">
|
||||
<div style="font-size:14px;font-weight:700;color:#ddd;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">${sys.name}</div>
|
||||
<div style="font-size:14px;font-weight:700;color:#ddd;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">${vvEscHtml(sys.name)}</div>
|
||||
<div style="font-size:10px;color:#555;margin-top:2px;">${sys.comment || ' '}</div>
|
||||
</div>
|
||||
<div style="display:flex;align-items:flex-start;gap:6px;flex-shrink:0;margin-left:6px;">
|
||||
@@ -771,9 +781,9 @@ function vvPollMonitor() {
|
||||
<div style="font-size:20px;font-weight:300;color:#ccc;line-height:1;">${timeStr}</div>
|
||||
<div style="font-size:10px;color:#555;margin-bottom:10px;">${dateStr} · ${tz}</div>
|
||||
<div style="display:grid;grid-template-columns:auto 1fr;gap:3px 8px;font-size:11px;">
|
||||
<span style="color:#444;">Model</span> <span style="color:#888;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">${sys.cpu_model}${_coreMeta}</span>
|
||||
<span style="color:#444;">Array</span> <span style="color:${arrayColor};font-weight:600;">${sys.array_state}</span>
|
||||
<span style="color:#444;">Uptime</span> <span style="color:#888;">${sys.uptime}</span>
|
||||
<span style="color:#444;">Model</span> <span style="color:#888;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">${vvEscHtml(sys.cpu_model)}${_coreMeta}</span>
|
||||
<span style="color:#444;">Array</span> <span style="color:${arrayColor};font-weight:600;">${vvEscHtml(sys.array_state)}</span>
|
||||
<span style="color:#444;">Uptime</span> <span style="color:#888;">${vvEscHtml(sys.uptime)}</span>
|
||||
<span style="color:#444;">Load</span> <span style="color:${_loadColor};">${_loadStr}</span>
|
||||
<span style="color:#444;">Running</span> <span style="color:#888;">${_runningCtrs} ctr${_runningCtrs !== 1 ? 's' : ''}${_runningVMs > 0 ? ` · ${_runningVMs} VM` : ''}</span>
|
||||
<span style="color:#444;">Version</span> <span style="color:#3a3a3a;">${ver}</span>
|
||||
@@ -812,8 +822,8 @@ function vvPollMonitor() {
|
||||
const ramAvail = rs.mem_used_pct > 0;
|
||||
const cpuHue = cpuAvail ? Math.round(120 * (1 - rs.cpu_load / 100)) : 0;
|
||||
const memHue = ramAvail ? Math.round(120 * (1 - rs.mem_used_pct / 100)) : 0;
|
||||
const cpuStr = cpuAvail ? `<span style="color:hsl(${cpuHue},70%,45%);font-weight:600;">${rs.cpu_load}%${rs.cpu_threads ? `<span style="color:#333;font-weight:400;"> · ${rs.cpu_threads}t</span>` : ''}</span>` : `<span style="color:#333;">—</span>`;
|
||||
const ramStr = ramAvail ? `<span style="color:hsl(${memHue},70%,45%);font-weight:600;">${rs.mem_used_pct}%${rs.mem_total_gb ? `<span style="color:#333;font-weight:400;"> · ${rs.mem_total_gb}G</span>` : ''}</span>` : `<span style="color:#333;">—</span>`;
|
||||
const cpuStr = cpuAvail ? `<span style="color:hsl(${cpuHue},70%,45%);font-weight:600;">${vvEscHtml(rs.cpu_load)}%${rs.cpu_threads ? `<span style="color:#333;font-weight:400;"> · ${vvEscHtml(rs.cpu_threads)}t</span>` : ''}</span>` : `<span style="color:#333;">—</span>`;
|
||||
const ramStr = ramAvail ? `<span style="color:hsl(${memHue},70%,45%);font-weight:600;">${vvEscHtml(rs.mem_used_pct)}%${rs.mem_total_gb ? `<span style="color:#333;font-weight:400;"> · ${vvEscHtml(rs.mem_total_gb)}G</span>` : ''}</span>` : `<span style="color:#333;">—</span>`;
|
||||
const arrColor = rs.array_state === 'Started' || rs.array_state === 'STARTED' ? '#4caf50' : '#f44336';
|
||||
const uptimeStr = rs.uptime && rs.uptime !== '—' ? rs.uptime : '—';
|
||||
|
||||
@@ -823,18 +833,18 @@ function vvPollMonitor() {
|
||||
const verMismatch = myVer && remoteVer && myVer !== remoteVer;
|
||||
const verWarn = verMismatch
|
||||
? `<div style="font-size:10px;color:#ff9800;margin-top:4px;padding:3px 6px;background:#1a1000;border:1px solid #3a2800;border-radius:3px;">
|
||||
⚠ Version mismatch: local ${myVer} · remote ${remoteVer}<br>
|
||||
⚠ Version mismatch: local ${vvEscHtml(myVer)} · remote ${vvEscHtml(remoteVer)}<br>
|
||||
<span style="color:#555;">Script sync ops are gated until versions match</span>
|
||||
</div>` : '';
|
||||
|
||||
const verRow = remoteVer ? `<span style="color:#444;">unRAID</span><span style="color:#3a3a3a;grid-column:span 3;">${remoteVer}</span>` : '';
|
||||
const verRow = remoteVer ? `<span style="color:#444;">unRAID</span><span style="color:#3a3a3a;grid-column:span 3;">${vvEscHtml(remoteVer)}</span>` : '';
|
||||
statsHtml = `<div style="display:grid;grid-template-columns:auto 1fr auto 1fr;gap:2px 8px;font-size:10px;margin-top:5px;margin-bottom:2px;">
|
||||
<span style="color:#444;">CPU</span>${cpuStr}
|
||||
<span style="color:#444;">RAM</span>${ramStr}
|
||||
<span style="color:#444;">Array</span>
|
||||
<span style="color:${arrColor};font-weight:600;">${rs.array_state}</span>
|
||||
<span style="color:${arrColor};font-weight:600;">${vvEscHtml(rs.array_state)}</span>
|
||||
<span style="color:#444;">Uptime</span>
|
||||
<span style="color:#555;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">${uptimeStr}</span>
|
||||
<span style="color:#555;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">${vvEscHtml(uptimeStr)}</span>
|
||||
${verRow}
|
||||
</div>${verWarn}`;
|
||||
} else if (rs && rs.no_api_key) {
|
||||
@@ -846,9 +856,9 @@ function vvPollMonitor() {
|
||||
ptHtml += `<div style="margin-bottom:8px;padding-bottom:8px;border-bottom:1px solid #222;">
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;">
|
||||
<div>
|
||||
<span style="font-size:10px;color:#555;margin-right:4px;">${h.id}</span>
|
||||
<span style="font-size:12px;color:#ccc;font-weight:500;">${h.owner || h.hostname}</span>${tags}
|
||||
<div style="font-size:10px;color:#555;margin-top:1px;">${h.hostname}</div>
|
||||
<span style="font-size:10px;color:#555;margin-right:4px;">${vvEscHtml(h.id)}</span>
|
||||
<span style="font-size:12px;color:#ccc;font-weight:500;">${vvEscHtml(h.owner || h.hostname)}</span>${tags}
|
||||
<div style="font-size:10px;color:#555;margin-top:1px;">${vvEscHtml(h.hostname)}</div>
|
||||
${onboardBadge}
|
||||
</div>
|
||||
<span style="color:${dot};font-size:10px;white-space:nowrap;">● ${label}</span>
|
||||
@@ -952,13 +962,13 @@ function vvPollMonitor() {
|
||||
if (fbActive.length) {
|
||||
fbActive.forEach(group => {
|
||||
fbHtml += `<div style="font-size:10px;color:#555;margin-bottom:4px;letter-spacing:.03em;">
|
||||
COVERING ${group.hostname}
|
||||
COVERING ${vvEscHtml(group.hostname)}
|
||||
</div>`;
|
||||
group.containers.forEach(c => {
|
||||
const img = c.image.includes('/') ? c.image.split('/').pop() : c.image;
|
||||
fbHtml += `<div style="display:flex;justify-content:space-between;align-items:center;
|
||||
background:#1a1a1a;border-radius:4px;padding:4px 8px;margin-bottom:3px;">
|
||||
<span style="color:#ccc;font-size:11px;font-weight:500;">${c.name}</span>
|
||||
<span style="color:#ccc;font-size:11px;font-weight:500;">${vvEscHtml(c.name)}</span>
|
||||
<span style="color:#444;font-size:9px;margin-left:8px;white-space:nowrap;">${img}</span>
|
||||
</div>`;
|
||||
});
|
||||
@@ -1022,8 +1032,8 @@ function vvPollMonitor() {
|
||||
|
||||
document.getElementById('vv-ups-body').innerHTML =
|
||||
`<div class="vv-banner ${statCls}" style="margin-bottom:8px;">
|
||||
<span>${ups.status}${onBatt ? ' — ON BATTERY' : ''}</span>
|
||||
<span style="font-size:11px;font-weight:400;opacity:0.8;">${ups.model}</span>
|
||||
<span>${vvEscHtml(ups.status)}${onBatt ? ' — ON BATTERY' : ''}</span>
|
||||
<span style="font-size:11px;font-weight:400;opacity:0.8;">${vvEscHtml(ups.model)}</span>
|
||||
</div>
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:8px;">
|
||||
<div>
|
||||
@@ -1293,7 +1303,7 @@ function vvPollMonitor() {
|
||||
const diff = now - r.ts;
|
||||
const ago = diff < 3600 ? Math.floor(diff / 60) + 'm' : Math.floor(diff / 3600) + 'h';
|
||||
html += `<div style="display:flex;justify-content:space-between;font-size:10px;color:#666;margin-bottom:2px;">
|
||||
<span style="white-space:nowrap;overflow:hidden;text-overflow:ellipsis;flex:1;">${r.name}</span>
|
||||
<span style="white-space:nowrap;overflow:hidden;text-overflow:ellipsis;flex:1;">${vvEscHtml(r.name)}</span>
|
||||
<span style="flex-shrink:0;margin-left:6px;color:#444;">${ago}</span>
|
||||
</div>`;
|
||||
});
|
||||
@@ -1494,7 +1504,7 @@ function vvPollMonitor() {
|
||||
html += `<div style="background:#0d1a0a;border:1px solid #1a3a0a;border-radius:3px;
|
||||
padding:4px 8px;margin-bottom:5px;">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:3px;">
|
||||
<span style="font-size:10px;color:#8bc34a;font-weight:600;">⟳ ${a.profile}</span>
|
||||
<span style="font-size:10px;color:#8bc34a;font-weight:600;">⟳ ${vvEscHtml(a.profile)}</span>
|
||||
<span style="font-size:10px;color:#6a8a4a;">${_dur(sec)}</span>
|
||||
</div>
|
||||
<div style="height:3px;background:#0a0a0a;border-radius:2px;overflow:hidden;">
|
||||
@@ -1529,7 +1539,7 @@ function vvPollMonitor() {
|
||||
|
||||
html += `<div style="margin-bottom:4px;">
|
||||
<div style="display:flex;justify-content:space-between;align-items:baseline;margin-bottom:2px;">
|
||||
<span style="font-size:10px;color:#555;width:46px;flex-shrink:0;">${meta.label}</span>
|
||||
<span style="font-size:10px;color:#555;width:46px;flex-shrink:0;">${vvEscHtml(meta.label)}</span>
|
||||
<span style="font-size:9px;color:#333;flex:1;text-align:right;margin-right:6px;">
|
||||
${s?.duration ? _dur(s.duration) : ''}</span>
|
||||
<span style="font-size:9px;color:#2a2a2a;width:28px;text-align:right;margin-right:5px;">
|
||||
@@ -1607,7 +1617,7 @@ function vvPollMonitor() {
|
||||
bodyEl.innerHTML =
|
||||
// header row: name + process count pill
|
||||
`<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:8px;">
|
||||
<div style="font-size:11px;color:#888;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;flex:1;">${gpu.name}</div>
|
||||
<div style="font-size:11px;color:#888;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;flex:1;">${vvEscHtml(gpu.name)}</div>
|
||||
<div style="margin-left:8px;background:${procColor}22;border:1px solid ${procColor};color:${procColor};
|
||||
padding:1px 8px;border-radius:10px;font-size:11px;white-space:nowrap;">${procCount} proc${procCount !== 1 ? 's' : ''}</div>
|
||||
</div>` +
|
||||
@@ -1699,7 +1709,7 @@ function vvPollMonitor() {
|
||||
const meth = s.method.replace('Transcode', 'TC').replace('Direct ', '');
|
||||
return `<div style="display:flex;align-items:center;gap:5px;margin-bottom:4px;min-width:0;overflow:hidden;">
|
||||
${vvSrvIcon(s.server_type, s.server)}
|
||||
<span style="font-size:10px;color:#888;flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">${s.title}</span>
|
||||
<span style="font-size:10px;color:#888;flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">${vvEscHtml(s.title)}</span>
|
||||
<span style="font-size:9px;color:#555;flex-shrink:0;">${typeLabel(s.type)}</span>
|
||||
<span style="font-size:9px;color:#ff9800;flex-shrink:0;">${meth}</span>
|
||||
</div>`;
|
||||
@@ -1799,15 +1809,15 @@ function vvPollFast() {
|
||||
const peakTx = Math.max(...vvNetTxHistory, 0);
|
||||
|
||||
const ipRows = [
|
||||
net.local_ip ? `<div><span style="color:#555;font-size:9px;">LAN </span>${net.local_ip}</div>` : '',
|
||||
net.ext_ip ? `<div><span style="color:#555;font-size:9px;">EXT </span>${net.ext_ip}</div>` : '',
|
||||
net.ts_ip ? `<div><span style="color:#555;font-size:9px;">TS </span>${net.ts_ip}</div>` : '',
|
||||
net.local_ip ? `<div><span style="color:#555;font-size:9px;">LAN </span>${vvEscHtml(net.local_ip)}</div>` : '',
|
||||
net.ext_ip ? `<div><span style="color:#555;font-size:9px;">EXT </span>${vvEscHtml(net.ext_ip)}</div>` : '',
|
||||
net.ts_ip ? `<div><span style="color:#555;font-size:9px;">TS </span>${vvEscHtml(net.ts_ip)}</div>` : '',
|
||||
].filter(Boolean).join('');
|
||||
|
||||
document.getElementById('vv-network-body').innerHTML =
|
||||
`<div style="display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:8px;">
|
||||
<div>
|
||||
<div style="font-size:12px;color:#888;margin-bottom:4px;">${net.iface} · ${linkLabel}</div>
|
||||
<div style="font-size:12px;color:#888;margin-bottom:4px;">${vvEscHtml(net.iface)} · ${linkLabel}</div>
|
||||
<div style="display:flex;gap:16px;font-size:13px;font-weight:600;">
|
||||
<span><span style="color:#4caf50;font-size:9px;margin-right:4px;">━ IN (RX)</span><span style="color:#4caf50;">${vvFmtBps(rx)}</span><span style="color:#444;font-size:9px;font-weight:400;margin-left:4px;">peak ${vvFmtBps(peakRx)}</span></span>
|
||||
<span><span style="color:#ff9800;font-size:9px;margin-right:4px;">━ OUT (TX)</span><span style="color:#ff9800;">${vvFmtBps(tx)}</span><span style="color:#444;font-size:9px;font-weight:400;margin-left:4px;">peak ${vvFmtBps(peakTx)}</span></span>
|
||||
@@ -1886,8 +1896,8 @@ function vvRenderStreams() {
|
||||
const badges = names.map(n => {
|
||||
const cnt = serverCounts[n] ?? 0;
|
||||
return cnt > 0
|
||||
? `<span class="vv-server-badge">${n} <b style="color:#ccc;">${cnt}</b></span>`
|
||||
: `<span class="vv-server-badge" style="color:#444;">${n}</span>`;
|
||||
? `<span class="vv-server-badge">${vvEscHtml(n)} <b style="color:#ccc;">${cnt}</b></span>`
|
||||
: `<span class="vv-server-badge" style="color:#444;">${vvEscHtml(n)}</span>`;
|
||||
}).join('');
|
||||
|
||||
// Per-chip shade: alternate bg brightness within a group to visually separate chips
|
||||
@@ -1902,8 +1912,11 @@ function vvRenderStreams() {
|
||||
const bg = (shades[cls] ?? ['',''])[i % 2];
|
||||
return bg ? ` style="background:${bg};"` : '';
|
||||
}
|
||||
// Escapes here rather than at the four call sites: every one passes plain text, and the labels
|
||||
// are not all ours — an unrecognised codec falls through vvCodecLabel() as the media server
|
||||
// spelled it, and the device type is derived from the client string the player reports.
|
||||
function vvChip(cls, label, i) {
|
||||
return `<span class="${cls}"${vvChipShade(cls, i)}>${label}</span>`;
|
||||
return `<span class="${cls}"${vvChipShade(cls, i)}>${vvEscHtml(label)}</span>`;
|
||||
}
|
||||
|
||||
// Device type summary — e.g. "3 Android 1 iOS 2 Roku"
|
||||
@@ -2008,11 +2021,11 @@ function vvRenderStreams() {
|
||||
return `<div>
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;font-size:11px;margin-bottom:2px;">
|
||||
<span style="color:${s.paused ? '#fdd835' : '#aaa'};white-space:nowrap;overflow:hidden;text-overflow:ellipsis;flex:1;">
|
||||
<span style="color:${iconColor};">${icon}</span> ${s.title}</span>
|
||||
<span style="color:#444;font-size:10px;margin-left:6px;flex-shrink:0;">${s.server}</span>
|
||||
<span style="color:${iconColor};">${icon}</span> ${vvEscHtml(s.title)}</span>
|
||||
<span style="color:#444;font-size:10px;margin-left:6px;flex-shrink:0;">${vvEscHtml(s.server)}</span>
|
||||
</div>
|
||||
<div style="display:flex;justify-content:space-between;font-size:10px;color:#555;margin-bottom:3px;">
|
||||
<span>${s.user}</span>
|
||||
<span>${vvEscHtml(s.user)}</span>
|
||||
${timeStr}
|
||||
</div>
|
||||
<div style="background:#1a1a1a;border-radius:3px;height:6px;overflow:hidden;">
|
||||
@@ -2223,7 +2236,14 @@ function vvToggleContainer(name) {
|
||||
}
|
||||
|
||||
function vvDockerAction(action, name, webui) {
|
||||
if (action === 'webui') { window.open(webui, '_blank'); return; }
|
||||
// Filtered again at the point of use, not only where the button was built. This value originates
|
||||
// in a container's template XML, and window.open() on a javascript: URL runs it with this page's
|
||||
// origin — the one sink where an unchecked scheme is not merely a broken link.
|
||||
if (action === 'webui') {
|
||||
const u = vvSafeUrl(webui);
|
||||
if (u) window.open(u, '_blank', 'noopener');
|
||||
return;
|
||||
}
|
||||
if (action === 'edit') {
|
||||
window.location.href = '/Docker?action=template&xmlTemplate=' +
|
||||
encodeURIComponent('/boot/config/plugins/dockerMan/templates-user/my-' + name + '.xml') + '&update=true';
|
||||
@@ -2270,7 +2290,7 @@ function vvRenderDockerFolders(data) {
|
||||
html += `<div class="vv-df-vm-row">
|
||||
<span class="vv-df-vm-icon">${osIcon(vm.os)}</span>
|
||||
<span style="width:7px;height:7px;border-radius:50%;background:${sc};flex-shrink:0;${pulse}"></span>
|
||||
<span class="vv-df-cname">${vm.name}</span>
|
||||
<span class="vv-df-cname">${vvEscHtml(vm.name)}</span>
|
||||
<span style="font-size:11px;font-weight:600;color:${sc};flex-shrink:0;">${stateLabel(vm.state)}</span>
|
||||
${meta}
|
||||
</div>`;
|
||||
@@ -2288,8 +2308,14 @@ function vvRenderDockerFolders(data) {
|
||||
const pulse = c.running ? 'animation:vv-pulse-dot 1s ease-in-out infinite;' : '';
|
||||
const active = vvDfActive === c.name;
|
||||
const sShort = c.status ? c.status.replace(/^Up\s+/, '').split(' ').slice(0,2).join(' ') : '—';
|
||||
const sn = c.name.replace(/\\/g,'\\\\').replace(/'/g,"\\'");
|
||||
const sw = (c.webui||'').replace(/\\/g,'\\\\').replace(/'/g,"\\'");
|
||||
// Escaped for two nested contexts at once: a JS string literal, and the double-quoted onclick
|
||||
// attribute holding it. The previous version did the first half only — \ and ' — which leaves
|
||||
// a " free to close the attribute and destroy every handler after it. Docker's own charset
|
||||
// makes that unreachable through a container name, but the WebUI value comes from template
|
||||
// XML and is under no such constraint.
|
||||
const jsq = v => vvEscAttr(String(v ?? '').replace(/\\/g,'\\\\').replace(/'/g,"\\'"));
|
||||
const sn = jsq(c.name);
|
||||
const sw = jsq(vvSafeUrl(c.webui));
|
||||
|
||||
let actionBar = '';
|
||||
if (active) {
|
||||
@@ -2309,8 +2335,8 @@ function vvRenderDockerFolders(data) {
|
||||
return `<div class="vv-df-container${active ? ' vv-df-active' : ''}"
|
||||
onclick="event.stopPropagation();vvToggleContainer('${sn}')">
|
||||
<span class="vv-df-dot" style="background:${dot};${pulse}"></span>
|
||||
<span class="vv-df-cname">${c.name}</span>
|
||||
<span class="vv-df-status">${sShort}</span>
|
||||
<span class="vv-df-cname">${vvEscHtml(c.name)}</span>
|
||||
<span class="vv-df-status">${vvEscHtml(sShort)}</span>
|
||||
</div>${actionBar}`;
|
||||
}
|
||||
|
||||
@@ -2320,20 +2346,20 @@ function vvRenderDockerFolders(data) {
|
||||
const running = f.containers.filter(c => c.running).length;
|
||||
const bColor = running === total ? '#4caf50' : running === 0 ? '#555' : '#ff9800';
|
||||
const badge = `<span style="font-size:10px;color:${bColor};flex-shrink:0;margin-left:auto;">${running}/${total}</span>`;
|
||||
const sid = f.id.replace(/\\/g,'\\\\').replace(/'/g,"\\'");
|
||||
const sid = vvEscAttr(String(f.id ?? '').replace(/\\/g,'\\\\').replace(/'/g,"\\'"));
|
||||
|
||||
let iconHtml = '';
|
||||
if (f.isEmoji) {
|
||||
iconHtml = `<span style="font-size:11px;flex-shrink:0;">${f.icon}</span>`;
|
||||
} else if (f.icon) {
|
||||
iconHtml = `<img src="${f.icon}" style="width:13px;height:13px;object-fit:contain;border-radius:2px;flex-shrink:0;" onerror="this.style.display='none'">`;
|
||||
iconHtml = `<span style="font-size:11px;flex-shrink:0;">${vvEscHtml(f.icon)}</span>`;
|
||||
} else if (vvSafeUrl(f.icon)) {
|
||||
iconHtml = `<img src="${vvEscAttr(vvSafeUrl(f.icon))}" style="width:13px;height:13px;object-fit:contain;border-radius:2px;flex-shrink:0;" onerror="this.style.display='none'">`;
|
||||
}
|
||||
|
||||
let out = `<div class="vv-df-folder">
|
||||
<div class="vv-df-folder-hdr" onclick="vvToggleFolder('${sid}')">
|
||||
<span class="vv-df-chevron">${open ? '▾' : '▸'}</span>
|
||||
${iconHtml}
|
||||
<span class="vv-df-fname">${f.name}</span>
|
||||
<span class="vv-df-fname">${vvEscHtml(f.name)}</span>
|
||||
${badge}
|
||||
</div>`;
|
||||
if (open) {
|
||||
|
||||
Reference in New Issue
Block a user