From 7f06ca9a4473a75578128c5c9c71055f1c02475e Mon Sep 17 00:00:00 2001 From: Shahar Talmi Date: Mon, 2 Mar 2015 21:46:51 +0200 Subject: [PATCH] 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 --- src/ngCookies/cookieWriter.js | 52 +++++++++++++++-------- src/ngCookies/cookies.js | 25 ++++++++--- test/ngCookies/cookieWriterSpec.js | 68 +++++++++++++++++++++++++++++- test/ngCookies/cookiesSpec.js | 22 +++++++++- 4 files changed, 140 insertions(+), 27 deletions(-) diff --git a/src/ngCookies/cookieWriter.js b/src/ngCookies/cookieWriter.js index 4cf714fbea50..b4f496f825bb 100644 --- a/src/ngCookies/cookieWriter.js +++ b/src/ngCookies/cookieWriter.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); }; } diff --git a/src/ngCookies/cookies.js b/src/ngCookies/cookies.js index 47539067b422..8164c4700f63 100644 --- a/src/ngCookies/cookies.js +++ b/src/ngCookies/cookies.js @@ -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); } }; }]); diff --git a/test/ngCookies/cookieWriterSpec.js b/test/ngCookies/cookieWriterSpec.js index 4bd9172e0abd..e94f2b16e7c7 100644 --- a/test/ngCookies/cookieWriterSpec.js +++ b/test/ngCookies/cookieWriterSpec.js @@ -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)$/); + }); + +}); diff --git a/test/ngCookies/cookiesSpec.js b/test/ngCookies/cookiesSpec.js index 9c4eda6ad43c..bedfcf8697b2 100644 --- a/test/ngCookies/cookiesSpec.js +++ b/test/ngCookies/cookiesSpec.js @@ -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'}); + })); });