Files
computer-playbook/roles/web-app-docs/files/javascript/versions.js
T
kevinveenbirkenbachandClaude Opus 5.5 6e045bced8 fix(web-app-docs): encode line breaks in redirects, navigate by built URLs
- py/http-response-splitting (server.py:87): _redirect percent-encodes CR
  and LF in the Location header
- js/xss-through-dom (versions.js:82, languages.js:25): both switchers
  navigate to the URL they built instead of select.value

The new cases in test_server.py, versions.test.js and languages.test.js
fail without the fix and pass with it.

🎯 Fix: 92 %
💥 Break: 3 %
💸 Cost: –
⏱️ Time: –
🛡️ Security: 100 %
🏆 Score: 89
🚦 Gate: ✅ 90/5, cost and time <= 100, security 100 %

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 16:54:27 +00:00

93 lines
3.0 KiB
JavaScript

(function () {
const LABELS = {
ready: "ready",
missing: "not built yet",
queued: "queued",
building: "building",
failed: "failed",
};
const POLL_MS = 2000;
function versionUrl(name, page) {
return page ? `/${name}/${page}.html` : `/${name}/`;
}
function describe(version) {
const label = LABELS[version.state];
return version.phase && version.state !== "ready" ? `${label}: ${version.phase}` : label;
}
function optionLabel(version) {
return version.built ? version.name : `${version.name} (${LABELS[version.state]})`;
}
function fillSwitcher(select, versions) {
const { current, page } = select.dataset;
const urls = versions.map((version) => versionUrl(version.name, page));
select.replaceChildren(
...versions.map((version, index) => {
const option = document.createElement("option");
option.value = urls[index];
option.textContent = optionLabel(version);
option.selected = version.name === current;
return option;
}),
);
select.onchange = () => {
window.location.href = urls[select.selectedIndex];
};
}
function fillOverview(tbody, versions) {
tbody.replaceChildren(
...versions.map((version) => {
const link = document.createElement("a");
link.href = versionUrl(version.name);
link.textContent = version.name;
const bar = document.createElement("progress");
bar.max = 100;
bar.value = version.progress;
const row = document.createElement("tr");
[link, document.createTextNode(describe(version)), bar].forEach((child) => {
const cell = document.createElement("td");
cell.append(child);
row.append(cell);
});
return row;
}),
);
}
function followBuild(section, versions) {
const version = versions.find((candidate) => candidate.name === section.dataset.build);
if (!version) {
return;
}
if (version.built) {
window.location.reload();
return;
}
section.querySelector("progress").value = version.progress;
section.querySelector("[data-phase]").textContent = describe(version);
section.querySelector("pre").textContent = version.log.join("\n");
}
async function refresh() {
const response = await fetch("/api/versions", { cache: "no-store" });
const versions = await response.json();
document.querySelectorAll("select.docs-version-switcher").forEach((select) => fillSwitcher(select, versions));
document.querySelectorAll("tbody[data-versions]").forEach((tbody) => fillOverview(tbody, versions));
document.querySelectorAll("section[data-build]").forEach((section) => followBuild(section, versions));
}
function start() {
refresh();
if (document.querySelector("tbody[data-versions], section[data-build]")) {
window.setInterval(refresh, POLL_MS);
}
}
window.docsVersions = { versionUrl, describe, optionLabel, fillSwitcher, fillOverview, followBuild, refresh };
document.addEventListener("DOMContentLoaded", start);
})();