From f4c1295c7bed9f0f90a60b6c9c7d2f05fe3e4105 Mon Sep 17 00:00:00 2001 From: Shahar Talmi Date: Mon, 2 Mar 2015 13:24:30 +0200 Subject: [PATCH 1/6] refactor($browser): split cookie access into $$cookieReader and $$cookieWriter services --- angularFiles.js | 8 +- src/AngularPublic.js | 6 +- src/ng/browser.js | 83 ----------- src/ng/cookieReader.js | 55 ++++++++ src/ng/http.js | 6 +- src/ngCookies/cookieWriter.js | 44 ++++++ src/ngCookies/cookies.js | 10 +- src/ngMock/angular-mocks.js | 21 --- test/ng/browserSpecs.js | 215 ----------------------------- test/ng/cookieReaderSpec.js | 104 ++++++++++++++ test/ng/httpSpec.js | 33 ++--- test/ngCookies/cookieWriterSpec.js | 132 ++++++++++++++++++ test/ngCookies/cookiesSpec.js | 107 +++++++------- 13 files changed, 425 insertions(+), 399 deletions(-) create mode 100644 src/ng/cookieReader.js create mode 100644 src/ngCookies/cookieWriter.js create mode 100644 test/ng/cookieReaderSpec.js create mode 100644 test/ngCookies/cookieWriterSpec.js diff --git a/angularFiles.js b/angularFiles.js index 696bd8e78ec8..20bd1c3edded 100755 --- a/angularFiles.js +++ b/angularFiles.js @@ -40,6 +40,7 @@ var angularFiles = { 'src/ng/timeout.js', 'src/ng/urlUtils.js', 'src/ng/window.js', + 'src/ng/cookieReader.js', 'src/ng/filter.js', 'src/ng/filter/filter.js', @@ -89,7 +90,8 @@ var angularFiles = { 'src/ngAnimate/animate.js' ], 'ngCookies': [ - 'src/ngCookies/cookies.js' + 'src/ngCookies/cookies.js', + 'src/ngCookies/cookieWriter.js' ], 'ngMessages': [ 'src/ngMessages/messages.js' @@ -162,7 +164,7 @@ var angularFiles = { 'src/publishExternalApis.js', '@angularSrcModules', '@angularScenario', - '@angularTest', + '@angularTest' ], 'karmaExclude': [ @@ -197,7 +199,7 @@ var angularFiles = { 'src/publishExternalApis.js', '@angularSrcModules', '@angularScenario', - '@angularTest', + '@angularTest' ], 'karmaJqueryExclude': [ diff --git a/src/AngularPublic.js b/src/AngularPublic.js index b81257b9fff7..31e8483ee6bf 100644 --- a/src/AngularPublic.js +++ b/src/AngularPublic.js @@ -84,7 +84,8 @@ $$RAFProvider, $$AsyncCallbackProvider, $WindowProvider, - $$jqLiteProvider + $$jqLiteProvider, + $$CookieReaderProvider */ @@ -238,7 +239,8 @@ function publishExternalAPI(angular) { $window: $WindowProvider, $$rAF: $$RAFProvider, $$asyncCallback: $$AsyncCallbackProvider, - $$jqLite: $$jqLiteProvider + $$jqLite: $$jqLiteProvider, + $$cookieReader: $$CookieReaderProvider }); } ]); diff --git a/src/ng/browser.js b/src/ng/browser.js index af2dd70e76a9..ad8ae3057644 100644 --- a/src/ng/browser.js +++ b/src/ng/browser.js @@ -324,89 +324,6 @@ function Browser(window, document, $log, $sniffer) { return href ? href.replace(/^(https?\:)?\/\/[^\/]*/, '') : ''; }; - ////////////////////////////////////////////////////////////// - // Cookies API - ////////////////////////////////////////////////////////////// - var lastCookies = {}; - var lastCookieString = ''; - var cookiePath = self.baseHref(); - - function safeDecodeURIComponent(str) { - try { - return decodeURIComponent(str); - } catch (e) { - return str; - } - } - - /** - * @name $browser#cookies - * - * @param {string=} name Cookie name - * @param {string=} value Cookie value - * - * @description - * The cookies method provides a 'private' low level access to browser cookies. - * It is not meant to be used directly, use the $cookie service instead. - * - * The return values vary depending on the arguments that the method was called with as follows: - * - * - cookies() -> hash of all cookies, this is NOT a copy of the internal state, so do not modify - * it - * - cookies(name, value) -> set name to value, if value is undefined delete the cookie - * - cookies(name) -> the same as (name, undefined) == DELETES (no one calls it right now that - * way) - * - * @returns {Object} Hash of all cookies (if called without any parameter) - */ - self.cookies = function(name, value) { - var cookieLength, cookieArray, cookie, i, index; - - if (name) { - if (value === undefined) { - rawDocument.cookie = encodeURIComponent(name) + "=;path=" + cookiePath + - ";expires=Thu, 01 Jan 1970 00:00:00 GMT"; - } else { - if (isString(value)) { - 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)!"); - } - } - } - } else { - if (rawDocument.cookie !== lastCookieString) { - lastCookieString = rawDocument.cookie; - cookieArray = lastCookieString.split("; "); - lastCookies = {}; - - for (i = 0; i < cookieArray.length; i++) { - cookie = cookieArray[i]; - index = cookie.indexOf('='); - if (index > 0) { //ignore nameless cookies - name = safeDecodeURIComponent(cookie.substring(0, index)); - // the first value that is seen for a cookie is the most - // specific one. values for the same cookie name that - // follow are for less specific paths. - if (lastCookies[name] === undefined) { - lastCookies[name] = safeDecodeURIComponent(cookie.substring(index + 1)); - } - } - } - } - return lastCookies; - } - }; - - /** * @name $browser#defer * @param {function()} fn A function, who's execution should be deferred. diff --git a/src/ng/cookieReader.js b/src/ng/cookieReader.js new file mode 100644 index 000000000000..53bc279f4269 --- /dev/null +++ b/src/ng/cookieReader.js @@ -0,0 +1,55 @@ +'use strict'; + +/** + * @name $$cookieReader + * @requires $document + * + * @description + * This is a private service for reading cookies used by $http and ngCookies + * + * @return {Object} a key/value map of the current cookies + */ +function $$CookieReader($document) { + var rawDocument = $document[0]; + var lastCookies = {}; + var lastCookieString = ''; + + function safeDecodeURIComponent(str) { + try { + return decodeURIComponent(str); + } catch (e) { + return str; + } + } + + return function() { + var cookieArray, cookie, i, index, name; + + if (rawDocument.cookie !== lastCookieString) { + lastCookieString = rawDocument.cookie; + cookieArray = lastCookieString.split('; '); + lastCookies = {}; + + for (i = 0; i < cookieArray.length; i++) { + cookie = cookieArray[i]; + index = cookie.indexOf('='); + if (index > 0) { //ignore nameless cookies + name = safeDecodeURIComponent(cookie.substring(0, index)); + // the first value that is seen for a cookie is the most + // specific one. values for the same cookie name that + // follow are for less specific paths. + if (lastCookies[name] === undefined) { + lastCookies[name] = safeDecodeURIComponent(cookie.substring(index + 1)); + } + } + } + } + return lastCookies; + }; +} + +$$CookieReader.$inject = ['$document']; + +function $$CookieReaderProvider() { + this.$get = $$CookieReader; +} diff --git a/src/ng/http.js b/src/ng/http.js index dc37c1625a09..95a33ae7a1fb 100644 --- a/src/ng/http.js +++ b/src/ng/http.js @@ -220,8 +220,8 @@ function $HttpProvider() { **/ var interceptorFactories = this.interceptors = []; - this.$get = ['$httpBackend', '$browser', '$cacheFactory', '$rootScope', '$q', '$injector', - function($httpBackend, $browser, $cacheFactory, $rootScope, $q, $injector) { + this.$get = ['$httpBackend', '$$cookieReader', '$cacheFactory', '$rootScope', '$q', '$injector', + function($httpBackend, $$cookieReader, $cacheFactory, $rootScope, $q, $injector) { var defaultCache = $cacheFactory('$http'); @@ -1066,7 +1066,7 @@ function $HttpProvider() { // send the request to the backend if (isUndefined(cachedResp)) { var xsrfValue = urlIsSameOrigin(config.url) - ? $browser.cookies()[config.xsrfCookieName || defaults.xsrfCookieName] + ? $$cookieReader()[config.xsrfCookieName || defaults.xsrfCookieName] : undefined; if (xsrfValue) { reqHeaders[(config.xsrfHeaderName || defaults.xsrfHeaderName)] = xsrfValue; diff --git a/src/ngCookies/cookieWriter.js b/src/ngCookies/cookieWriter.js new file mode 100644 index 000000000000..4cf714fbea50 --- /dev/null +++ b/src/ngCookies/cookieWriter.js @@ -0,0 +1,44 @@ +'use strict'; + +/** + * @name $$cookieWriter + * @requires $document + * + * @description + * This is a private service for writing cookies + * + * @param {string} name Cookie name + * @param {string=} value Cookie value (if undefined, cookie will be deleted) + */ +function $$CookieWriter($document, $log, $browser) { + var cookiePath = $browser.baseHref(); + var rawDocument = $document[0]; + + return function(name, value) { + 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)!"); + } + } + } + }; +} + +$$CookieWriter.$inject = ['$document', '$log', '$browser']; + +angular.module('ngCookies').provider('$$cookieWriter', function $$CookieWriterProvider() { + this.$get = $$CookieWriter; +}); diff --git a/src/ngCookies/cookies.js b/src/ngCookies/cookies.js index fcb66e62d16a..d458243b25d1 100644 --- a/src/ngCookies/cookies.js +++ b/src/ngCookies/cookies.js @@ -43,7 +43,7 @@ angular.module('ngCookies', ['ng']). * }]); * ``` */ - factory('$cookies', ['$rootScope', '$browser', function($rootScope, $browser) { + factory('$cookies', ['$rootScope', '$browser', '$$cookieReader', '$$cookieWriter', function($rootScope, $browser, $$cookieReader, $$cookieWriter) { var cookies = {}, lastCookies = {}, lastBrowserCookies, @@ -53,7 +53,7 @@ angular.module('ngCookies', ['ng']). //creates a poller fn that copies all cookies from the $browser to service & inits the service $browser.addPollFn(function() { - var currentCookies = $browser.cookies(); + var currentCookies = $$cookieReader(); if (lastBrowserCookies != currentCookies) { //relies on browser.cookies() impl lastBrowserCookies = currentCookies; copy(currentCookies, lastCookies); @@ -85,7 +85,7 @@ angular.module('ngCookies', ['ng']). //delete any cookies deleted in $cookies for (name in lastCookies) { if (isUndefined(cookies[name])) { - $browser.cookies(name, undefined); + $$cookieWriter(name, undefined); } } @@ -97,7 +97,7 @@ angular.module('ngCookies', ['ng']). cookies[name] = value; } if (value !== lastCookies[name]) { - $browser.cookies(name, value); + $$cookieWriter(name, value); updated = true; } } @@ -105,7 +105,7 @@ angular.module('ngCookies', ['ng']). //verify what was actually stored if (updated) { updated = false; - browserCookies = $browser.cookies(); + browserCookies = $$cookieReader(); for (name in cookies) { if (cookies[name] !== browserCookies[name]) { diff --git a/src/ngMock/angular-mocks.js b/src/ngMock/angular-mocks.js index dcefe12008ca..ca4e271cd181 100644 --- a/src/ngMock/angular-mocks.js +++ b/src/ngMock/angular-mocks.js @@ -59,8 +59,6 @@ angular.mock.$Browser = function() { self.$$checkUrlChange = angular.noop; - self.cookieHash = {}; - self.lastCookieHash = {}; self.deferredFns = []; self.deferredNextId = 0; @@ -163,25 +161,6 @@ angular.mock.$Browser.prototype = { return this.$$state; }, - cookies: function(name, value) { - if (name) { - if (angular.isUndefined(value)) { - delete this.cookieHash[name]; - } else { - if (angular.isString(value) && //strings only - value.length <= 4096) { //strict cookie storage limits - this.cookieHash[name] = value; - } - } - } else { - if (!angular.equals(this.cookieHash, this.lastCookieHash)) { - this.lastCookieHash = angular.copy(this.cookieHash); - this.cookieHash = angular.copy(this.cookieHash); - } - return this.cookieHash; - } - }, - notifyWhenNoOutstandingRequests: function(fn) { fn(); } diff --git a/test/ng/browserSpecs.js b/test/ng/browserSpecs.js index 65321c24a499..fa7d8686d915 100755 --- a/test/ng/browserSpecs.js +++ b/test/ng/browserSpecs.js @@ -189,10 +189,6 @@ describe('browser', function() { } }); - it('should contain cookie cruncher', function() { - expect(browser.cookies).toBeDefined(); - }); - describe('outstading requests', function() { it('should process callbacks immedietly with no outstanding requests', function() { var callback = jasmine.createSpy('callback'); @@ -253,217 +249,6 @@ describe('browser', function() { }); - describe('cookies', function() { - - function deleteAllCookies() { - var cookies = document.cookie.split(";"); - var path = location.pathname; - - for (var i = 0; i < cookies.length; i++) { - var cookie = cookies[i]; - var eqPos = cookie.indexOf("="); - var name = eqPos > -1 ? cookie.substr(0, eqPos) : cookie; - var parts = path.split('/'); - while (parts.length) { - document.cookie = name + "=;path=" + (parts.join('/') || '/') + ";expires=Thu, 01 Jan 1970 00:00:00 GMT"; - parts.pop(); - } - } - } - - beforeEach(function() { - deleteAllCookies(); - expect(document.cookie).toEqual(''); - }); - - - afterEach(function() { - deleteAllCookies(); - expect(document.cookie).toEqual(''); - }); - - - describe('remove all via (null)', function() { - - it('should do nothing when no cookies are set', function() { - browser.cookies(null); - expect(document.cookie).toEqual(''); - expect(browser.cookies()).toEqual({}); - }); - - }); - - describe('remove via cookies(cookieName, undefined)', function() { - - it('should remove a cookie when it is present', function() { - document.cookie = 'foo=bar;path=/'; - - browser.cookies('foo', undefined); - - expect(document.cookie).toEqual(''); - expect(browser.cookies()).toEqual({}); - }); - - - it('should do nothing when an nonexisting cookie is being removed', function() { - browser.cookies('doesntexist', undefined); - expect(document.cookie).toEqual(''); - expect(browser.cookies()).toEqual({}); - }); - }); - - - describe('put via cookies(cookieName, string)', function() { - - it('should create and store a cookie', function() { - browser.cookies('cookieName', 'cookie=Value'); - expect(document.cookie).toMatch(/cookieName=cookie%3DValue;? ?/); - expect(browser.cookies()).toEqual({'cookieName':'cookie=Value'}); - }); - - - it('should overwrite an existing unsynced cookie', function() { - document.cookie = "cookie=new;path=/"; - - var oldVal = browser.cookies('cookie', 'newer'); - - expect(document.cookie).toEqual('cookie=newer'); - expect(browser.cookies()).toEqual({'cookie':'newer'}); - expect(oldVal).not.toBeDefined(); - }); - - it('should encode both name and value', function() { - browser.cookies('cookie1=', 'val;ue'); - browser.cookies('cookie2=bar;baz', 'val=ue'); - - var rawCookies = document.cookie.split("; "); //order is not guaranteed, so we need to parse - expect(rawCookies.length).toEqual(2); - expect(rawCookies).toContain('cookie1%3D=val%3Bue'); - expect(rawCookies).toContain('cookie2%3Dbar%3Bbaz=val%3Due'); - }); - - it('should log warnings when 4kb per cookie storage limit is reached', function() { - var i, longVal = '', cookieStr; - - for (i = 0; i < 4083; i++) { - longVal += 'x'; - } - - cookieStr = document.cookie; - browser.cookies('x', longVal); //total size 4093-4096, so it should go through - expect(document.cookie).not.toEqual(cookieStr); - expect(browser.cookies()['x']).toEqual(longVal); - expect(logs.warn).toEqual([]); - - browser.cookies('x', longVal + 'xxxx'); //total size 4097-4099, a warning should be logged - expect(logs.warn).toEqual( - [["Cookie 'x' possibly not set or overflowed because it was too large (4097 > 4096 " + - "bytes)!"]]); - - //force browser to dropped a cookie and make sure that the cache is not out of sync - browser.cookies('x', 'shortVal'); - expect(browser.cookies().x).toEqual('shortVal'); //needed to prime the cache - cookieStr = document.cookie; - browser.cookies('x', longVal + longVal + longVal); //should be too long for all browsers - - if (document.cookie !== cookieStr) { - this.fail(new Error("browser didn't drop long cookie when it was expected. make the " + - "cookie in this test longer")); - } - - expect(browser.cookies().x).toEqual('shortVal'); - }); - }); - - describe('put via cookies(cookieName, string), if no ', function() { - beforeEach(function() { - fakeDocument.basePath = undefined; - }); - - it('should default path in cookie to "" (empty string)', function() { - browser.cookies('cookie', 'bender'); - // This only fails in Safari and IE when cookiePath returns undefined - // Where it now succeeds since baseHref return '' instead of undefined - expect(document.cookie).toEqual('cookie=bender'); - }); - }); - - describe('get via cookies()[cookieName]', function() { - - it('should return undefined for nonexistent cookie', function() { - expect(browser.cookies().nonexistent).not.toBeDefined(); - }); - - - it('should return a value for an existing cookie', function() { - document.cookie = "foo=bar=baz;path=/"; - expect(browser.cookies().foo).toEqual('bar=baz'); - }); - - it('should return the the first value provided for a cookie', function() { - // For a cookie that has different values that differ by path, the - // value for the most specific path appears first. browser.cookies() - // should provide that value for the cookie. - document.cookie = 'foo="first"; foo="second"'; - expect(browser.cookies()['foo']).toBe('"first"'); - }); - - it('should decode cookie values that were encoded by puts', function() { - document.cookie = "cookie2%3Dbar%3Bbaz=val%3Due;path=/"; - expect(browser.cookies()['cookie2=bar;baz']).toEqual('val=ue'); - }); - - - it('should preserve leading & trailing spaces in names and values', function() { - browser.cookies(' cookie name ', ' cookie value '); - expect(browser.cookies()[' cookie name ']).toEqual(' cookie value '); - expect(browser.cookies()['cookie name']).not.toBeDefined(); - }); - - it('should decode special characters in cookie values', function() { - document.cookie = 'cookie_name=cookie_value_%E2%82%AC'; - expect(browser.cookies()['cookie_name']).toEqual('cookie_value_€'); - }); - - it('should not decode cookie values that do not appear to be encoded', function() { - // see #9211 - sometimes cookies contain a value that causes decodeURIComponent to throw - document.cookie = 'cookie_name=cookie_value_%XX'; - expect(browser.cookies()['cookie_name']).toEqual('cookie_value_%XX'); - }); - }); - - - describe('getAll via cookies()', function() { - - it('should return cookies as hash', function() { - document.cookie = "foo1=bar1;path=/"; - document.cookie = "foo2=bar2;path=/"; - expect(browser.cookies()).toEqual({'foo1':'bar1', 'foo2':'bar2'}); - }); - - - it('should return empty hash if no cookies exist', function() { - expect(browser.cookies()).toEqual({}); - }); - }); - - - it('should pick up external changes made to browser cookies', function() { - browser.cookies('oatmealCookie', 'drool'); - expect(browser.cookies()).toEqual({'oatmealCookie':'drool'}); - - document.cookie = 'oatmealCookie=changed;path=/'; - expect(browser.cookies().oatmealCookie).toEqual('changed'); - }); - - - it('should initialize cookie cache with existing cookies', function() { - document.cookie = "existingCookie=existingValue;path=/"; - expect(browser.cookies()).toEqual({'existingCookie':'existingValue'}); - }); - - }); - describe('poller', function() { it('should call functions in pollFns in regular intervals', function() { diff --git a/test/ng/cookieReaderSpec.js b/test/ng/cookieReaderSpec.js new file mode 100644 index 000000000000..29a672953d26 --- /dev/null +++ b/test/ng/cookieReaderSpec.js @@ -0,0 +1,104 @@ +'use strict'; + +describe('$$cookieReader', function() { + var $$cookieReader; + + function deleteAllCookies() { + var cookies = document.cookie.split(";"); + var path = location.pathname; + + for (var i = 0; i < cookies.length; i++) { + var cookie = cookies[i]; + var eqPos = cookie.indexOf("="); + var name = eqPos > -1 ? cookie.substr(0, eqPos) : cookie; + var parts = path.split('/'); + while (parts.length) { + document.cookie = name + "=;path=" + (parts.join('/') || '/') + ";expires=Thu, 01 Jan 1970 00:00:00 GMT"; + parts.pop(); + } + } + } + + beforeEach(function() { + deleteAllCookies(); + expect(document.cookie).toEqual(''); + + inject(function(_$$cookieReader_) { + $$cookieReader = _$$cookieReader_; + }); + }); + + + afterEach(function() { + deleteAllCookies(); + expect(document.cookie).toEqual(''); + }); + + + describe('get via $$cookieReader()[cookieName]', function() { + + it('should return undefined for nonexistent cookie', function() { + expect($$cookieReader().nonexistent).not.toBeDefined(); + }); + + + it('should return a value for an existing cookie', function() { + document.cookie = "foo=bar=baz;path=/"; + expect($$cookieReader().foo).toEqual('bar=baz'); + }); + + it('should return the the first value provided for a cookie', function() { + // For a cookie that has different values that differ by path, the + // value for the most specific path appears first. $$cookieReader() + // should provide that value for the cookie. + document.cookie = 'foo="first"; foo="second"'; + expect($$cookieReader()['foo']).toBe('"first"'); + }); + + it('should decode cookie values that were encoded by puts', function() { + document.cookie = "cookie2%3Dbar%3Bbaz=val%3Due;path=/"; + expect($$cookieReader()['cookie2=bar;baz']).toEqual('val=ue'); + }); + + + it('should preserve leading & trailing spaces in names and values', function() { + document.cookie = '%20cookie%20name%20=%20cookie%20value%20'; + expect($$cookieReader()[' cookie name ']).toEqual(' cookie value '); + expect($$cookieReader()['cookie name']).not.toBeDefined(); + }); + + it('should decode special characters in cookie values', function() { + document.cookie = 'cookie_name=cookie_value_%E2%82%AC'; + expect($$cookieReader()['cookie_name']).toEqual('cookie_value_€'); + }); + + it('should not decode cookie values that do not appear to be encoded', function() { + // see #9211 - sometimes cookies contain a value that causes decodeURIComponent to throw + document.cookie = 'cookie_name=cookie_value_%XX'; + expect($$cookieReader()['cookie_name']).toEqual('cookie_value_%XX'); + }); + }); + + + describe('getAll via $$cookieReader()', function() { + + it('should return cookies as hash', function() { + document.cookie = "foo1=bar1;path=/"; + document.cookie = "foo2=bar2;path=/"; + expect($$cookieReader()).toEqual({'foo1':'bar1', 'foo2':'bar2'}); + }); + + + it('should return empty hash if no cookies exist', function() { + expect($$cookieReader()).toEqual({}); + }); + }); + + + it('should initialize cookie cache with existing cookies', function() { + document.cookie = "existingCookie=existingValue;path=/"; + expect($$cookieReader()).toEqual({'existingCookie':'existingValue'}); + }); + +}); + diff --git a/test/ng/httpSpec.js b/test/ng/httpSpec.js index 1a394f3e3fbf..dbb6290323ae 100644 --- a/test/ng/httpSpec.js +++ b/test/ng/httpSpec.js @@ -2,10 +2,16 @@ describe('$http', function() { - var callback; + var callback, mockedCookies; beforeEach(function() { callback = jasmine.createSpy('done'); + mockedCookies = {}; + module({ + $$cookieReader: function() { + return mockedCookies; + } + }); }); beforeEach(module(function($exceptionHandlerProvider) { @@ -691,7 +697,7 @@ describe('$http', function() { }); it('should not set XSRF cookie for cross-domain requests', inject(function($browser) { - $browser.cookies('XSRF-TOKEN', 'secret'); + mockedCookies['XSRF-TOKEN'] = 'secret'; $browser.url('http://host.com/base'); $httpBackend.expect('GET', 'http://www.test.com/url', undefined, function(headers) { return headers['X-XSRF-TOKEN'] === undefined; @@ -733,15 +739,15 @@ describe('$http', function() { $httpBackend.flush(); }); - it('should set the XSRF cookie into a XSRF header', inject(function($browser) { + it('should set the XSRF cookie into a XSRF header', inject(function() { function checkXSRF(secret, header) { return function(headers) { return headers[header || 'X-XSRF-TOKEN'] == secret; }; } - $browser.cookies('XSRF-TOKEN', 'secret'); - $browser.cookies('aCookie', 'secret2'); + mockedCookies['XSRF-TOKEN'] = 'secret'; + mockedCookies['aCookie'] = 'secret2'; $httpBackend.expect('GET', '/url', undefined, checkXSRF('secret')).respond(''); $httpBackend.expect('POST', '/url', undefined, checkXSRF('secret')).respond(''); $httpBackend.expect('PUT', '/url', undefined, checkXSRF('secret')).respond(''); @@ -809,23 +815,18 @@ describe('$http', function() { expect(config.foo).toBeUndefined(); }); - it('should check the cache before checking the XSRF cookie', inject(function($browser, $cacheFactory) { - var testCache = $cacheFactory('testCache'), - executionOrder = []; + it('should check the cache before checking the XSRF cookie', inject(function($cacheFactory) { + var testCache = $cacheFactory('testCache'); - spyOn($browser, 'cookies').andCallFake(function() { - executionOrder.push('cookies'); - return {'XSRF-TOKEN':'foo'}; - }); spyOn(testCache, 'get').andCallFake(function() { - executionOrder.push('cache'); + mockedCookies['XSRF-TOKEN'] = 'foo'; }); - $httpBackend.expect('GET', '/url', undefined).respond(''); + $httpBackend.expect('GET', '/url', undefined, function(headers) { + return headers['X-XSRF-TOKEN'] === 'foo'; + }).respond(''); $http({url: '/url', method: 'GET', cache: testCache}); $httpBackend.flush(); - - expect(executionOrder).toEqual(['cache', 'cookies']); })); }); diff --git a/test/ngCookies/cookieWriterSpec.js b/test/ngCookies/cookieWriterSpec.js new file mode 100644 index 000000000000..4bd9172e0abd --- /dev/null +++ b/test/ngCookies/cookieWriterSpec.js @@ -0,0 +1,132 @@ +'use strict'; + +describe('$$cookieWriter', function() { + var $$cookieWriter; + + function deleteAllCookies() { + var cookies = document.cookie.split(";"); + var path = location.pathname; + + for (var i = 0; i < cookies.length; i++) { + var cookie = cookies[i]; + var eqPos = cookie.indexOf("="); + var name = eqPos > -1 ? cookie.substr(0, eqPos) : cookie; + var parts = path.split('/'); + while (parts.length) { + document.cookie = name + "=;path=" + (parts.join('/') || '/') + ";expires=Thu, 01 Jan 1970 00:00:00 GMT"; + parts.pop(); + } + } + } + + beforeEach(function() { + deleteAllCookies(); + expect(document.cookie).toEqual(''); + + module('ngCookies'); + inject(function(_$$cookieWriter_) { + $$cookieWriter = _$$cookieWriter_; + }); + }); + + + afterEach(function() { + deleteAllCookies(); + expect(document.cookie).toEqual(''); + }); + + + describe('remove via $$cookieWriter(cookieName, undefined)', function() { + + it('should remove a cookie when it is present', function() { + document.cookie = 'foo=bar;path=/'; + + $$cookieWriter('foo', undefined); + + expect(document.cookie).toEqual(''); + }); + + + it('should do nothing when an nonexisting cookie is being removed', function() { + $$cookieWriter('doesntexist', undefined); + expect(document.cookie).toEqual(''); + }); + }); + + + describe('put via $$cookieWriter(cookieName, string)', function() { + + it('should create and store a cookie', function() { + $$cookieWriter('cookieName', 'cookie=Value'); + expect(document.cookie).toMatch(/cookieName=cookie%3DValue;? ?/); + }); + + + it('should overwrite an existing unsynced cookie', function() { + document.cookie = "cookie=new;path=/"; + + var oldVal = $$cookieWriter('cookie', 'newer'); + + expect(document.cookie).toEqual('cookie=newer'); + expect(oldVal).not.toBeDefined(); + }); + + it('should encode both name and value', function() { + $$cookieWriter('cookie1=', 'val;ue'); + $$cookieWriter('cookie2=bar;baz', 'val=ue'); + + var rawCookies = document.cookie.split("; "); //order is not guaranteed, so we need to parse + expect(rawCookies.length).toEqual(2); + expect(rawCookies).toContain('cookie1%3D=val%3Bue'); + expect(rawCookies).toContain('cookie2%3Dbar%3Bbaz=val%3Due'); + }); + + it('should log warnings when 4kb per cookie storage limit is reached', inject(function($log) { + var i, longVal = '', cookieStr; + + for (i = 0; i < 4083; i++) { + longVal += 'x'; + } + + cookieStr = document.cookie; + $$cookieWriter('x', longVal); //total size 4093-4096, so it should go through + expect(document.cookie).not.toEqual(cookieStr); + expect(document.cookie).toEqual('x=' + longVal); + expect($log.warn.logs).toEqual([]); + + $$cookieWriter('x', longVal + 'xxxx'); //total size 4097-4099, a warning should be logged + expect($log.warn.logs).toEqual( + [["Cookie 'x' possibly not set or overflowed because it was too large (4097 > 4096 " + + "bytes)!"]]); + + //force browser to dropped a cookie and make sure that the cache is not out of sync + $$cookieWriter('x', 'shortVal'); + expect(document.cookie).toEqual('x=shortVal'); //needed to prime the cache + cookieStr = document.cookie; + $$cookieWriter('x', longVal + longVal + longVal); //should be too long for all browsers + + if (document.cookie !== cookieStr) { + this.fail(new Error("browser didn't drop long cookie when it was expected. make the " + + "cookie in this test longer")); + } + + expect(document.cookie).toEqual('x=shortVal'); + $log.reset(); + })); + }); + + describe('put via $$cookieWriter(cookieName, string), if no ', function() { + beforeEach(inject(function($browser) { + $browser.$$baseHref = undefined; + })); + + it('should default path in cookie to "" (empty string)', function() { + $$cookieWriter('cookie', 'bender'); + // This only fails in Safari and IE when cookiePath returns undefined + // Where it now succeeds since baseHref return '' instead of undefined + expect(document.cookie).toEqual('cookie=bender'); + }); + }); + +}); + diff --git a/test/ngCookies/cookiesSpec.js b/test/ngCookies/cookiesSpec.js index 0a36c92f2aef..1d19221e1f7f 100644 --- a/test/ngCookies/cookiesSpec.js +++ b/test/ngCookies/cookiesSpec.js @@ -1,11 +1,24 @@ 'use strict'; describe('$cookies', function() { - beforeEach(module('ngCookies', function($provide) { - $provide.factory('$browser', function() { - return angular.extend(new angular.mock.$Browser(), {cookieHash: {preexisting:'oldCookie'}}); + var mockedCookies; + + beforeEach(function() { + var lastCookies = {}; + mockedCookies = {preexisting:'oldCookie'}; + module('ngCookies', { + $$cookieWriter: function(name, value) { + mockedCookies[name] = value; + }, + $$cookieReader: function() { + if (!angular.equals(lastCookies, mockedCookies)) { + lastCookies = angular.copy(mockedCookies); + mockedCookies = angular.copy(mockedCookies); + } + return mockedCookies; + } }); - })); + }); it('should provide access to existing cookies via object properties and keep them in sync', @@ -14,45 +27,45 @@ describe('$cookies', function() { // access internal cookie storage of the browser mock directly to simulate behavior of // document.cookie - $browser.cookieHash['brandNew'] = 'cookie'; + mockedCookies['brandNew'] = 'cookie'; $browser.poll(); expect($cookies).toEqual({'preexisting': 'oldCookie', 'brandNew':'cookie'}); - $browser.cookieHash['brandNew'] = 'cookie2'; + mockedCookies['brandNew'] = 'cookie2'; $browser.poll(); expect($cookies).toEqual({'preexisting': 'oldCookie', 'brandNew':'cookie2'}); - delete $browser.cookieHash['brandNew']; + delete mockedCookies['brandNew']; $browser.poll(); expect($cookies).toEqual({'preexisting': 'oldCookie'}); })); it('should create or update a cookie when a value is assigned to a property', - inject(function($cookies, $browser, $rootScope) { + inject(function($cookies, $$cookieReader, $rootScope) { $cookies.oatmealCookie = 'nom nom'; $rootScope.$digest(); - expect($browser.cookies()). + expect($$cookieReader()). toEqual({'preexisting': 'oldCookie', 'oatmealCookie':'nom nom'}); $cookies.oatmealCookie = 'gone'; $rootScope.$digest(); - expect($browser.cookies()). + expect($$cookieReader()). toEqual({'preexisting': 'oldCookie', 'oatmealCookie': 'gone'}); })); it('should convert non-string values to string', - inject(function($cookies, $browser, $rootScope) { + inject(function($cookies, $$cookieReader, $rootScope) { $cookies.nonString = [1, 2, 3]; $cookies.nullVal = null; $cookies.undefVal = undefined; var preexisting = $cookies.preexisting = function() {}; $rootScope.$digest(); - expect($browser.cookies()).toEqual({ + expect($$cookieReader()).toEqual({ 'preexisting': '' + preexisting, 'nonString': '1,2,3', 'nullVal': 'null', @@ -68,81 +81,73 @@ describe('$cookies', function() { it('should remove a cookie when a $cookies property is deleted', - inject(function($cookies, $browser, $rootScope) { + inject(function($cookies, $browser, $rootScope, $$cookieReader) { $cookies.oatmealCookie = 'nom nom'; $rootScope.$digest(); $browser.poll(); - expect($browser.cookies()). + expect($$cookieReader()). toEqual({'preexisting': 'oldCookie', 'oatmealCookie':'nom nom'}); delete $cookies.oatmealCookie; $rootScope.$digest(); - expect($browser.cookies()).toEqual({'preexisting': 'oldCookie'}); - })); - - - it('should drop or reset cookies that browser refused to store', - inject(function($cookies, $browser, $rootScope) { - var i, longVal; - - for (i = 0; i < 5000; i++) { - longVal += '*'; - } - - //drop if no previous value - $cookies.longCookie = longVal; - $rootScope.$digest(); - expect($cookies).toEqual({'preexisting': 'oldCookie'}); - - - //reset if previous value existed - $cookies.longCookie = 'shortVal'; - $rootScope.$digest(); - expect($cookies).toEqual({'preexisting': 'oldCookie', 'longCookie': 'shortVal'}); - $cookies.longCookie = longVal; - $rootScope.$digest(); - expect($cookies).toEqual({'preexisting': 'oldCookie', 'longCookie': 'shortVal'}); + expect($$cookieReader()).toEqual({'preexisting': 'oldCookie'}); })); }); describe('$cookieStore', function() { + var mockedCookies; + + beforeEach(function() { + var lastCookies = {}; + mockedCookies = {}; + module('ngCookies', { + $$cookieWriter: function(name, value) { + mockedCookies[name] = value; + }, + $$cookieReader: function() { + if (!angular.equals(lastCookies, mockedCookies)) { + lastCookies = angular.copy(mockedCookies); + mockedCookies = angular.copy(mockedCookies); + } + return mockedCookies; + } + }); + }); - beforeEach(module('ngCookies')); - - it('should serialize objects to json', inject(function($cookieStore, $browser, $rootScope) { + it('should serialize objects to json', inject(function($cookieStore, $$cookieReader, $rootScope) { $cookieStore.put('objectCookie', {id: 123, name: 'blah'}); $rootScope.$digest(); - expect($browser.cookies()).toEqual({'objectCookie': '{"id":123,"name":"blah"}'}); + expect($$cookieReader()).toEqual({'objectCookie': '{"id":123,"name":"blah"}'}); })); - it('should deserialize json to object', inject(function($cookieStore, $browser) { - $browser.cookies('objectCookie', '{"id":123,"name":"blah"}'); + it('should deserialize json to object', inject(function($cookieStore, $browser, $$cookieWriter) { + $$cookieWriter('objectCookie', '{"id":123,"name":"blah"}'); $browser.poll(); expect($cookieStore.get('objectCookie')).toEqual({id: 123, name: 'blah'}); })); - it('should delete objects from the store when remove is called', inject(function($cookieStore, $browser, $rootScope) { + it('should delete objects from the store when remove is called', inject(function($cookieStore, $browser, $rootScope, $$cookieReader) { $cookieStore.put('gonner', { "I'll":"Be Back"}); $rootScope.$digest(); //force eval in test $browser.poll(); - expect($browser.cookies()).toEqual({'gonner': '{"I\'ll":"Be Back"}'}); + expect($$cookieReader()).toEqual({'gonner': '{"I\'ll":"Be Back"}'}); $cookieStore.remove('gonner'); $rootScope.$digest(); - expect($browser.cookies()).toEqual({}); + expect($$cookieReader()).toEqual({}); })); - it('should handle empty string value cookies', inject(function($cookieStore, $browser, $rootScope) { + it('should handle empty string value cookies', inject(function($cookieStore, $browser, $rootScope, $$cookieReader) { $cookieStore.put("emptyCookie",''); $rootScope.$digest(); - expect($browser.cookies()). + expect($$cookieReader()). toEqual({ 'emptyCookie': '""' }); expect($cookieStore.get("emptyCookie")).toEqual(''); - $browser.cookieHash['blankCookie'] = ''; + mockedCookies['blankCookie'] = ''; $browser.poll(); expect($cookieStore.get("blankCookie")).toEqual(''); })); From 44298b3d0e6910d439f076009098b0306601d213 Mon Sep 17 00:00:00 2001 From: Shahar Talmi Date: Mon, 2 Mar 2015 13:42:21 +0200 Subject: [PATCH 2/6] refactor(ngCookies): split $cookies/$cookieStore to two files --- angularFiles.js | 1 + src/ngCookies/cookieStore.js | 76 +++++++++++++++++++++++++++++++ src/ngCookies/cookies.js | 75 ------------------------------ test/ngCookies/cookieStoreSpec.js | 58 +++++++++++++++++++++++ test/ngCookies/cookiesSpec.js | 58 ----------------------- 5 files changed, 135 insertions(+), 133 deletions(-) create mode 100644 src/ngCookies/cookieStore.js create mode 100644 test/ngCookies/cookieStoreSpec.js diff --git a/angularFiles.js b/angularFiles.js index 20bd1c3edded..7b8ac77b9b08 100755 --- a/angularFiles.js +++ b/angularFiles.js @@ -91,6 +91,7 @@ var angularFiles = { ], 'ngCookies': [ 'src/ngCookies/cookies.js', + 'src/ngCookies/cookieStore.js', 'src/ngCookies/cookieWriter.js' ], 'ngMessages': [ diff --git a/src/ngCookies/cookieStore.js b/src/ngCookies/cookieStore.js new file mode 100644 index 000000000000..62c9d790b37a --- /dev/null +++ b/src/ngCookies/cookieStore.js @@ -0,0 +1,76 @@ +'use strict'; + +angular.module('ngCookies'). +/** + * @ngdoc service + * @name $cookieStore + * @requires $cookies + * + * @description + * Provides a key-value (string-object) storage, that is backed by session cookies. + * Objects put or retrieved from this storage are automatically serialized or + * deserialized by angular's toJson/fromJson. + * + * Requires the {@link ngCookies `ngCookies`} module to be installed. + * + * @example + * + * ```js + * angular.module('cookieStoreExample', ['ngCookies']) + * .controller('ExampleController', ['$cookieStore', function($cookieStore) { + * // Put cookie + * $cookieStore.put('myFavorite','oatmeal'); + * // Get cookie + * var favoriteCookie = $cookieStore.get('myFavorite'); + * // Removing a cookie + * $cookieStore.remove('myFavorite'); + * }]); + * ``` + */ + factory('$cookieStore', ['$cookies', function($cookies) { + + return { + /** + * @ngdoc method + * @name $cookieStore#get + * + * @description + * Returns the value of given cookie key + * + * @param {string} key Id to use for lookup. + * @returns {Object} Deserialized cookie value, undefined if the cookie does not exist. + */ + get: function(key) { + var value = $cookies[key]; + return value ? angular.fromJson(value) : value; + }, + + /** + * @ngdoc method + * @name $cookieStore#put + * + * @description + * Sets a value for given cookie key + * + * @param {string} key Id for the `value`. + * @param {Object} value Value to be stored. + */ + put: function(key, value) { + $cookies[key] = angular.toJson(value); + }, + + /** + * @ngdoc method + * @name $cookieStore#remove + * + * @description + * Remove given cookie + * + * @param {string} key Id of the key-value pair to delete. + */ + remove: function(key) { + delete $cookies[key]; + } + }; + + }]); diff --git a/src/ngCookies/cookies.js b/src/ngCookies/cookies.js index d458243b25d1..3a49c4a3f8fa 100644 --- a/src/ngCookies/cookies.js +++ b/src/ngCookies/cookies.js @@ -120,79 +120,4 @@ angular.module('ngCookies', ['ng']). } } } - }]). - - - /** - * @ngdoc service - * @name $cookieStore - * @requires $cookies - * - * @description - * Provides a key-value (string-object) storage, that is backed by session cookies. - * Objects put or retrieved from this storage are automatically serialized or - * deserialized by angular's toJson/fromJson. - * - * Requires the {@link ngCookies `ngCookies`} module to be installed. - * - * @example - * - * ```js - * angular.module('cookieStoreExample', ['ngCookies']) - * .controller('ExampleController', ['$cookieStore', function($cookieStore) { - * // Put cookie - * $cookieStore.put('myFavorite','oatmeal'); - * // Get cookie - * var favoriteCookie = $cookieStore.get('myFavorite'); - * // Removing a cookie - * $cookieStore.remove('myFavorite'); - * }]); - * ``` - */ - factory('$cookieStore', ['$cookies', function($cookies) { - - return { - /** - * @ngdoc method - * @name $cookieStore#get - * - * @description - * Returns the value of given cookie key - * - * @param {string} key Id to use for lookup. - * @returns {Object} Deserialized cookie value, undefined if the cookie does not exist. - */ - get: function(key) { - var value = $cookies[key]; - return value ? angular.fromJson(value) : value; - }, - - /** - * @ngdoc method - * @name $cookieStore#put - * - * @description - * Sets a value for given cookie key - * - * @param {string} key Id for the `value`. - * @param {Object} value Value to be stored. - */ - put: function(key, value) { - $cookies[key] = angular.toJson(value); - }, - - /** - * @ngdoc method - * @name $cookieStore#remove - * - * @description - * Remove given cookie - * - * @param {string} key Id of the key-value pair to delete. - */ - remove: function(key) { - delete $cookies[key]; - } - }; - }]); diff --git a/test/ngCookies/cookieStoreSpec.js b/test/ngCookies/cookieStoreSpec.js new file mode 100644 index 000000000000..3a73afc5c2f4 --- /dev/null +++ b/test/ngCookies/cookieStoreSpec.js @@ -0,0 +1,58 @@ +'use strict'; + +describe('$cookieStore', function() { + var mockedCookies; + + beforeEach(function() { + var lastCookies = {}; + mockedCookies = {}; + module('ngCookies', { + $$cookieWriter: function(name, value) { + mockedCookies[name] = value; + }, + $$cookieReader: function() { + if (!angular.equals(lastCookies, mockedCookies)) { + lastCookies = angular.copy(mockedCookies); + mockedCookies = angular.copy(mockedCookies); + } + return mockedCookies; + } + }); + }); + + it('should serialize objects to json', inject(function($cookieStore, $$cookieReader, $rootScope) { + $cookieStore.put('objectCookie', {id: 123, name: 'blah'}); + $rootScope.$digest(); + expect($$cookieReader()).toEqual({'objectCookie': '{"id":123,"name":"blah"}'}); + })); + + + it('should deserialize json to object', inject(function($cookieStore, $browser, $$cookieWriter) { + $$cookieWriter('objectCookie', '{"id":123,"name":"blah"}'); + $browser.poll(); + expect($cookieStore.get('objectCookie')).toEqual({id: 123, name: 'blah'}); + })); + + + it('should delete objects from the store when remove is called', inject(function($cookieStore, $browser, $rootScope, $$cookieReader) { + $cookieStore.put('gonner', { "I'll":"Be Back"}); + $rootScope.$digest(); //force eval in test + $browser.poll(); + expect($$cookieReader()).toEqual({'gonner': '{"I\'ll":"Be Back"}'}); + + $cookieStore.remove('gonner'); + $rootScope.$digest(); + expect($$cookieReader()).toEqual({}); + })); + it('should handle empty string value cookies', inject(function($cookieStore, $browser, $rootScope, $$cookieReader) { + $cookieStore.put("emptyCookie",''); + $rootScope.$digest(); + expect($$cookieReader()). + toEqual({ 'emptyCookie': '""' }); + expect($cookieStore.get("emptyCookie")).toEqual(''); + + mockedCookies['blankCookie'] = ''; + $browser.poll(); + expect($cookieStore.get("blankCookie")).toEqual(''); + })); +}); diff --git a/test/ngCookies/cookiesSpec.js b/test/ngCookies/cookiesSpec.js index 1d19221e1f7f..be3cd306f5a6 100644 --- a/test/ngCookies/cookiesSpec.js +++ b/test/ngCookies/cookiesSpec.js @@ -94,61 +94,3 @@ describe('$cookies', function() { expect($$cookieReader()).toEqual({'preexisting': 'oldCookie'}); })); }); - - -describe('$cookieStore', function() { - var mockedCookies; - - beforeEach(function() { - var lastCookies = {}; - mockedCookies = {}; - module('ngCookies', { - $$cookieWriter: function(name, value) { - mockedCookies[name] = value; - }, - $$cookieReader: function() { - if (!angular.equals(lastCookies, mockedCookies)) { - lastCookies = angular.copy(mockedCookies); - mockedCookies = angular.copy(mockedCookies); - } - return mockedCookies; - } - }); - }); - - it('should serialize objects to json', inject(function($cookieStore, $$cookieReader, $rootScope) { - $cookieStore.put('objectCookie', {id: 123, name: 'blah'}); - $rootScope.$digest(); - expect($$cookieReader()).toEqual({'objectCookie': '{"id":123,"name":"blah"}'}); - })); - - - it('should deserialize json to object', inject(function($cookieStore, $browser, $$cookieWriter) { - $$cookieWriter('objectCookie', '{"id":123,"name":"blah"}'); - $browser.poll(); - expect($cookieStore.get('objectCookie')).toEqual({id: 123, name: 'blah'}); - })); - - - it('should delete objects from the store when remove is called', inject(function($cookieStore, $browser, $rootScope, $$cookieReader) { - $cookieStore.put('gonner', { "I'll":"Be Back"}); - $rootScope.$digest(); //force eval in test - $browser.poll(); - expect($$cookieReader()).toEqual({'gonner': '{"I\'ll":"Be Back"}'}); - - $cookieStore.remove('gonner'); - $rootScope.$digest(); - expect($$cookieReader()).toEqual({}); - })); - it('should handle empty string value cookies', inject(function($cookieStore, $browser, $rootScope, $$cookieReader) { - $cookieStore.put("emptyCookie",''); - $rootScope.$digest(); - expect($$cookieReader()). - toEqual({ 'emptyCookie': '""' }); - expect($cookieStore.get("emptyCookie")).toEqual(''); - - mockedCookies['blankCookie'] = ''; - $browser.poll(); - expect($cookieStore.get("blankCookie")).toEqual(''); - })); -}); From 61dbacc3b5208957502f8005fb856b28586a5dc2 Mon Sep 17 00:00:00 2001 From: Shahar Talmi Date: Mon, 2 Mar 2015 20:52:58 +0200 Subject: [PATCH 3/6] feat($cookies): move logic into $cookies and deprecate $cookieStore The new API on `$cookies` includes: * `get` * `put` * `getObject` * `putObject` * `getAll` * `remove` The new API no longer polls the browser for changes to the cookies and no longer copy cookie values onto the `$cookies` object. The polling is expensive and caused issues with the `$cookies` properties not synchronizing correctly with the actual browser cookie values. The reason the polling was originally added was to allow communication between different tabs, but there are better ways to do this today (for example `localStorage`). DEPRECATION NOTICE: `$cookieStore` is now deprecated as all the useful logic has been moved to `$cookies`, to which `$cookieStore` now simply delegates calls. BREAKING CHANGE: `$cookies` no longer exposes properties that represent the current browser cookie values. Now you must explicitly the methods described above to access the cookie values. This also means that you can no longer watch the `$cookies` properties for changes to the browser's cookies. This feature is generally only needed if a 3rd party library was programmatically changing the cookies at runtime. If you rely on this then you must either write code that can react to the 3rd party library making the changes to cookies or implement your own polling mechanism. Closes #6411 Closes #7631 --- src/ngCookies/cookieStore.js | 13 ++- src/ngCookies/cookies.js | 160 ++++++++++++++++-------------- test/ngCookies/cookieStoreSpec.js | 63 +++--------- test/ngCookies/cookiesSpec.js | 100 +++++++------------ 4 files changed, 145 insertions(+), 191 deletions(-) diff --git a/src/ngCookies/cookieStore.js b/src/ngCookies/cookieStore.js index 62c9d790b37a..76a141114303 100644 --- a/src/ngCookies/cookieStore.js +++ b/src/ngCookies/cookieStore.js @@ -4,6 +4,7 @@ angular.module('ngCookies'). /** * @ngdoc service * @name $cookieStore + * @deprecated * @requires $cookies * * @description @@ -13,6 +14,11 @@ angular.module('ngCookies'). * * Requires the {@link ngCookies `ngCookies`} module to be installed. * + *
+ * **Note:** The $cookieStore service is deprecated. + * Please use the {@link ngCookies.$cookies `$cookies`} service instead. + *
+ * * @example * * ```js @@ -41,8 +47,7 @@ angular.module('ngCookies'). * @returns {Object} Deserialized cookie value, undefined if the cookie does not exist. */ get: function(key) { - var value = $cookies[key]; - return value ? angular.fromJson(value) : value; + return $cookies.getObject(key); }, /** @@ -56,7 +61,7 @@ angular.module('ngCookies'). * @param {Object} value Value to be stored. */ put: function(key, value) { - $cookies[key] = angular.toJson(value); + $cookies.putObject(key, value); }, /** @@ -69,7 +74,7 @@ angular.module('ngCookies'). * @param {string} key Id of the key-value pair to delete. */ remove: function(key) { - delete $cookies[key]; + $cookies.remove(key); } }; diff --git a/src/ngCookies/cookies.js b/src/ngCookies/cookies.js index 3a49c4a3f8fa..47539067b422 100644 --- a/src/ngCookies/cookies.js +++ b/src/ngCookies/cookies.js @@ -25,9 +25,9 @@ angular.module('ngCookies', ['ng']). * @description * Provides read/write access to browser's cookies. * - * Only a simple Object is exposed and by adding or removing properties to/from this object, new - * cookies are created/deleted at the end of current $eval. - * The object's properties can only be strings. + * BREAKING CHANGE: `$cookies` no longer exposes properties that represent the + * current browser cookie values. Now you must use the get/put/remove/etc. methods + * as described below. * * Requires the {@link ngCookies `ngCookies`} module to be installed. * @@ -37,87 +37,95 @@ angular.module('ngCookies', ['ng']). * angular.module('cookiesExample', ['ngCookies']) * .controller('ExampleController', ['$cookies', function($cookies) { * // Retrieving a cookie - * var favoriteCookie = $cookies.myFavorite; + * var favoriteCookie = $cookies.get('myFavorite'); * // Setting a cookie - * $cookies.myFavorite = 'oatmeal'; + * $cookies.put('myFavorite', 'oatmeal'); * }]); * ``` */ - factory('$cookies', ['$rootScope', '$browser', '$$cookieReader', '$$cookieWriter', function($rootScope, $browser, $$cookieReader, $$cookieWriter) { - var cookies = {}, - lastCookies = {}, - lastBrowserCookies, - runEval = false, - copy = angular.copy, - isUndefined = angular.isUndefined; + factory('$cookies', ['$$cookieReader', '$$cookieWriter', function($$cookieReader, $$cookieWriter) { + return { + /** + * @ngdoc method + * @name $cookies#get + * + * @description + * Returns the value of given cookie key + * + * @param {string} key Id to use for lookup. + * @returns {string} Raw cookie value. + */ + get: function(key) { + return $$cookieReader()[key]; + }, - //creates a poller fn that copies all cookies from the $browser to service & inits the service - $browser.addPollFn(function() { - var currentCookies = $$cookieReader(); - if (lastBrowserCookies != currentCookies) { //relies on browser.cookies() impl - lastBrowserCookies = currentCookies; - copy(currentCookies, lastCookies); - copy(currentCookies, cookies); - if (runEval) $rootScope.$apply(); - } - })(); - - runEval = true; - - //at the end of each eval, push cookies - //TODO: this should happen before the "delayed" watches fire, because if some cookies are not - // strings or browser refuses to store some cookies, we update the model in the push fn. - $rootScope.$watch(push); - - return cookies; + /** + * @ngdoc method + * @name $cookies#getObject + * + * @description + * Returns the deserialized value of given cookie key + * + * @param {string} key Id to use for lookup. + * @returns {Object} Deserialized cookie value. + */ + getObject: function(key) { + var value = $$cookieReader()[key]; + return value ? angular.fromJson(value) : value; + }, + /** + * @ngdoc method + * @name $cookies#getAll + * + * @description + * Returns a key value object with all the cookies + * + * @returns {Object} All cookies + */ + getAll: function() { + return $$cookieReader(); + }, - /** - * Pushes all the cookies from the service to the browser and verifies if all cookies were - * stored. - */ - function push() { - var name, - value, - browserCookies, - updated; - - //delete any cookies deleted in $cookies - for (name in lastCookies) { - if (isUndefined(cookies[name])) { - $$cookieWriter(name, undefined); - } - } - - //update all cookies updated in $cookies - for (name in cookies) { - value = cookies[name]; - if (!angular.isString(value)) { - value = '' + value; - cookies[name] = value; - } - if (value !== lastCookies[name]) { - $$cookieWriter(name, value); - updated = true; - } - } + /** + * @ngdoc method + * @name $cookies#put + * + * @description + * Sets a value for given cookie key + * + * @param {string} key Id for the `value`. + * @param {string} value Raw value to be stored. + */ + put: function(key, value) { + $$cookieWriter(key, value); + }, - //verify what was actually stored - if (updated) { - updated = false; - browserCookies = $$cookieReader(); + /** + * @ngdoc method + * @name $cookies#putObject + * + * @description + * Serializes and sets a value for given cookie key + * + * @param {string} key Id for the `value`. + * @param {Object} value Value to be stored. + */ + putObject: function(key, value) { + $$cookieWriter(key, angular.toJson(value)); + }, - for (name in cookies) { - if (cookies[name] !== browserCookies[name]) { - //delete or reset all cookies that the browser dropped from $cookies - if (isUndefined(browserCookies[name])) { - delete cookies[name]; - } else { - cookies[name] = browserCookies[name]; - } - updated = true; - } - } + /** + * @ngdoc method + * @name $cookies#remove + * + * @description + * Remove given cookie + * + * @param {string} key Id of the key-value pair to delete. + */ + remove: function(key) { + $$cookieWriter(key, undefined); } - } + }; }]); diff --git a/test/ngCookies/cookieStoreSpec.js b/test/ngCookies/cookieStoreSpec.js index 3a73afc5c2f4..20436761b9f3 100644 --- a/test/ngCookies/cookieStoreSpec.js +++ b/test/ngCookies/cookieStoreSpec.js @@ -1,58 +1,27 @@ 'use strict'; describe('$cookieStore', function() { - var mockedCookies; - - beforeEach(function() { - var lastCookies = {}; - mockedCookies = {}; - module('ngCookies', { - $$cookieWriter: function(name, value) { - mockedCookies[name] = value; - }, - $$cookieReader: function() { - if (!angular.equals(lastCookies, mockedCookies)) { - lastCookies = angular.copy(mockedCookies); - mockedCookies = angular.copy(mockedCookies); - } - return mockedCookies; - } - }); - }); - - it('should serialize objects to json', inject(function($cookieStore, $$cookieReader, $rootScope) { - $cookieStore.put('objectCookie', {id: 123, name: 'blah'}); - $rootScope.$digest(); - expect($$cookieReader()).toEqual({'objectCookie': '{"id":123,"name":"blah"}'}); + + beforeEach(module('ngCookies', { + $cookies: jasmine.createSpyObj('$cookies', ['getObject', 'putObject', 'remove']) })); - it('should deserialize json to object', inject(function($cookieStore, $browser, $$cookieWriter) { - $$cookieWriter('objectCookie', '{"id":123,"name":"blah"}'); - $browser.poll(); - expect($cookieStore.get('objectCookie')).toEqual({id: 123, name: 'blah'}); + it('should get cookie', inject(function($cookieStore, $cookies) { + $cookies.getObject.andReturn('value'); + expect($cookieStore.get('name')).toBe('value'); + expect($cookies.getObject).toHaveBeenCalledWith('name'); })); - it('should delete objects from the store when remove is called', inject(function($cookieStore, $browser, $rootScope, $$cookieReader) { - $cookieStore.put('gonner', { "I'll":"Be Back"}); - $rootScope.$digest(); //force eval in test - $browser.poll(); - expect($$cookieReader()).toEqual({'gonner': '{"I\'ll":"Be Back"}'}); - - $cookieStore.remove('gonner'); - $rootScope.$digest(); - expect($$cookieReader()).toEqual({}); + it('should put cookie', inject(function($cookieStore, $cookies) { + $cookieStore.put('name', 'value'); + expect($cookies.putObject).toHaveBeenCalledWith('name', 'value'); })); - it('should handle empty string value cookies', inject(function($cookieStore, $browser, $rootScope, $$cookieReader) { - $cookieStore.put("emptyCookie",''); - $rootScope.$digest(); - expect($$cookieReader()). - toEqual({ 'emptyCookie': '""' }); - expect($cookieStore.get("emptyCookie")).toEqual(''); - - mockedCookies['blankCookie'] = ''; - $browser.poll(); - expect($cookieStore.get("blankCookie")).toEqual(''); + + + it('should remove cookie', inject(function($cookieStore, $cookies) { + $cookieStore.remove('name'); + expect($cookies.remove).toHaveBeenCalledWith('name'); })); -}); + }); diff --git a/test/ngCookies/cookiesSpec.js b/test/ngCookies/cookiesSpec.js index be3cd306f5a6..9c4eda6ad43c 100644 --- a/test/ngCookies/cookiesSpec.js +++ b/test/ngCookies/cookiesSpec.js @@ -4,93 +4,65 @@ describe('$cookies', function() { var mockedCookies; beforeEach(function() { - var lastCookies = {}; - mockedCookies = {preexisting:'oldCookie'}; + mockedCookies = {}; module('ngCookies', { $$cookieWriter: function(name, value) { mockedCookies[name] = value; }, $$cookieReader: function() { - if (!angular.equals(lastCookies, mockedCookies)) { - lastCookies = angular.copy(mockedCookies); - mockedCookies = angular.copy(mockedCookies); - } return mockedCookies; } }); }); - it('should provide access to existing cookies via object properties and keep them in sync', - inject(function($cookies, $browser, $rootScope) { - expect($cookies).toEqual({'preexisting': 'oldCookie'}); - - // access internal cookie storage of the browser mock directly to simulate behavior of - // document.cookie - mockedCookies['brandNew'] = 'cookie'; - $browser.poll(); - - expect($cookies).toEqual({'preexisting': 'oldCookie', 'brandNew':'cookie'}); + it('should serialize objects to json', inject(function($cookies) { + $cookies.putObject('objectCookie', {id: 123, name: 'blah'}); + expect($cookies.get('objectCookie')).toEqual('{"id":123,"name":"blah"}'); + })); - mockedCookies['brandNew'] = 'cookie2'; - $browser.poll(); - expect($cookies).toEqual({'preexisting': 'oldCookie', 'brandNew':'cookie2'}); - delete mockedCookies['brandNew']; - $browser.poll(); - expect($cookies).toEqual({'preexisting': 'oldCookie'}); + it('should deserialize json to object', inject(function($cookies) { + $cookies.put('objectCookie', '{"id":123,"name":"blah"}'); + expect($cookies.getObject('objectCookie')).toEqual({id: 123, name: 'blah'}); })); - it('should create or update a cookie when a value is assigned to a property', - inject(function($cookies, $$cookieReader, $rootScope) { - $cookies.oatmealCookie = 'nom nom'; - $rootScope.$digest(); - - expect($$cookieReader()). - toEqual({'preexisting': 'oldCookie', 'oatmealCookie':'nom nom'}); + it('should delete objects from the store when remove is called', inject(function($cookies) { + $cookies.putObject('gonner', { "I'll":"Be Back"}); + expect($cookies.get('gonner')).toEqual('{"I\'ll":"Be Back"}'); + $cookies.remove('gonner'); + expect($cookies.get('gonner')).toEqual(undefined); + })); - $cookies.oatmealCookie = 'gone'; - $rootScope.$digest(); - expect($$cookieReader()). - toEqual({'preexisting': 'oldCookie', 'oatmealCookie': 'gone'}); + it('should handle empty string value cookies', inject(function($cookies) { + $cookies.putObject("emptyCookie",''); + expect($cookies.get('emptyCookie')).toEqual('""'); + expect($cookies.getObject("emptyCookie")).toEqual(''); + mockedCookies['blankCookie'] = ''; + expect($cookies.getObject("blankCookie")).toEqual(''); })); - it('should convert non-string values to string', - inject(function($cookies, $$cookieReader, $rootScope) { - $cookies.nonString = [1, 2, 3]; - $cookies.nullVal = null; - $cookies.undefVal = undefined; - var preexisting = $cookies.preexisting = function() {}; - $rootScope.$digest(); - expect($$cookieReader()).toEqual({ - 'preexisting': '' + preexisting, - 'nonString': '1,2,3', - 'nullVal': 'null', - 'undefVal': 'undefined' - }); - expect($cookies).toEqual({ - 'preexisting': '' + preexisting, - 'nonString': '1,2,3', - 'nullVal': 'null', - 'undefVal': 'undefined' - }); + it('should put cookie value without serializing', inject(function($cookies) { + $cookies.put('name', 'value'); + $cookies.put('name2', '"value2"'); + expect($cookies.get('name')).toEqual('value'); + expect($cookies.getObject('name2')).toEqual('value2'); })); - it('should remove a cookie when a $cookies property is deleted', - inject(function($cookies, $browser, $rootScope, $$cookieReader) { - $cookies.oatmealCookie = 'nom nom'; - $rootScope.$digest(); - $browser.poll(); - expect($$cookieReader()). - toEqual({'preexisting': 'oldCookie', 'oatmealCookie':'nom nom'}); - - delete $cookies.oatmealCookie; - $rootScope.$digest(); + it('should get cookie value without deserializing', inject(function($cookies) { + $cookies.put('name', 'value'); + $cookies.putObject('name2', 'value2'); + expect($cookies.get('name')).toEqual('value'); + expect($cookies.get('name2')).toEqual('"value2"'); + })); - expect($$cookieReader()).toEqual({'preexisting': 'oldCookie'}); + it('should get all the cookies', inject(function($cookies) { + $cookies.put('name', 'value'); + $cookies.putObject('name2', 'value2'); + expect($cookies.getAll()).toEqual({name: 'value', name2: '"value2"'}); })); -}); + }); From 7ba78ce7c964dd2f8774ddc7f51edbbdcc9c711f Mon Sep 17 00:00:00 2001 From: Shahar Talmi Date: Mon, 2 Mar 2015 21:46:51 +0200 Subject: [PATCH 4/6] 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'}); + })); }); From 1f774874291766c0e06657778250f6baa3da0d18 Mon Sep 17 00:00:00 2001 From: Shahar Talmi Date: Mon, 2 Mar 2015 22:08:04 +0200 Subject: [PATCH 5/6] feat($cookiesProvider): provide path, domain, expires and secure options This change provides properties on `$cookiesProvider` so that you can set the application level default options for cookies that are set using the `$cookies` service --- src/ngCookies/cookies.js | 109 +++++++++++++++++++++------------- test/ngCookies/cookiesSpec.js | 56 +++++++++++++++++ 2 files changed, 125 insertions(+), 40 deletions(-) diff --git a/src/ngCookies/cookies.js b/src/ngCookies/cookies.js index 8164c4700f63..bde93ce407e4 100644 --- a/src/ngCookies/cookies.js +++ b/src/ngCookies/cookies.js @@ -19,31 +19,66 @@ angular.module('ngCookies', ['ng']). /** - * @ngdoc service - * @name $cookies - * + * @ngdoc provider + * @name $cookiesProvider * @description - * Provides read/write access to browser's cookies. - * - * BREAKING CHANGE: `$cookies` no longer exposes properties that represent the - * current browser cookie values. Now you must use the get/put/remove/etc. methods - * as described below. - * - * Requires the {@link ngCookies `ngCookies`} module to be installed. - * - * @example - * - * ```js - * angular.module('cookiesExample', ['ngCookies']) - * .controller('ExampleController', ['$cookies', function($cookies) { - * // Retrieving a cookie - * var favoriteCookie = $cookies.get('myFavorite'); - * // Setting a cookie - * $cookies.put('myFavorite', 'oatmeal'); - * }]); - * ``` - */ - factory('$cookies', ['$$cookieReader', '$$cookieWriter', function($$cookieReader, $$cookieWriter) { + * Use `$cookiesProvider` to change the default behavior of the {@link ngCookies.$cookies $cookies} service. + * */ + provider('$cookies', [function $CookiesProvider() { + /** + * @ngdoc property + * @name $cookiesProvider#defaults + * @description + * + * Object containing default options to pass when setting cookies. + * + * 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. + * + * Note: by default the address that appears in your tag will be used as path. + * This is import so that cookies will be visible for all routes in case html5mode is enabled + * + **/ + var defaults = this.defaults = {}; + + function calcOptions(options) { + return options ? angular.extend({}, defaults, options) : defaults; + } + + /** + * @ngdoc service + * @name $cookies + * + * @description + * Provides read/write access to browser's cookies. + * + * BREAKING CHANGE: `$cookies` no longer exposes properties that represent the + * current browser cookie values. Now you must use the get/put/remove/etc. methods + * as described below. + * + * Requires the {@link ngCookies `ngCookies`} module to be installed. + * + * @example + * + * ```js + * angular.module('cookiesExample', ['ngCookies']) + * .controller('ExampleController', ['$cookies', function($cookies) { + * // Retrieving a cookie + * var favoriteCookie = $cookies.get('myFavorite'); + * // Setting a cookie + * $cookies.put('myFavorite', 'oatmeal'); + * }]); + * ``` + */ + this.$get = ['$$cookieReader', '$$cookieWriter', function($$cookieReader, $$cookieWriter) { return { /** * @ngdoc method @@ -70,7 +105,7 @@ angular.module('ngCookies', ['ng']). * @returns {Object} Deserialized cookie value. */ getObject: function(key) { - var value = $$cookieReader()[key]; + var value = this.get(key); return value ? angular.fromJson(value) : value; }, @@ -96,20 +131,11 @@ 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. + * @param {Object=} options Options object. + * See {@link ngCookies.$cookiesProvider#defaults $cookiesProvider.defaults} */ put: function(key, value, options) { - $$cookieWriter(key, value, options); + $$cookieWriter(key, value, calcOptions(options)); }, /** @@ -122,9 +148,10 @@ angular.module('ngCookies', ['ng']). * @param {string} key Id for the `value`. * @param {Object} value Value to be stored. * @param {Object=} options Options object. + * See {@link ngCookies.$cookiesProvider#defaults $cookiesProvider.defaults} */ putObject: function(key, value, options) { - $$cookieWriter(key, angular.toJson(value), options); + this.put(key, angular.toJson(value), options); }, /** @@ -136,9 +163,11 @@ angular.module('ngCookies', ['ng']). * * @param {string} key Id of the key-value pair to delete. * @param {Object=} options Options object. + * See {@link ngCookies.$cookiesProvider#defaults $cookiesProvider.defaults} */ remove: function(key, options) { - $$cookieWriter(key, undefined, options); + $$cookieWriter(key, undefined, calcOptions(options)); } }; - }]); + }]; + }]); diff --git a/test/ngCookies/cookiesSpec.js b/test/ngCookies/cookiesSpec.js index bedfcf8697b2..0fff36178d8b 100644 --- a/test/ngCookies/cookiesSpec.js +++ b/test/ngCookies/cookiesSpec.js @@ -83,4 +83,60 @@ describe('$cookies', function() { $cookies.remove('name', {path: '/a/b'}); expect($$cookieWriter).toHaveBeenCalledWith('name', undefined, {path: '/a/b'}); })); + + + it('should pass default options on put', function() { + module(function($cookiesProvider) { + $cookiesProvider.defaults.secure = true; + }); + inject(function($cookies, $$cookieWriter) { + $cookies.put('name', 'value', {path: '/a/b'}); + expect($$cookieWriter).toHaveBeenCalledWith('name', 'value', {path: '/a/b', secure: true}); + }); + }); + + + it('should pass default options on putObject', function() { + module(function($cookiesProvider) { + $cookiesProvider.defaults.secure = true; + }); + inject(function($cookies, $$cookieWriter) { + $cookies.putObject('name', 'value', {path: '/a/b'}); + expect($$cookieWriter).toHaveBeenCalledWith('name', '"value"', {path: '/a/b', secure: true}); + }); + }); + + + it('should pass default options on remove', function() { + module(function($cookiesProvider) { + $cookiesProvider.defaults.secure = true; + }); + inject(function($cookies, $$cookieWriter) { + $cookies.remove('name', {path: '/a/b'}); + expect($$cookieWriter).toHaveBeenCalledWith('name', undefined, {path: '/a/b', secure: true}); + }); + }); + + + it('should let passed options override default options', function() { + module(function($cookiesProvider) { + $cookiesProvider.defaults.secure = true; + }); + inject(function($cookies, $$cookieWriter) { + $cookies.put('name', 'value', {secure: false}); + expect($$cookieWriter).toHaveBeenCalledWith('name', 'value', {secure: false}); + }); + }); + + + it('should pass default options if no options are passed', function() { + module(function($cookiesProvider) { + $cookiesProvider.defaults.secure = true; + }); + inject(function($cookies, $$cookieWriter) { + $cookies.put('name', 'value'); + expect($$cookieWriter).toHaveBeenCalledWith('name', 'value', {secure: true}); + }); + }); + }); From ed35586e7cc399f0ec84415fe391927533710a80 Mon Sep 17 00:00:00 2001 From: Shahar Talmi Date: Mon, 2 Mar 2015 22:16:52 +0200 Subject: [PATCH 6/6] refactor($browser): remove private polling mechanism The only feature of Angular using this mechanism was `$cookies`, which no longer mirrors the browser cookie values and so does not need to poll. --- src/ng/browser.js | 43 ------------------------------------- src/ngMock/angular-mocks.js | 6 ------ test/ng/browserSpecs.js | 33 ---------------------------- test/ng/locationSpec.js | 1 - 4 files changed, 83 deletions(-) diff --git a/src/ng/browser.js b/src/ng/browser.js index ad8ae3057644..028536c340d6 100644 --- a/src/ng/browser.js +++ b/src/ng/browser.js @@ -73,11 +73,6 @@ function Browser(window, document, $log, $sniffer) { * @param {function()} callback Function that will be called when no outstanding request */ self.notifyWhenNoOutstandingRequests = function(callback) { - // force browser to execute all pollFns - this is needed so that cookies and other pollers fire - // at some deterministic time in respect to the test runner's actions. Leaving things up to the - // regular poller would result in flaky tests. - forEach(pollFns, function(pollFn) { pollFn(); }); - if (outstandingRequestCount === 0) { callback(); } else { @@ -85,44 +80,6 @@ function Browser(window, document, $log, $sniffer) { } }; - ////////////////////////////////////////////////////////////// - // Poll Watcher API - ////////////////////////////////////////////////////////////// - var pollFns = [], - pollTimeout; - - /** - * @name $browser#addPollFn - * - * @param {function()} fn Poll function to add - * - * @description - * Adds a function to the list of functions that poller periodically executes, - * and starts polling if not started yet. - * - * @returns {function()} the added function - */ - self.addPollFn = function(fn) { - if (isUndefined(pollTimeout)) startPoller(100, setTimeout); - pollFns.push(fn); - return fn; - }; - - /** - * @param {number} interval How often should browser call poll functions (ms) - * @param {function()} setTimeout Reference to a real or fake `setTimeout` function. - * - * @description - * Configures the poller to run in the specified intervals, using the specified - * setTimeout fn and kicks it off. - */ - function startPoller(interval, setTimeout) { - (function check() { - forEach(pollFns, function(pollFn) { pollFn(); }); - pollTimeout = setTimeout(check, interval); - })(); - } - ////////////////////////////////////////////////////////////// // URL API ////////////////////////////////////////////////////////////// diff --git a/src/ngMock/angular-mocks.js b/src/ngMock/angular-mocks.js index ca4e271cd181..1b8be7d77b96 100644 --- a/src/ngMock/angular-mocks.js +++ b/src/ngMock/angular-mocks.js @@ -138,11 +138,6 @@ angular.mock.$Browser.prototype = { }); }, - addPollFn: function(pollFn) { - this.pollFns.push(pollFn); - return pollFn; - }, - url: function(url, replace, state) { if (angular.isUndefined(state)) { state = null; @@ -2148,7 +2143,6 @@ if (window.jasmine || window.mocha) { if (injector) { injector.get('$rootElement').off(); - injector.get('$browser').pollFns.length = 0; } // clean up jquery's fragment cache diff --git a/test/ng/browserSpecs.js b/test/ng/browserSpecs.js index fa7d8686d915..3166a08a3599 100755 --- a/test/ng/browserSpecs.js +++ b/test/ng/browserSpecs.js @@ -249,37 +249,6 @@ describe('browser', function() { }); - describe('poller', function() { - - it('should call functions in pollFns in regular intervals', function() { - var log = ''; - browser.addPollFn(function() {log+='a';}); - browser.addPollFn(function() {log+='b';}); - expect(log).toEqual(''); - fakeWindow.setTimeout.flush(); - expect(log).toEqual('ab'); - fakeWindow.setTimeout.flush(); - expect(log).toEqual('abab'); - }); - - it('should startPoller', function() { - expect(fakeWindow.timeouts.length).toEqual(0); - - browser.addPollFn(function() {}); - expect(fakeWindow.timeouts.length).toEqual(1); - - //should remain 1 as it is the check fn - browser.addPollFn(function() {}); - expect(fakeWindow.timeouts.length).toEqual(1); - }); - - it('should return fn that was passed into addPollFn', function() { - var fn = function() { return 1; }; - var returnedFn = browser.addPollFn(fn); - expect(returnedFn).toBe(fn); - }); - }); - describe('url', function() { var pushState, replaceState, locationReplace; @@ -723,7 +692,6 @@ describe('browser', function() { fakeWindow.location.href = newUrl; }); $provide.value('$browser', browser); - browser.pollFns = []; sniffer.history = options.history; $provide.value('$sniffer', sniffer); @@ -846,7 +814,6 @@ describe('browser', function() { beforeEach(module(function($provide, $locationProvider) { $provide.value('$browser', browser); - browser.pollFns = []; })); it('should not interfere with legacy browser url replace behavior', function() { diff --git a/test/ng/locationSpec.js b/test/ng/locationSpec.js index de0561d0c16f..bbeef29f88e6 100644 --- a/test/ng/locationSpec.js +++ b/test/ng/locationSpec.js @@ -99,7 +99,6 @@ describe('$location', function() { /* global Browser: false */ var b = new Browser($window, $document, fakeLog, sniffer); - b.pollFns = []; return b; }; });