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 class << self
include URIFix if defined?(URIFix) 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 # Parses a Set-Cookie header value +set_cookie+ into an array of
# Cookie objects. Parts (separated by commas) that are malformed # Cookie objects. Parts (separated by commas) that are malformed
# are ignored. # are ignored.
@ -277,20 +290,8 @@ class HTTP::Cookie
@domain = @domain_name.hostname @domain = @domain_name.hostname
end 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) def path=(path)
@path = normalize_path(path) @path = HTTP::Cookie.normalize_path(path)
end end
def origin=(origin) def origin=(origin)
@ -298,7 +299,7 @@ class HTTP::Cookie
raise ArgumentError, "origin cannot be changed once it is set" raise ArgumentError, "origin cannot be changed once it is set"
origin = URI(origin) origin = URI(origin)
self.domain ||= origin.host self.domain ||= origin.host
self.path ||= (normalize_uri_path(origin) + './').path self.path ||= (HTTP::Cookie.normalize_path(origin) + './').path
acceptable_from_uri?(origin) or acceptable_from_uri?(origin) or
raise ArgumentError, "unacceptable cookie sent from URI #{origin}" raise ArgumentError, "unacceptable cookie sent from URI #{origin}"
@origin = origin @origin = origin
@ -351,7 +352,7 @@ class HTTP::Cookie
raise "cannot tell if this cookie is valid because the domain is unknown" raise "cannot tell if this cookie is valid because the domain is unknown"
end end
return false if secure? && uri.scheme != 'https' 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 end
# Returns a string for use in a Cookie header value, # 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 if @for_domain || @domain != DomainName.new(origin.host).hostname
string << "; domain=#{@domain}" string << "; domain=#{@domain}"
end end
if (normalize_uri_path(origin) + './').path != @path if (HTTP::Cookie.normalize_path(origin) + './').path != @path
string << "; path=#{@path}" string << "; path=#{@path}"
end end
if expires = @expires if expires = @expires

View file

@ -1,6 +1,4 @@
module HTTP require 'http/cookie'
autoload :Cookie, 'http/cookie'
end
## ##
# This class is used to manage the Cookies that have been returned from # This class is used to manage the Cookies that have been returned from
@ -8,15 +6,25 @@ end
class HTTP::CookieJar class HTTP::CookieJar
autoload :AbstractSaver, 'http/cookie_jar/abstract_saver' autoload :AbstractSaver, 'http/cookie_jar/abstract_saver'
autoload :AbstractStore, 'http/cookie_jar/abstract_store'
attr_reader :jar attr_reader :store
def initialize def initialize(store = :hash, options = nil)
@jar = {} 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 end
def initialize_copy other # :nodoc: def initialize_copy(other)
@jar = Marshal.load Marshal.dump other.jar @store = other.instance_eval { @store.dup }
end end
# Add a +cookie+ to the jar and return self. # Add a +cookie+ to the jar and return self.
@ -24,16 +32,8 @@ class HTTP::CookieJar
if cookie.domain.nil? || cookie.path.nil? if cookie.domain.nil? || cookie.path.nil?
raise ArgumentError, "a cookie with unknown domain or path cannot be added" raise ArgumentError, "a cookie with unknown domain or path cannot be added"
end 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 self
end end
alias << add alias << add
@ -53,12 +53,23 @@ class HTTP::CookieJar
each(url) { return false } each(url) { return false }
return true return true
else else
@jar.empty? @store.empty?
end end
end end
# Iterate over cookies. If +uri+ is given, cookies not for the # Iterates over all cookies that are not expired.
# URL/URI are excluded. #
# 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) def each(uri = nil, &block)
block_given? or return enum_for(__method__, uri) block_given? or return enum_for(__method__, uri)
@ -68,11 +79,7 @@ class HTTP::CookieJar
} }
end end
@jar.each { |domain, paths| @store.each(uri, &block)
paths.each { |path, hash|
hash.each_value(&block)
}
}
self self
end end
include Enumerable include Enumerable
@ -202,22 +209,13 @@ class HTTP::CookieJar
# Clear the cookie jar and return self. # Clear the cookie jar and return self.
def clear def clear
@jar.clear @store.clear
self self
end end
protected
# Remove expired cookies and return self. # Remove expired cookies and return self.
def cleanup session = false def cleanup(session = false)
@jar.each do |domain, paths| @store.cleanup session
paths.each do |path, hash|
hash.delete_if { |cookie_name, cookie|
cookie.expired? || (session && cookie.session?)
}
end
end
self self
end end
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