Introduce an abstraction layer for the cookie store.

CookieJar#jar is removed and #store is added instead.
This commit is contained in:
Akinori MUSHA 2013-03-15 02:25:41 +09:00
parent 1f5eb6bc7f
commit d004408296
4 changed files with 236 additions and 53 deletions

View file

@ -131,6 +131,19 @@ class HTTP::Cookie
class << self
include URIFix if defined?(URIFix)
# Normalizes a given path. If it is empty, the root path '/' is
# returned. If a URI object is given, returns a new URI object
# with the path part normalized.
def normalize_path(uri)
# Currently does not replace // to /
case uri
when URI
uri.path.empty? ? uri + '/' : uri
else
uri.empty? ? '/' : uri
end
end
# Parses a Set-Cookie header value +set_cookie+ into an array of
# Cookie objects. Parts (separated by commas) that are malformed
# are ignored.
@ -277,20 +290,8 @@ class HTTP::Cookie
@domain = @domain_name.hostname
end
def normalize_uri_path(uri)
# Currently does not replace // to /
uri.path.empty? ? uri + '/' : uri
end
private :normalize_uri_path
def normalize_path(path)
# Currently does not replace // to /
path.empty? ? '/' : path
end
private :normalize_path
def path=(path)
@path = normalize_path(path)
@path = HTTP::Cookie.normalize_path(path)
end
def origin=(origin)
@ -298,7 +299,7 @@ class HTTP::Cookie
raise ArgumentError, "origin cannot be changed once it is set"
origin = URI(origin)
self.domain ||= origin.host
self.path ||= (normalize_uri_path(origin) + './').path
self.path ||= (HTTP::Cookie.normalize_path(origin) + './').path
acceptable_from_uri?(origin) or
raise ArgumentError, "unacceptable cookie sent from URI #{origin}"
@origin = origin
@ -351,7 +352,7 @@ class HTTP::Cookie
raise "cannot tell if this cookie is valid because the domain is unknown"
end
return false if secure? && uri.scheme != 'https'
acceptable_from_uri?(uri) && normalize_path(uri.path).start_with?(@path)
acceptable_from_uri?(uri) && HTTP::Cookie.normalize_path(uri.path).start_with?(@path)
end
# Returns a string for use in a Cookie header value,
@ -375,7 +376,7 @@ class HTTP::Cookie
if @for_domain || @domain != DomainName.new(origin.host).hostname
string << "; domain=#{@domain}"
end
if (normalize_uri_path(origin) + './').path != @path
if (HTTP::Cookie.normalize_path(origin) + './').path != @path
string << "; path=#{@path}"
end
if expires = @expires

View file

@ -1,6 +1,4 @@
module HTTP
autoload :Cookie, 'http/cookie'
end
require 'http/cookie'
##
# This class is used to manage the Cookies that have been returned from
@ -8,15 +6,25 @@ end
class HTTP::CookieJar
autoload :AbstractSaver, 'http/cookie_jar/abstract_saver'
autoload :AbstractStore, 'http/cookie_jar/abstract_store'
attr_reader :jar
attr_reader :store
def initialize
@jar = {}
def initialize(store = :hash, options = nil)
case store
when Symbol
@store = AbstractStore.implementation(store).new(options)
when AbstractStore
options.empty? or
raise ArgumentError, 'wrong number of arguments (%d for 1)' % (1 + options.size)
@store = store
else
raise TypeError, 'wrong object given as cookie store: %s' % store.inspect
end
end
def initialize_copy other # :nodoc:
@jar = Marshal.load Marshal.dump other.jar
def initialize_copy(other)
@store = other.instance_eval { @store.dup }
end
# Add a +cookie+ to the jar and return self.
@ -24,16 +32,8 @@ class HTTP::CookieJar
if cookie.domain.nil? || cookie.path.nil?
raise ArgumentError, "a cookie with unknown domain or path cannot be added"
end
normal_domain = cookie.domain_name.hostname
path_cookies = ((@jar[normal_domain] ||= {})[cookie.path] ||= {})
if cookie.expired?
path_cookies.delete(cookie.name)
else
path_cookies[cookie.name] = cookie
end
@store.add(cookie)
self
end
alias << add
@ -53,12 +53,23 @@ class HTTP::CookieJar
each(url) { return false }
return true
else
@jar.empty?
@store.empty?
end
end
# Iterate over cookies. If +uri+ is given, cookies not for the
# URL/URI are excluded.
# Iterates over all cookies that are not expired.
#
# Available option keywords are below:
#
# * +uri+
#
# Specify a URI/URL indicating the destination of the cookies
# being selected. Every cookie yielded should be good to send to
# the given URI, i.e. cookie.valid_for_uri?(uri) evaluates to
# true.
#
# If (and only if) this option is given, last access time of each
# cookie is updated to the current time.
def each(uri = nil, &block)
block_given? or return enum_for(__method__, uri)
@ -68,11 +79,7 @@ class HTTP::CookieJar
}
end
@jar.each { |domain, paths|
paths.each { |path, hash|
hash.each_value(&block)
}
}
@store.each(uri, &block)
self
end
include Enumerable
@ -202,22 +209,13 @@ class HTTP::CookieJar
# Clear the cookie jar and return self.
def clear
@jar.clear
@store.clear
self
end
protected
# Remove expired cookies and return self.
def cleanup session = false
@jar.each do |domain, paths|
paths.each do |path, hash|
hash.delete_if { |cookie_name, cookie|
cookie.expired? || (session && cookie.session?)
}
end
end
def cleanup(session = false)
@store.cleanup session
self
end
end

View file

@ -0,0 +1,90 @@
require 'http/cookie_jar'
class HTTP::CookieJar::AbstractStore
class << self
@@class_map = {}
# Gets an implementation class by the name, optionally trying to
# load "http/cookie_jar/*_store" if not found. If loading fails,
# KeyError is raised.
def implementation(symbol)
@@class_map.fetch(symbol)
rescue KeyError
begin
require 'http/cookie_jar/%s_store' % symbol
@@class_map.fetch(symbol)
rescue LoadError, KeyError => e
raise KeyError, 'cookie store unavailable: %s' % symbol.inspect
end
end
def inherited(subclass)
@@class_map[class_to_symbol(subclass)] = subclass
end
def class_to_symbol(klass)
klass.name[/[^:]+?(?=Store$|$)/].downcase.to_sym
end
end
def default_options
{}
end
private :default_options
def initialize(options = nil)
options ||= {}
@logger = options[:logger]
# Initializes each instance variable of the same name as option
# keyword.
default_options.each_pair { |key, default|
instance_variable_set("@#{key}", options.key?(key) ? options[key] : default)
}
end
def initialize_copy(other)
raise
self
end
def add(cookie)
raise
self
end
# Iterates over all cookies that are not expired.
#
# Available option keywords are below:
#
# * +uri+
#
# Specify a URI object indicating the destination of the cookies
# being selected. Every cookie yielded should be good to send to
# the given URI, i.e. cookie.valid_for_uri?(uri) evaluates to
# true.
#
# If (and only if) this option is given, last access time of each
# cookie is updated to the current time.
def each(options = nil, &block)
raise
self
end
include Enumerable
def clear
raise
self
end
def cleanup(session = false)
if session
select { |cookie| cookie.session? || cookie.expired? }
else
select(&:expired?)
end.each { |cookie|
add(cookie.expire)
}
# subclasses can optionally remove over-the-limit cookies.
self
end
end

View file

@ -0,0 +1,94 @@
require 'http/cookie_jar'
class Array
def sort_by!(&block)
replace(sort_by(&block))
end unless method_defined?(:sort_by!)
end
class HTTP::CookieJar
class HashStore < AbstractStore
def default_options
{}
end
def initialize(options = nil)
super
@jar = {}
# {
# hostname => {
# path => {
# name => cookie,
# ...
# },
# ...
# },
# ...
# }
@gc_index = 0
end
def initialize_copy(other)
@jar = Marshal.load(Marshal.dump(other.instance_variable_get(:@jar)))
end
def add(cookie)
path_cookies = ((@jar[cookie.domain_name.hostname] ||= {})[cookie.path] ||= {})
if cookie.expired?
path_cookies.delete(cookie.name)
else
path_cookies[cookie.name] = cookie
end
self
end
def each(uri = nil)
if uri
uri = URI(uri)
thost = DomainName.new(uri.host)
tpath = HTTP::Cookie.normalize_path(uri.path)
@jar.each { |domain, paths|
next unless thost.cookie_domain?(domain)
paths.each { |path, hash|
next unless tpath.start_with?(path)
hash.delete_if { |name, cookie|
if cookie.expired?
true
else
cookie.accessed_at = Time.now
yield cookie
false
end
}
}
}
else
@jar.each { |domain, paths|
paths.each { |path, hash|
hash.delete_if { |name, cookie|
if cookie.expired?
true
else
yield cookie
false
end
}
}
}
end
end
def clear
@jar.clear
self
end
def empty?
@jar.empty?
end
end
end