From d135dff5cd0dee76ece9ccb4c46362e1a298c9c0 Mon Sep 17 00:00:00 2001 From: Gmer4Lfe Date: Thu, 6 Aug 2026 22:08:10 -0400 Subject: [PATCH] An unescaped quote ended the onclick attribute early, so clicking an error line did nothing --- Plugin/unraid/pages/scheduler.php | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/Plugin/unraid/pages/scheduler.php b/Plugin/unraid/pages/scheduler.php index 2970178..6c26f37 100644 --- a/Plugin/unraid/pages/scheduler.php +++ b/Plugin/unraid/pages/scheduler.php @@ -1159,6 +1159,15 @@ function vvEscHtml(s) { return s.replace(/&/g,'&').replace(//g,'>'); } +// For text going INSIDE a double-quoted attribute, which vvEscHtml does not cover: it leaves " +// alone, and a quote there ends the attribute early and silently destroys the handler after it. +// That is not a theoretical hazard — it shipped, and an onclick built from JSON.stringify() output +// was truncated to "vvErrOpenAtLine(" and did nothing at all when clicked. +function vvEscAttr(s) { + return String(s).replace(/&/g,'&').replace(/"/g,'"') + .replace(//g,'>'); +} + function vvPost(url, data) { const params = new URLSearchParams({csrf_token, ...data}); return fetch(url, { @@ -2787,16 +2796,20 @@ function vvUpdateErrors(errors) { const now = Math.floor(Date.now() / 1000); let html = '
'; for (const e of unacked) { - const jobId = "'" + (e.script + '.sh').replace(/\\/g,"\\\\").replace(/'/g,"\\'") + "'"; - // JSON.stringify rather than the hand-rolled quote escaping above: this one is embedded in an - // attribute alongside the error text, which is arbitrary log output and not to be trusted to - // contain no quotes. - const scriptJs = vvEscHtml(JSON.stringify(e.script)); - const lineJs = vvEscHtml(JSON.stringify(e.line)); + // JSON.stringify for correct JS literals, then attribute-escaped: the error text is arbitrary + // log output and absolutely will contain quotes eventually. + const scriptJs = vvEscAttr(JSON.stringify(e.script)); + const lineJs = vvEscAttr(JSON.stringify(e.line)); const label = e.script.split('/').pop().replace(/_/g, ' '); html += '
' + '
' - + '' + vvEscHtml(label) + '' + // The name goes to the same place as the text below it. Two clickable halves of one row + // behaving differently — one acking, one not — is how an error looks acknowledged when + // it is not, which is worse than either behaviour on its own. + + '' + + vvEscHtml(label) + '' + '' + vvFmtAge(now - e.ts) + '' + '' + '
'