1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344
|
# :markup: markdown
require 'http/cookie'
##
# This class is used to manage the Cookies that have been returned from
# any particular website.
class HTTP::CookieJar
class << self
def const_missing(name)
case name.to_s
when /\A([A-Za-z]+)Store\z/
file = 'http/cookie_jar/%s_store' % $1.downcase
when /\A([A-Za-z]+)Saver\z/
file = 'http/cookie_jar/%s_saver' % $1.downcase
end
begin
require file
rescue LoadError
raise NameError, 'can\'t resolve constant %s; failed to load %s' % [name, file]
end
if const_defined?(name)
const_get(name)
else
raise NameError, 'can\'t resolve constant %s after loading %s' % [name, file]
end
end
end
attr_reader :store
def get_impl(base, value, *args)
case value
when base
value
when Symbol
begin
base.implementation(value).new(*args)
rescue IndexError => e
raise ArgumentError, e.message
end
when Class
if base >= value
value.new(*args)
else
raise TypeError, 'not a subclass of %s: %s' % [base, value]
end
else
raise TypeError, 'invalid object: %s' % value.inspect
end
end
private :get_impl
# Generates a new cookie jar.
#
# Available option keywords are as below:
#
# :store
# : The store class that backs this jar. (default: `:hash`)
# A symbol addressing a store class, a store class, or an instance
# of a store class is accepted. Symbols are mapped to store
# classes, like `:hash` to HTTP::CookieJar::HashStore and `:mozilla`
# to HTTP::CookieJar::MozillaStore.
#
# Any options given are passed through to the initializer of the
# specified store class. For example, the `:mozilla`
# (HTTP::CookieJar::MozillaStore) store class requires a `:filename`
# option. See individual store classes for details.
def initialize(options = nil)
opthash = {
:store => :hash,
}
opthash.update(options) if options
@store = get_impl(AbstractStore, opthash[:store], opthash)
end
# The copy constructor. Not all backend store classes support cloning.
def initialize_copy(other)
@store = other.instance_eval { @store.dup }
end
# Adds a cookie to the jar if it is acceptable, and returns self in
# any case. A given cookie must have domain and path attributes
# set, or ArgumentError is raised.
#
# Whether a cookie with the `for_domain` flag on overwrites another
# with the flag off or vice versa depends on the store used. See
# individual store classes for that matter.
#
# ### Compatibility Note for Mechanize::Cookie users
#
# In HTTP::Cookie, each cookie object can store its origin URI
# (cf. #origin). While the origin URI of a cookie can be set
# manually by #origin=, one is typically given in its generation.
# To be more specific, HTTP::Cookie.new takes an `:origin` option
# and HTTP::Cookie.parse takes one via the second argument.
#
# # Mechanize::Cookie
# jar.add(origin, cookie)
# jar.add!(cookie) # no acceptance check is performed
#
# # HTTP::Cookie
# jar.origin = origin
# jar.add(cookie) # acceptance check is performed
def add(cookie)
@store.add(cookie) if
begin
cookie.acceptable?
rescue RuntimeError => e
raise ArgumentError, e.message
end
self
end
alias << add
# Deletes a cookie that has the same name, domain and path as a
# given cookie from the jar and returns self.
#
# How the `for_domain` flag value affects the set of deleted cookies
# depends on the store used. See individual store classes for that
# matter.
def delete(cookie)
@store.delete(cookie)
self
end
# Gets an array of cookies sorted by the path and creation time. If
# `url` is given, only ones that should be sent to the URL/URI are
# selected, with the access time of each of them updated.
def cookies(url = nil)
each(url).sort
end
# Tests if the jar is empty. If `url` is given, tests if there is
# no cookie for the URL.
def empty?(url = nil)
if url
each(url) { return false }
return true
else
@store.empty?
end
end
# Iterates over all cookies that are not expired in no particular
# order.
#
# An optional argument `uri` specifies 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) the `uri` option is given, last access time of
# each cookie is updated to the current time.
def each(uri = nil, &block) # :yield: cookie
block_given? or return enum_for(__method__, uri)
if uri
uri = URI(uri)
return self unless URI::HTTP === uri && uri.host
end
@store.each(uri, &block)
self
end
include Enumerable
# Parses a Set-Cookie field value `set_cookie` assuming that it is
# sent from a source URL/URI `origin`, and adds the cookies parsed
# as valid and considered acceptable to the jar. Returns an array
# of cookies that have been added.
#
# If a block is given, it is called for each cookie and the cookie
# is added only if the block returns a true value.
#
# `jar.parse(set_cookie, origin)` is a shorthand for this:
#
# HTTP::Cookie.parse(set_cookie, origin) { |cookie|
# jar.add(cookie)
# }
#
# See HTTP::Cookie.parse for available options.
def parse(set_cookie, origin, options = nil) # :yield: cookie
if block_given?
HTTP::Cookie.parse(set_cookie, origin, options).tap { |cookies|
cookies.select! { |cookie|
yield(cookie) && add(cookie)
}
}
else
HTTP::Cookie.parse(set_cookie, origin, options) { |cookie|
add(cookie)
}
end
end
# call-seq:
# jar.save(filename_or_io, **options)
# jar.save(filename_or_io, format = :yaml, **options)
#
# Saves the cookie jar into a file or an IO in the format specified
# and returns self. If a given object responds to #write it is
# taken as an IO, or taken as a filename otherwise.
#
# Available option keywords are below:
#
# * `:format`
#
# Specifies the format for saving. A saver class, a symbol
# addressing a saver class, or a pre-generated instance of a
# saver class is accepted.
#
# <dl class="rdoc-list note-list">
# <dt>:yaml</dt>
# <dd>YAML structure (default)</dd>
# <dt>:cookiestxt</dt>
# <dd>Mozilla's cookies.txt format</dd>
# </dl>
#
# * `:session`
#
# <dl class="rdoc-list note-list">
# <dt>true</dt>
# <dd>Save session cookies as well.</dd>
# <dt>false</dt>
# <dd>Do not save session cookies. (default)</dd>
# </dl>
#
# All options given are passed through to the underlying cookie
# saver module's constructor.
def save(writable, *options)
opthash = {
:format => :yaml,
:session => false,
}
case options.size
when 0
when 1
case options = options.first
when Symbol
opthash[:format] = options
else
if hash = Hash.try_convert(options)
opthash.update(hash)
end
end
when 2
opthash[:format], options = options
if hash = Hash.try_convert(options)
opthash.update(hash)
end
else
raise ArgumentError, 'wrong number of arguments (%d for 1-3)' % (1 + options.size)
end
saver = get_impl(AbstractSaver, opthash[:format], opthash)
if writable.respond_to?(:write)
saver.save(writable, self)
else
File.open(writable, 'w') { |io|
saver.save(io, self)
}
end
self
end
# call-seq:
# jar.load(filename_or_io, **options)
# jar.load(filename_or_io, format = :yaml, **options)
#
# Loads cookies recorded in a file or an IO in the format specified
# into the jar and returns self. If a given object responds to
# \#read it is taken as an IO, or taken as a filename otherwise.
#
# Available option keywords are below:
#
# * `:format`
#
# Specifies the format for loading. A saver class, a symbol
# addressing a saver class, or a pre-generated instance of a
# saver class is accepted.
#
# <dl class="rdoc-list note-list">
# <dt>:yaml</dt>
# <dd>YAML structure (default)</dd>
# <dt>:cookiestxt</dt>
# <dd>Mozilla's cookies.txt format</dd>
# </dl>
#
# All options given are passed through to the underlying cookie
# saver module's constructor.
def load(readable, *options)
opthash = {
:format => :yaml,
:session => false,
}
case options.size
when 0
when 1
case options = options.first
when Symbol
opthash[:format] = options
else
if hash = Hash.try_convert(options)
opthash.update(hash)
end
end
when 2
opthash[:format], options = options
if hash = Hash.try_convert(options)
opthash.update(hash)
end
else
raise ArgumentError, 'wrong number of arguments (%d for 1-3)' % (1 + options.size)
end
saver = get_impl(AbstractSaver, opthash[:format], opthash)
if readable.respond_to?(:write)
saver.load(readable, self)
else
File.open(readable, 'r') { |io|
saver.load(io, self)
}
end
self
end
# Clears the cookie jar and returns self.
def clear
@store.clear
self
end
# Removes expired cookies and returns self. If `session` is true,
# all session cookies are removed as well.
def cleanup(session = false)
@store.cleanup session
self
end
end
|