diff --git a/src/ng/browser.js b/src/ng/browser.js index 6bec52805722..2ad547d88f96 100644 --- a/src/ng/browser.js +++ b/src/ng/browser.js @@ -258,6 +258,7 @@ function Browser(window, document, $log, $sniffer) { var lastCookies = {}; var lastCookieString = ''; var cookiePath = self.baseHref(); + var lastSkipEncodeCookieFlag = false; /** * @name $browser#cookies @@ -281,53 +282,105 @@ function Browser(window, document, $log, $sniffer) { */ self.cookies = function(name, value) { /* global escape: false, unescape: false */ - var cookieLength, cookieArray, cookie, i, index; - if (name) { - if (value === undefined) { - rawDocument.cookie = escape(name) + "=;path=" + cookiePath + - ";expires=Thu, 01 Jan 1970 00:00:00 GMT"; - } else { - if (isString(value)) { - cookieLength = (rawDocument.cookie = escape(name) + '=' + escape(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)!"); - } - } - } + self.setCookieWithOptions(name, value); } 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 = unescape(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] = unescape(cookie.substring(index + 1)); - } + self.cookieDecoded(name, false); + return lastCookies; + } + }; + + /** + * @name ng.$browser#setCookieWithOptions + * @methodOf ng.$browser + * + * @param {string=} name Cookie name + * @param {string=} value Cookie value + * @param {object=} options Cookie options + * - expires: Date instance or days as number + * - path: path of domain to store cookie + * - domain: domain to store cookie under + * - secure: Boolean + * - skipEncode: If custom encoding is already done, provide as true + * + * @description Sets a cookie under the given name with value respecting options + * If name is defined and value undefined, the cookie deleted + */ + self.setCookieWithOptions = function(name, value, options) { + var expires, expireDate, cookieValue; + if (name && value === undefined) { + rawDocument.cookie = escape(name) + "=;path=" + cookiePath + + ";expires=Thu, 01 Jan 1970 00:00:00 GMT"; + } + if (!name || !isString(value)) + return; + options = (options === undefined) ? {} : options; + options.path = (options.path === undefined) ? cookiePath : options.path; + lastSkipEncodeCookieFlag = (typeof options.skipEncode === 'boolean') ? options.skipEncode : false; + if (typeof options.expires === 'number') { + expireDate = new Date(); + expireDate.setTime(expireDate.getTime() + options.expires * 864e+5); + expires = expireDate.toUTCString(); + } else if (options.expires instanceof Date) { + expires = options.expires.toUTCString(); + } + if (!options.skipEncode) { + name = escape(name); + value = escape(value); + } + cookieValue = (rawDocument.cookie = [ + name, '=', value, + expires ? '; expires=' + expires : '', + options.path ? '; path=' + options.path : '', + options.domain ? '; domain=' + options.domain : '', + options.secure ? '; secure' : '' + ].join('')); + if (cookieValue.length > 4096) { + $log.warn("Cookie '"+ name + + "' possibly not set or overflowed because it was too large ("+ + cookieValue.length + " > 4096 bytes)!"); + } + return cookieValue; + }; + + /** + * @name ng.$browser#cookieDecoded + * @methodOf ng.$browser + * + * @param {string=} name Cookie name + * @param {boolean=} skipDecode True if decoding on name and value should be skipped + * @returns {string} cookie value for name, undefined if doesn't exist + */ + self.cookieDecoded = function(name, skipDecode) { + var cookieName, cookieValue, cookieArray, cookie, i, index; + if (rawDocument.cookie !== lastCookieString || skipDecode !== lastSkipEncodeCookieFlag) { + lastSkipEncodeCookieFlag = (typeof skipDecode === 'boolean') ? skipDecode : false; + 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 + cookieName = cookie.substring(0, index); + if (!skipDecode) + cookieName = unescape(cookieName); + // 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[cookieName] === undefined) { + cookieValue = cookie.substring(index + 1); + if (!skipDecode) + cookieValue = unescape(cookieValue); + lastCookies[cookieName] = cookieValue; } } } - return lastCookies; } + return lastCookies[name]; }; - /** * @name $browser#defer * @param {function()} fn A function, who's execution should be deferred. diff --git a/test/ng/browserSpecs.js b/test/ng/browserSpecs.js index 4157ecbdb7fd..10637ef5d82e 100755 --- a/test/ng/browserSpecs.js +++ b/test/ng/browserSpecs.js @@ -340,6 +340,90 @@ describe('browser', function() { }); }); + describe('setCookieWithOptions', function() { + + it('should encode cookies correctly ignoring options', function() { + browser.setCookieWithOptions('cookie1=', 'val;ue'); + browser.setCookieWithOptions('cookie2=bar;baz', 'val=ue'); + + var rawCookies = document.cookie.split("; "); + expect(rawCookies.length).toEqual(2); + expect(rawCookies).toContain('cookie1%3D=val%3Bue'); + expect(rawCookies).toContain('cookie2%3Dbar%3Bbaz=val%3Due'); + }); + + it('should encode cookies correctly skipping encoding', function() { + browser.setCookieWithOptions('cookie1:', 'val$ue', {skipEncode:true}); + browser.setCookieWithOptions('cookie2$bar:baz', 'val$ue', {skipEncode:true}); + + var rawCookies = document.cookie.split("; "); + expect(rawCookies.length).toEqual(2); + expect(rawCookies).toContain('cookie1:=val$ue'); + expect(rawCookies).toContain('cookie2$bar:baz=val$ue'); + }); + + it('should set expires options correctly with date', function() { + var future = new Date('Fri, 31 Dec 9999 23:59:59 GMT') + var options = {expires:future} + var cookieValue = browser.setCookieWithOptions('cookie1', 'value', options); + + var expected = 'cookie1=value; expires=' + future.toUTCString() + '; path=/'; + expect(cookieValue).toEqual(expected); + + var rawCookies = document.cookie.split("; "); + expect(rawCookies.length).toEqual(1); + expect(rawCookies).toContain('cookie1=value'); + }); + + it('should set expires options correctly with number', function() { + var daysAhead = 3 + var options = {expires:daysAhead} + var cookieValue = browser.setCookieWithOptions('cookie1', 'value', options); + var ahead = new Date(); + ahead.setTime(ahead.getTime() + daysAhead * 864e+5) + var sansMinutesSeconds = ahead.toUTCString().split(":")[0]; + expect(cookieValue.indexOf("expires=" + sansMinutesSeconds)).not.toBe(-1); + }); + + it('should not set expires when none given', function() { + var cookieValue = browser.setCookieWithOptions('cookie1', 'value', {}); + expect(cookieValue.indexOf("expires=")).toBe(-1); + }); + + it('should set path from options correctly', function() { + var options = {path:"/foo/bar"}; + var cookieValue = browser.setCookieWithOptions('cookie1', 'value', options); + expect(cookieValue.indexOf("path=" + options.path)).not.toBe(-1); + }); + + it('should set secure from options correctly', function() { + var options = {secure:true}; + var cookieValue = browser.setCookieWithOptions('cookie1', 'value', options); + expect(cookieValue.indexOf("; secure")).not.toBe(-1); + options.secure = false; + cookieValue = browser.setCookieWithOptions('cookie1', 'value', options); + expect(cookieValue.indexOf("; secure")).toBe(-1); + }); + + }); + + describe('cookieDecoded', function() { + + it('should retrieve cookies correctly', function() { + document.cookie = "foo=bar=baz;path=/"; + expect(browser.cookieDecoded('foo')).toEqual('bar=baz'); + }); + + it('should decode cookies correctly', function() { + document.cookie = 'oatmeal%3ACookie=cha%3Anged;path=/'; + expect(browser.cookieDecoded('oatmeal:Cookie')).toEqual('cha:nged'); + }); + + it('should skip decoding when skipDecode is true', function() { + document.cookie = 'oatmeal%3ACookie=cha%3Anged;path=/'; + expect(browser.cookieDecoded('oatmeal%3ACookie', true)).toEqual('cha%3Anged'); + }); + }); it('should pick up external changes made to browser cookies', function() { browser.cookies('oatmealCookie', 'drool');