Skip to content

Commit 902cfa1

Browse files
committed
optimize cookie serialization and set sameSite to strict by default, to prepare for browser changes
1 parent cea1d50 commit 902cfa1

File tree

6 files changed

+146
-28
lines changed

6 files changed

+146
-28
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
### 5.0.1
2+
3+
- optimize cookie serialization and set sameSite to strict by default, to prepare for browser changes
4+
15
### 5.0.0
26

37
- **BREAKING** needs i18next >= 19.5.0

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ As with all modules you can either pass the constructor function (class) to the
6666
htmlTag: document.documentElement,
6767

6868
// optional set cookie options, reference:[MDN Set-Cookie docs](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie)
69-
cookieOptions: {path:'/'}
69+
cookieOptions: { path: '/', sameSite: 'strict' }
7070
}
7171
```
7272

i18nextBrowserLanguageDetector.js

Lines changed: 78 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -40,26 +40,90 @@
4040
return obj;
4141
}
4242

43+
// eslint-disable-next-line no-control-regex
44+
var fieldContentRegExp = /^[\u0009\u0020-\u007e\u0080-\u00ff]+$/;
45+
46+
var serializeCookie = function serializeCookie(name, val, options) {
47+
var opt = options || {};
48+
opt.path = opt.path || '/';
49+
var value = encodeURIComponent(val);
50+
var str = name + '=' + value;
51+
52+
if (opt.maxAge > 0) {
53+
var maxAge = opt.maxAge - 0;
54+
if (isNaN(maxAge)) throw new Error('maxAge should be a Number');
55+
str += '; Max-Age=' + Math.floor(maxAge);
56+
}
57+
58+
if (opt.domain) {
59+
if (!fieldContentRegExp.test(opt.domain)) {
60+
throw new TypeError('option domain is invalid');
61+
}
62+
63+
str += '; Domain=' + opt.domain;
64+
}
65+
66+
if (opt.path) {
67+
if (!fieldContentRegExp.test(opt.path)) {
68+
throw new TypeError('option path is invalid');
69+
}
70+
71+
str += '; Path=' + opt.path;
72+
}
73+
74+
if (opt.expires) {
75+
if (typeof opt.expires.toUTCString !== 'function') {
76+
throw new TypeError('option expires is invalid');
77+
}
78+
79+
str += '; Expires=' + opt.expires.toUTCString();
80+
}
81+
82+
if (opt.httpOnly) str += '; HttpOnly';
83+
if (opt.secure) str += '; Secure';
84+
85+
if (opt.sameSite) {
86+
var sameSite = typeof opt.sameSite === 'string' ? opt.sameSite.toLowerCase() : opt.sameSite;
87+
88+
switch (sameSite) {
89+
case true:
90+
str += '; SameSite=Strict';
91+
break;
92+
93+
case 'lax':
94+
str += '; SameSite=Lax';
95+
break;
96+
97+
case 'strict':
98+
str += '; SameSite=Strict';
99+
break;
100+
101+
case 'none':
102+
str += '; SameSite=None';
103+
break;
104+
105+
default:
106+
throw new TypeError('option sameSite is invalid');
107+
}
108+
}
109+
110+
return str;
111+
};
112+
43113
var cookie = {
44114
create: function create(name, value, minutes, domain) {
45115
var cookieOptions = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : {
46-
path: '/'
116+
path: '/',
117+
sameSite: 'strict'
47118
};
48-
var expires;
49119

50120
if (minutes) {
51-
var date = new Date();
52-
date.setTime(date.getTime() + minutes * 60 * 1000);
53-
expires = '; expires=' + date.toUTCString();
54-
} else expires = '';
55-
56-
domain = domain ? 'domain=' + domain + ';' : '';
57-
cookieOptions = Object.keys(cookieOptions).reduce(function (acc, key) {
58-
return acc + ';' + key.replace(/([A-Z])/g, function ($1) {
59-
return '-' + $1.toLowerCase();
60-
}) + '=' + cookieOptions[key];
61-
}, '');
62-
document.cookie = name + '=' + encodeURIComponent(value) + expires + ';' + domain + cookieOptions;
121+
cookieOptions.expires = new Date();
122+
cookieOptions.expires.setTime(cookieOptions.expires.getTime() + minutes * 60 * 1000);
123+
}
124+
125+
if (domain) cookieOptions.domain = domain;
126+
document.cookie = serializeCookie(name, encodeURIComponent(value), cookieOptions);
63127
},
64128
read: function read(name) {
65129
var nameEQ = name + '=';

i18nextBrowserLanguageDetector.min.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/browserLookups/cookie.js

Lines changed: 61 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,66 @@
1+
// eslint-disable-next-line no-control-regex
2+
const fieldContentRegExp = /^[\u0009\u0020-\u007e\u0080-\u00ff]+$/
3+
4+
const serializeCookie = (name, val, options) => {
5+
const opt = options || {}
6+
opt.path = opt.path || '/'
7+
const value = encodeURIComponent(val)
8+
let str = name + '=' + value
9+
if (opt.maxAge > 0) {
10+
const maxAge = opt.maxAge - 0
11+
if (isNaN(maxAge)) throw new Error('maxAge should be a Number')
12+
str += '; Max-Age=' + Math.floor(maxAge)
13+
}
14+
if (opt.domain) {
15+
if (!fieldContentRegExp.test(opt.domain)) {
16+
throw new TypeError('option domain is invalid')
17+
}
18+
str += '; Domain=' + opt.domain
19+
}
20+
if (opt.path) {
21+
if (!fieldContentRegExp.test(opt.path)) {
22+
throw new TypeError('option path is invalid')
23+
}
24+
str += '; Path=' + opt.path
25+
}
26+
if (opt.expires) {
27+
if (typeof opt.expires.toUTCString !== 'function') {
28+
throw new TypeError('option expires is invalid')
29+
}
30+
str += '; Expires=' + opt.expires.toUTCString()
31+
}
32+
if (opt.httpOnly) str += '; HttpOnly'
33+
if (opt.secure) str += '; Secure'
34+
if (opt.sameSite) {
35+
const sameSite = typeof opt.sameSite === 'string' ? opt.sameSite.toLowerCase() : opt.sameSite
36+
switch (sameSite) {
37+
case true:
38+
str += '; SameSite=Strict'
39+
break
40+
case 'lax':
41+
str += '; SameSite=Lax'
42+
break
43+
case 'strict':
44+
str += '; SameSite=Strict'
45+
break
46+
case 'none':
47+
str += '; SameSite=None'
48+
break
49+
default:
50+
throw new TypeError('option sameSite is invalid')
51+
}
52+
}
53+
return str
54+
}
55+
156
let cookie = {
2-
create: function(name,value,minutes,domain,cookieOptions = {path: '/'}) {
3-
let expires;
57+
create: function(name, value, minutes, domain, cookieOptions = { path: '/', sameSite: 'strict' }) {
458
if (minutes) {
5-
let date = new Date();
6-
date.setTime(date.getTime() + (minutes * 60 * 1000));
7-
expires = '; expires=' + date.toUTCString();
8-
}
9-
else expires = '';
10-
domain = domain ? 'domain=' + domain + ';' : '';
11-
cookieOptions = Object.keys(cookieOptions).reduce((acc, key) => acc + ';' +
12-
key.replace(/([A-Z])/g, ($1) => '-' + $1.toLowerCase()) + '=' + cookieOptions[key], '',);
13-
document.cookie = name + '=' + encodeURIComponent(value) + expires + ';' + domain + cookieOptions;
59+
cookieOptions.expires = new Date();
60+
cookieOptions.expires.setTime(cookieOptions.expires.getTime() + (minutes * 60 * 1000));
61+
}
62+
if (domain) cookieOptions.domain = domain;
63+
document.cookie = serializeCookie(name, encodeURIComponent(value), cookieOptions);
1464
},
1565

1666
read: function(name) {

test/languageDetector.test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ describe('language detector', () => {
2222
}
2323
ld.cacheUserLanguage('it', ['cookie'])
2424
expect(global.document.cookie).to.match(/i18next=it/)
25-
expect(global.document.cookie).to.match(/path=\//)
25+
expect(global.document.cookie).to.match(/Path=\//)
2626
// expect(global.document.cookie).to.match(/my=cookie/)
2727
})
2828
})

0 commit comments

Comments
 (0)