Skip to content
This repository was archived by the owner on Apr 12, 2024. It is now read-only.

feat($browser): add cookie options plus ability to skip encoding #6507

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
131 changes: 92 additions & 39 deletions src/ng/browser.js
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,7 @@ function Browser(window, document, $log, $sniffer) {
var lastCookies = {};
var lastCookieString = '';
var cookiePath = self.baseHref();
var lastSkipEncodeCookieFlag = false;

/**
* @name $browser#cookies
Expand All @@ -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.
Expand Down
84 changes: 84 additions & 0 deletions test/ng/browserSpecs.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down