Skip to content

Commit 1257445

Browse files
Merge pull request js-cookie#20 from js-cookie/server-encoding-test
Test how browsers handle encoding when communicating with a server
2 parents a38840b + ad741fd commit 1257445

8 files changed

Lines changed: 661 additions & 298 deletions

File tree

CONTRIBUTING.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ Start a test server from the project root:
4141

4242
$ grunt connect:tests
4343

44-
This will automatically open the test suite at http://127.0.0.1:9998 in the default browser, with livereload enabled.
44+
This will automatically open the test suite at http://127.0.0.1:10000 in the default browser, with livereload enabled.
4545

4646
_Note: we recommend cleaning all the browser cookies before running the tests, that can avoid false positive failures._
4747

Gruntfile.js

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -55,23 +55,23 @@ module.exports = function (grunt) {
5555
}
5656
},
5757
connect: {
58-
saucelabs: {
58+
'build-sauce': {
5959
options: {
6060
port: 9999,
6161
base: ['.', 'test']
6262
}
6363
},
64-
build: {
64+
'build-qunit': {
6565
options: {
6666
port: 9998,
6767
base: ['.', 'test']
6868
}
6969
},
7070
tests: {
7171
options: {
72-
port: 9998,
72+
port: 10000,
7373
base: ['.', 'test'],
74-
open: 'http://127.0.0.1:9998',
74+
open: 'http://127.0.0.1:10000',
7575
keepalive: true,
7676
livereload: true
7777
}
@@ -178,8 +178,8 @@ module.exports = function (grunt) {
178178
}
179179
}
180180

181-
grunt.registerTask('saucelabs', ['connect:saucelabs', 'saucelabs-qunit']);
182-
grunt.registerTask('test', ['jshint', 'connect:build', 'qunit', 'nodeunit']);
181+
grunt.registerTask('saucelabs', ['connect:build-sauce', 'saucelabs-qunit']);
182+
grunt.registerTask('test', ['jshint', 'connect:build-qunit', 'qunit', 'nodeunit']);
183183

184184
grunt.registerTask('dev', ['test', 'uglify', 'compare_size']);
185185
grunt.registerTask('ci', ['test', 'saucelabs']);

README.md

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -130,8 +130,9 @@ Cookies.getJSON(); // => { name: { foo: 'bar' } }
130130

131131
## Encoding
132132

133-
This project is [RFC 6265](http://tools.ietf.org/html/rfc6265#section-4.1.1) compliant.
134-
However, all special characters that are not allowed in the cookie-value or cookie-name are encoded/decoded with each UTF-8 Hex equivalent. Special characters that consistently work among all supported browsers are not encoded/decoded this way.
133+
This project is [RFC 6265](http://tools.ietf.org/html/rfc6265#section-4.1.1) compliant. All special characters that are not allowed in the cookie-name or cookie-value are encoded with each one's UTF-8 Hex equivalent.
134+
The only character in cookie-name or cookie-value that is allowed and still encoded is the percent `%` character, it is escaped in order to interpret the input as literal.
135+
To override the default cookie decoding you need to use a [converter](#converter).
135136

136137
## Cookie Attributes
137138

@@ -200,24 +201,34 @@ Cookies.get('name'); // => 'value' (if already in secure protocol)
200201
Cookies.remove('name', { secure: true });
201202
```
202203

203-
## Converters
204+
## Converter
204205

205-
Provide a conversion function as optional second argument for reading, in order to change the cookie's value
206-
to a different representation on the fly.
206+
Create a new instance of the api that overrides the default decoding implementation.
207+
All methods that rely in a proper decoding to work, such as `Cookies.remove()` and `Cookies.get()`, will run the converter first for each cookie.
208+
The returning String will be used as the cookie value.
207209

208-
Example for parsing the value from a cookie generated with PHP's `setcookie()` method:
210+
Example from reading one of the cookies that can only be decoded using the `escape` function:
209211

210212
```javascript
211-
// 'cookie+with+space' => 'cookie with space'
212-
Cookies.get('foo', function (value) {
213-
return value.replace(/\+/g, ' ');
213+
document.cookie = 'escaped=%u5317';
214+
document.cookie = 'default=%E5%8C%97';
215+
var cookies = Cookies.withConverter(function (value, name) {
216+
if ( name === 'escaped' ) {
217+
return unescape(value);
218+
}
214219
});
220+
cookies.get('escaped'); //
221+
cookies.get('default'); //
222+
cookies.get(); // { escaped: '北', default: '北' }
215223
```
216224

217-
Dealing with cookies that have been encoded using `escape` (3rd party cookies):
225+
Example for parsing the value from a cookie generated with PHP's `setcookie()` method:
218226

219227
```javascript
220-
Cookies.get('foo', unescape);
228+
// 'cookie+with+space' => 'cookie with space'
229+
Cookies.withConverter(function (value) {
230+
return value.replace(/\+/g, ' ');
231+
}).get('foo');
221232
```
222233

223234
## Contributing

src/js.cookie.js

Lines changed: 91 additions & 110 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
/*global escape: true */
12
/*!
23
* Javascript Cookie v2.0.0-pre
34
* https://github.com/js-cookie/js-cookie
@@ -22,49 +23,8 @@
2223
};
2324
}
2425
}(function () {
25-
var unallowedChars = {
26-
';': '%3B',
27-
',': '%2C',
28-
'"': '%22'
29-
};
30-
var unallowedCharsInName = extend(unallowedChars, {
31-
'=': '%3D',
32-
'\t': '%09'
33-
});
34-
var unallowedCharsInValue = extend(unallowedChars, {
35-
' ': '%20'
36-
});
37-
38-
function encode (value, charmap) {
39-
for (var character in charmap) {
40-
value = value
41-
.replace(new RegExp(character, 'g'), charmap[character]);
42-
}
43-
return value;
44-
}
45-
46-
function decode (value, charmap) {
47-
for (var character in charmap) {
48-
value = value
49-
.replace(new RegExp(charmap[character], 'g'), character);
50-
}
51-
return value;
52-
}
53-
54-
function processRead (value, converter, json) {
55-
if (value.charAt(0) === '"') {
56-
value = value.slice(1, -1);
57-
}
58-
59-
value = decode(value, unallowedCharsInValue);
60-
61-
if (json) {
62-
try {
63-
value = JSON.parse(value);
64-
} catch(e) {}
65-
}
66-
67-
return converter ? converter(value) : value;
26+
function decode (value) {
27+
return value.replace(/(%[0-9A-Z]{2})+/g, decodeURIComponent);
6828
}
6929

7030
function extend () {
@@ -79,87 +39,108 @@
7939
return result;
8040
}
8141

82-
var api = function (key, value, options) {
83-
var converter, result;
84-
var args = [].slice.call(arguments);
42+
function init(converter) {
43+
var processRead = function (value, name, json) {
44+
if (value.charAt(0) === '"') {
45+
value = value.slice(1, -1);
46+
}
8547

86-
if (typeof value === 'function') {
87-
converter = value;
88-
args.length = 1;
89-
}
48+
value = converter && converter(value, name) || decode(value);
9049

91-
// Write
50+
if (json) {
51+
try {
52+
value = JSON.parse(value);
53+
} catch(e) {}
54+
}
9255

93-
if (args.length > 1) {
94-
options = extend(api.defaults, options);
56+
return value;
57+
};
58+
var api = function (key, value, options) {
59+
var result;
60+
var args = [].slice.call(arguments);
9561

96-
if (typeof options.expires === 'number') {
97-
var expires = new Date();
98-
expires.setMilliseconds(expires.getMilliseconds() + options.expires * 864e+5);
99-
options.expires = expires;
100-
}
62+
// Write
63+
64+
if (args.length > 1) {
65+
options = extend(api.defaults, options);
10166

102-
try {
103-
result = JSON.stringify(value);
104-
if (/^(?:\{[\w\W]*\}|\[[\w\W]*\])$/.test(result)) {
105-
value = result;
67+
if (typeof options.expires === 'number') {
68+
var expires = new Date();
69+
expires.setMilliseconds(expires.getMilliseconds() + options.expires * 864e+5);
70+
options.expires = expires;
10671
}
107-
} catch(e) {}
10872

109-
value = encode(String(value), unallowedCharsInValue);
73+
try {
74+
result = JSON.stringify(value);
75+
if (/^(?:\{[\w\W]*\}|\[[\w\W]*\])$/.test(result)) {
76+
value = result;
77+
}
78+
} catch(e) {}
79+
80+
value = encodeURIComponent(String(value));
81+
value = value.replace(/%(23|24|26|2B|3A|3C|3E|3D|2F|3F|40|5B|5D|5E|60|7B|7D|7C)/g, decodeURIComponent);
82+
83+
key = encodeURIComponent(String(key));
84+
key = key.replace(/%(23|24|26|2B|5E|60|7C)/g, decodeURIComponent);
85+
key = key.replace(/[\(\)]/g, escape);
86+
87+
return (document.cookie = [
88+
key, '=', value,
89+
options.expires && '; expires=' + options.expires.toUTCString(), // use expires attribute, max-age is not supported by IE
90+
options.path && '; path=' + options.path,
91+
options.domain && '; domain=' + options.domain,
92+
options.secure && '; secure'
93+
].join(''));
94+
}
11095

111-
return (document.cookie = [
112-
encode(key, unallowedCharsInName), '=', value,
113-
options.expires && '; expires=' + options.expires.toUTCString(), // use expires attribute, max-age is not supported by IE
114-
options.path && '; path=' + options.path,
115-
options.domain && '; domain=' + options.domain,
116-
options.secure && '; secure'
117-
].join(''));
118-
}
96+
// Read
11997

120-
// Read
98+
if (!key) {
99+
result = {};
100+
}
121101

122-
if (!key) {
123-
result = {};
124-
}
102+
// To prevent the for loop in the first place assign an empty array
103+
// in case there are no cookies at all. Also prevents odd result when
104+
// calling "get()"
105+
var cookies = document.cookie ? document.cookie.split('; ') : [];
106+
var i = 0;
125107

126-
// To prevent the for loop in the first place assign an empty array
127-
// in case there are no cookies at all. Also prevents odd result when
128-
// calling "get()"
129-
var cookies = document.cookie ? document.cookie.split('; ') : [];
130-
var i = 0;
108+
for (; i < cookies.length; i++) {
109+
var parts = cookies[i].split('='),
110+
name = decode(parts.shift()),
111+
cookie = parts.join('=');
131112

132-
for (; i < cookies.length; i++) {
133-
var parts = cookies[i].split('='),
134-
name = decode(parts.shift(), unallowedCharsInName),
135-
cookie = parts.join('=');
113+
if (key === name) {
114+
result = processRead(cookie, name, this.json);
115+
break;
116+
}
136117

137-
if (key === name) {
138-
result = processRead(cookie, converter, this.json);
139-
break;
118+
if (!key) {
119+
result[name] = processRead(cookie, name, this.json);
120+
}
140121
}
141122

142-
if (!key) {
143-
result[name] = processRead(cookie, converter, this.json);
144-
}
145-
}
123+
return result;
124+
};
146125

147-
return result;
148-
};
149-
150-
api.get = api.set = api;
151-
api.getJSON = function() {
152-
return api.apply({
153-
json: true
154-
}, [].slice.call(arguments));
155-
};
156-
api.defaults = {};
157-
158-
api.remove = function (key, options) {
159-
// Must not alter options, thus extending a fresh object...
160-
api(key, '', extend(options, { expires: -1 }));
161-
return !api(key);
162-
};
163-
164-
return api;
126+
api.get = api.set = api;
127+
api.getJSON = function () {
128+
return api.apply({
129+
json: true
130+
}, [].slice.call(arguments));
131+
};
132+
api.defaults = {};
133+
134+
api.remove = function (key, options) {
135+
// Must not alter options, thus extending a fresh object...
136+
api(key, '', extend(options, { expires: -1 }));
137+
return !api(key);
138+
};
139+
140+
api.withConverter = init;
141+
142+
return api;
143+
}
144+
145+
return init();
165146
}));

0 commit comments

Comments
 (0)