Introduce an abstraction layer for saving (serializing) CookieJar.

CookieJar#save is the new name for the now obsolete #save_as.

CookieJar#save and #load now accept IO-like instead of a filename.

Change the YAML file format, and make #load discard incompatible data.
This commit is contained in:
Akinori MUSHA 2013-03-15 02:16:55 +09:00
parent fd7450717a
commit 1f5eb6bc7f
7 changed files with 256 additions and 120 deletions

View file

@ -63,8 +63,6 @@ Or install it yourself as:
- Print kind error messages to make migration from Mechanize::Cookie easier
- Make serializers pluggable/autoloadable and prepare a binary friendly API
## Contributing
1. Fork it

View file

@ -20,8 +20,6 @@ class HTTP::Cookie
secure httponly
expires created_at accessed_at
]
True = "TRUE"
False = "FALSE"
# In Ruby < 1.9.3 URI() does not accept an URI object.
if RUBY_VERSION < "1.9.3"
@ -247,35 +245,6 @@ class HTTP::Cookie
}
}
end
# Parses a line from cookies.txt and returns a cookie object if
# the line represents a cookie record or returns nil otherwise.
def parse_cookiestxt_line(line)
return nil if line.match(/^#/)
domain,
s_for_domain, # Whether this cookie is for domain
path, # Path for which the cookie is relevant
s_secure, # Requires a secure connection
s_expires, # Time the cookie expires (Unix epoch time)
name, value = line.split("\t", 7)
return nil if value.nil?
value.chomp!
if (expires_seconds = s_expires.to_i).nonzero?
expires = Time.at(expires_seconds)
return nil if expires < Time.now
end
HTTP::Cookie.new(name, value,
:domain => domain,
:for_domain => s_for_domain == True,
:path => path,
:secure => s_secure == True,
:expires => expires,
:version => 0)
end
end
def name=(name)
@ -392,19 +361,6 @@ class HTTP::Cookie
end
alias to_s cookie_value
# Serializes the cookie into a cookies.txt line.
def to_cookiestxt_line(linefeed = "\n")
[
@domain,
@for_domain ? True : False,
@path,
@secure ? True : False,
@expires.to_i,
@name,
@value
].join("\t") << linefeed
end
# Returns a string for use in a Set-Cookie header value. If the
# cookie does not have an origin set, one must be given from the
# argument.

View file

@ -7,7 +7,7 @@ end
# any particular website.
class HTTP::CookieJar
include Enumerable
autoload :AbstractSaver, 'http/cookie_jar/abstract_saver'
attr_reader :jar
@ -75,13 +75,15 @@ class HTTP::CookieJar
}
self
end
include Enumerable
# call-seq:
# jar.save_as(file, format = :yaml)
# jar.save_as(file, options)
# jar.save(filename_or_io, **options)
# jar.save(filename_or_io, format = :yaml, **options)
#
# Save the cookie jar to a file in the format specified and return
# self.
# Save the cookie jar into a file or an IO in the format specified
# and return self. If the given object responds to #write it is
# taken as an IO, or taken as a filename otherwise.
#
# Available option keywords are below:
#
@ -95,65 +97,108 @@ class HTTP::CookieJar
# Save session cookies as well.
# [+false+]
# Do not save session cookies. (default)
def save_as(file, options = nil)
if Symbol === options
format = options
session = false
#
# All options given are passed through to the underlying cookie
# saver module.
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
opthash.update(options) if options
end
when 2
opthash[:format], options = options
opthash.update(options) if options
else
options ||= {}
format = options[:format] || :yaml
session = !!options[:session]
raise ArgumentError, 'wrong number of arguments (%d for 1-3)' % (1 + options.size)
end
jar = dup
jar.cleanup !session
begin
saver = AbstractSaver.implementation(opthash[:format]).new(opthash)
rescue KeyError => e
raise ArgumentError, e.message
end
open(file, 'w') { |f|
case format
when :yaml then
require_yaml
YAML.dump(jar.jar, f)
when :cookiestxt then
jar.dump_cookiestxt(f)
else
raise ArgumentError, "Unknown cookie jar file format"
end
}
if writable.respond_to?(:write)
saver.save(writable, self)
else
File.open(writable, 'w') { |io|
saver.save(io, self)
}
end
self
end
# Load cookie jar from a file in the format specified.
#
# Available formats:
# :yaml <- YAML structure.
# :cookiestxt <- Mozilla's cookies.txt format
def load(file, format = :yaml)
File.open(file) { |f|
case format
when :yaml then
require_yaml
@jar = YAML.load(f)
when :cookiestxt then
load_cookiestxt(f)
else
raise ArgumentError, "Unknown cookie jar file format"
end
}
cleanup
# An obsolete name for save().
def save_as(*args)
warn "%s() is obsolete; use save()." % __method__
save(*args)
end
def require_yaml # :nodoc:
begin
require 'psych'
rescue LoadError
# call-seq:
# jar.load(filename_or_io, **options)
# jar.load(filename_or_io, format = :yaml, **options)
#
# Load cookies recorded in a file or an IO in the format specified
# into the jar and return self. If the given object responds to
# #read it is taken as an IO, or taken as a filename otherwise.
#
# Available option keywords are below:
#
# * +format+
# [<tt>:yaml</tt>]
# YAML structure (default)
# [<tt>:cookiestxt</tt>]
# Mozilla's cookies.txt format
#
# All options given are passed through to the underlying cookie
# saver module.
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
opthash.update(options) if options
end
when 2
opthash[:format], options = options
opthash.update(options) if options
else
raise ArgumentError, 'wrong number of arguments (%d for 1-3)' % (1 + options.size)
end
require 'yaml'
begin
saver = AbstractSaver.implementation(opthash[:format]).new(opthash)
rescue KeyError => e
raise ArgumentError, e.message
end
if readable.respond_to?(:write)
saver.load(readable, self)
else
File.open(readable, 'r') { |io|
saver.load(io, self)
}
end
self
end
private :require_yaml
# Clear the cookie jar and return self.
def clear
@ -161,26 +206,6 @@ class HTTP::CookieJar
self
end
# Read cookies from Mozilla cookies.txt-style IO stream and return
# self.
def load_cookiestxt(io)
io.each_line do |line|
c = HTTP::Cookie.parse_cookiestxt_line(line) and add(c)
end
self
end
# Write cookies to Mozilla cookies.txt-style IO stream and return
# self.
def dump_cookiestxt(io)
io.puts "# HTTP Cookie File"
to_a.each do |cookie|
io.print cookie.to_cookiestxt_line
end
self
end
protected
# Remove expired cookies and return self.

View file

@ -0,0 +1,53 @@
require 'http/cookie_jar'
class HTTP::CookieJar::AbstractSaver
class << self
@@class_map = {}
# Gets an implementation class by the name, optionally trying to
# load "http/cookie_jar/*_saver" if not found. If loading fails,
# KeyError is raised.
def implementation(symbol)
@@class_map.fetch(symbol)
rescue KeyError
begin
require 'http/cookie_jar/%s_saver' % symbol
@@class_map.fetch(symbol)
rescue LoadError, KeyError => e
raise KeyError, 'cookie saver unavailable: %s' % symbol.inspect
end
end
def inherited(subclass)
@@class_map[class_to_symbol(subclass)] = subclass
end
def class_to_symbol(klass)
klass.name[/[^:]+?(?=Saver$|$)/].downcase.to_sym
end
end
def default_options
{}
end
private :default_options
def initialize(options = nil)
options ||= {}
@logger = options[:logger]
@session = options[:session]
# 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 save(io, jar)
raise
end
def load(io, jar)
raise
end
end

View file

@ -0,0 +1,74 @@
require 'http/cookie_jar'
# CookiestxtSaver saves and loads cookies in the cookies.txt format.
class HTTP::CookieJar::CookiestxtSaver < HTTP::CookieJar::AbstractSaver
True = "TRUE"
False = "FALSE"
def save(io, jar)
io.puts @header if @header
jar.each { |cookie|
next if !@session && cookie.session?
io.print cookie_to_record(cookie)
}
end
def load(io, jar)
io.each_line { |line|
cookie = parse_record(line) and jar.add(cookie)
}
end
private
def default_options
{
header: "# HTTP Cookie File",
linefeed: "\n",
}
end
# Serializes the cookie into a cookies.txt line.
def cookie_to_record(cookie)
cookie.instance_eval {
[
@domain,
@for_domain ? True : False,
@path,
@secure ? True : False,
@expires.to_i,
@name,
@value
]
}.join("\t") << @linefeed
end
# Parses a line from cookies.txt and returns a cookie object if the
# line represents a cookie record or returns nil otherwise.
def parse_record(line)
return nil if line.match(/^#/)
domain,
s_for_domain, # Whether this cookie is for domain
path, # Path for which the cookie is relevant
s_secure, # Requires a secure connection
s_expires, # Time the cookie expires (Unix epoch time)
name, value = line.split("\t", 7)
return nil if value.nil?
value.chomp!
if (expires_seconds = s_expires.to_i).nonzero?
expires = Time.at(expires_seconds)
return nil if expires < Time.now
end
HTTP::Cookie.new(name, value,
:domain => domain,
:for_domain => s_for_domain == True,
:path => path,
:secure => s_secure == True,
:expires => expires,
:version => 0)
end
end

View file

@ -0,0 +1,30 @@
require 'http/cookie_jar'
begin
require 'psych'
rescue LoadError
end
require 'yaml'
# YAMLSaver saves and loads cookies in the YAML format.
class HTTP::CookieJar::YAMLSaver < HTTP::CookieJar::AbstractSaver
def save(io, jar)
YAML.dump(@session ? jar.to_a : jar.reject(&:session?), io)
end
def load(io, jar)
begin
YAML.load(io)
rescue ArgumentError
@logger.warn "incompatible YAML cookie data discarded" if @logger
return
end.each { |cookie|
jar.add(cookie)
}
end
private
def default_options
{}
end
end

View file

@ -301,7 +301,7 @@ class TestHTTPCookieJar < Test::Unit::TestCase
assert_equal(3, @jar.cookies(url).length)
in_tmpdir do
value = @jar.save_as("cookies.yml")
value = @jar.save("cookies.yml")
assert_same @jar, value
jar = HTTP::CookieJar.new
@ -333,7 +333,7 @@ class TestHTTPCookieJar < Test::Unit::TestCase
assert_equal(3, @jar.cookies(url).length)
in_tmpdir do
@jar.save_as("cookies.yml", :format => :yaml, :session => true)
@jar.save("cookies.yml", :format => :yaml, :session => true)
jar = HTTP::CookieJar.new
jar.load("cookies.yml")
@ -365,7 +365,7 @@ class TestHTTPCookieJar < Test::Unit::TestCase
assert_equal(3, @jar.cookies(url).length)
in_tmpdir do
@jar.save_as("cookies.txt", :cookiestxt)
@jar.save("cookies.txt", :cookiestxt)
jar = HTTP::CookieJar.new
jar.load("cookies.txt", :cookiestxt) # HACK test the format