Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Fetching contributors…

Cannot retrieve contributors at this time

163 lines (143 sloc) 6.349 kb
# Cookies.
URL = require("url")
core = require("jsdom").dom.level3.core
# Maintains cookies for a Browser instance. This is actually a domain/path
# specific scope around the global cookies collection.
#
# See [RFC 2109](http://tools.ietf.org/html/rfc2109.html) and
# [document.cookie](http://developer.mozilla.org/en/document.cookie)
class Cookies
constructor: (browser, cookies, hostname, pathname)->
pathname = "/" if !pathname || pathname == ""
# Cookie header values are (supposed to be) quoted. This function strips
# double quotes aroud value, if it finds both quotes.
dequote = (value)-> value.replace(/^"(.*)"$/, "$1")
domainMatch = (domain, hostname)->
return true if domain == hostname
return domain.charAt(0) == "." && domain.substring(1) == hostname.replace(/^[^.]\./, "")
# Return all the cookies that match the given hostname/path, from most
# specific to least specific. Returns array of arrays, each item is
# [domain, path, name, cookie].
selected = ->
matching = []
for domain, in_domain of cookies
# Ignore cookies that don't match the exact hostname, or .domain.
continue unless domainMatch(domain, hostname)
# Ignore cookies that don't match the path.
for path, in_path of in_domain
continue unless pathname.indexOf(path) == 0
for name, cookie of in_path
# Delete expired cookies.
if typeof cookie.expires == "number" && cookie.expires <= browser.clock
delete in_path[name]
else
matching.push [domain, path, name, cookie]
# Sort from most specific to least specified. Only worry about path
# (longest is more specific)
matching.sort (a,b) -> a[1].length - b[1].length
# Serialize cookie object into RFC2109 representation.
serialize = (domain, path, name, cookie)->
str = "#{name}=#{cookie.value}; domain=#{domain}; path=#{path}"
str = str + "; max-age=#{cookie.expires - browser.clock}" if cookie.expires
str = str + "; secure" if cookie.secure
str
#### cookies(host, path).get name => String
#
# Returns the value of a cookie.
#
# name -- Cookie name
# Returns cookie value if known
this.get = (name)->
for match in selected()
return match[3].value if match[2] == name
#### cookies(host, path).set name, value, options?
#
# Sets a cookie (deletes if expires/max-age is in the past).
#
# name -- Cookie name
# value -- Cookie value
# options -- Options max-age, expires, secure
this.set = (name, value, options = {})->
name = name.toLowerCase()
state = { value: value.toString() }
if options.expires
state.expires = options.expires.getTime()
else
maxage = options["max-age"]
state.expires = browser.clock + maxage if typeof maxage is "number"
state.secure = true if options.secure
if typeof state.expires is "number" && state.expires <= browser.clock
if in_domain = cookies[hostname]
if in_path = in_domain[pathname]
delete in_path[name]
else
in_domain = cookies[hostname] ||= {}
in_path = in_domain[pathname] ||= {}
in_path[name] = state
#### cookies(host, path).remove name
#
# Deletes a cookie.
#
# name -- Cookie name
this.remove = (name, options = {})->
if in_domain = cookies[hostname]
if in_path = in_domain[pathname]
delete in_path[name.toLowerCase()]
# Update cookies from serialized form. This method works equally well for
# the Set-Cookie header and value passed to document.cookie setter.
#
# serialized -- Serialized form
this.update = (serialized)->
return unless serialized
for cookie in serialized.split(/,(?=[^;,]*=)|,$/)
fields = cookie.split(/;+/)
first = fields[0].trim()
[name, value] = first.split(/\=/, 2)
options = { value: value }
domain = path = null
for field in fields
[key, val] = field.trim().split(/\=/, 2)
switch key.toLowerCase()
when "domain" then domain = dequote(val)
when "path" then path = dequote(val).replace(/%[^\/]*$/, "")
when "expires" then options.expires = new Date(dequote(val)).getTime()
when "max-age" then options.expires = browser.clock + parseInt(dequote(val), 10)
when "secure" then options.secure = true
continue if domain && !domainMatch(domain, hostname)
continue if path && pathname.indexOf(path) != 0
if options.expires && options.expires <= browser.clock
if in_domain = cookies[domain || hostname]
if in_path = in_domain[path || pathname]
delete in_path[name]
else
in_domain = cookies[domain || hostname] ||= {}
in_path = in_domain[path || pathname] ||= {}
in_path[name] = options
# Adds Cookie header suitable for sending to the server.
this.addHeader = (headers)->
header = ("#{match[2]}=\"#{match[3].value}\";$Path=\"#{match[1]}\"" for match in selected()).join("; ")
if header.length > 0
headers.cookie = "$Version=\"1\"; #{header}"
# Returns key/value pairs of all cookies in this domain/path.
@__defineGetter__ "pairs", ->
("#{match[2]}=#{match[3].value}" for match in selected()).join("; ")
this.dump = ->
(serialize.apply this, match for match in selected()).join("\n")
# ### document.cookie => String
#
# Returns name=value; pairs
core.HTMLDocument.prototype.__defineGetter__ "cookie", -> @parentWindow.cookies.pairs
# ### document.cookie = String
#
# Accepts serialized form (same as Set-Cookie header) and updates cookie from
# new values.
core.HTMLDocument.prototype.__defineSetter__ "cookie", (cookie)-> @parentWindow.cookies.update cookie
exports.use = (browser)->
cookies = {}
# Creates and returns cookie access scopes to given host/path.
access = (hostname, pathname)->
new Cookies(browser, cookies, hostname, pathname)
# Add cookies accessor to window: documents need this.
extend = (window)->
window.__defineGetter__ "cookies", -> access(@location.hostname, @location.pathname)
return access: access, extend: extend
Jump to Line
Something went wrong with that request. Please try again.