Permalink
Browse files

feat($cookies): allow passing cookie options

The `put`, `putObject` and `remove` methods now take an options parameter
where you can provide additional options for the cookie value, such as `expires`,
`path`, `domain` and `secure`.

Closes #8324
Closes #3988
Closes #1786
Closes #950
  • Loading branch information...
shahata authored and petebacondarwin committed Mar 2, 2015
1 parent 38fbe3e commit 92c366d205da36ec26502aded23db71a6473dad7
Showing with 140 additions and 27 deletions.
  1. +34 −18 src/ngCookies/cookieWriter.js
  2. +19 −6 src/ngCookies/cookies.js
  3. +67 −1 test/ngCookies/cookieWriterSpec.js
  4. +20 −2 test/ngCookies/cookiesSpec.js
@@ -9,31 +9,47 @@
*
* @param {string} name Cookie name
* @param {string=} value Cookie value (if undefined, cookie will be deleted)
* @param {Object=} options Object with options that need to be stored for the cookie.
*/
function $$CookieWriter($document, $log, $browser) {
var cookiePath = $browser.baseHref();
var rawDocument = $document[0];
return function(name, value) {
function buildCookieString(name, value, options) {
var path, expires;
options = options || {};
expires = options.expires;
path = angular.isDefined(options.path) ? options.path : cookiePath;
if (value === undefined) {
rawDocument.cookie = encodeURIComponent(name) + "=;path=" + cookiePath +
";expires=Thu, 01 Jan 1970 00:00:00 GMT";
} else {
if (angular.isString(value)) {
var cookieLength = (rawDocument.cookie = encodeURIComponent(name) + '=' + encodeURIComponent(value) +
';path=' + cookiePath).length + 1;
// per http://www.ietf.org/rfc/rfc2109.txt browser must allow at minimum:
// - 300 cookies
// - 20 cookies per unique domain
// - 4096 bytes per cookie
if (cookieLength > 4096) {
$log.warn("Cookie '" + name +
"' possibly not set or overflowed because it was too large (" +
cookieLength + " > 4096 bytes)!");
}
}
expires = 'Thu, 01 Jan 1970 00:00:00 GMT';
value = '';
}
if (angular.isString(expires)) {
expires = new Date(expires);
}
var str = encodeURIComponent(name) + '=' + encodeURIComponent(value);
str += path ? ';path=' + path : '';
str += options.domain ? ';domain=' + options.domain : '';
str += expires ? ';expires=' + expires.toUTCString() : '';
str += options.secure ? ';secure' : '';
// per http://www.ietf.org/rfc/rfc2109.txt browser must allow at minimum:
// - 300 cookies
// - 20 cookies per unique domain
// - 4096 bytes per cookie
var cookieLength = str.length + 1;
if (cookieLength > 4096) {
$log.warn("Cookie '" + name +
"' possibly not set or overflowed because it was too large (" +
cookieLength + " > 4096 bytes)!");
}
return str;
}
return function(name, value, options) {
rawDocument.cookie = buildCookieString(name, value, options);
};
}
View
@@ -96,9 +96,20 @@ angular.module('ngCookies', ['ng']).
*
* @param {string} key Id for the `value`.
* @param {string} value Raw value to be stored.
* @param {Object=} options Object with options that need to be stored for the cookie.
* The object may have following properties:
*
* - **path** - `{string}` - The cookie will be available only for this path and its
* sub-paths. By default, this would be the URL that appears in your base tag.
* - **domain** - `{string}` - The cookie will be available only for this domain and
* its sub-domains. For obvious security reasons the user agent will not accept the
* cookie if the current domain is not a sub domain or equals to the requested domain.
* - **expires** - `{string|Date}` - String of the form "Wdy, DD Mon YYYY HH:MM:SS GMT"
* or a Date object indicating the exact date/time this cookie will expire.
* - **secure** - `{boolean}` - The cookie will be available only in secured connection.
*/
put: function(key, value) {
$$cookieWriter(key, value);
put: function(key, value, options) {
$$cookieWriter(key, value, options);
},
/**
@@ -110,9 +121,10 @@ angular.module('ngCookies', ['ng']).
*
* @param {string} key Id for the `value`.
* @param {Object} value Value to be stored.
* @param {Object=} options Options object.
*/
putObject: function(key, value) {
$$cookieWriter(key, angular.toJson(value));
putObject: function(key, value, options) {
$$cookieWriter(key, angular.toJson(value), options);
},
/**
@@ -123,9 +135,10 @@ angular.module('ngCookies', ['ng']).
* Remove given cookie
*
* @param {string} key Id of the key-value pair to delete.
* @param {Object=} options Options object.
*/
remove: function(key) {
$$cookieWriter(key, undefined);
remove: function(key, options) {
$$cookieWriter(key, undefined, options);
}
};
}]);
@@ -127,6 +127,72 @@ describe('$$cookieWriter', function() {
expect(document.cookie).toEqual('cookie=bender');
});
});
});
describe('cookie options', function() {
var fakeDocument, $$cookieWriter;
function getLastCookieAssignment(key) {
return fakeDocument[0].cookie
.split(';')
.reduce(function(prev, value) {
var pair = value.split('=', 2);
if (pair[0] === key) {
if (prev === undefined) {
return pair[1] === undefined ? true : pair[1];
} else {
throw 'duplicate key in cookie string';
}
} else {
return prev;
}
}, undefined);
}
beforeEach(function() {
fakeDocument = [{cookie: ''}];
module('ngCookies', {$document: fakeDocument});
inject(function($browser) {
$browser.$$baseHref = '/a/b';
});
inject(function(_$$cookieWriter_) {
$$cookieWriter = _$$cookieWriter_;
});
});
it('should use baseHref as default path', function() {
$$cookieWriter('name', 'value');
expect(getLastCookieAssignment('path')).toBe('/a/b');
});
it('should accept path option', function() {
$$cookieWriter('name', 'value', {path: '/c/d'});
expect(getLastCookieAssignment('path')).toBe('/c/d');
});
it('should accept domain option', function() {
$$cookieWriter('name', 'value', {domain: '.example.com'});
expect(getLastCookieAssignment('domain')).toBe('.example.com');
});
it('should accept secure option', function() {
$$cookieWriter('name', 'value', {secure: true});
expect(getLastCookieAssignment('secure')).toBe(true);
});
it('should accept expires option on set', function() {
$$cookieWriter('name', 'value', {expires: 'Fri, 19 Dec 2014 00:00:00 GMT'});
expect(getLastCookieAssignment('expires')).toMatch(/^Fri, 19 Dec 2014 00:00:00 (UTC|GMT)$/);
});
it('should always use epoch time as expire time on remove', function() {
$$cookieWriter('name', undefined, {expires: 'Fri, 19 Dec 2014 00:00:00 GMT'});
expect(getLastCookieAssignment('expires')).toMatch(/^Thu, 0?1 Jan 1970 00:00:00 (UTC|GMT)$/);
});
it('should accept date object as expires option', function() {
$$cookieWriter('name', 'value', {expires: new Date(Date.UTC(1981, 11, 27))});
expect(getLastCookieAssignment('expires')).toMatch(/^Sun, 27 Dec 1981 00:00:00 (UTC|GMT)$/);
});
});
@@ -6,9 +6,9 @@ describe('$cookies', function() {
beforeEach(function() {
mockedCookies = {};
module('ngCookies', {
$$cookieWriter: function(name, value) {
$$cookieWriter: jasmine.createSpy('$$cookieWriter').andCallFake(function(name, value) {
mockedCookies[name] = value;
},
}),
$$cookieReader: function() {
return mockedCookies;
}
@@ -65,4 +65,22 @@ describe('$cookies', function() {
$cookies.putObject('name2', 'value2');
expect($cookies.getAll()).toEqual({name: 'value', name2: '"value2"'});
}));
it('should pass options on put', inject(function($cookies, $$cookieWriter) {
$cookies.put('name', 'value', {path: '/a/b'});
expect($$cookieWriter).toHaveBeenCalledWith('name', 'value', {path: '/a/b'});
}));
it('should pass options on putObject', inject(function($cookies, $$cookieWriter) {
$cookies.putObject('name', 'value', {path: '/a/b'});
expect($$cookieWriter).toHaveBeenCalledWith('name', '"value"', {path: '/a/b'});
}));
it('should pass options on remove', inject(function($cookies, $$cookieWriter) {
$cookies.remove('name', {path: '/a/b'});
expect($$cookieWriter).toHaveBeenCalledWith('name', undefined, {path: '/a/b'});
}));
});

5 comments on commit 92c366d

@grantgeorge

This comment has been minimized.

Show comment
Hide comment

yayyy!

@cristianrgreco

This comment has been minimized.

Show comment
Hide comment

yayyy!

@mniehe

This comment has been minimized.

Show comment
Hide comment
@mniehe

mniehe Mar 12, 2015

@shahata, just curious why the expiry is being passed as a string, converted to a Date object, then converted back to a string. Wouldn't it be easier to pass the expiry as a Date originally?

@shahata, just curious why the expiry is being passed as a string, converted to a Date object, then converted back to a string. Wouldn't it be easier to pass the expiry as a Date originally?

@grantgeorge

This comment has been minimized.

Show comment
Hide comment
@grantgeorge

grantgeorge Mar 13, 2015

@mniehe my understanding is that if the expiration date is a string it does a conversion otherwise it proceeds without the conversion.

@mniehe my understanding is that if the expiration date is a string it does a conversion otherwise it proceeds without the conversion.

@mniehe

This comment has been minimized.

Show comment
Hide comment
@mniehe

mniehe Apr 1, 2015

Right, my bad. Thanks for updating this!

Right, my bad. Thanks for updating this!

Please sign in to comment.