Sweep every onclick built from data: an unescaped quote silently killed nine handlers
This commit is contained in:
@@ -591,6 +591,18 @@ function _dur(s) {
|
||||
return sec + 's';
|
||||
}
|
||||
|
||||
// Escapes text going INSIDE a double-quoted HTML attribute. A bare " there ends the attribute
|
||||
// early and silently destroys everything after it — an onclick built by string concatenation
|
||||
// becomes a syntax error and the element simply stops responding, with nothing logged.
|
||||
//
|
||||
// Duplicated from scheduler.php rather than shared: js/varaverk.js is loaded after the page
|
||||
// partials, so a page cannot rely on it during its own setup. Four lines in two places beats a
|
||||
// load-order bug that only shows up on a slow load.
|
||||
function vvRyEscAttr(s) {
|
||||
return String(s).replace(/&/g,'&').replace(/"/g,'"')
|
||||
.replace(/</g,'<').replace(/>/g,'>');
|
||||
}
|
||||
|
||||
// ── Status + active card ──────────────────────────────────────────────────────
|
||||
function _statusCard(data) {
|
||||
const en = data.enabled;
|
||||
@@ -1207,14 +1219,14 @@ function vvRyEBrowse(key) {
|
||||
let h = '';
|
||||
if (d.parent) {
|
||||
const pn = d.parent === '/' ? '/' : (d.parent.replace(/^.*\//,'') || d.parent) + '/';
|
||||
h += `<div class="vv-ms-dir parent" onclick="vvRyENavTo('${key}',${JSON.stringify(d.parent)})">↑ ${pn}</div>`;
|
||||
h += `<div class="vv-ms-dir parent" onclick="vvRyENavTo('${key}',${vvRyEscAttr(JSON.stringify(d.parent))})">↑ ${pn}</div>`;
|
||||
}
|
||||
if (!d.dirs.length) {
|
||||
h += '<div class="vv-ms-dir" style="color:#2a2a2a;cursor:default;">— empty —</div>';
|
||||
} else {
|
||||
for (const dir of d.dirs) {
|
||||
const name = dir.replace(/^.*\//,'') || dir;
|
||||
h += `<div class="vv-ms-dir" onclick="vvRyENavTo('${key}',${JSON.stringify(dir)})" title="${dir}">▶ ${name}</div>`;
|
||||
h += `<div class="vv-ms-dir" onclick="vvRyENavTo('${key}',${vvRyEscAttr(JSON.stringify(dir))})" title="${vvRyEscAttr(dir)}">▶ ${name}</div>`;
|
||||
}
|
||||
}
|
||||
dirsEl.innerHTML = h;
|
||||
@@ -1653,7 +1665,7 @@ function _vvMsRenderDirs(dirsElId, dirs, parent, navFn) {
|
||||
|
||||
if (parent !== null) {
|
||||
const pname = parent === '/' ? '/' : (parent.replace(/^.*\//, '') || parent) + '/';
|
||||
html += `<div class="vv-ms-dir parent" onclick="${navFn}(${JSON.stringify(parent)})">↑ ${pname}</div>`;
|
||||
html += `<div class="vv-ms-dir parent" onclick="${navFn}(${vvRyEscAttr(JSON.stringify(parent))})">↑ ${pname}</div>`;
|
||||
}
|
||||
|
||||
if (!dirs.length) {
|
||||
@@ -1661,7 +1673,7 @@ function _vvMsRenderDirs(dirsElId, dirs, parent, navFn) {
|
||||
} else {
|
||||
for (const d of dirs) {
|
||||
const name = d.replace(/^.*\//, '') || d;
|
||||
html += `<div class="vv-ms-dir" onclick="${navFn}(${JSON.stringify(d)})" title="${d}">▶ ${name}</div>`;
|
||||
html += `<div class="vv-ms-dir" onclick="${navFn}(${vvRyEscAttr(JSON.stringify(d))})" title="${vvRyEscAttr(d)}">▶ ${name}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1985,21 +1985,21 @@ function _vvImpRender(d) {
|
||||
|
||||
if (d.parent !== null) {
|
||||
const pname = d.parent === '/' ? '/' : (d.parent.replace(/^.*\//, '') || d.parent) + '/';
|
||||
html += `<div class="vv-imp-row" onclick="_vvImpBrowse(${JSON.stringify(d.parent)})"
|
||||
html += `<div class="vv-imp-row" onclick="_vvImpBrowse(${vvEscAttr(JSON.stringify(d.parent))})"
|
||||
style="padding:5px 12px;font-size:11px;color:#444;font-style:italic;cursor:pointer;font-family:monospace;">↑ ${pname}</div>`;
|
||||
}
|
||||
|
||||
for (const dir of d.dirs) {
|
||||
const name = dir.replace(/^.*\//, '') || dir;
|
||||
html += `<div class="vv-imp-row" onclick="_vvImpBrowse(${JSON.stringify(dir)})" title="${_vvImpEsc(dir)}"
|
||||
html += `<div class="vv-imp-row" onclick="_vvImpBrowse(${vvEscAttr(JSON.stringify(dir))})" title="${vvEscAttr(dir)}"
|
||||
style="padding:5px 12px;font-size:11px;color:#666;cursor:pointer;font-family:monospace;">▶ ${_vvImpEsc(name)}</div>`;
|
||||
}
|
||||
|
||||
for (const file of d.files) {
|
||||
const name = file.replace(/^.*\//, '') || file;
|
||||
const sel = file === _vvImpSelected;
|
||||
html += `<div class="vv-imp-row vv-imp-file${sel ? ' vv-imp-file-sel' : ''}" data-path="${_vvImpEsc(file)}"
|
||||
onclick="_vvImpSelectFile(${JSON.stringify(file)})" title="${_vvImpEsc(file)}"
|
||||
html += `<div class="vv-imp-row vv-imp-file${sel ? ' vv-imp-file-sel' : ''}" data-path="${vvEscAttr(file)}"
|
||||
onclick="_vvImpSelectFile(${vvEscAttr(JSON.stringify(file))})" title="${vvEscAttr(file)}"
|
||||
style="padding:5px 12px;font-size:11px;cursor:pointer;font-family:monospace;
|
||||
color:${sel ? '#4caf50' : '#4a9eff'};background:${sel ? '#0f1f0f' : 'transparent'};">📄 ${_vvImpEsc(name)}</div>`;
|
||||
}
|
||||
@@ -2677,8 +2677,8 @@ function vvUpdateLocks(locks) {
|
||||
html += '<div class="vv-lock-row">'
|
||||
+ '<span class="vv-lk-name">' + vvEscHtml(lk.name) + '</span>'
|
||||
+ '<span class="vv-lk-age">' + vvFmtAge(lk.age) + '</span>'
|
||||
+ '<button class="vv-btn-sm vv-lock-clear" onclick="vvClearLock(\''
|
||||
+ vvEscHtml(lk.file) + '\',this)">Clear</button>'
|
||||
+ '<button class="vv-btn-sm vv-lock-clear" onclick="vvClearLock('
|
||||
+ vvEscAttr(JSON.stringify(lk.file)) + ',this)">Clear</button>'
|
||||
+ '</div>';
|
||||
}
|
||||
body.innerHTML = html + '</div>';
|
||||
@@ -2811,7 +2811,7 @@ function vvUpdateErrors(errors) {
|
||||
+ ' onclick="vvErrOpenAtLine(' + scriptJs + ',' + e.ts + ',' + lineJs + ',this)">'
|
||||
+ vvEscHtml(label) + '</span>'
|
||||
+ '<span class="vv-err-age">' + vvFmtAge(now - e.ts) + '</span>'
|
||||
+ '<button class="vv-btn-sm vv-ack-btn" onclick="vvAckError(\'' + e.script.replace(/\\/g,"\\\\").replace(/'/g,"\\'") + "'," + e.ts + ',this)">Ack</button>'
|
||||
+ '<button class="vv-btn-sm vv-ack-btn" onclick="vvAckError(' + scriptJs + ',' + e.ts + ',this)">Ack</button>'
|
||||
+ '</div>'
|
||||
+ '<div class="vv-err-line vv-err-line-open"'
|
||||
+ ' title="Open the log at this line — also acknowledges it"'
|
||||
@@ -3173,10 +3173,10 @@ function vvLoadRecentActivity() {
|
||||
? '<button class="vv-activity-ask' + (bad ? ' vv-ask-bad' : '') + '"'
|
||||
+ ' title="' + (bad ? 'Ask the assistant why this run failed'
|
||||
: 'Ask the assistant how this run went') + '"'
|
||||
+ ' onclick="vvAiAskRun(' + JSON.stringify(r.id) + ',' + bad + ',event)">'
|
||||
+ ' onclick="vvAiAskRun(' + vvEscAttr(JSON.stringify(r.id)) + ',' + bad + ',event)">'
|
||||
+ (bad ? 'why?' : 'recap') + '</button>'
|
||||
: '';
|
||||
html += '<div class="vv-activity-row" onclick="vvOpenRight(' + JSON.stringify(r.id) + ')">'
|
||||
html += '<div class="vv-activity-row" onclick="vvOpenRight(' + vvEscAttr(JSON.stringify(r.id)) + ')">'
|
||||
+ '<span class="vv-activity-dot ' + cls + '">●</span>'
|
||||
+ '<span class="vv-activity-label">' + vvEscHtml(r.label) + '</span>'
|
||||
+ '<span class="vv-activity-ago">' + vvTimeAgo(now - r.start) + '</span>'
|
||||
|
||||
Reference in New Issue
Block a user