/
cookie_jar.rb
206 lines (187 loc) · 5.83 KB
/
cookie_jar.rb
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
require 'hanami/utils/hash'
module Hanami
module Action
# A set of HTTP Cookies
#
# It acts as an Hash
#
# @since 0.1.0
#
# @see Hanami::Action::Cookies#cookies
class CookieJar
# The key that returns raw cookies from the Rack env
#
# @since 0.1.0
# @api private
HTTP_HEADER = 'HTTP_COOKIE'.freeze
# The key used by Rack to set the session cookie
#
# We let CookieJar to NOT take care of this cookie, but it leaves the
# responsibility to the Rack middleware that handle sessions.
#
# This prevents <tt>Set-Cookie</tt> to be sent twice.
#
# @since 0.5.1
# @api private
#
# @see https://github.com/hanami/controller/issues/138
RACK_SESSION_KEY = :'rack.session'
# The key used by Rack to set the cookies as an Hash in the env
#
# @since 0.1.0
# @api private
COOKIE_HASH_KEY = 'rack.request.cookie_hash'.freeze
# The key used by Rack to set the cookies as a String in the env
#
# @since 0.1.0
# @api private
COOKIE_STRING_KEY = 'rack.request.cookie_string'.freeze
# @since 0.4.5
# @api private
COOKIE_SEPARATOR = ';,'.freeze
# Initialize the CookieJar
#
# @param env [Hash] a raw Rack env
# @param headers [Hash] the response headers
#
# @return [CookieJar]
#
# @since 0.1.0
def initialize(env, headers, default_options)
@_headers = headers
@cookies = Utils::Hash.new(extract(env)).symbolize!
@default_options = default_options
end
# Finalize itself, by setting the proper headers to add and remove
# cookies, before the response is returned to the webserver.
#
# @return [void]
#
# @since 0.1.0
#
# @see Hanami::Action::Cookies#finish
def finish
@cookies.delete(RACK_SESSION_KEY)
@cookies.each do |k,v|
next unless changed?(k)
v.nil? ? delete_cookie(k) : set_cookie(k, _merge_default_values(v))
end if changed?
end
# Returns the object associated with the given key
#
# @param key [Symbol] the key
#
# @return [Object,nil] return the associated object, if found
#
# @since 0.2.0
def [](key)
@cookies[key]
end
# Associate the given value with the given key and store them
#
# @param key [Symbol] the key
# @param value [#to_s,Hash] value that can be serialized as a string or
# expressed as a Hash
# @option value [String] :domain - The domain
# @option value [String] :path - The path
# @option value [Integer] :max_age - Duration expressed in seconds
# @option value [Time] :expires - Expiration time
# @option value [TrueClass,FalseClass] :secure - Restrict cookie to secure
# connections
# @option value [TrueClass,FalseClass] :httponly - Restrict JavaScript
# access
#
# @return [void]
#
# @since 0.2.0
#
# @see http://en.wikipedia.org/wiki/HTTP_cookie
def []=(key, value)
changes << key
@cookies[key] = value
end
private
# Keep track of changed keys
#
# @since 0.7.0
# @api private
def changes
@changes ||= Set.new
end
# Check if the entire set of cookies has changed within the current request.
# If <tt>key</tt> is given, it checks the associated cookie has changed.
#
# @since 0.7.0
# @api private
def changed?(key = nil)
if key.nil?
changes.any?
else
changes.include?(key)
end
end
# Merge default cookies options with values provided by user
#
# Cookies values provided by user are respected
#
# @since 0.4.0
# @api private
def _merge_default_values(value)
cookies_options = if value.is_a? Hash
value.merge! _add_expires_option(value)
else
{ value: value }
end
@default_options.merge cookies_options
end
# Add expires option to cookies if :max_age presents
#
# @since 0.4.3
# @api private
def _add_expires_option(value)
if value.has_key?(:max_age) && !value.has_key?(:expires)
{ expires: (Time.now + value[:max_age]) }
else
{}
end
end
# Extract the cookies from the raw Rack env.
#
# This implementation is borrowed from Rack::Request#cookies.
#
# @since 0.1.0
# @api private
def extract(env)
hash = env[COOKIE_HASH_KEY] ||= {}
string = env[HTTP_HEADER]
return hash if string == env[COOKIE_STRING_KEY]
# TODO Next Rack 1.7.x ?? version will have ::Rack::Utils.parse_cookies
# We can then replace the following lines.
hash.clear
# According to RFC 2109:
# If multiple cookies satisfy the criteria above, they are ordered in
# the Cookie header such that those with more specific Path attributes
# precede those with less specific. Ordering with respect to other
# attributes (e.g., Domain) is unspecified.
cookies = ::Rack::Utils.parse_query(string, COOKIE_SEPARATOR) { |s| ::Rack::Utils.unescape(s) rescue s }
cookies.each { |k,v| hash[k] = Array === v ? v.first : v }
env[COOKIE_STRING_KEY] = string
hash
end
# Set a cookie in the headers
#
# @since 0.1.0
# @api private
def set_cookie(key, value)
::Rack::Utils.set_cookie_header!(@_headers, key, value)
end
# Remove a cookie from the headers
#
# @since 0.1.0
# @api private
def delete_cookie(key)
::Rack::Utils.delete_cookie_header!(@_headers, key, {})
end
end
end
end