Permalink
Browse files

Changed browser.cookies from getter to function that accepts cookie

domain (host and port) and path, and returns wrapper to access
specific cookie context.
  • Loading branch information...
assaf committed Dec 20, 2010
1 parent d6803cc commit 36a44cdd8f4ae8bd40de5868ef1f237dc1875991
View
@@ -1,3 +1,10 @@
+Version 0.6.1 Pending
+
+ Changed browser.cookies from getter to function that accepts cookie domain
+ (host and port) and path, and returns wrapper to access specific cookie
+ context.
+
+
Version 0.6.0 2010-12-20
First release that I could use to test an existing project.
View
@@ -8,32 +8,40 @@ require "./forms"
# The browser maintains state for cookies and localStorage.
class Browser
constructor: ->
- # Start out with an empty window
- window = jsdom.createWindow(jsdom.dom.level3.html)
- window.browser = this
- # ### browser.window => Window
- #
- # Returns the main window.
- @__defineGetter__ "window", -> window
- # ### browser.document => Document
- #
- # Retursn the main window's document. Only valid after opening a document
- # (Browser.open).
- @__defineGetter__ "document", -> window.document
-
-
# Cookies and storage.
cookies = require("./cookies").use(this)
- window.cookies = cookies
- # ### browser.cookies => Cookies
+ storage = require("./storage").use(this)
+ # ### browser.cookies(host, path) => Cookies
+ #
+ # Returns all the cookies for this host/path. Path defaults to "/".
+ this.cookies = (host, path)-> cookies.access(host, path)
+ # ### brower.localStorage(host) => Storage
+ #
+ # Returns local Storage based on the document origin (hostname/port). This
+ # is the same storage area you can access from any document of that origin.
+ this.localStorage = (host)-> storage.local(host)
+ # ### brower.sessionStorage(host) => Storage
#
- # Returns all the cookies for this browser.
- @__defineGetter__ "cookies", -> cookies
- require("./storage").attach this, window
+ # Returns session Storage based on the document origin (hostname/port). This
+ # is the same storage area you can access from any document of that origin.
+ this.sessionStorage = (host)-> storage.session(host)
+ # Event loop.
+ eventloop = require("./eventloop").use(this)
+ # ### browser.clock
+ #
+ # The current clock time. Initialized to current system time when creating
+ # a new browser, but doesn't advance except by setting it explicitly or
+ # firing timeout/interval events.
+ @clock = new Date().getTime()
+ # ### browser.now => Date
+ #
+ # Date object with current time, based on browser clock.
+ @__defineGetter__ "now", -> new Date(clock)
- # Attach history/location objects to window/document.
- require("./history").attach this, window
+ # History, page loading, XHR.
+ history = require("./history").use(this)
+ xhr = require("./xhr").use(this)
# ### browser.location => Location
#
# Return the location of the current document (same as window.location.href).
@@ -45,21 +53,30 @@ class Browser
@__defineSetter__ "location", (url)-> window.location = url
- # ### browser.clock
+
+ # Window management.
+ createWindow = ->
+ window = jsdom.createWindow(jsdom.dom.level3.html)
+ window.__defineGetter__ "browser", => this
+ cookies.extend window
+ storage.extend window
+ eventloop.extend window
+ history.extend window
+ xhr.extend window
+ window
+
+ # ### browser.window => Window
#
- # The current clock time. Initialized to current system time when creating
- # a new browser, but doesn't advance except by setting it explicitly or
- # firing timeout/interval events.
- @clock = new Date().getTime()
- # ### browser.now => Date
+ # Returns the main window.
+ @__defineGetter__ "window", -> window
+ # ### browser.document => Document
#
- # Date object with current time, based on browser clock.
- @__defineGetter__ "now", -> new Date(clock)
- require("./eventloop").attach this, window
+ # Retursn the main window's document. Only valid after opening a document
+ # (Browser.open).
+ @__defineGetter__ "document", -> window.document
- # All asynchronous processing handled by event loop.
- require("./xhr").attach this, window
+ window = createWindow()
# TODO: Fix
window.Image = ->
View
@@ -2,170 +2,161 @@
URL = require("url")
core = require("jsdom").dom.level3.core
-# ## browser.cookies
-#
-# Maintains cookies for a Browser instance.
+
+# 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 are mapped by domain first, path second.
- cookies = {}
+ constructor: (browser, cookies, hostname, pathname)->
+ pathname = "/" if !pathname || pathname == ""
- # 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
+ # 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].
- filter = (url)->
+ selected = ->
matching = []
- hostname = url.hostname
- pathname = url.pathname
- pathname = "/" if !pathname || pathname == ""
- for domain, inDomain of cookies
+ for domain, in_domain of cookies
# Ignore cookies that don't match the exact hostname, or .domain.
- continue unless hostname == domain || (domain.charAt(0) == "." && hostname.lastIndexOf(domain) + domain.length == hostname.length)
+ continue unless domainMatch(domain, hostname)
# Ignore cookies that don't match the path.
- for path, inPath of inDomain
+ for path, in_path of in_domain
continue unless pathname.indexOf(path) == 0
- for name, cookie of inPath
+ for name, cookie of in_path
# Delete expired cookies.
- if inPath.expires && inPath.expires <= browser.clock
- delete inPath[name]
+ 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
- # 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")
-
+ # 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.get name, url? => String
+ #### cookies(host, path).get name => String
#
- # Returns the value of a cookie. Using cookie name alone, returns first
- # cookie to match the current browser.location.
+ # Returns the value of a cookie.
#
# name -- Cookie name
- # url -- Uses hostname/pathname to filter cookies
# Returns cookie value if known
- this.get = (name, url = browser.location)->
- url = URL.parse(url)
- for match in filter(url)
+ this.get = (name)->
+ for match in selected()
return match[3].value if match[2] == name
-
- #### cookies.set name, value, options?
+
+ #### cookies(host, path).set name, value, options?
#
- # Sets a cookie (deletes if expires/max-age is in the past). You can specify
- # the cookie domain and path as part of the options object, or have it
- # default to the current browser.location.
+ # Sets a cookie (deletes if expires/max-age is in the past).
#
# name -- Cookie name
# value -- Cookie value
- # options -- Options domain, path, max-age/expires and secure
+ # options -- Options max-age, expires, secure
this.set = (name, value, options = {})->
name = name.toLowerCase()
- value = value.toString()
- options.domain ||= browser.location?.hostname
- throw new Error("No location for cookie, please call with options.domain") unless options.domain
- options.path ||= browser.location?.pathname
- options.path = "/" if !options.path || options.path == ""
+ state = { value: value.toString() }
if options.expires
- expires = options.expires.getTime()
+ state.expires = options.expires.getTime()
else
maxage = options["max-age"]
- expires = browser.clock + maxage if typeof maxage is "number"
- if expires && expires <= browser.clock
- inDomain = cookies[options.domain]
- inPath = inDomain[options.path] if inDomain
- delete inPath[name] if inPath
+ 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
- inDomain = cookies[options.domain] ||= {}
- inPath = inDomain[options.path] ||= {}
- inPath[name] = { value: value, expires: expires, secure: !!options.secure }
-
- #### cookies.delete name, options?
+ in_domain = cookies[hostname] ||= {}
+ in_path = in_domain[pathname] ||= {}
+ in_path[name] = state
+
+ #### cookies(host, path).remove name
#
- # Deletes a cookie. You can specify # the cookie domain and path as part of
- # the options object, or have it # default to the current browser.location.
+ # Deletes a cookie.
#
# name -- Cookie name
- # options -- Optional domain and path
- this.delete = (name, options = {})->
- @set name, "", { expires: 0, domain: options.domain, path: options.path }
-
- #### cookies.dump => String
- #
- # Returns all the cookies in serialized form, one on each line.
- this.dump = ->
- serialized = []
- for domain, inDomain of cookies
- for path, inPath of inDomain
- for name, cookie of inPath
- serialized.push serialize(domain, path, name, cookie)
- serialized.join("\n")
-
- # Returns key/value pairs of all cookies that match a given url.
- this._pairs = (url)->
- set = []
- for match in filter(url)
- set.push "#{match[2]}=#{match[3].value}"
- set.join("; ")
+ 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.
#
- # url -- Document location or request URL
# serialized -- Serialized form
- this._update = (url, serialized)->
+ this.update = (serialized)->
return unless serialized
for cookie in serialized.split(/,(?=[^;,]*=)|,$/)
fields = cookie.split(/;+/)
first = fields[0].trim()
[name, value] = first.split(/\=/, 2)
- options = {}
+ options = { value: value }
+ domain = path = null
for field in fields
[key, val] = field.trim().split(/\=/, 2)
- # val = dequote(val.trim()) if val
switch key.toLowerCase()
- when "domain" then options.domain = dequote(val)
- when "path" then options.path = dequote(val)
- when "expires" then options.expires = new Date(dequote(val))
- when "max-age" then options["max-age"] = parseInt(dequote(val), 10)
+ 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
- options.domain ||= url.hostname
- options.path ||= url.pathname.replace(/%[^\/]*$/, "")
- options.secure ||= false
- @set name, dequote(value), options
-
- # Adds Cookie header suitable for sending to the server. Needs request
- # URL to figure out which cookies to send.
- this._addHeader = (url, headers)->
- header = ("#{match[2]}=\"#{match[3].value}\";$Path=\"#{match[1]}\"" for match in filter(url)).join("; ")
+ 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(@parentWindow.location)
+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
+core.HTMLDocument.prototype.__defineSetter__ "cookie", (cookie)-> @parentWindow.cookies.update cookie
-# Add cookies support to browser. Returns Cookies object.
exports.use = (browser)->
- new Cookies(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
@@ -143,12 +143,13 @@ class EventLoop
waiting = []
-# Attach event loop to window: creates new event loop and adds
-# timeout/interval methods and XHR class.
-exports.attach = (browser, window)->
- eventLoop = new EventLoop(browser, window)
- for fn in ["setTimeout", "setInterval", "clearTimeout", "clearInterval"]
- window[fn] = -> eventLoop[fn].apply(window, arguments)
- window.queue = eventLoop.queue
- window.wait = eventLoop.wait
- window.request = eventLoop.request
+exports.use = (browser)->
+ # Add event loop to window.
+ extend = (window)->
+ eventLoop = new EventLoop(browser, window)
+ for fn in ["setTimeout", "setInterval", "clearTimeout", "clearInterval"]
+ window[fn] = -> eventLoop[fn].apply(window, arguments)
+ window.queue = eventLoop.queue
+ window.wait = eventLoop.wait
+ window.request = eventLoop.request
+ return extend: extend
Oops, something went wrong.

0 comments on commit 36a44cd

Please sign in to comment.