mirror of
https://github.com/samsonjs/http-cookie.git
synced 2026-03-25 08:55:53 +00:00
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:
parent
fd7450717a
commit
1f5eb6bc7f
7 changed files with 256 additions and 120 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
53
lib/http/cookie_jar/abstract_saver.rb
Normal file
53
lib/http/cookie_jar/abstract_saver.rb
Normal 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
|
||||
74
lib/http/cookie_jar/cookiestxt_saver.rb
Normal file
74
lib/http/cookie_jar/cookiestxt_saver.rb
Normal 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
|
||||
30
lib/http/cookie_jar/yaml_saver.rb
Normal file
30
lib/http/cookie_jar/yaml_saver.rb
Normal 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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue