From 8d2de1b7f09e2162f7c14ab8902011b034b63461 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcos=20C=C3=A1ceres?= Date: Wed, 16 Feb 2022 15:52:09 +1100 Subject: [PATCH] feat(w3c/headers): validate header links --- src/w3c/headers.js | 53 +++++++++++++++++++---------- src/w3c/templates/cgbg-headers.js | 4 +-- tests/spec/w3c/headers-spec.js | 55 ++++++++++++++++++++----------- 3 files changed, 73 insertions(+), 39 deletions(-) diff --git a/src/w3c/headers.js b/src/w3c/headers.js index b6962e2ffd..db0937e74d 100644 --- a/src/w3c/headers.js +++ b/src/w3c/headers.js @@ -393,11 +393,22 @@ export async function run(conf) { } if (conf.isEd) conf.thisVersion = conf.edDraftURI; - if (conf.isCGBG) validateCGBG(conf); + if (conf.isCGBG) { + validateCGBG(conf); + } + if (conf.isTagEditorFinding && !conf.latestVersion) { + conf.latestVersion = null; + } if (conf.latestVersion !== null) { conf.latestVersion = conf.latestVersion ? w3Url(conf.latestVersion) : w3Url(`${pubSpace}/${conf.shortName}/`); + const exists = await resourceExists(conf.latestVersion); + if (!exists && conf.specStatus !== "FPWD") { + const msg = `The "Latest published version:" header link points to a URL that does not exist.`; + const hint = docLink`Check that the ${"[shortname]"} is correct and you are using the right ${"[specStatus]"} for this kind of document.`; + showWarning(msg, name, { hint }); + } } if (conf.latestVersion) validateIfAllowedOnTR(conf); @@ -474,7 +485,9 @@ export async function run(conf) { conf.publishISODate = conf.publishDate.toISOString(); conf.shortISODate = ISODate.format(conf.publishDate); validatePatentPolicies(conf); - await deriveHistoryURI(conf); + + conf.historyURI = await deriveHistoryURI(conf); + if (conf.isTagEditorFinding) { delete conf.thisVersion; delete conf.latestVersion; @@ -668,7 +681,6 @@ function validateIfAllowedOnTR(conf) { const msg = docLink`Documents with a status of \`"${conf.specStatus}"\` can't be published on the W3C's /TR/ (Technical Report) space.`; const hint = docLink`Ask a W3C Team Member for a W3C URL where the report can be published and change ${"[latestVersion]"} to something else.`; showError(msg, name, { hint }); - return; } } @@ -694,9 +706,6 @@ function derivePubSpace(conf) { function validateCGBG(conf) { const reportType = status2text[conf.specStatus]; - const latestVersionURL = conf.latestVersion - ? new URL(w3Url(conf.latestVersion)) - : null; if (!conf.wg) { const msg = docLink`The ${"[group]"} configuration option is required for this kind of document (${reportType}).`; @@ -704,8 +713,16 @@ function validateCGBG(conf) { return; } + if (conf.specStatus.endsWith("-DRAFT") && !conf.latestVersion) { + conf.latestVersion = null; + return; + } + // Deal with final reports if (conf.isCGFinal) { + const latestVersionURL = conf.latestVersion + ? new URL(w3Url(conf.latestVersion)) + : null; // Final report require a w3.org URL. const isW3C = latestVersionURL?.origin === "https://www.w3.org" || @@ -720,18 +737,17 @@ function validateCGBG(conf) { } async function deriveHistoryURI(conf) { - if (!conf.shortName || conf.historyURI === null || !conf.latestVersion) { - return; // Nothing to do + if (!conf.shortName || !conf.latestVersion) { + return null; } const canShowHistory = conf.isEd || trStatus.includes(conf.specStatus); - if (conf.historyURI && !canShowHistory) { - const msg = docLink`The ${"[historyURI]"} can't be used with non /TR/ documents.`; + if (!canShowHistory && conf.historyURI) { + const msg = docLink`The ${"[historyURI]"} can't be used with non-standards track documents.`; const hint = docLink`Please remove ${"[historyURI]"}.`; showError(msg, name, { hint }); - conf.historyURI = null; - return; + return conf.historyURI; } const historyURL = new URL( @@ -753,13 +769,16 @@ async function deriveHistoryURI(conf) { // Do a fetch HEAD request to see if the history exists... // We don't discriminate... if it's on the W3C website with a history, // we show it. + const exists = await resourceExists(historyURL); + return exists ? historyURL.href : null; +} + +async function resourceExists(url) { try { - const response = await fetch(historyURL, { method: "HEAD" }); - if (response.ok) { - conf.historyURI = response.url; - } + const response = await fetch(url, { method: "HEAD" }); + return response.ok; } catch { - // Ignore fetch errors + return false; } } diff --git a/src/w3c/templates/cgbg-headers.js b/src/w3c/templates/cgbg-headers.js index 03a2585587..0db26c6514 100644 --- a/src/w3c/templates/cgbg-headers.js +++ b/src/w3c/templates/cgbg-headers.js @@ -37,10 +37,10 @@ export default (conf, options) => { > ` : ""} - ${"latestVersion" in conf // latestVersion can be falsy + ${conf.latestVersion !== null ? html`
${l10n.latest_published_version}
- ${conf.latestVersion + ${conf.latestVersion !== "" ? html`${conf.latestVersion}` diff --git a/tests/spec/w3c/headers-spec.js b/tests/spec/w3c/headers-spec.js index 21b85eb7cd..f706e84258 100644 --- a/tests/spec/w3c/headers-spec.js +++ b/tests/spec/w3c/headers-spec.js @@ -16,9 +16,11 @@ import { makeDefaultBody, makeRSDoc, makeStandardOps, + warningFilters, } from "../SpecHelper.js"; const headerErrors = errorFilters.filter("w3c/headers"); +const headerWarnings = warningFilters.filter("w3c/headers"); const defaultErrors = errorFilters.filter("w3c/defaults"); const findContent = string => { @@ -62,26 +64,27 @@ describe("W3C — Headers", () => { expect(exportedDoc.querySelector(".head details[open]")).toBeTruthy(); }); - it("links to the 'kinds of documents' only for W3C documents", async () => { - const statuses = ["FPWD", "WD", "CR", "CRD", "PR", "REC", "NOTE"]; - for (const specStatus of statuses) { + for (const specStatus of recTrackStatus) { + it(`links to the 'kinds of documents' only for W3C documents with status ${specStatus}`, async () => { const doc = await makeRSDoc( makeStandardOps({ specStatus, group: "webapps" }) ); const w3cLink = doc.querySelector( `.head a[href='https://www.w3.org/standards/types#${specStatus}']` ); - expect(w3cLink).withContext(`specStatus: ${specStatus}`).toBeTruthy(); - } + expect(w3cLink).toBeTruthy(); + }); + } - for (const specStatus of ["unofficial", "base"]) { + for (const specStatus of noTrackStatus) { + it(`doesn't link to the 'kinds of documents' for non-rec track ${specStatus}`, async () => { const doc = await makeRSDoc(makeStandardOps({ specStatus })); const w3cLink = doc.querySelector( ".head a[href='https://www.w3.org/standards/types#UD']" ); - expect(w3cLink).withContext(`specStatus: ${specStatus}`).toBeNull(); - } - }); + expect(w3cLink).toBeNull(); + }); + } describe("prevRecShortname & prevRecURI", () => { it("takes prevRecShortname and prevRecURI into account", async () => { @@ -1437,6 +1440,21 @@ describe("W3C — Headers", () => { expect(latestVersionLink.textContent).toBe("https://www.w3.org/TR/foo/"); }); + it("warns if latestVersion URL doesn't exist", async () => { + const ops = makeStandardOps({ + shortName: "foo", + specStatus: "WD", + group: "webapps", + github: "w3c/respec", + }); + const doc = await makeRSDoc(ops); + const warnings = headerWarnings(doc); + expect(warnings).toHaveSize(1); + expect(warnings[0].message).toContain( + `The "Latest published version:" header link points to a URL that does not exist` + ); + }); + it("allows skipping latest published version link in initial ED", async () => { const ops = makeStandardOps({ specStatus: "ED", @@ -1543,7 +1561,7 @@ describe("W3C — Headers", () => { ); }); - for (const specStatus of cgbgStatus.filter(s => s.endsWith("-DRAFT"))) { + for (const specStatus of cgStatus.filter(s => s.endsWith("-DRAFT"))) { it(`doesn't set latestVersion URL for ${specStatus} status`, async () => { const ops = makeStandardOps({ shortName: "some-report", @@ -1551,11 +1569,7 @@ describe("W3C — Headers", () => { group: "wicg", }); const doc = await makeRSDoc(ops); - const terms = [...doc.querySelectorAll(".head dt")]; - const latestVersion = terms.find( - el => el.textContent.trim() === "Latest published version:" - ); - expect(latestVersion).toHaveSize(0); + expect(contains(doc, "dt", "Latest published version:")).toHaveSize(0); }); } for (const specStatus of noTrackStatus) { @@ -1989,7 +2003,7 @@ describe("W3C — Headers", () => { { specStatus: "BG-FINAL", group: "publishingbg" }, ]; for (const { specStatus, group } of finalReportStatus) { - it("requires that the ${specStatus} latestVersion be a w3c URL", async () => { + it(`requires that the ${specStatus} latestVersion be a w3c URL`, async () => { const ops = makeStandardOps({ specStatus, group, @@ -2550,6 +2564,7 @@ describe("W3C — Headers", () => { const ops = makeStandardOps({ shortName: "payment-request", specStatus: "ED", + group: "payments", }); const doc = await makeRSDoc(ops); const [history] = contains(doc, ".head dt", "History:"); @@ -2562,8 +2577,8 @@ describe("W3C — Headers", () => { ); }); - for (const specStatus of trStatus) { - it(`includes the history for "${specStatus}" rec-track status`, async () => { + for (const specStatus of recTrackStatus) { + it(`includes the history for rec-track "${specStatus}" docs`, async () => { const shortName = `push-api`; const ops = makeStandardOps({ shortName, @@ -2575,9 +2590,9 @@ describe("W3C — Headers", () => { expect(history).withContext(specStatus).toBeTruthy(); expect(history.nextElementSibling).withContext(specStatus).toBeTruthy(); const historyLink = history.nextElementSibling.querySelector("a"); - expect(historyLink).toBeTruthy(); + expect(historyLink).withContext(specStatus).toBeTruthy(); expect(historyLink.href).toBe( - `https://www.w3.org/standards/history/${shortName}/` + `https://www.w3.org/standards/history/${shortName}` ); }); }