diff --git a/client/src/constants.ts b/client/src/constants.ts index 1bc2fa606ec3..751abcf06809 100644 --- a/client/src/constants.ts +++ b/client/src/constants.ts @@ -25,37 +25,15 @@ export const AUTOCOMPLETE_SEARCH_WIDGET = JSON.parse( // We could encode the list in the SSR rendering but that means the client side // code needs to depend on having access to the `window` global first. export const VALID_LOCALES = new Set([ - "ar", - "bg", - "bn", - "ca", "de", - "el", "en-US", "es", - "fa", - "fi", "fr", - "he", - "hi-IN", - "hu", - "id", - "it", "ja", - "kab", "ko", - "ms", - "my", - "nl", "pl", "pt-BR", - "pt-PT", "ru", - "sv-SE", - "th", - "tr", - "uk", - "vi", "zh-CN", "zh-TW", ]); diff --git a/deployer/aws-lambda/content-origin-request/index.js b/deployer/aws-lambda/content-origin-request/index.js index c02d0a732f73..4b41fa70fc7f 100644 --- a/deployer/aws-lambda/content-origin-request/index.js +++ b/deployer/aws-lambda/content-origin-request/index.js @@ -105,12 +105,14 @@ exports.handler = async (event) => { return redirect(`/${request.uri.replace(/^\/+/g, "")}`); } - const { url, status } = resolveFundamental(request.uri); + let { url, status } = resolveFundamental(request.uri); if (url) { - // TODO: Do we want to add the query string to the redirect? - // If we decide we do, then we probably need to change - // the caching policy on the "*/docs/* behavior" to - // cache based on the query strings as well. + // NOTE: The query string is not forwarded for document requests, + // as directed by their origin request policy, so it's safe to + // assume "request.querystring" is empty for document requests. + if (request.querystring) { + url += (url.includes("?") ? "&" : "?") + request.querystring; + } return redirect(url, { status, cacheControlSeconds: THIRTY_DAYS, @@ -127,6 +129,7 @@ exports.handler = async (event) => { const path = request.uri.endsWith("/") ? request.uri.slice(0, -1) : request.uri; + // Note that "getLocale" only returns valid locales, never a retired locale. const locale = getLocale(request); // The only time we actually want a trailing slash is when the URL is just // the locale. E.g. `/en-US/` (not `/en-US`) diff --git a/deployer/aws-lambda/content-origin-request/server.test.js b/deployer/aws-lambda/content-origin-request/server.test.js index 39d8e74474dd..7ecf299df0b3 100644 --- a/deployer/aws-lambda/content-origin-request/server.test.js +++ b/deployer/aws-lambda/content-origin-request/server.test.js @@ -207,3 +207,34 @@ describe("redirect double-slash prefix URIs", () => { expect(r.headers["location"]).toBe("/blablabla"); }); }); + +describe("retired locale redirects", () => { + it("should 302 redirect a retired locale (accept-language)", async () => { + const r = await get("/", { + "Accept-language": "sv-SE", + }); + expect(r.statusCode).toBe(302); + expect(r.headers["location"]).toBe("/en-US/"); + }); + it("should 302 redirect a retired locale (preferredlocale cookie)", async () => { + const r = await get("/docs/Web/HTTP", { + Cookie: "preferredlocale=it", + }); + expect(r.statusCode).toBe(302); + expect(r.headers["location"]).toBe("/en-US/docs/Web/HTTP"); + }); + it("should 302 redirect a retired locale (no query string)", async () => { + const r = await get("/sv-SE/docs/Web/HTML"); + expect(r.statusCode).toBe(302); + expect(r.headers["location"]).toBe( + "/en-US/docs/Web/HTML?retiredLocale=sv-SE" + ); + }); + it("should 302 redirect a retired locale (query string, improper locale)", async () => { + const r = await get("/BN/search?q=video"); + expect(r.statusCode).toBe(302); + expect(r.headers["location"]).toBe( + "/en-US/search?retiredLocale=bn&q=video" + ); + }); +}); diff --git a/libs/constants/index.js b/libs/constants/index.js index 79781a519221..3c5618edf0af 100644 --- a/libs/constants/index.js +++ b/libs/constants/index.js @@ -1,38 +1,43 @@ const VALID_LOCALES = new Map( + [ + "de", + "en-US", + "es", + "fr", + "ja", + "ko", + "pl", + "pt-BR", + "ru", + "zh-CN", + "zh-TW", + ].map((x) => [x.toLowerCase(), x]) +); + +const RETIRED_LOCALES = new Map( [ "ar", "bg", "bn", "ca", - "de", "el", - "en-US", - "es", "fa", "fi", - "fr", "he", "hi-IN", "hu", "id", "it", - "ja", "kab", - "ko", "ms", "my", "nl", - "pl", - "pt-BR", "pt-PT", - "ru", "sv-SE", "th", "tr", "uk", "vi", - "zh-CN", - "zh-TW", ].map((x) => [x.toLowerCase(), x]) ); @@ -41,7 +46,7 @@ const DEFAULT_LOCALE = "en-US"; const LOCALE_ALIASES = new Map([ // Case is not important on either the keys or the values. ["en", "en-us"], - ["pt", "pt-PT"], // Note! Portugal Portugese is the default + ["pt", "pt-br"], ["cn", "zh-cn"], ["zh", "zh-cn"], ["zh-hans", "zh-cn"], @@ -64,6 +69,7 @@ const ACTIVE_LOCALES = new Set([ module.exports = { ACTIVE_LOCALES, VALID_LOCALES, + RETIRED_LOCALES, DEFAULT_LOCALE, LOCALE_ALIASES, PREFERRED_LOCALE_COOKIE_NAME, diff --git a/libs/fundamental-redirects/index.js b/libs/fundamental-redirects/index.js index 5343752b8859..1e37d29589ea 100644 --- a/libs/fundamental-redirects/index.js +++ b/libs/fundamental-redirects/index.js @@ -1,4 +1,9 @@ -const { VALID_LOCALES, LOCALE_ALIASES } = require("../constants"); +const { + DEFAULT_LOCALE, + VALID_LOCALES, + LOCALE_ALIASES, + RETIRED_LOCALES, +} = require("../constants"); const startRe = /^\^?\/?/; const startTemplate = /^\//; @@ -65,12 +70,12 @@ for (const locale of VALID_LOCALES.keys()) { } for (const [alias, correct] of LOCALE_ALIASES) { - // E.g. things like `en` -> `en-us` or `pt` -> `pt-pt` + // E.g. things like `en` -> `en-us` or `pt` -> `pt-br` fixableLocales.set(alias, correct); } -// All things like `/en_Us/docs/...` -> `/en-US/docs/...` const LOCALE_PATTERNS = [ + // All things like `/en_Us/docs/...` -> `/en-US/docs/...` redirect( new RegExp( `^(?${Array.from(fixableLocales.keys()).join( @@ -92,6 +97,21 @@ const LOCALE_PATTERNS = [ }, { permanent: true } ), + // Retired locales + redirect( + new RegExp( + `^(?${Array.from(RETIRED_LOCALES.keys()).join( + "|" + )})(/(?.*)|$)`, + "i" + ), + ({ locale, suffix }) => { + const join = suffix && suffix.includes("?") ? "&" : "?"; + return `/${DEFAULT_LOCALE}/${ + (suffix || "") + join + }retiredLocale=${RETIRED_LOCALES.get(locale.toLowerCase())}`; + } + ), ]; // Redirects/rewrites/aliases migrated from SCL3 httpd config diff --git a/testing/integration/headless/map_301.py b/testing/integration/headless/map_301.py index d3f96afd2d83..00756973905c 100644 --- a/testing/integration/headless/map_301.py +++ b/testing/integration/headless/map_301.py @@ -1069,8 +1069,8 @@ url_test("/EN-US/?next=FOO", "/en-US/?next=FOO", status_code=302), url_test("/eN-us/docs/Web", "/en-US/docs/Web", status_code=302), url_test("/eN-us/docs/Web/", "/en-US/docs/Web", status_code=302), - url_test("/eN-us/docs/Web?next=FOO", "/en-US/docs/Web?next=FOO", status_code=302), - url_test("/eN-us/docs/Web/?next=FOO", "/en-US/docs/Web?next=FOO", status_code=302), + url_test("/eN-us/docs/Web?next=FOO", "/en-US/docs/Web", status_code=302), + url_test("/eN-us/docs/Web/?next=FOO", "/en-US/docs/Web", status_code=302), url_test("/en-uS/search", "/en-US/search", status_code=302), url_test("/en-uS/search/", "/en-US/search", status_code=302), url_test("/en-Us/search?q=video", "/en-US/search?q=video", status_code=302), diff --git a/testing/integration/headless/test_redirects.py b/testing/integration/headless/test_redirects.py index 70b43145b996..3bb71f9c7458 100644 --- a/testing/integration/headless/test_redirects.py +++ b/testing/integration/headless/test_redirects.py @@ -1,5 +1,6 @@ import pytest +from . import request from utils.urls import assert_valid_url from .map_301 import ( @@ -116,3 +117,59 @@ def test_firefox_source_docs_redirects(url, base_url): def test_misc_redirects(url, base_url): url["base_url"] = base_url assert_valid_url(**url) + + +@pytest.mark.parametrize( + "retired_locale", + ( + "ar", + "bg", + "bn", + "ca", + "el", + "fa", + "fi", + "he", + "hi-IN", + "hu", + "id", + "it", + "kab", + "ms", + "my", + "nl", + "pt-PT", + "sv-SE", + "th", + "tr", + "uk", + "vi", + ), +) +@pytest.mark.parametrize( + "slug", + [ + "", + "/", + "/docs/Web", + "/docs/Web/", + "/search", + "/search/", + "/search?q=video", + "/search/?q=video", + "/signup", + "/settings", + ], +) +def test_retired_locale_redirects(base_url, slug, retired_locale): + """Ensure that requests for retired locales properly redirect.""" + resp = request("get", f"{base_url}/{retired_locale}{slug}") + assert resp.status_code == 302 + slug_parts = slug.split("?") + expected_slug = slug_parts[0].lstrip("/") + expected_qs = f"?retiredLocale={retired_locale}" + if len(slug_parts) > 1: + expected_qs += f"&{slug_parts[1]}" + assert ( + resp.headers["Location"] == f"/en-US/{expected_slug}{expected_qs}" + ), f"{resp.headers['Location']} is not /en-US/{expected_slug}{expected_qs}" diff --git a/testing/tests/developing.test.js b/testing/tests/developing.test.js index 66585fcd10e6..00519790dfb4 100644 --- a/testing/tests/developing.test.js +++ b/testing/tests/developing.test.js @@ -166,12 +166,12 @@ describe("Testing the Express server", () => { const response = await got(serverURL("/"), { followRedirect: false, headers: { - Cookie: "preferredlocale=SV-se", + Cookie: "preferredlocale=ja", "Accept-language": "fr-CH, fr;q=0.9, en;q=0.8, de;q=0.7, *;q=0.5", }, }); expect(response.statusCode).toBe(302); - expect(response.headers.location).toBe("/sv-SE/"); + expect(response.headers.location).toBe("/ja/"); }); }); diff --git a/testing/tests/headless.test.js b/testing/tests/headless.test.js index 734a053a7281..c026c73b96be 100644 --- a/testing/tests/headless.test.js +++ b/testing/tests/headless.test.js @@ -218,10 +218,10 @@ describe("Basic viewing of functional pages", () => { }); it("should suggest the en-US equivalent on non-en-US pages not found", async () => { - await page.goto(testURL("/sv-SE/docs/Web/foo")); + await page.goto(testURL("/ja/docs/Web/foo")); await expect(page).toMatch("Page not found"); - await expect(page).toMatch("/sv-SE/docs/Web/foo could not be found"); - // Simply by swapping the "sv-SE" for "en-US" it's able to find the index.json + await expect(page).toMatch("/ja/docs/Web/foo could not be found"); + // Simply by swapping the "ja" for "en-US" it's able to find the index.json // for that slug and present a link to it. await expect(page).toMatch("Good news!"); await expect(page).toMatchElement("a", { diff --git a/testing/tests/redirects.test.js b/testing/tests/redirects.test.js index e0086dc78ce0..1f6c4bf8a1b6 100644 --- a/testing/tests/redirects.test.js +++ b/testing/tests/redirects.test.js @@ -978,12 +978,14 @@ const LOCALE_ALIAS_URLS = [].concat( url_test("/en-gb/docs/Foo/bar", null, { statusCode: 404 }), url_test("/en_gb/docs/Foo/bar", null, { statusCode: 404 }), - url_test("/PT-PT/docs/Foo/bar", null, { statusCode: 404 }), + url_test("/PT-PT/docs/Foo/bar", "/en-US/docs/Foo/bar?retiredLocale=pt-PT", { + statusCode: 302, + }), url_test("/XY-PQ/docs/Foo/bar", null, { statusCode: 404 }), url_test("/en/docs/Foo/bar", "/en-US/docs/Foo/bar"), url_test("/En_uS/docs/Foo/bar", "/en-US/docs/Foo/bar"), - url_test("/pt/docs/Foo/bar", "/pt-PT/docs/Foo/bar"), + url_test("/pt/docs/Foo/bar", "/pt-BR/docs/Foo/bar"), url_test("/Fr-FR/docs/Foo/bar", "/fr/docs/Foo/bar"), url_test("/JA-JP/docs/Foo/bar", "/ja/docs/Foo/bar"), url_test("/JA-JA/docs/Foo/bar", "/ja/docs/Foo/bar"), @@ -1002,6 +1004,191 @@ const LOCALE_ALIAS_URLS = [].concat( url_test("/zh", "/zh-CN/") ); +const RETIRED_LOCALE_URLS = [].concat( + url_test("/ar", "/en-US/?retiredLocale=ar", { statusCode: 302 }), + url_test("/ar/", "/en-US/?retiredLocale=ar", { statusCode: 302 }), + url_test("/bg", "/en-US/?retiredLocale=bg", { statusCode: 302 }), + url_test("/bg/", "/en-US/?retiredLocale=bg", { statusCode: 302 }), + url_test("/bn", "/en-US/?retiredLocale=bn", { statusCode: 302 }), + url_test("/bn/", "/en-US/?retiredLocale=bn", { statusCode: 302 }), + url_test("/ca", "/en-US/?retiredLocale=ca", { statusCode: 302 }), + url_test("/ca/", "/en-US/?retiredLocale=ca", { statusCode: 302 }), + url_test("/el", "/en-US/?retiredLocale=el", { statusCode: 302 }), + url_test("/el/", "/en-US/?retiredLocale=el", { statusCode: 302 }), + url_test("/fa", "/en-US/?retiredLocale=fa", { statusCode: 302 }), + url_test("/FA/", "/en-US/?retiredLocale=fa", { statusCode: 302 }), + url_test("/fi", "/en-US/?retiredLocale=fi", { statusCode: 302 }), + url_test("/fi/", "/en-US/?retiredLocale=fi", { statusCode: 302 }), + url_test("/he", "/en-US/?retiredLocale=he", { statusCode: 302 }), + url_test("/he/", "/en-US/?retiredLocale=he", { statusCode: 302 }), + url_test("/hi-In", "/en-US/?retiredLocale=hi-IN", { statusCode: 302 }), + url_test("/hi-IN/", "/en-US/?retiredLocale=hi-IN", { statusCode: 302 }), + url_test("/hu", "/en-US/?retiredLocale=hu", { statusCode: 302 }), + url_test("/hu/", "/en-US/?retiredLocale=hu", { statusCode: 302 }), + url_test("/id", "/en-US/?retiredLocale=id", { statusCode: 302 }), + url_test("/ID/", "/en-US/?retiredLocale=id", { statusCode: 302 }), + url_test("/it", "/en-US/?retiredLocale=it", { statusCode: 302 }), + url_test("/it/", "/en-US/?retiredLocale=it", { statusCode: 302 }), + url_test("/kab", "/en-US/?retiredLocale=kab", { statusCode: 302 }), + url_test("/KaB/", "/en-US/?retiredLocale=kab", { statusCode: 302 }), + url_test("/ms", "/en-US/?retiredLocale=ms", { statusCode: 302 }), + url_test("/ms/", "/en-US/?retiredLocale=ms", { statusCode: 302 }), + url_test("/my", "/en-US/?retiredLocale=my", { statusCode: 302 }), + url_test("/my/", "/en-US/?retiredLocale=my", { statusCode: 302 }), + url_test("/nl", "/en-US/?retiredLocale=nl", { statusCode: 302 }), + url_test("/nl/", "/en-US/?retiredLocale=nl", { statusCode: 302 }), + url_test("/pt-Pt", "/en-US/?retiredLocale=pt-PT", { statusCode: 302 }), + url_test("/pt-PT/", "/en-US/?retiredLocale=pt-PT", { statusCode: 302 }), + url_test("/sv-SE", "/en-US/?retiredLocale=sv-SE", { statusCode: 302 }), + url_test("/sv-se/", "/en-US/?retiredLocale=sv-SE", { statusCode: 302 }), + url_test("/th", "/en-US/?retiredLocale=th", { statusCode: 302 }), + url_test("/th/", "/en-US/?retiredLocale=th", { statusCode: 302 }), + url_test("/tr", "/en-US/?retiredLocale=tr", { statusCode: 302 }), + url_test("/tr/", "/en-US/?retiredLocale=tr", { statusCode: 302 }), + url_test("/uk", "/en-US/?retiredLocale=uk", { statusCode: 302 }), + url_test("/uk/", "/en-US/?retiredLocale=uk", { statusCode: 302 }), + url_test("/vi", "/en-US/?retiredLocale=vi", { statusCode: 302 }), + url_test("/vi/", "/en-US/?retiredLocale=vi", { statusCode: 302 }), + url_test("/ar/docs/Web", "/en-US/docs/Web?retiredLocale=ar", { + statusCode: 302, + }), + url_test("/bg/docs/Web/", "/en-US/docs/Web/?retiredLocale=bg", { + statusCode: 302, + }), + url_test("/bn/docs/Web", "/en-US/docs/Web?retiredLocale=bn", { + statusCode: 302, + }), + url_test("/Ca/docs/Web/", "/en-US/docs/Web/?retiredLocale=ca", { + statusCode: 302, + }), + url_test("/el/docs/Web", "/en-US/docs/Web?retiredLocale=el", { + statusCode: 302, + }), + url_test("/FA/docs/Web", "/en-US/docs/Web?retiredLocale=fa", { + statusCode: 302, + }), + url_test("/fi/docs/Web", "/en-US/docs/Web?retiredLocale=fi", { + statusCode: 302, + }), + url_test("/he/docs/Web", "/en-US/docs/Web?retiredLocale=he", { + statusCode: 302, + }), + url_test("/hi-IN/docs/Web", "/en-US/docs/Web?retiredLocale=hi-IN", { + statusCode: 302, + }), + url_test("/hu/docs/Web", "/en-US/docs/Web?retiredLocale=hu", { + statusCode: 302, + }), + url_test("/ID/docs/Web", "/en-US/docs/Web?retiredLocale=id", { + statusCode: 302, + }), + url_test("/it/docs/Web", "/en-US/docs/Web?retiredLocale=it", { + statusCode: 302, + }), + url_test("/KaB/docs/Web", "/en-US/docs/Web?retiredLocale=kab", { + statusCode: 302, + }), + url_test("/ms/docs/Web", "/en-US/docs/Web?retiredLocale=ms", { + statusCode: 302, + }), + url_test("/my/docs/Web", "/en-US/docs/Web?retiredLocale=my", { + statusCode: 302, + }), + url_test("/nl/docs/Web", "/en-US/docs/Web?retiredLocale=nl", { + statusCode: 302, + }), + url_test("/pt-PT/docs/Web", "/en-US/docs/Web?retiredLocale=pt-PT", { + statusCode: 302, + }), + url_test("/sv-se/docs/Web", "/en-US/docs/Web?retiredLocale=sv-SE", { + statusCode: 302, + }), + url_test("/th/docs/Web", "/en-US/docs/Web?retiredLocale=th", { + statusCode: 302, + }), + url_test("/tr/docs/Web", "/en-US/docs/Web?retiredLocale=tr", { + statusCode: 302, + }), + url_test("/uk/docs/Web", "/en-US/docs/Web?retiredLocale=uk", { + statusCode: 302, + }), + url_test("/vi/docs/Web", "/en-US/docs/Web?retiredLocale=vi", { + statusCode: 302, + }), + url_test("/ar/search?q=video", "/en-US/search?q=video&retiredLocale=ar", { + statusCode: 302, + }), + url_test("/bg/search?q=video", "/en-US/search?q=video&retiredLocale=bg", { + statusCode: 302, + }), + url_test("/bn/search?q=video", "/en-US/search?q=video&retiredLocale=bn", { + statusCode: 302, + }), + url_test("/Ca/search?q=video", "/en-US/search?q=video&retiredLocale=ca", { + statusCode: 302, + }), + url_test("/el/search?q=video", "/en-US/search?q=video&retiredLocale=el", { + statusCode: 302, + }), + url_test("/FA/search?q=video", "/en-US/search?q=video&retiredLocale=fa", { + statusCode: 302, + }), + url_test("/fi/search?q=video", "/en-US/search?q=video&retiredLocale=fi", { + statusCode: 302, + }), + url_test("/he/search?q=video", "/en-US/search?q=video&retiredLocale=he", { + statusCode: 302, + }), + url_test( + "/hi-IN/search?q=video", + "/en-US/search?q=video&retiredLocale=hi-IN", + { statusCode: 302 } + ), + url_test("/hu/search?q=video", "/en-US/search?q=video&retiredLocale=hu", { + statusCode: 302, + }), + url_test("/ID/search?q=video", "/en-US/search?q=video&retiredLocale=id", { + statusCode: 302, + }), + url_test("/it/search?q=video", "/en-US/search?q=video&retiredLocale=it", { + statusCode: 302, + }), + url_test("/KaB/search?q=video", "/en-US/search?q=video&retiredLocale=kab", { + statusCode: 302, + }), + url_test("/ms/search?q=video", "/en-US/search?q=video&retiredLocale=ms", { + statusCode: 302, + }), + url_test("/my/search?q=video", "/en-US/search?q=video&retiredLocale=my", { + statusCode: 302, + }), + url_test("/nl/search?q=video", "/en-US/search?q=video&retiredLocale=nl", { + statusCode: 302, + }), + url_test( + "/pt-PT/search?q=video", + "/en-US/search?q=video&retiredLocale=pt-PT", + { statusCode: 302 } + ), + url_test( + "/sv-se/search?q=video", + "/en-US/search?q=video&retiredLocale=sv-SE", + { statusCode: 302 } + ), + url_test("/th/search?q=video", "/en-US/search?q=video&retiredLocale=th", { + statusCode: 302, + }), + url_test("/tr/search?q=video", "/en-US/search?q=video&retiredLocale=tr", { + statusCode: 302, + }), + url_test("/uk/search?q=video", "/en-US/search?q=video&retiredLocale=uk", { + statusCode: 302, + }), + url_test("/vi/search?q=video", "/en-US/search?q=video&retiredLocale=vi", { + statusCode: 302, + }) +); + const MISC_REDIRECT_URLS = [].concat( url_test("/fr/account", "/fr/settings", { statusCode: 302 }), url_test("/en-US/account", "/en-US/settings", { statusCode: 302 }), @@ -1167,6 +1354,12 @@ describe("locale alias redirects", () => { } }); +describe("retired locale redirects", () => { + for (const [url, t] of RETIRED_LOCALE_URLS) { + it(url, t); + } +}); + const CORE_JAVASCRIPT_1_5_URLs = [].concat( url_test( "/en-US/docs/Core_JavaScript_1.5_Reference/Operators/Special_Operators/typeof_Operator", diff --git a/tool/cli.js b/tool/cli.js index f46f8633def9..f529e0ecd0ff 100644 --- a/tool/cli.js +++ b/tool/cli.js @@ -410,8 +410,11 @@ program // Someplace to put the map into an object so it can be saved into `saveHistory` const allHistory = {}; for (const [relPath, value] of map) { - allHistory[relPath] = value; const locale = relPath.split(path.sep)[0]; + if (!VALID_LOCALES.has(locale)) { + continue; + } + allHistory[relPath] = value; if (!historyPerLocale[locale]) { historyPerLocale[locale] = {}; }