Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Serialize objects of key/value pairs #47

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ var cookie = require('cookie');
var hdr = cookie.serialize('foo', 'bar');
// hdr = 'foo=bar';

var hdr = cookie.serialize({ foo: 'bar', cat: 'meow', dog: 'ruff' })
// hdr = 'foo=bar; cat=meow; dog=ruff'

var cookies = cookie.parse('foo=bar; cat=meow; dog=ruff');
// cookies = { foo: 'bar', cat: 'meow', dog: 'ruff' };
```
Expand Down
59 changes: 57 additions & 2 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -82,12 +82,23 @@ function parse(str, options) {
/**
* Serialize data into a cookie header.
*
* Serialize the a name value pair into a cookie string suitable for
* http headers. An optional options object specified cookie parameters.
* If the first parameter is an object, serialize the key-value pairs
* in the object into a cookie string suitable for http headers. An
* optional options object can be used to specify the encoding. If only
* one key-value pair is in the first parmater, then the options object can also
* specify cookie parameters. If more than one key-value pairs are in the
* first parameter, then the options object may only specify an encoding.
*
* If the first parameter is a string, serialize the name value pair
* into a cookie string suitable for http headers. An optional options
* object specifies cookie parameters and encoding.
*
* serialize('foo', 'bar', { httpOnly: true })
* => "foo=bar; httpOnly"
*
* serialize({ foo: 'bar', cat: 'meow' })
* => "foo=bar; cat=meow"
*
* @param {string} name
* @param {string} val
* @param {object} [options]
Expand All @@ -96,6 +107,50 @@ function parse(str, options) {
*/

function serialize(name, val, options) {
if( typeof name === 'object') {
var cookies = name;
var serializeOptions = val;

var cookieNames = Object.keys(cookies);
if(0 === cookieNames.length) {
return '';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps return undefined for this case? Not too clear, but just feel like setting the Set-Cookie header to the result would make more sense if undefined was returned, since that would just not send the header instead of send an empty header.

} else if(cookieNames.length > 1) {
// If there are more than one cookies to serialize, only allow
// an encoding option to be set
var opt = serializeOptions || {};
serializeOptions = {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't really make any sense. People supplying the object as a variable would have no way to understand if their options are going to be honored. Besides, it seems perfectly valid to want to set all the cookies to have httpOnly: true, secure: true, etc.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could be wrong about this, but my thinking was that if we are given several cookies to serialize, they want those cookies in the Cookie request header which, from what I read in RFC6265 Section 5.4 should be serialized as the cookie name followed by an '=' followed by the cookie value with all cookies separated by a semicolon. So if we want to serialize multiple cookies into a single string, we should not allow additional cookie parameters in the string (again, I could be wrong about this, please correct me if so).

One possible option is: given an object with multiple cookies and an options object with cookie parameters we can return an array of serialized cookie strings, one for each cookie, with all the parameters set as could be used in a Set-Cookie header. Another option is to let the caller indicate in the options object whether they want their cookies serialized for a Set-Cookie response header or a Cookie request header.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The serialize method outputs a string that is for the Set-Cookie response header, not the Cookie request header.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see, so if we are given multiple cookies to serialize, would it be best to return an array of serialized cookie strings rather than a single string with all cookies?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea, that makes sense :)

encode: opt.encode || encode
};
}

var serializedCookies = [];
cookieNames.forEach(function(cookieName) {
serializedCookies.push(serializeNameValue(cookieName, cookies[cookieName], serializeOptions));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We already know the number of cookies ahead time time; forcing the runtime to keep expanding the array is not very efficient.

});

return serializedCookies.join('; ');
} else {
return serializeNameValue(name, val, options);
}
}

/**
* Serialize name value pair into a cookie header.
*
* Serialize the a name value pair into a cookie string suitable for
* http headers. An optional options object specified cookie parameters.
*
* serialize('foo', 'bar', { httpOnly: true })
* => "foo=bar; httpOnly"
*
* @param {string} name
* @param {string} val
* @param {object} [options]
* @return {string}
* @private
*/

function serializeNameValue(name, val, options) {
var opt = options || {};
var enc = opt.encode || encode;

Expand Down
120 changes: 119 additions & 1 deletion test/serialize.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,32 @@ test('basic', function() {
assert.equal('foo=', cookie.serialize('foo', ''));
assert.throws(cookie.serialize.bind(cookie, 'foo\n', 'bar'), /argument name is invalid/);
assert.throws(cookie.serialize.bind(cookie, 'foo\u280a', 'bar'), /argument name is invalid/);

assert.equal('foo=bar', cookie.serialize({ foo: 'bar' }));
assert.equal('foo=bar; cat=meow; dog=ruff', cookie.serialize({ foo: 'bar', cat: 'meow', dog: 'ruff' }));
assert.equal('foo=', cookie.serialize({ foo: '' }));
assert.equal('foo=; cat=meow', cookie.serialize({ foo: '', cat: 'meow' }));
assert.equal('', cookie.serialize({}));
});

test('path', function() {
assert.equal('foo=bar; Path=/', cookie.serialize('foo', 'bar', {
path: '/'
}));

assert.equal('foo=bar; Path=/', cookie.serialize(
{ foo: 'bar' },
{ path: '/' }
));

assert.throws(cookie.serialize.bind(cookie, 'foo', 'bar', {
path: '/\n'
}), /option path is invalid/);

assert.throws(cookie.serialize.bind(cookie,
{ foo: 'bar' },
{ path: '/\n' }),
/option path is invalid/);
});

test('secure', function() {
Expand All @@ -31,22 +47,56 @@ test('secure', function() {
assert.equal('foo=bar', cookie.serialize('foo', 'bar', {
secure: false
}));

assert.equal('foo=bar; Secure', cookie.serialize(
{ foo: 'bar' },
{ secure: true }
));

assert.equal('foo=bar', cookie.serialize(
{ foo: 'bar' },
{ secure: false }
));
});

test('domain', function() {
assert.equal('foo=bar; Domain=example.com', cookie.serialize('foo', 'bar', {
domain: 'example.com'
}));

assert.equal('foo=bar; Domain=example.com', cookie.serialize(
{ foo: 'bar' },
{ domain: 'example.com' }
));

assert.throws(cookie.serialize.bind(cookie, 'foo', 'bar', {
domain: 'example.com\n'
}), /option domain is invalid/);

assert.throws(cookie.serialize.bind(cookie,
{ foo: 'bar' },
{ domain: 'example.com\n' }),
/option domain is invalid/);
});

test('httpOnly', function() {
assert.equal('foo=bar; HttpOnly', cookie.serialize('foo', 'bar', {
httpOnly: true
}));

assert.equal('foo=bar', cookie.serialize('foo', 'bar', {
httpOnly: false
}));

assert.equal('foo=bar; HttpOnly', cookie.serialize(
{ foo: 'bar' },
{ httpOnly: true }
));

assert.equal('foo=bar', cookie.serialize(
{ foo: 'bar' },
{ httpOnly: false }
));
});

test('maxAge', function() {
Expand All @@ -57,6 +107,16 @@ test('maxAge', function() {
assert.equal('foo=bar; Max-Age=0', cookie.serialize('foo', 'bar', {
maxAge: 0
}));

assert.equal('foo=bar; Max-Age=1000', cookie.serialize(
{ foo: 'bar' },
{ maxAge: 1000 }
));

assert.equal('foo=bar; Max-Age=0', cookie.serialize(
{ foo: 'bar' },
{ maxAge: 0 }
));
});

test('firstPartyOnly', function() {
Expand All @@ -67,10 +127,22 @@ test('firstPartyOnly', function() {
assert.equal('foo=bar', cookie.serialize('foo', 'bar', {
firstPartyOnly: false
}));

assert.equal('foo=bar; First-Party-Only', cookie.serialize(
{ foo: 'bar' },
{ firstPartyOnly: true }
));

assert.equal('foo=bar', cookie.serialize(
{ foo: 'bar' },
{ firstPartyOnly: false }
));
});

test('escaping', function() {
assert.deepEqual('cat=%2B%20', cookie.serialize('cat', '+ '));
assert.deepEqual('cat=%2B%20', cookie.serialize({ cat: '+ ' }));
assert.deepEqual('cat=%2B%20; dog=%2C%20', cookie.serialize({ cat: '+ ', dog: ', ' }));
});

test('parse->serialize', function() {
Expand All @@ -80,6 +152,25 @@ test('parse->serialize', function() {

assert.deepEqual({ cat: ' ";/' }, cookie.parse(
cookie.serialize('cat', ' ";/')));

assert.deepEqual({ cat: 'foo=123&name=baz five' }, cookie.parse(
cookie.serialize({ cat: 'foo=123&name=baz five' })));

assert.deepEqual({ cat: ' ";/' }, cookie.parse(
cookie.serialize({ cat: ' ";/' })));
});

test('serialize->parse', function() {

assert.equal('foo=bar; cat=meow; dog=ruff', cookie.serialize(
cookie.parse('foo=bar; cat=meow; dog=ruff')));

assert.equal('foo=bar', cookie.serialize(
cookie.parse('foo=bar')));

assert.equal('foo=; cat=meow', cookie.serialize(
cookie.parse('foo=; cat=meow')));

});

test('unencoded', function() {
Expand All @@ -90,4 +181,31 @@ test('unencoded', function() {
assert.throws(cookie.serialize.bind(cookie, 'cat', '+ \n', {
encode: function(value) { return value; }
}), /argument val is invalid/);
})

assert.deepEqual('cat=+ ', cookie.serialize(
{ cat: '+ ' },
{ encode: function(value) { return value; }
}));

assert.deepEqual('cat=+ ; dog=, ', cookie.serialize(
{ cat: '+ ', dog: ', ' },
{ encode: function(value) { return value; }
}));

assert.throws(cookie.serialize.bind(cookie,
{ cat: '+ \n' },
{ encode: function(value) { return value; } }),
/argument val is invalid/);
});

test('many cookies many options', function() {

assert.equal('foo=bar; cat=meow; dog=ruff', cookie.serialize(
{ foo: 'bar', cat: 'meow', dog: 'ruff' },
{ domain: 'example.com' }));

assert.equal('cat=+ ; dog=, ', cookie.serialize(
{ cat: '+ ', dog: ', ' },
{ domain: 'example.com', encode: function(value) { return value; } }));

});