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...
1 parent 38fbe3e commit 92c366d205da36ec26502aded23db71a6473dad7 @shahata shahata committed with petebacondarwin Mar 2, 2015
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);
};
}
@@ -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

yayyy!

@cristianrgreco

yayyy!

@mniehe
mniehe commented on 92c366d 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?

@grantgeorge

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

@mniehe
mniehe commented on 92c366d Apr 1, 2015

Right, my bad. Thanks for updating this!

Please sign in to comment.