Skip to content

Commit e4937a6

Browse files
authored
set a cookie when changing language (mdn#1926)
* set a cookie when changing language Part of #1889 * some small fixes * make cookie secure unless on localhost
1 parent aab921f commit e4937a6

12 files changed

Lines changed: 241 additions & 34 deletions

File tree

client/src/ui/molecules/language-menu/index.tsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ const LANGUAGES = new Map(
1212
})
1313
);
1414

15+
// This needs to match what's set in 'libs/constants.js' on the server/builder!
16+
const PREFERRED_LOCALE_COOKIE_NAME = "preferredlocale";
17+
1518
export function LanguageMenu({
1619
locale,
1720
translations,
@@ -38,6 +41,17 @@ export function LanguageMenu({
3841
// The default is the current locale itself. If that's what's chosen,
3942
// don't bother redirecting.
4043
if (localeURL !== locale) {
44+
for (const translation of translations) {
45+
if (translation.url === localeURL) {
46+
let cookieValue = `${PREFERRED_LOCALE_COOKIE_NAME}=${
47+
translation.locale
48+
};max-age=${60 * 60 * 24 * 365 * 3};path=/`;
49+
if (document.location.hostname !== "localhost") {
50+
cookieValue += ";secure";
51+
}
52+
document.cookie = cookieValue;
53+
}
54+
}
4155
navigate(localeURL);
4256
}
4357
}}

deployer/aws-lambda/content-origin-request/index.js

Lines changed: 4 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,9 @@
11
const sanitizeFilename = require("sanitize-filename");
22
const { resolveFundamental } = require("@yari-internal/fundamental-redirects");
3-
const { DEFAULT_LOCALE, VALID_LOCALES } = require("@yari-internal/constants");
4-
const acceptLanguageParser = require("accept-language-parser");
3+
const { getLocale } = require("@yari-internal/get-locale");
54

65
const CONTENT_DEVELOPMENT_DOMAIN = ".content.dev.mdn.mozit.cloud";
76

8-
const VALID_LOCALES_LIST = [...VALID_LOCALES.values()];
9-
10-
function getLocale(request, fallback = DEFAULT_LOCALE) {
11-
// Do we want to support a language cookie? Add it here!
12-
// Each header in request.headers is always a list of objects.
13-
const acceptLangHeaders = request.headers["accept-language"];
14-
const { value = null } = (acceptLangHeaders && acceptLangHeaders[0]) || {};
15-
const locale =
16-
value &&
17-
acceptLanguageParser.pick(VALID_LOCALES_LIST, value, { loose: true });
18-
return locale || fallback;
19-
}
20-
217
/*
228
* NOTE: This function is derived from the function of the same name within
239
* ../../content/utils.js. It differs only in its final "join", which
@@ -96,7 +82,9 @@ exports.handler = async (event, _context) => {
9682
? request.uri.slice(0, -1)
9783
: request.uri;
9884
const locale = getLocale(request);
99-
return redirect(`/${locale}${path}`);
85+
// The only time we actually want a trailing slash is when the URL is just
86+
// the locale. E.g. `/en-US/` (not `/en-US`)
87+
return redirect(`/${locale}${path || "/"}`);
10088
}
10189

10290
// A document URL with a trailing slash should redirect

deployer/aws-lambda/content-origin-request/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,11 @@
77
"make-package": "yarn install && zip -r -X function.zip . -i index.js 'node_modules/*'"
88
},
99
"dependencies": {
10-
"@yari-internal/fundamental-redirects": "file:../../../libs/fundamental-redirects",
1110
"@yari-internal/constants": "file:../../../libs/constants",
11+
"@yari-internal/fundamental-redirects": "file:../../../libs/fundamental-redirects",
12+
"@yari-internal/get-locale": "file:../../../libs/get-locale",
1213
"accept-language-parser": "^1.5.0",
14+
"cookie": "0.4.1",
1315
"sanitize-filename": "^1.6.3"
1416
},
1517
"engines": {

deployer/aws-lambda/content-origin-request/yarn.lock

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,25 @@
22
# yarn lockfile v1
33

44

5+
"@yari-internal/constants@file:../../../libs/constants":
6+
version "0.0.1"
7+
58
"@yari-internal/fundamental-redirects@file:../../../libs/fundamental-redirects":
69
version "0.0.1"
710

8-
"@yari-internal/constants@file:../../../libs/constants":
11+
"@yari-internal/get-locale@file:../../../libs/get-locale":
912
version "0.0.1"
1013

1114
accept-language-parser@^1.5.0:
1215
version "1.5.0"
1316
resolved "https://registry.yarnpkg.com/accept-language-parser/-/accept-language-parser-1.5.0.tgz#8877c54040a8dcb59e0a07d9c1fde42298334791"
1417
integrity sha1-iHfFQECo3LWeCgfZwf3kIpgzR5E=
1518

19+
cookie@0.4.1:
20+
version "0.4.1"
21+
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.1.tgz#afd713fe26ebd21ba95ceb61f9a8116e50a537d1"
22+
integrity sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==
23+
1624
sanitize-filename@^1.6.3:
1725
version "1.6.3"
1826
resolved "https://registry.yarnpkg.com/sanitize-filename/-/sanitize-filename-1.6.3.tgz#755ebd752045931977e30b2025d340d7c9090378"

libs/constants/index.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,13 @@ const LOCALE_ALIASES = new Map([
4848
["zh-hant", "zh-tw"],
4949
]);
5050

51+
// This must match what we do in `language-menu/index.tsx` where the cookie
52+
// gets set in the client!
53+
const PREFERRED_LOCALE_COOKIE_NAME = "preferredlocale";
54+
5155
module.exports = {
5256
VALID_LOCALES,
5357
DEFAULT_LOCALE,
5458
LOCALE_ALIASES,
59+
PREFERRED_LOCALE_COOKIE_NAME,
5560
};

libs/get-locale/index.js

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
const { parse } = require("cookie");
2+
const acceptLanguageParser = require("accept-language-parser");
3+
4+
const {
5+
DEFAULT_LOCALE,
6+
VALID_LOCALES,
7+
PREFERRED_LOCALE_COOKIE_NAME,
8+
} = require("../constants");
9+
const VALID_LOCALES_LIST = [...VALID_LOCALES.values()];
10+
11+
// From https://github.com/aws-samples/cloudfront-authorization-at-edge/blob/01c1bc843d478977005bde86f5834ce76c479eec/src/lambda-edge/shared/shared.ts#L216
12+
// but rewritten in JavaScript (from TypeScript).
13+
function extractCookiesFromHeaders(headers) {
14+
// Cookies are present in the HTTP header "Cookie" that may be present multiple times.
15+
// This utility function parses occurrences of that header and splits out all the cookies and their values
16+
// A simple object is returned that allows easy access by cookie name: e.g. cookies["nonce"]
17+
if (!headers["cookie"]) {
18+
return {};
19+
}
20+
const cookies = headers["cookie"].reduce(
21+
(reduced, header) => Object.assign(reduced, parse(header.value)),
22+
{}
23+
);
24+
25+
return cookies;
26+
}
27+
28+
function getCookie(headers, cookieKey) {
29+
return extractCookiesFromHeaders(headers)[cookieKey];
30+
}
31+
32+
function getLocale(request, fallback = DEFAULT_LOCALE) {
33+
// First try by cookie.
34+
const cookieLocale = getCookie(request.headers, PREFERRED_LOCALE_COOKIE_NAME);
35+
if (cookieLocale) {
36+
// If it's valid, stick to it.
37+
if (VALID_LOCALES.has(cookieLocale.toLowerCase())) {
38+
return VALID_LOCALES.get(cookieLocale.toLowerCase());
39+
}
40+
}
41+
42+
// Each header in request.headers is always a list of objects.
43+
const acceptLangHeaders = request.headers["accept-language"];
44+
const { value = null } = (acceptLangHeaders && acceptLangHeaders[0]) || {};
45+
const locale =
46+
value &&
47+
acceptLanguageParser.pick(VALID_LOCALES_LIST, value, { loose: true });
48+
return locale || fallback;
49+
}
50+
51+
module.exports = {
52+
getLocale,
53+
};

libs/get-locale/package.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"name": "@yari-internal/get-locale",
3+
"license": "MPL-2.0",
4+
"private": true,
5+
"version": "0.0.1",
6+
"main": "index.js"
7+
}

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,16 @@
3030
"dependencies": {
3131
"@caporal/core": "2.0.2",
3232
"@mdn/browser-compat-data": "2.0.7",
33+
"accept-language-parser": "1.5.0",
3334
"braces": "^3.0.2",
3435
"chalk": "4.1.0",
3536
"cheerio": "1.0.0-rc.3",
3637
"chokidar": "^3.4.3",
3738
"clean-webpack-plugin": "3.0.0",
3839
"cli-progress": "^3.8.2",
3940
"compression": "1.7.4",
41+
"cookie": "0.4.1",
42+
"cookie-parser": "1.4.5",
4043
"cross-env": "^7.0.3",
4144
"cssesc": "^3.0.0",
4245
"diff": "5.0.0",

server/index.js

Lines changed: 9 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ const chalk = require("chalk");
55
const express = require("express");
66
const send = require("send");
77
const proxy = require("express-http-proxy");
8+
const cookieParser = require("cookie-parser");
89
const openEditor = require("open-editor");
910

1011
const {
@@ -13,13 +14,7 @@ const {
1314
buildLiveSamplePageFromURL,
1415
renderContributorsTxt,
1516
} = require("../build");
16-
const {
17-
CONTENT_ROOT,
18-
Document,
19-
Redirect,
20-
Image,
21-
resolveFundamental,
22-
} = require("../content");
17+
const { CONTENT_ROOT, Document, Redirect, Image } = require("../content");
2318
// eslint-disable-next-line node/no-missing-require
2419
const { prepareDoc, renderDocHTML } = require("../ssr/dist/main");
2520

@@ -28,19 +23,17 @@ const documentRouter = require("./document");
2823
const fakeV1APIRouter = require("./fake-v1-api");
2924
const { searchRoute } = require("./document-watch");
3025
const flawsRoute = require("./flaws");
31-
const { staticMiddlewares } = require("./middlewares");
26+
const { staticMiddlewares, originRequestMiddleware } = require("./middlewares");
3227

3328
const app = express();
29+
3430
app.use(express.json());
3531

36-
app.use((req, res, next) => {
37-
// If we have a fundamental redirect mimic out Lambda@Edge and redirect.
38-
const { url: fundamentalRedirectUrl, status } = resolveFundamental(req.url);
39-
if (fundamentalRedirectUrl && status) {
40-
return res.redirect(status, fundamentalRedirectUrl);
41-
}
42-
return next();
43-
});
32+
// Needed because we read cookies in the code that mimics what we do in Lambda@Edge.
33+
app.use(cookieParser());
34+
35+
app.use(originRequestMiddleware);
36+
4437
app.use(staticMiddlewares);
4538

4639
app.use(express.urlencoded({ extended: true }));

server/middlewares.js

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
const express = require("express");
22

3+
const { resolveFundamental } = require("../libs/fundamental-redirects");
4+
const { getLocale } = require("../libs/get-locale");
35
const { STATIC_ROOT } = require("./constants");
46

57
// Lowercase every request because every possible file we might have
@@ -11,6 +13,41 @@ const slugRewrite = (req, res, next) => {
1113
next();
1214
};
1315

16+
/**
17+
* This function is returns an object with {url:string, status:number}
18+
* if there's some place to redirect to, otherwise an empty object.
19+
*/
20+
const originRequest = (req, res, next) => {
21+
const { url: fundamentalRedirectUrl, status } = resolveFundamental(req.url);
22+
if (fundamentalRedirectUrl && status) {
23+
res.redirect(status, fundamentalRedirectUrl);
24+
} else if (req.url === "/" || req.url.startsWith("/docs/")) {
25+
// Fake it so it becomes like Lambda@Edge
26+
req.headers.cookie = [
27+
{
28+
// The `req.cookies` comes from cookie-parser
29+
value: Object.entries(req.cookies)
30+
.map(([key, value]) => `${key}=${value}`)
31+
.join(";"),
32+
},
33+
];
34+
if (req.headers["accept-language"]) {
35+
// Lambda@Edge expects it to be an array of objects
36+
req.headers["accept-language"] = [
37+
{ value: req.headers["accept-language"] },
38+
];
39+
}
40+
const path = req.url.endsWith("/") ? req.url.slice(0, -1) : req.url;
41+
const locale = getLocale(req);
42+
// The only time we actually want a trailing slash is when the URL is just
43+
// the locale. E.g. `/en-US/` (not `/en-US`)
44+
res.redirect(302, `/${locale}${path || "/"}`);
45+
} else {
46+
next();
47+
}
48+
};
49+
1450
module.exports = {
1551
staticMiddlewares: [slugRewrite, express.static(STATIC_ROOT)],
52+
originRequestMiddleware: originRequest,
1653
};

0 commit comments

Comments
 (0)