Import cookie related stuff from Mechanize.

This commit is contained in:
Akinori MUSHA 2012-10-14 18:52:52 +09:00
commit 060fc63c2a
13 changed files with 1630 additions and 0 deletions

17
.gitignore vendored Normal file
View file

@ -0,0 +1,17 @@
*.gem
*.rbc
.bundle
.config
.yardoc
Gemfile.lock
InstalledFiles
_yardoc
coverage
doc/
lib/bundler/man
pkg
rdoc
spec/reports
test/tmp
test/version_tmp
tmp

4
Gemfile Normal file
View file

@ -0,0 +1,4 @@
source 'https://rubygems.org'
# Specify your gem's dependencies in http-cookie.gemspec
gemspec

23
LICENSE.txt Normal file
View file

@ -0,0 +1,23 @@
Copyright (c) 2011-2012 Akinori MUSHA, Eric Hodel
Copyright (c) 2006-2011 Aaron Patterson, Mike Dalessio
MIT License
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

49
README.md Normal file
View file

@ -0,0 +1,49 @@
# HTTP::Cookie
HTTP::Cookie is a ruby library to handle HTTP cookies in a way both
compliant with RFCs and compatible with today's major browsers.
It was originally a part of the Mechanize library, separated as an
independent library in the hope of serving as a common component that
is reusable from any HTTP related piece of software.
## Installation
Add this line to your application's Gemfile:
gem 'http-cookie'
And then execute:
$ bundle
Or install it yourself as:
$ gem install http-cookie
## Usage
# Initialize a cookie jar
jar = HTTP::CookieJar.new
# Load from a file
jar.load(filename) if File.exist?(filename)
# Store received cookies
HTTP::Cookie.parse(uri, set_cookie_header_value) { |cookie|
jar << cookie
}
# Extract cookies to send
cookie_value_to_send = jar.cookies(uri).join(', ')
# Save to a file
jar.save_as(filename)
## Contributing
1. Fork it
2. Create your feature branch (`git checkout -b my-new-feature`)
3. Commit your changes (`git commit -am 'Add some feature'`)
4. Push to the branch (`git push origin my-new-feature`)
5. Create new Pull Request

20
Rakefile Normal file
View file

@ -0,0 +1,20 @@
require "bundler/gem_tasks"
require 'rake/testtask'
Rake::TestTask.new(:test) do |test|
test.libs << 'lib' << 'test'
test.pattern = 'test/**/test_*.rb'
test.verbose = true
end
task :default => :test
require 'rdoc/task'
Rake::RDocTask.new do |rdoc|
version = HTTP::Cookie::VERSION
rdoc.rdoc_dir = 'rdoc'
rdoc.title = "http-cookie #{version}"
rdoc.rdoc_files.include('lib/**/*.rb')
rdoc.rdoc_files.include(Bundler::GemHelper.gemspec.extra_rdoc_files)
end

26
http-cookie.gemspec Normal file
View file

@ -0,0 +1,26 @@
# -*- encoding: utf-8 -*-
lib = File.expand_path('../lib', __FILE__)
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
require 'http/cookie/version'
Gem::Specification.new do |gem|
gem.name = "http-cookie"
gem.version = HTTP::Cookie::VERSION
gem.authors, gem.email = {
'Akinori MUSHA' => 'knu@idaemons.org',
'Aaron Patterson' => 'aaronp@rubyforge.org',
'Eric Hodel' => 'drbrain@segment7.net',
'Mike Dalessio' => 'mike.dalessio@gmail.com',
}.instance_eval { [keys, values] }
gem.description = %q{A Ruby library to handle HTTP Cookies}
gem.summary = %q{A Ruby library to handle HTTP Cookies}
gem.homepage = "https://github.com/sparklemotion/http-cookie"
gem.files = `git ls-files`.split($/)
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
gem.require_paths = ["lib"]
gem.add_runtime_dependency("domain_name", ["~> 0.5"])
end

1
lib/http-cookie.rb Normal file
View file

@ -0,0 +1 @@
require 'http/cookie'

243
lib/http/cookie.rb Normal file
View file

@ -0,0 +1,243 @@
require 'http/cookie/version'
require 'time'
require 'webrick/httputils'
require 'domain_name'
module HTTP
autoload :CookieJar, 'http/cookie_jar'
end
# This class is used to represent an HTTP Cookie.
class HTTP::Cookie
attr_reader :name
attr_accessor :value, :version
attr_accessor :domain, :path, :secure
attr_accessor :comment, :max_age
attr_accessor :session
attr_accessor :created_at
attr_accessor :accessed_at
# :call-seq:
# new(name, value)
# new(name, value, attr_hash)
# new(attr_hash)
#
# Creates a cookie object. For each key of +attr_hash+, the setter
# is called if defined. Each key can be either a symbol or a
# string, downcased or not.
#
# e.g.
# new("uid", "a12345")
# new("uid", "a12345", :domain => 'example.org',
# :for_domain => true, :expired => Time.now + 7*86400)
# new("name" => "uid", "value" => "a12345", "Domain" => 'www.example.org')
#
def initialize(*args)
@version = 0 # Netscape Cookie
@domain = @path = @secure = @comment = @max_age =
@expires = @comment_url = @discard = @port = nil
@created_at = @accessed_at = Time.now
case args.size
when 2
@name, @value = *args
@for_domain = false
return
when 3
@name, @value, attr_hash = *args
when 1
attr_hash = args.first
else
raise ArgumentError, "wrong number of arguments (#{args.size} for 1-3)"
end
for_domain = false
attr_hash.each_pair { |key, val|
skey = key.to_s.downcase
skey.sub!(/[!?]\z/, '')
case skey
when 'for_domain'
for_domain = !!val
when 'name'
@name = val
when 'value'
@value = val
else
setter = :"#{skey}="
send(setter, val) if respond_to?(setter)
end
}
@for_domain = for_domain
end
# If this flag is true, this cookie will be sent to any host in the
# +domain+. If it is false, this cookie will be sent only to the
# host indicated by the +domain+.
attr_accessor :for_domain
alias for_domain? for_domain
class << self
# Parses a Set-Cookie header line +str+ sent from +uri+ into an
# array of Cookie objects. Note that this array may contain
# nil's when some of the cookie-pairs are malformed.
def parse(uri, str, log = nil)
return str.split(/,(?=[^;,]*=)|,$/).map { |c|
cookie_elem = c.split(/;+/)
first_elem = cookie_elem.shift
first_elem.strip!
key, value = first_elem.split(/\=/, 2)
begin
cookie = new(key, value.dup)
rescue
log.warn("Couldn't parse key/value: #{first_elem}") if log
next
end
cookie_elem.each do |pair|
pair.strip!
key, value = pair.split(/=/, 2)
next unless key
value = WEBrick::HTTPUtils.dequote(value.strip) if value
case key.downcase
when 'domain'
next unless value && !value.empty?
begin
cookie.domain = value
cookie.for_domain = true
rescue
log.warn("Couldn't parse domain: #{value}") if log
end
when 'path'
next unless value && !value.empty?
cookie.path = value
when 'expires'
next unless value && !value.empty?
begin
cookie.expires = Time::parse(value)
rescue
log.warn("Couldn't parse expires: #{value}") if log
end
when 'max-age'
next unless value && !value.empty?
begin
cookie.max_age = Integer(value)
rescue
log.warn("Couldn't parse max age '#{value}'") if log
end
when 'comment'
next unless value
cookie.comment = value
when 'version'
next unless value
begin
cookie.version = Integer(value)
rescue
log.warn("Couldn't parse version '#{value}'") if log
cookie.version = nil
end
when 'secure'
cookie.secure = true
end
end
cookie.path ||= (uri + './').path
cookie.secure ||= false
cookie.domain ||= uri.host
# RFC 6265 4.1.2.2
cookie.expires = Time.now + cookie.max_age if cookie.max_age
cookie.session = !cookie.expires
# Move this in to the cookie jar
yield cookie if block_given?
cookie
}
end
end
alias set_domain domain=
# Sets the domain attribute. A leading dot in +domain+ implies
# turning the +for_domain?+ flag on.
def domain=(domain)
if DomainName === domain
@domain_name = domain
else
domain.is_a?(String) or
(domain.respond_to?(:to_str) && (domain = domain.to_str).is_a?(String)) or
raise TypeError, "#{domain.class} is not a String"
if domain.start_with?('.')
@for_domain = true
domain = domain[1..-1]
end
# Do we really need to support this?
if domain.match(/\A([^:]+):[0-9]+\z/)
domain = $1
end
@domain_name = DomainName.new(domain)
end
set_domain(@domain_name.hostname)
end
def expires=(t)
@expires = t && (t.is_a?(Time) ? t.httpdate : t.to_s)
end
def expires
@expires && Time.parse(@expires)
end
def expired?
return false unless expires
Time.now > expires
end
alias secure? secure
def acceptable_from_uri?(uri)
host = DomainName.new(uri.host)
# RFC 6265 5.3
# When the user agent "receives a cookie":
return host.hostname == domain unless @for_domain
if host.cookie_domain?(@domain_name)
true
elsif host.hostname == domain
@for_domain = false
true
else
false
end
end
def valid_for_uri?(uri)
return false if secure? && uri.scheme != 'https'
acceptable_from_uri?(uri) && uri.path.start_with?(path)
end
def to_s
"#{@name}=#{@value}"
end
def init_with(coder)
yaml_initialize(coder.tag, coder.map)
end
def yaml_initialize(tag, map)
@for_domain = true # for forward compatibility
map.each { |key, value|
case key
when 'domain'
self.domain = value # ditto
else
instance_variable_set(:"@#{key}", value)
end
}
end
end

View file

@ -0,0 +1,5 @@
module HTTP
class Cookie
VERSION = "0.0.1"
end
end

221
lib/http/cookie_jar.rb Normal file
View file

@ -0,0 +1,221 @@
module HTTP
autoload :Cookie, 'http/cookie'
end
##
# This class is used to manage the Cookies that have been returned from
# any particular website.
class HTTP::CookieJar
include Enumerable
# add_cookie wants something resembling a URI.
attr_reader :jar
def initialize
@jar = {}
end
def initialize_copy other # :nodoc:
@jar = Marshal.load Marshal.dump other.jar
end
# Add a +cookie+ to the jar if it is considered acceptable from
# +uri+. Return nil if the cookie was not added, otherwise return
# the cookie added.
def add(uri, cookie)
return nil unless cookie.acceptable_from_uri?(uri)
add!(cookie)
cookie
end
# Add a +cookie+ to the jar and return self.
def add!(cookie)
normal_domain = cookie.domain.downcase
@jar[normal_domain] ||= {} unless @jar.has_key?(normal_domain)
@jar[normal_domain][cookie.path] ||= {}
@jar[normal_domain][cookie.path][cookie.name] = cookie
self
end
alias << add!
# Fetch the cookies that should be used for the URI object passed in.
def cookies(url)
cleanup
url.path = '/' if url.path.empty?
now = Time.now
select { |cookie|
!cookie.expired? && cookie.valid_for_uri?(url) && (cookie.accessed_at = now)
}.sort_by { |cookie|
# RFC 6265 5.4
# Precedence: 1. longer path 2. older creation
[-cookie.path.length, cookie.created_at]
}
end
def empty?(url)
cookies(url).length > 0 ? false : true
end
def each
block_given? or return enum_for(__method__)
cleanup
@jar.each { |domain, paths|
paths.each { |path, hash|
hash.each_value { |cookie|
yield cookie
}
}
}
end
# call-seq:
# jar.save_as(file, format = :yaml)
# jar.save_as(file, options)
#
# Save the cookie jar to a file in the format specified and return
# self.
#
# Available option keywords are below:
#
# * +format+
# [<tt>:yaml</tt>]
# YAML structure (default)
# [<tt>:cookiestxt</tt>]
# Mozilla's cookies.txt format
# * +session+
# [+true+]
# 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
else
options ||= {}
format = options[:format] || :yaml
session = !!options[:session]
end
jar = dup
jar.cleanup !session
open(file, 'w') { |f|
case format
when :yaml then
load_yaml
YAML.dump(jar.jar, f)
when :cookiestxt then
jar.dump_cookiestxt(f)
else
raise ArgumentError, "Unknown cookie jar file format"
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)
@jar = open(file) { |f|
case format
when :yaml then
load_yaml
YAML.load(f)
when :cookiestxt then
load_cookiestxt(f)
else
raise ArgumentError, "Unknown cookie jar file format"
end
}
cleanup
self
end
def load_yaml # :nodoc:
begin
require 'psych'
rescue LoadError
end
require 'yaml'
end
# Clear the cookie jar
def clear!
@jar = {}
end
# Read cookies from Mozilla cookies.txt-style IO stream
def load_cookiestxt(io)
now = Time.now
io.each_line do |line|
line.chomp!
line.gsub!(/#.+/, '')
fields = line.split("\t")
next if fields.length != 7
expires_seconds = fields[4].to_i
expires = (expires_seconds == 0) ? nil : Time.at(expires_seconds)
next if expires and (expires < now)
c = HTTP::Cookie.new(fields[5], fields[6])
c.domain = fields[0]
c.for_domain = (fields[1] == "TRUE") # Whether this cookie is for domain
c.path = fields[2] # Path for which the cookie is relevant
c.secure = (fields[3] == "TRUE") # Requires a secure connection
c.expires = expires # Time the cookie expires.
c.version = 0 # Conforms to Netscape cookie spec.
add!(c)
end
@jar
end
# Write cookies to Mozilla cookies.txt-style IO stream
def dump_cookiestxt(io)
to_a.each do |cookie|
io.puts([
cookie.domain,
cookie.for_domain? ? "TRUE" : "FALSE",
cookie.path,
cookie.secure ? "TRUE" : "FALSE",
cookie.expires.to_i.to_s,
cookie.name,
cookie.value
].join("\t"))
end
end
protected
# Remove expired cookies
def cleanup session = false
@jar.each do |domain, paths|
paths.each do |path, names|
names.each do |cookie_name, cookie|
paths[path].delete(cookie_name) if
cookie.expired? or (session and cookie.session)
end
end
end
end
end

21
test/helper.rb Normal file
View file

@ -0,0 +1,21 @@
require 'rubygems'
require 'test/unit'
require 'uri'
require 'http/cookie'
module Enumerable
def combine
masks = inject([[], 1]){|(ar, m), e| [ar << m, m << 1 ] }[0]
all = masks.inject(0){ |al, m| al|m }
result = []
for i in 1..all do
tmp = []
each_with_index do |e, idx|
tmp << e unless (masks[idx] & i) == 0
end
result << tmp
end
result
end
end

482
test/test_http_cookie.rb Normal file
View file

@ -0,0 +1,482 @@
require File.expand_path('helper', File.dirname(__FILE__))
class TestHTTPCookie < Test::Unit::TestCase
#class TestHTTPCookie < MiniTest::Unit::TestCase
def silently
warn_level = $VERBOSE
$VERBOSE = false
res = yield
$VERBOSE = warn_level
res
end
def test_parse_dates
url = URI.parse('http://localhost/')
yesterday = Time.now - 86400
dates = [ "14 Apr 89 03:20:12",
"14 Apr 89 03:20 GMT",
"Fri, 17 Mar 89 4:01:33",
"Fri, 17 Mar 89 4:01 GMT",
"Mon Jan 16 16:12 PDT 1989",
"Mon Jan 16 16:12 +0130 1989",
"6 May 1992 16:41-JST (Wednesday)",
#"22-AUG-1993 10:59:12.82",
"22-AUG-1993 10:59pm",
"22-AUG-1993 12:59am",
"22-AUG-1993 12:59 PM",
#"Friday, August 04, 1995 3:54 PM",
#"06/21/95 04:24:34 PM",
#"20/06/95 21:07",
"95-06-08 19:32:48 EDT",
]
dates.each do |date|
cookie = "PREF=1; expires=#{date}"
silently do
HTTP::Cookie.parse(url, cookie) { |c|
assert c.expires, "Tried parsing: #{date}"
assert_equal(true, c.expires < yesterday)
}
end
end
end
def test_parse_empty
cookie_str = 'a=b; ; c=d'
uri = URI.parse 'http://example'
HTTP::Cookie.parse uri, cookie_str do |cookie|
assert_equal 'a', cookie.name
assert_equal 'b', cookie.value
end
end
def test_parse_no_space
cookie_str = "foo=bar;Expires=Sun, 06 Nov 2011 00:28:06 GMT;Path=/"
uri = URI.parse 'http://example'
HTTP::Cookie.parse uri, cookie_str do |cookie|
assert_equal 'foo', cookie.name
assert_equal 'bar', cookie.value
assert_equal '/', cookie.path
assert_equal Time.at(1320539286), cookie.expires
end
end
def test_parse_quoted
cookie_str =
"quoted=\"value\"; Expires=Sun, 06 Nov 2011 00:11:18 GMT; Path=/"
uri = URI.parse 'http://example'
HTTP::Cookie.parse uri, cookie_str do |cookie|
assert_equal 'quoted', cookie.name
assert_equal '"value"', cookie.value
end
end
def test_parse_weird_cookie
cookie = 'n/a, ASPSESSIONIDCSRRQDQR=FBLDGHPBNDJCPCGNCPAENELB; path=/'
url = URI.parse('http://www.searchinnovation.com/')
HTTP::Cookie.parse(url, cookie) { |c|
assert_equal('ASPSESSIONIDCSRRQDQR', c.name)
assert_equal('FBLDGHPBNDJCPCGNCPAENELB', c.value)
}
end
def test_double_semicolon
double_semi = 'WSIDC=WEST;; domain=.williams-sonoma.com; path=/'
url = URI.parse('http://williams-sonoma.com/')
HTTP::Cookie.parse(url, double_semi) { |cookie|
assert_equal('WSIDC', cookie.name)
assert_equal('WEST', cookie.value)
}
end
def test_parse_bad_version
bad_cookie = 'PRETANET=TGIAqbFXtt; Name=/PRETANET; Path=/; Version=1.2; Content-type=text/html; Domain=192.168.6.196; expires=Friday, 13-November-2026 23:01:46 GMT;'
url = URI.parse('http://localhost/')
HTTP::Cookie.parse(url, bad_cookie) { |cookie|
assert_nil(cookie.version)
}
end
def test_parse_bad_max_age
bad_cookie = 'PRETANET=TGIAqbFXtt; Name=/PRETANET; Path=/; Max-Age=1.2; Content-type=text/html; Domain=192.168.6.196; expires=Friday, 13-November-2026 23:01:46 GMT;'
url = URI.parse('http://localhost/')
HTTP::Cookie.parse(url, bad_cookie) { |cookie|
assert_nil(cookie.max_age)
}
end
def test_parse_date_fail
url = URI.parse('http://localhost/')
dates = [
"20/06/95 21:07",
]
silently do
dates.each do |date|
cookie = "PREF=1; expires=#{date}"
HTTP::Cookie.parse(url, cookie) { |c|
assert_equal(true, c.expires.nil?)
}
end
end
end
def test_parse_domain_dot
url = URI.parse('http://host.example.com/')
cookie_str = 'a=b; domain=.example.com'
cookie = HTTP::Cookie.parse(url, cookie_str).first
assert_equal 'example.com', cookie.domain
assert cookie.for_domain?
end
def test_parse_domain_no_dot
url = URI.parse('http://host.example.com/')
cookie_str = 'a=b; domain=example.com'
cookie = HTTP::Cookie.parse(url, cookie_str).first
assert_equal 'example.com', cookie.domain
assert cookie.for_domain?
end
def test_parse_domain_none
url = URI.parse('http://example.com/')
cookie_str = 'a=b;'
cookie = HTTP::Cookie.parse(url, cookie_str).first
assert_equal 'example.com', cookie.domain
assert !cookie.for_domain?
end
def test_parse_max_age
url = URI.parse('http://localhost/')
date = 'Mon, 19 Feb 2012 19:26:04 GMT'
cookie = HTTP::Cookie.parse(url, "name=Akinori; expires=#{date}").first
assert_equal Time.at(1329679564), cookie.expires
cookie = HTTP::Cookie.parse(url, 'name=Akinori; max-age=3600').first
assert_in_delta Time.now + 3600, cookie.expires, 1
# Max-Age has precedence over Expires
cookie = HTTP::Cookie.parse(url, "name=Akinori; max-age=3600; expires=#{date}").first
assert_in_delta Time.now + 3600, cookie.expires, 1
cookie = HTTP::Cookie.parse(url, "name=Akinori; expires=#{date}; max-age=3600").first
assert_in_delta Time.now + 3600, cookie.expires, 1
end
def test_parse_expires_session
url = URI.parse('http://localhost/')
[
'name=Akinori',
'name=Akinori; expires',
'name=Akinori; max-age',
'name=Akinori; expires=',
'name=Akinori; max-age=',
].each { |str|
cookie = HTTP::Cookie.parse(url, str).first
assert cookie.session, str
}
[
'name=Akinori; expires=Mon, 19 Feb 2012 19:26:04 GMT',
'name=Akinori; max-age=3600',
].each { |str|
cookie = HTTP::Cookie.parse(url, str).first
assert !cookie.session, str
}
end
def test_parse_many
url = URI 'http://example/'
cookie_str =
"name=Aaron; Domain=localhost; Expires=Sun, 06 Nov 2011 00:29:51 GMT; Path=/, " \
"name=Aaron; Domain=localhost; Expires=Sun, 06 Nov 2011 00:29:51 GMT; Path=/, " \
"name=Aaron; Domain=localhost; Expires=Sun, 06 Nov 2011 00:29:51 GMT; Path=/, " \
"name=Aaron; Domain=localhost; Expires=Sun, 06 Nov 2011 00:29:51 GMT; Path=/; HttpOnly, " \
"expired=doh; Expires=Fri, 04 Nov 2011 00:29:51 GMT; Path=/, " \
"a_path=some_path; Expires=Sun, 06 Nov 2011 00:29:51 GMT; Path=/some_path, " \
"no_path1=no_path; Expires=Sun, 06 Nov 2011 00:29:52 GMT, no_expires=nope; Path=/, " \
"no_path2=no_path; Expires=Sun, 06 Nov 2011 00:29:52 GMT; no_expires=nope; Path, " \
"no_path3=no_path; Expires=Sun, 06 Nov 2011 00:29:52 GMT; no_expires=nope; Path=, " \
"no_domain1=no_domain; Expires=Sun, 06 Nov 2011 00:29:53 GMT; no_expires=nope, " \
"no_domain2=no_domain; Expires=Sun, 06 Nov 2011 00:29:53 GMT; no_expires=nope; Domain, " \
"no_domain3=no_domain; Expires=Sun, 06 Nov 2011 00:29:53 GMT; no_expires=nope; Domain="
cookies = HTTP::Cookie.parse url, cookie_str
assert_equal 13, cookies.length
name = cookies.find { |c| c.name == 'name' }
assert_equal "Aaron", name.value
assert_equal "/", name.path
assert_equal Time.at(1320539391), name.expires
a_path = cookies.find { |c| c.name == 'a_path' }
assert_equal "some_path", a_path.value
assert_equal "/some_path", a_path.path
assert_equal Time.at(1320539391), a_path.expires
no_expires = cookies.find { |c| c.name == 'no_expires' }
assert_equal "nope", no_expires.value
assert_equal "/", no_expires.path
assert_nil no_expires.expires
no_path_cookies = cookies.select { |c| c.value == 'no_path' }
assert_equal 3, no_path_cookies.size
no_path_cookies.each { |c|
assert_equal "/", c.path, c.name
assert_equal Time.at(1320539392), c.expires, c.name
}
no_domain_cookies = cookies.select { |c| c.value == 'no_domain' }
assert_equal 3, no_domain_cookies.size
no_domain_cookies.each { |c|
assert !c.for_domain?, c.name
assert_equal c.domain, url.host, c.name
assert_equal Time.at(1320539393), c.expires, c.name
}
assert cookies.find { |c| c.name == 'expired' }
end
def test_parse_valid_cookie
url = URI.parse('http://rubyforge.org/')
cookie_params = {}
cookie_params['expires'] = 'expires=Sun, 27-Sep-2037 00:00:00 GMT'
cookie_params['path'] = 'path=/'
cookie_params['domain'] = 'domain=.rubyforge.org'
cookie_params['httponly'] = 'HttpOnly'
cookie_value = '12345%7D=ASDFWEE345%3DASda'
expires = Time.parse('Sun, 27-Sep-2037 00:00:00 GMT')
cookie_params.keys.combine.each do |c|
cookie_text = "#{cookie_value}; "
c.each_with_index do |key, idx|
if idx == (c.length - 1)
cookie_text << "#{cookie_params[key]}"
else
cookie_text << "#{cookie_params[key]}; "
end
end
cookie = nil
HTTP::Cookie.parse(url, cookie_text) { |p_cookie| cookie = p_cookie }
assert_equal('12345%7D=ASDFWEE345%3DASda', cookie.to_s)
assert_equal('/', cookie.path)
# if expires was set, make sure we parsed it
if c.find { |k| k == 'expires' }
assert_equal(expires, cookie.expires)
else
assert_nil(cookie.expires)
end
end
end
def test_parse_valid_cookie_empty_value
url = URI.parse('http://rubyforge.org/')
cookie_params = {}
cookie_params['expires'] = 'expires=Sun, 27-Sep-2037 00:00:00 GMT'
cookie_params['path'] = 'path=/'
cookie_params['domain'] = 'domain=.rubyforge.org'
cookie_params['httponly'] = 'HttpOnly'
cookie_value = '12345%7D='
expires = Time.parse('Sun, 27-Sep-2037 00:00:00 GMT')
cookie_params.keys.combine.each do |c|
cookie_text = "#{cookie_value}; "
c.each_with_index do |key, idx|
if idx == (c.length - 1)
cookie_text << "#{cookie_params[key]}"
else
cookie_text << "#{cookie_params[key]}; "
end
end
cookie = nil
HTTP::Cookie.parse(url, cookie_text) { |p_cookie| cookie = p_cookie }
assert_equal('12345%7D=', cookie.to_s)
assert_equal('', cookie.value)
assert_equal('/', cookie.path)
# if expires was set, make sure we parsed it
if c.find { |k| k == 'expires' }
assert_equal(expires, cookie.expires)
else
assert_nil(cookie.expires)
end
end
end
# If no path was given, use the one from the URL
def test_cookie_using_url_path
url = URI.parse('http://rubyforge.org/login.php')
cookie_params = {}
cookie_params['expires'] = 'expires=Sun, 27-Sep-2037 00:00:00 GMT'
cookie_params['path'] = 'path=/'
cookie_params['domain'] = 'domain=.rubyforge.org'
cookie_params['httponly'] = 'HttpOnly'
cookie_value = '12345%7D=ASDFWEE345%3DASda'
expires = Time.parse('Sun, 27-Sep-2037 00:00:00 GMT')
cookie_params.keys.combine.each do |c|
next if c.find { |k| k == 'path' }
cookie_text = "#{cookie_value}; "
c.each_with_index do |key, idx|
if idx == (c.length - 1)
cookie_text << "#{cookie_params[key]}"
else
cookie_text << "#{cookie_params[key]}; "
end
end
cookie = nil
HTTP::Cookie.parse(url, cookie_text) { |p_cookie| cookie = p_cookie }
assert_equal('12345%7D=ASDFWEE345%3DASda', cookie.to_s)
assert_equal('/', cookie.path)
# if expires was set, make sure we parsed it
if c.find { |k| k == 'expires' }
assert_equal(expires, cookie.expires)
else
assert_nil(cookie.expires)
end
end
end
# Test using secure cookies
def test_cookie_with_secure
url = URI.parse('http://rubyforge.org/')
cookie_params = {}
cookie_params['expires'] = 'expires=Sun, 27-Sep-2037 00:00:00 GMT'
cookie_params['path'] = 'path=/'
cookie_params['domain'] = 'domain=.rubyforge.org'
cookie_params['secure'] = 'secure'
cookie_value = '12345%7D=ASDFWEE345%3DASda'
expires = Time.parse('Sun, 27-Sep-2037 00:00:00 GMT')
cookie_params.keys.combine.each do |c|
next unless c.find { |k| k == 'secure' }
cookie_text = "#{cookie_value}; "
c.each_with_index do |key, idx|
if idx == (c.length - 1)
cookie_text << "#{cookie_params[key]}"
else
cookie_text << "#{cookie_params[key]}; "
end
end
cookie = nil
HTTP::Cookie.parse(url, cookie_text) { |p_cookie| cookie = p_cookie }
assert_equal('12345%7D=ASDFWEE345%3DASda', cookie.to_s)
assert_equal('/', cookie.path)
assert_equal(true, cookie.secure)
# if expires was set, make sure we parsed it
if c.find { |k| k == 'expires' }
assert_equal(expires, cookie.expires)
else
assert_nil(cookie.expires)
end
end
end
def test_parse_cookie_no_spaces
url = URI.parse('http://rubyforge.org/')
cookie_params = {}
cookie_params['expires'] = 'expires=Sun, 27-Sep-2037 00:00:00 GMT'
cookie_params['path'] = 'path=/'
cookie_params['domain'] = 'domain=.rubyforge.org'
cookie_params['httponly'] = 'HttpOnly'
cookie_value = '12345%7D=ASDFWEE345%3DASda'
expires = Time.parse('Sun, 27-Sep-2037 00:00:00 GMT')
cookie_params.keys.combine.each do |c|
cookie_text = "#{cookie_value};"
c.each_with_index do |key, idx|
if idx == (c.length - 1)
cookie_text << "#{cookie_params[key]}"
else
cookie_text << "#{cookie_params[key]};"
end
end
cookie = nil
HTTP::Cookie.parse(url, cookie_text) { |p_cookie| cookie = p_cookie }
assert_equal('12345%7D=ASDFWEE345%3DASda', cookie.to_s)
assert_equal('/', cookie.path)
# if expires was set, make sure we parsed it
if c.find { |k| k == 'expires' }
assert_equal(expires, cookie.expires)
else
assert_nil(cookie.expires)
end
end
end
def test_new
cookie = HTTP::Cookie.new('key', 'value')
assert_equal 'key', cookie.name
assert_equal 'value', cookie.value
assert_equal nil, cookie.expires
# Minimum unit for the expires attribute is second
expires = Time.at((Time.now + 3600).to_i)
cookie = HTTP::Cookie.new('key', 'value', :expires => expires.dup)
assert_equal 'key', cookie.name
assert_equal 'value', cookie.value
assert_equal expires, cookie.expires
cookie = HTTP::Cookie.new(:value => 'value', :name => 'key', :expires => expires.dup)
assert_equal 'key', cookie.name
assert_equal 'value', cookie.value
assert_equal expires, cookie.expires
end
def test_domain=
url = URI.parse('http://host.dom.example.com:8080/')
cookie_str = 'a=b; domain=Example.Com'
cookie = HTTP::Cookie.parse(url, cookie_str).first
assert 'example.com', cookie.domain
cookie.domain = DomainName(url.host)
assert 'host.dom.example.com', cookie.domain
cookie.domain = 'Dom.example.com'
assert 'dom.example.com', cookie.domain
cookie.domain = Object.new.tap { |o|
def o.to_str
'Example.com'
end
}
assert 'example.com', cookie.domain
end
end

View file

@ -0,0 +1,518 @@
require File.expand_path('helper', File.dirname(__FILE__))
class TestMechanizeCookieJar < Test::Unit::TestCase
#class TestMechanizeCookieJar < MiniTest::Unit::TestCase
def setup
@jar = HTTP::CookieJar.new
end
def in_tmpdir
Dir.mktmpdir do |dir|
Dir.chdir dir do
yield
end
end
end
def cookie_values(options = {})
{
:name => 'Foo',
:value => 'Bar',
:path => '/',
:expires => Time.now + (10 * 86400),
:for_domain => true,
:domain => 'rubyforge.org'
}.merge(options)
end
def test_two_cookies_same_domain_and_name_different_paths
url = URI 'http://rubyforge.org/'
cookie = HTTP::Cookie.new(cookie_values)
@jar.add(url, cookie)
@jar.add(url, HTTP::Cookie.new(cookie_values(:path => '/onetwo')))
assert_equal(1, @jar.cookies(url).length)
assert_equal 2, @jar.cookies(URI('http://rubyforge.org/onetwo')).length
end
def test_domain_case
url = URI 'http://rubyforge.org/'
# Add one cookie with an expiration date in the future
cookie = HTTP::Cookie.new(cookie_values)
@jar.add(url, cookie)
assert_equal(1, @jar.cookies(url).length)
@jar.add(url, HTTP::Cookie.new(
cookie_values(:domain => 'RuByForge.Org', :name => 'aaron')))
assert_equal(2, @jar.cookies(url).length)
url2 = URI 'http://RuByFoRgE.oRg/'
assert_equal(2, @jar.cookies(url2).length)
end
def test_host_only
url = URI.parse('http://rubyforge.org/')
@jar.add(url, HTTP::Cookie.new(
cookie_values(:domain => 'rubyforge.org', :for_domain => false)))
assert_equal(1, @jar.cookies(url).length)
assert_equal(1, @jar.cookies(URI('http://RubyForge.org/')).length)
assert_equal(1, @jar.cookies(URI('https://RubyForge.org/')).length)
assert_equal(0, @jar.cookies(URI('http://www.rubyforge.org/')).length)
end
def test_empty_value
values = cookie_values(:value => "")
url = URI 'http://rubyforge.org/'
# Add one cookie with an expiration date in the future
cookie = HTTP::Cookie.new(values)
@jar.add(url, cookie)
assert_equal(1, @jar.cookies(url).length)
@jar.add url, HTTP::Cookie.new(values.merge(:domain => 'RuByForge.Org',
:name => 'aaron'))
assert_equal(2, @jar.cookies(url).length)
url2 = URI 'http://RuByFoRgE.oRg/'
assert_equal(2, @jar.cookies(url2).length)
end
def test_add_future_cookies
url = URI 'http://rubyforge.org/'
# Add one cookie with an expiration date in the future
cookie = HTTP::Cookie.new(cookie_values)
@jar.add(url, cookie)
assert_equal(1, @jar.cookies(url).length)
# Add the same cookie, and we should still only have one
@jar.add(url, HTTP::Cookie.new(cookie_values))
assert_equal(1, @jar.cookies(url).length)
# Make sure we can get the cookie from different paths
assert_equal(1, @jar.cookies(URI('http://rubyforge.org/login')).length)
# Make sure we can't get the cookie from different domains
assert_equal(0, @jar.cookies(URI('http://google.com/')).length)
end
def test_add_multiple_cookies
url = URI 'http://rubyforge.org/'
# Add one cookie with an expiration date in the future
cookie = HTTP::Cookie.new(cookie_values)
@jar.add(url, cookie)
assert_equal(1, @jar.cookies(url).length)
# Add the same cookie, and we should still only have one
@jar.add(url, HTTP::Cookie.new(cookie_values(:name => 'Baz')))
assert_equal(2, @jar.cookies(url).length)
# Make sure we can get the cookie from different paths
assert_equal(2, @jar.cookies(URI('http://rubyforge.org/login')).length)
# Make sure we can't get the cookie from different domains
assert_equal(0, @jar.cookies(URI('http://google.com/')).length)
end
def test_add_rejects_cookies_that_do_not_contain_an_embedded_dot
url = URI 'http://rubyforge.org/'
tld_cookie = HTTP::Cookie.new(cookie_values(:domain => '.org'))
@jar.add(url, tld_cookie)
single_dot_cookie = HTTP::Cookie.new(cookie_values(:domain => '.'))
@jar.add(url, single_dot_cookie)
assert_equal(0, @jar.cookies(url).length)
end
def test_fall_back_rules_for_local_domains
url = URI 'http://www.example.local'
tld_cookie = HTTP::Cookie.new(cookie_values(:domain => '.local'))
@jar.add(url, tld_cookie)
assert_equal(0, @jar.cookies(url).length)
sld_cookie = HTTP::Cookie.new(cookie_values(:domain => '.example.local'))
@jar.add(url, sld_cookie)
assert_equal(1, @jar.cookies(url).length)
end
def test_add_makes_exception_for_localhost
url = URI 'http://localhost'
tld_cookie = HTTP::Cookie.new(cookie_values(:domain => 'localhost'))
@jar.add(url, tld_cookie)
assert_equal(1, @jar.cookies(url).length)
end
def test_add_cookie_for_the_parent_domain
url = URI 'http://x.foo.com'
cookie = HTTP::Cookie.new(cookie_values(:domain => '.foo.com'))
@jar.add(url, cookie)
assert_equal(1, @jar.cookies(url).length)
end
def test_add_does_not_reject_cookies_from_a_nested_subdomain
url = URI 'http://y.x.foo.com'
cookie = HTTP::Cookie.new(cookie_values(:domain => '.foo.com'))
@jar.add(url, cookie)
assert_equal(1, @jar.cookies(url).length)
end
def test_cookie_without_leading_dot_does_not_cause_substring_match
url = URI 'http://arubyforge.org/'
cookie = HTTP::Cookie.new(cookie_values(:domain => 'rubyforge.org'))
@jar.add(url, cookie)
assert_equal(0, @jar.cookies(url).length)
end
def test_cookie_without_leading_dot_matches_subdomains
url = URI 'http://admin.rubyforge.org/'
cookie = HTTP::Cookie.new(cookie_values(:domain => 'rubyforge.org'))
@jar.add(url, cookie)
assert_equal(1, @jar.cookies(url).length)
end
def test_cookies_with_leading_dot_match_subdomains
url = URI 'http://admin.rubyforge.org/'
@jar.add(url, HTTP::Cookie.new(cookie_values(:domain => '.rubyforge.org')))
assert_equal(1, @jar.cookies(url).length)
end
def test_cookies_with_leading_dot_match_parent_domains
url = URI 'http://rubyforge.org/'
@jar.add(url, HTTP::Cookie.new(cookie_values(:domain => '.rubyforge.org')))
assert_equal(1, @jar.cookies(url).length)
end
def test_cookies_with_leading_dot_match_parent_domains_exactly
url = URI 'http://arubyforge.org/'
@jar.add(url, HTTP::Cookie.new(cookie_values(:domain => '.rubyforge.org')))
assert_equal(0, @jar.cookies(url).length)
end
def test_cookie_for_ipv4_address_matches_the_exact_ipaddress
url = URI 'http://192.168.0.1/'
cookie = HTTP::Cookie.new(cookie_values(:domain => '192.168.0.1'))
@jar.add(url, cookie)
assert_equal(1, @jar.cookies(url).length)
end
def test_cookie_for_ipv4_address_does_not_cause_subdomain_match
url = URI 'http://192.168.0.1/'
cookie = HTTP::Cookie.new(cookie_values(:domain => '.0.1'))
@jar.add(url, cookie)
assert_equal(0, @jar.cookies(url).length)
end
def test_cookie_for_ipv6_address_matches_the_exact_ipaddress
url = URI 'http://[fe80::0123:4567:89ab:cdef]/'
cookie = HTTP::Cookie.new(cookie_values(:domain => '[fe80::0123:4567:89ab:cdef]'))
@jar.add(url, cookie)
assert_equal(1, @jar.cookies(url).length)
end
def test_cookies_dot
url = URI 'http://www.host.example/'
@jar.add(url,
HTTP::Cookie.new(cookie_values(:domain => 'www.host.example')))
url = URI 'http://wwwxhost.example/'
assert_equal(0, @jar.cookies(url).length)
end
def test_clear_bang
url = URI 'http://rubyforge.org/'
# Add one cookie with an expiration date in the future
cookie = HTTP::Cookie.new(cookie_values)
@jar.add(url, cookie)
@jar.add(url, HTTP::Cookie.new(cookie_values(:name => 'Baz')))
assert_equal(2, @jar.cookies(url).length)
@jar.clear!
assert_equal(0, @jar.cookies(url).length)
end
def test_save_cookies_yaml
url = URI 'http://rubyforge.org/'
# Add one cookie with an expiration date in the future
cookie = HTTP::Cookie.new(cookie_values)
s_cookie = HTTP::Cookie.new(cookie_values(:name => 'Bar',
:expires => nil,
:session => true))
@jar.add(url, cookie)
@jar.add(url, s_cookie)
@jar.add(url, HTTP::Cookie.new(cookie_values(:name => 'Baz')))
assert_equal(3, @jar.cookies(url).length)
in_tmpdir do
value = @jar.save_as("cookies.yml")
assert_same @jar, value
jar = HTTP::CookieJar.new
jar.load("cookies.yml")
assert_equal(2, jar.cookies(url).length)
end
assert_equal(3, @jar.cookies(url).length)
end
def test_save_session_cookies_yaml
url = URI 'http://rubyforge.org/'
# Add one cookie with an expiration date in the future
cookie = HTTP::Cookie.new(cookie_values)
s_cookie = HTTP::Cookie.new(cookie_values(:name => 'Bar',
:expires => nil,
:session => true))
@jar.add(url, cookie)
@jar.add(url, s_cookie)
@jar.add(url, HTTP::Cookie.new(cookie_values(:name => 'Baz')))
assert_equal(3, @jar.cookies(url).length)
in_tmpdir do
@jar.save_as("cookies.yml", :format => :yaml, :session => true)
jar = HTTP::CookieJar.new
jar.load("cookies.yml")
assert_equal(3, jar.cookies(url).length)
end
assert_equal(3, @jar.cookies(url).length)
end
def test_save_cookies_cookiestxt
url = URI 'http://rubyforge.org/'
# Add one cookie with an expiration date in the future
cookie = HTTP::Cookie.new(cookie_values)
s_cookie = HTTP::Cookie.new(cookie_values(:name => 'Bar',
:expires => nil,
:session => true))
@jar.add(url, cookie)
@jar.add(url, s_cookie)
@jar.add(url, HTTP::Cookie.new(cookie_values(:name => 'Baz')))
assert_equal(3, @jar.cookies(url).length)
in_tmpdir do
@jar.save_as("cookies.txt", :cookiestxt)
jar = HTTP::CookieJar.new
jar.load("cookies.txt", :cookiestxt) # HACK test the format
assert_equal(2, jar.cookies(url).length)
end
assert_equal(3, @jar.cookies(url).length)
end
def test_expire_cookies
url = URI 'http://rubyforge.org/'
# Add one cookie with an expiration date in the future
cookie = HTTP::Cookie.new(cookie_values)
@jar.add(url, cookie)
assert_equal(1, @jar.cookies(url).length)
# Add a second cookie
@jar.add(url, HTTP::Cookie.new(cookie_values(:name => 'Baz')))
assert_equal(2, @jar.cookies(url).length)
# Make sure we can get the cookie from different paths
assert_equal(2, @jar.cookies(URI('http://rubyforge.org/login')).length)
# Expire the first cookie
@jar.add(url, HTTP::Cookie.new(
cookie_values(:expires => Time.now - (10 * 86400))))
assert_equal(1, @jar.cookies(url).length)
# Expire the second cookie
@jar.add(url, HTTP::Cookie.new(
cookie_values( :name => 'Baz', :expires => Time.now - (10 * 86400))))
assert_equal(0, @jar.cookies(url).length)
end
def test_session_cookies
values = cookie_values(:expires => nil)
url = URI 'http://rubyforge.org/'
# Add one cookie with an expiration date in the future
cookie = HTTP::Cookie.new(values)
@jar.add(url, cookie)
assert_equal(1, @jar.cookies(url).length)
# Add a second cookie
@jar.add(url, HTTP::Cookie.new(values.merge(:name => 'Baz')))
assert_equal(2, @jar.cookies(url).length)
# Make sure we can get the cookie from different paths
assert_equal(2, @jar.cookies(URI('http://rubyforge.org/login')).length)
# Expire the first cookie
@jar.add(url, HTTP::Cookie.new(values.merge(:expires => Time.now - (10 * 86400))))
assert_equal(1, @jar.cookies(url).length)
# Expire the second cookie
@jar.add(url, HTTP::Cookie.new(
values.merge(:name => 'Baz', :expires => Time.now - (10 * 86400))))
assert_equal(0, @jar.cookies(url).length)
# When given a URI with a blank path, CookieJar#cookies should return
# cookies with the path '/':
url = URI 'http://rubyforge.org'
assert_equal '', url.path
assert_equal(0, @jar.cookies(url).length)
# Now add a cookie with the path set to '/':
@jar.add(url, HTTP::Cookie.new(values.merge( :name => 'has_root_path',
:path => '/')))
assert_equal(1, @jar.cookies(url).length)
end
def test_paths
values = cookie_values(:path => "/login", :expires => nil)
url = URI 'http://rubyforge.org/login'
# Add one cookie with an expiration date in the future
cookie = HTTP::Cookie.new(values)
@jar.add(url, cookie)
assert_equal(1, @jar.cookies(url).length)
# Add a second cookie
@jar.add(url, HTTP::Cookie.new(values.merge( :name => 'Baz' )))
assert_equal(2, @jar.cookies(url).length)
# Make sure we don't get the cookie in a different path
assert_equal(0, @jar.cookies(URI('http://rubyforge.org/hello')).length)
assert_equal(0, @jar.cookies(URI('http://rubyforge.org/')).length)
# Expire the first cookie
@jar.add(url, HTTP::Cookie.new(values.merge( :expires => Time.now - (10 * 86400))))
assert_equal(1, @jar.cookies(url).length)
# Expire the second cookie
@jar.add(url, HTTP::Cookie.new(values.merge( :name => 'Baz',
:expires => Time.now - (10 * 86400))))
assert_equal(0, @jar.cookies(url).length)
end
def test_save_and_read_cookiestxt
url = URI 'http://rubyforge.org/'
# Add one cookie with an expiration date in the future
cookie = HTTP::Cookie.new(cookie_values)
@jar.add(url, cookie)
@jar.add(url, HTTP::Cookie.new(cookie_values(:name => 'Baz')))
assert_equal(2, @jar.cookies(url).length)
in_tmpdir do
@jar.save_as("cookies.txt", :cookiestxt)
@jar.clear!
@jar.load("cookies.txt", :cookiestxt)
end
assert_equal(2, @jar.cookies(url).length)
end
def test_save_and_read_cookiestxt_with_session_cookies
url = URI 'http://rubyforge.org/'
@jar.add(url, HTTP::Cookie.new(cookie_values(:expires => nil)))
in_tmpdir do
@jar.save_as("cookies.txt", :cookiestxt)
@jar.clear!
@jar.load("cookies.txt", :cookiestxt)
end
assert_equal(1, @jar.cookies(url).length)
assert_nil @jar.cookies(url).first.expires
end
def test_save_and_read_expired_cookies
url = URI 'http://rubyforge.org/'
@jar.jar['rubyforge.org'] = {}
@jar.add url, HTTP::Cookie.new(cookie_values)
# HACK no asertion
end
def test_ssl_cookies
# thanks to michal "ocher" ochman for reporting the bug responsible for this test.
values = cookie_values(:expires => nil)
values_ssl = values.merge(:name => 'Baz', :domain => "#{values[:domain]}:443")
url = URI 'https://rubyforge.org/login'
cookie = HTTP::Cookie.new(values)
@jar.add(url, cookie)
assert_equal(1, @jar.cookies(url).length, "did not handle SSL cookie")
cookie = HTTP::Cookie.new(values_ssl)
@jar.add(url, cookie)
assert_equal(2, @jar.cookies(url).length, "did not handle SSL cookie with :443")
end
def test_secure_cookie
nurl = URI 'http://rubyforge.org/login'
surl = URI 'https://rubyforge.org/login'
ncookie = HTTP::Cookie.new(cookie_values(:name => 'Foo1'))
scookie = HTTP::Cookie.new(cookie_values(:name => 'Foo2', :secure => true))
@jar.add(nurl, ncookie)
@jar.add(nurl, scookie)
@jar.add(surl, ncookie)
@jar.add(surl, scookie)
assert_equal('Foo1', @jar.cookies(nurl).map { |c| c.name }.sort.join(' ') )
assert_equal('Foo1 Foo2', @jar.cookies(surl).map { |c| c.name }.sort.join(' ') )
end
end