rack 0.9.1 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of rack might be problematic. Click here for more details.
- data/COPYING +1 -1
- data/RDOX +115 -16
- data/README +54 -7
- data/Rakefile +61 -85
- data/SPEC +50 -17
- data/bin/rackup +9 -5
- data/example/protectedlobster.ru +1 -1
- data/lib/rack.rb +7 -3
- data/lib/rack/auth/abstract/handler.rb +13 -4
- data/lib/rack/auth/digest/md5.rb +1 -1
- data/lib/rack/auth/digest/request.rb +2 -2
- data/lib/rack/auth/openid.rb +344 -302
- data/lib/rack/builder.rb +1 -5
- data/lib/rack/chunked.rb +49 -0
- data/lib/rack/conditionalget.rb +4 -0
- data/lib/rack/content_length.rb +7 -3
- data/lib/rack/content_type.rb +23 -0
- data/lib/rack/deflater.rb +83 -74
- data/lib/rack/directory.rb +5 -2
- data/lib/rack/file.rb +4 -1
- data/lib/rack/handler.rb +22 -1
- data/lib/rack/handler/cgi.rb +7 -3
- data/lib/rack/handler/fastcgi.rb +26 -24
- data/lib/rack/handler/lsws.rb +7 -4
- data/lib/rack/handler/mongrel.rb +5 -3
- data/lib/rack/handler/scgi.rb +5 -3
- data/lib/rack/handler/thin.rb +3 -0
- data/lib/rack/handler/webrick.rb +11 -5
- data/lib/rack/lint.rb +138 -66
- data/lib/rack/lock.rb +16 -0
- data/lib/rack/mime.rb +4 -4
- data/lib/rack/mock.rb +3 -3
- data/lib/rack/reloader.rb +88 -46
- data/lib/rack/request.rb +46 -10
- data/lib/rack/response.rb +15 -3
- data/lib/rack/rewindable_input.rb +98 -0
- data/lib/rack/session/abstract/id.rb +71 -82
- data/lib/rack/session/cookie.rb +2 -0
- data/lib/rack/session/memcache.rb +59 -47
- data/lib/rack/session/pool.rb +56 -29
- data/lib/rack/showexceptions.rb +2 -1
- data/lib/rack/showstatus.rb +1 -1
- data/lib/rack/urlmap.rb +12 -5
- data/lib/rack/utils.rb +115 -65
- data/rack.gemspec +54 -0
- data/test/multipart/binary +0 -0
- data/test/multipart/empty +10 -0
- data/test/multipart/ie +6 -0
- data/test/multipart/nested +10 -0
- data/test/multipart/none +9 -0
- data/test/multipart/text +10 -0
- data/test/spec_rack_auth_basic.rb +5 -1
- data/test/spec_rack_auth_digest.rb +93 -36
- data/test/spec_rack_auth_openid.rb +47 -100
- data/test/spec_rack_builder.rb +2 -2
- data/test/spec_rack_chunked.rb +62 -0
- data/test/spec_rack_conditionalget.rb +7 -7
- data/test/spec_rack_content_type.rb +30 -0
- data/test/spec_rack_deflater.rb +36 -14
- data/test/spec_rack_directory.rb +1 -1
- data/test/spec_rack_file.rb +11 -0
- data/test/spec_rack_handler.rb +21 -2
- data/test/spec_rack_lint.rb +163 -44
- data/test/spec_rack_lock.rb +38 -0
- data/test/spec_rack_mock.rb +6 -1
- data/test/spec_rack_request.rb +81 -12
- data/test/spec_rack_response.rb +46 -2
- data/test/spec_rack_rewindable_input.rb +118 -0
- data/test/spec_rack_session_memcache.rb +170 -62
- data/test/spec_rack_session_pool.rb +129 -41
- data/test/spec_rack_static.rb +2 -2
- data/test/spec_rack_thin.rb +3 -2
- data/test/spec_rack_urlmap.rb +10 -0
- data/test/spec_rack_utils.rb +214 -49
- data/test/spec_rack_webrick.rb +7 -0
- data/test/unregistered_handler/rack/handler/unregistered.rb +7 -0
- data/test/unregistered_handler/rack/handler/unregistered_long_one.rb +7 -0
- metadata +95 -6
- data/AUTHORS +0 -8
@@ -1,54 +1,67 @@
|
|
1
1
|
# AUTHOR: blink <blinketje@gmail.com>; blink#ruby-lang@irc.freenode.net
|
2
2
|
# bugrep: Andreas Zehnder
|
3
3
|
|
4
|
-
require 'rack/utils'
|
5
4
|
require 'time'
|
5
|
+
require 'rack/request'
|
6
|
+
require 'rack/response'
|
6
7
|
|
7
8
|
module Rack
|
9
|
+
|
8
10
|
module Session
|
11
|
+
|
9
12
|
module Abstract
|
13
|
+
|
10
14
|
# ID sets up a basic framework for implementing an id based sessioning
|
11
15
|
# service. Cookies sent to the client for maintaining sessions will only
|
12
|
-
# contain an id reference. Only #get_session and #set_session
|
13
|
-
#
|
16
|
+
# contain an id reference. Only #get_session and #set_session are
|
17
|
+
# required to be overwritten.
|
14
18
|
#
|
15
19
|
# All parameters are optional.
|
16
20
|
# * :key determines the name of the cookie, by default it is
|
17
21
|
# 'rack.session'
|
18
|
-
# * :domain and :
|
19
|
-
#
|
20
|
-
# * :
|
21
|
-
#
|
22
|
-
#
|
22
|
+
# * :path, :domain, :expire_after, :secure, and :httponly set the related
|
23
|
+
# cookie options as by Rack::Response#add_cookie
|
24
|
+
# * :defer will not set a cookie in the response.
|
25
|
+
# * :renew (implementation dependent) will prompt the generation of a new
|
26
|
+
# session id, and migration of data to be referenced at the new id. If
|
27
|
+
# :defer is set, it will be overridden and the cookie will be set.
|
28
|
+
# * :sidbits sets the number of bits in length that a generated session
|
29
|
+
# id will be.
|
30
|
+
#
|
31
|
+
# These options can be set on a per request basis, at the location of
|
32
|
+
# env['rack.session.options']. Additionally the id of the session can be
|
33
|
+
# found within the options hash at the key :id. It is highly not
|
34
|
+
# recommended to change its value.
|
35
|
+
#
|
36
|
+
# Is Rack::Utils::Context compatible.
|
37
|
+
|
23
38
|
class ID
|
24
|
-
attr_reader :key
|
25
39
|
DEFAULT_OPTIONS = {
|
26
|
-
:key => 'rack.session',
|
27
40
|
:path => '/',
|
28
41
|
:domain => nil,
|
29
42
|
:expire_after => nil,
|
30
43
|
:secure => false,
|
31
44
|
:httponly => true,
|
45
|
+
:defer => false,
|
46
|
+
:renew => false,
|
32
47
|
:sidbits => 128
|
33
48
|
}
|
34
49
|
|
50
|
+
attr_reader :key, :default_options
|
35
51
|
def initialize(app, options={})
|
52
|
+
@app = app
|
53
|
+
@key = options[:key] || "rack.session"
|
36
54
|
@default_options = self.class::DEFAULT_OPTIONS.merge(options)
|
37
|
-
@key = @default_options[:key]
|
38
|
-
@default_context = context app
|
39
55
|
end
|
40
56
|
|
41
57
|
def call(env)
|
42
|
-
|
58
|
+
context(env)
|
43
59
|
end
|
44
60
|
|
45
|
-
def context(app)
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
commit_session env, response
|
50
|
-
response
|
51
|
-
end
|
61
|
+
def context(env, app=@app)
|
62
|
+
load_session(env)
|
63
|
+
status, headers, body = app.call(env)
|
64
|
+
commit_session(env, status, headers, body)
|
52
65
|
end
|
53
66
|
|
54
67
|
private
|
@@ -56,6 +69,7 @@ module Rack
|
|
56
69
|
# Generate a new session id using Ruby #rand. The size of the
|
57
70
|
# session id is controlled by the :sidbits option.
|
58
71
|
# Monkey patch this to use custom methods for session id generation.
|
72
|
+
|
59
73
|
def generate_sid
|
60
74
|
"%0#{@default_options[:sidbits] / 4}x" %
|
61
75
|
rand(2**@default_options[:sidbits] - 1)
|
@@ -65,87 +79,62 @@ module Rack
|
|
65
79
|
# environment to #get_session. It then sets the resulting session into
|
66
80
|
# 'rack.session', and places options and session metadata into
|
67
81
|
# 'rack.session.options'.
|
82
|
+
|
68
83
|
def load_session(env)
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
84
|
+
request = Rack::Request.new(env)
|
85
|
+
session_id = request.cookies[@key]
|
86
|
+
|
87
|
+
begin
|
88
|
+
session_id, session = get_session(env, session_id)
|
89
|
+
env['rack.session'] = session
|
90
|
+
rescue
|
91
|
+
env['rack.session'] = Hash.new
|
74
92
|
end
|
75
93
|
|
76
|
-
options = @default_options.
|
77
|
-
merge(
|
78
|
-
|
79
|
-
env['rack.session'] = session
|
80
|
-
env['rack.session.options'] = options
|
81
|
-
|
82
|
-
return true
|
94
|
+
env['rack.session.options'] = @default_options.
|
95
|
+
merge(:id => session_id)
|
83
96
|
end
|
84
97
|
|
85
98
|
# Acquires the session from the environment and the session id from
|
86
|
-
# the session options and passes them to #set_session.
|
87
|
-
#
|
88
|
-
|
89
|
-
unless response.is_a?(Array)
|
90
|
-
puts 'Response: '+response.inspect if $DEBUG
|
91
|
-
raise ArgumentError, 'Response is not an array.'
|
92
|
-
end
|
99
|
+
# the session options and passes them to #set_session. If successful
|
100
|
+
# and the :defer option is not true, a cookie will be added to the
|
101
|
+
# response with the session's id.
|
93
102
|
|
103
|
+
def commit_session(env, status, headers, body)
|
104
|
+
session = env['rack.session']
|
94
105
|
options = env['rack.session.options']
|
95
|
-
|
96
|
-
|
97
|
-
|
106
|
+
session_id = options[:id]
|
107
|
+
|
108
|
+
if not session_id = set_session(env, session_id, session, options)
|
109
|
+
env["rack.errors"].puts("Warning! #{self.class.name} failed to save session. Content dropped.")
|
110
|
+
[status, headers, body]
|
111
|
+
elsif options[:defer] and not options[:renew]
|
112
|
+
env["rack.errors"].puts("Defering cookie for #{session_id}") if $VERBOSE
|
113
|
+
[status, headers, body]
|
114
|
+
else
|
115
|
+
cookie = Hash.new
|
116
|
+
cookie[:value] = session_id
|
117
|
+
cookie[:expires] = Time.now + options[:expire_after] unless options[:expire_after].nil?
|
118
|
+
response = Rack::Response.new(body, status, headers)
|
119
|
+
response.set_cookie(@key, cookie.merge(options))
|
120
|
+
response.to_a
|
98
121
|
end
|
99
|
-
|
100
|
-
sid, time, z = options.values_at(:id, :at, :by)
|
101
|
-
unless self == z
|
102
|
-
warn "#{self} not managing this session."
|
103
|
-
return
|
104
|
-
end
|
105
|
-
|
106
|
-
unless env['rack.session'].is_a?(Hash)
|
107
|
-
warn 'Session: '+sid.inspect+"\n"+session.inspect if $DEBUG
|
108
|
-
raise TypeError, 'Session not a Hash'
|
109
|
-
end
|
110
|
-
|
111
|
-
unless set_session(env, sid)
|
112
|
-
warn "Session not saved." if $DEBUG
|
113
|
-
warn "#{env['rack.session'].inspect} has been lost."if $DEBUG
|
114
|
-
return false
|
115
|
-
end
|
116
|
-
|
117
|
-
cookie = Utils.escape(@key)+'='+Utils.escape(sid)
|
118
|
-
cookie<< "; domain=#{options[:domain]}" if options[:domain]
|
119
|
-
cookie<< "; path=#{options[:path]}" if options[:path]
|
120
|
-
if options[:expire_after]
|
121
|
-
expiry = time + options[:expire_after]
|
122
|
-
cookie<< "; expires=#{expiry.httpdate}"
|
123
|
-
end
|
124
|
-
cookie<< "; Secure" if options[:secure]
|
125
|
-
cookie<< "; HttpOnly" if options[:httponly]
|
126
|
-
|
127
|
-
case a = (h = response[1])['Set-Cookie']
|
128
|
-
when Array then a << cookie
|
129
|
-
when String then h['Set-Cookie'] = [a, cookie]
|
130
|
-
when nil then h['Set-Cookie'] = cookie
|
131
|
-
end
|
132
|
-
|
133
|
-
return true
|
134
122
|
end
|
135
123
|
|
136
|
-
#
|
137
|
-
#
|
124
|
+
# All thread safety and session retrival proceedures should occur here.
|
125
|
+
# Should return [session_id, session].
|
138
126
|
# If nil is provided as the session id, generation of a new valid id
|
139
127
|
# should occur within.
|
128
|
+
|
140
129
|
def get_session(env, sid)
|
141
|
-
raise '#get_session
|
130
|
+
raise '#get_session not implemented.'
|
142
131
|
end
|
143
132
|
|
144
133
|
# All thread safety and session storage proceedures should occur here.
|
145
134
|
# Should return true or false dependant on whether or not the session
|
146
135
|
# was saved or not.
|
147
|
-
def set_session(env, sid)
|
148
|
-
raise '#set_session
|
136
|
+
def set_session(env, sid, session, options)
|
137
|
+
raise '#set_session not implemented.'
|
149
138
|
end
|
150
139
|
end
|
151
140
|
end
|
data/lib/rack/session/cookie.rb
CHANGED
@@ -21,76 +21,88 @@ module Rack
|
|
21
21
|
|
22
22
|
class Memcache < Abstract::ID
|
23
23
|
attr_reader :mutex, :pool
|
24
|
-
DEFAULT_OPTIONS = Abstract::ID::DEFAULT_OPTIONS.merge
|
24
|
+
DEFAULT_OPTIONS = Abstract::ID::DEFAULT_OPTIONS.merge \
|
25
25
|
:namespace => 'rack:session',
|
26
26
|
:memcache_server => 'localhost:11211'
|
27
|
-
})
|
28
27
|
|
29
28
|
def initialize(app, options={})
|
30
29
|
super
|
31
|
-
|
32
|
-
unless @pool.servers.any?{|s|s.alive?}
|
33
|
-
raise "#{self} unable to find server during initialization."
|
34
|
-
end
|
30
|
+
|
35
31
|
@mutex = Mutex.new
|
32
|
+
@pool = MemCache.
|
33
|
+
new @default_options[:memcache_server], @default_options
|
34
|
+
raise 'No memcache servers' unless @pool.servers.any?{|s|s.alive?}
|
36
35
|
end
|
37
36
|
|
38
|
-
|
37
|
+
def generate_sid
|
38
|
+
loop do
|
39
|
+
sid = super
|
40
|
+
break sid unless @pool.get(sid, true)
|
41
|
+
end
|
42
|
+
end
|
39
43
|
|
40
44
|
def get_session(env, sid)
|
41
|
-
session =
|
42
|
-
|
45
|
+
session = @pool.get(sid) if sid
|
46
|
+
@mutex.lock if env['rack.multithread']
|
47
|
+
unless sid and session
|
48
|
+
env['rack.errors'].puts("Session '#{sid.inspect}' not found, initializing...") if $VERBOSE and not sid.nil?
|
43
49
|
session = {}
|
44
|
-
|
45
|
-
@
|
46
|
-
|
47
|
-
raise RuntimeError, 'Unique id finding looping excessively' if (lc+=1) > 1000
|
48
|
-
sid = generate_sid
|
49
|
-
ret = @pool.add(sid, session)
|
50
|
-
end until /^STORED/ =~ ret
|
51
|
-
end
|
52
|
-
end
|
53
|
-
class << session
|
54
|
-
@deleted = []
|
55
|
-
def delete key
|
56
|
-
(@deleted||=[]) << key
|
57
|
-
super
|
58
|
-
end
|
50
|
+
sid = generate_sid
|
51
|
+
ret = @pool.add sid, session
|
52
|
+
raise "Session collision on '#{sid.inspect}'" unless /^STORED/ =~ ret
|
59
53
|
end
|
60
|
-
|
54
|
+
session.instance_variable_set('@old', {}.merge(session))
|
55
|
+
return [sid, session]
|
61
56
|
rescue MemCache::MemCacheError, Errno::ECONNREFUSED # MemCache server cannot be contacted
|
62
57
|
warn "#{self} is unable to find server."
|
63
58
|
warn $!.inspect
|
64
59
|
return [ nil, {} ]
|
60
|
+
ensure
|
61
|
+
@mutex.unlock if env['rack.multithread']
|
65
62
|
end
|
66
63
|
|
67
|
-
def set_session(env,
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
session.instance_eval do
|
79
|
-
@deleted.each{|k| old_session.delete(k) } if defined? @deleted
|
80
|
-
end
|
81
|
-
@pool.set sid, old_session.merge(session), expiry
|
82
|
-
[old_session, session]
|
64
|
+
def set_session(env, session_id, new_session, options)
|
65
|
+
expiry = options[:expire_after]
|
66
|
+
expiry = expiry.nil? ? 0 : expiry + 1
|
67
|
+
|
68
|
+
@mutex.lock if env['rack.multithread']
|
69
|
+
session = @pool.get(session_id) || {}
|
70
|
+
if options[:renew] or options[:drop]
|
71
|
+
@pool.delete session_id
|
72
|
+
return false if options[:drop]
|
73
|
+
session_id = generate_sid
|
74
|
+
@pool.add session_id, 0 # so we don't worry about cache miss on #set
|
83
75
|
end
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
end if $DEBUG and env['rack.multithread']
|
89
|
-
return true
|
76
|
+
old_session = new_session.instance_variable_get('@old') || {}
|
77
|
+
session = merge_sessions session_id, old_session, new_session, session
|
78
|
+
@pool.set session_id, session, expiry
|
79
|
+
return session_id
|
90
80
|
rescue MemCache::MemCacheError, Errno::ECONNREFUSED # MemCache server cannot be contacted
|
91
81
|
warn "#{self} is unable to find server."
|
92
82
|
warn $!.inspect
|
93
83
|
return false
|
84
|
+
ensure
|
85
|
+
@mutex.unlock if env['rack.multithread']
|
86
|
+
end
|
87
|
+
|
88
|
+
private
|
89
|
+
|
90
|
+
def merge_sessions sid, old, new, cur=nil
|
91
|
+
cur ||= {}
|
92
|
+
unless Hash === old and Hash === new
|
93
|
+
warn 'Bad old or new sessions provided.'
|
94
|
+
return cur
|
95
|
+
end
|
96
|
+
|
97
|
+
delete = old.keys - new.keys
|
98
|
+
warn "//@#{sid}: delete #{delete*','}" if $VERBOSE and not delete.empty?
|
99
|
+
delete.each{|k| cur.delete k }
|
100
|
+
|
101
|
+
update = new.keys.select{|k| new[k] != old[k] }
|
102
|
+
warn "//@#{sid}: update #{update*','}" if $VERBOSE and not update.empty?
|
103
|
+
update.each{|k| cur[k] = new[k] }
|
104
|
+
|
105
|
+
cur
|
94
106
|
end
|
95
107
|
end
|
96
108
|
end
|
data/lib/rack/session/pool.rb
CHANGED
@@ -13,19 +13,20 @@ module Rack
|
|
13
13
|
# In the context of a multithreaded environment, sessions being
|
14
14
|
# committed to the pool is done in a merging manner.
|
15
15
|
#
|
16
|
+
# The :drop option is available in rack.session.options if you with to
|
17
|
+
# explicitly remove the session from the session cache.
|
18
|
+
#
|
16
19
|
# Example:
|
17
20
|
# myapp = MyRackApp.new
|
18
21
|
# sessioned = Rack::Session::Pool.new(myapp,
|
19
|
-
# :key => 'rack.session',
|
20
22
|
# :domain => 'foo.com',
|
21
|
-
# :path => '/',
|
22
23
|
# :expire_after => 2592000
|
23
24
|
# )
|
24
25
|
# Rack::Handler::WEBrick.run sessioned
|
25
26
|
|
26
27
|
class Pool < Abstract::ID
|
27
28
|
attr_reader :mutex, :pool
|
28
|
-
DEFAULT_OPTIONS = Abstract::ID::DEFAULT_OPTIONS.
|
29
|
+
DEFAULT_OPTIONS = Abstract::ID::DEFAULT_OPTIONS.merge :drop => false
|
29
30
|
|
30
31
|
def initialize(app, options={})
|
31
32
|
super
|
@@ -33,40 +34,66 @@ module Rack
|
|
33
34
|
@mutex = Mutex.new
|
34
35
|
end
|
35
36
|
|
36
|
-
|
37
|
+
def generate_sid
|
38
|
+
loop do
|
39
|
+
sid = super
|
40
|
+
break sid unless @pool.key? sid
|
41
|
+
end
|
42
|
+
end
|
37
43
|
|
38
44
|
def get_session(env, sid)
|
39
|
-
session = @
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
@pool[sid] ||= {}
|
45
|
+
session = @pool[sid] if sid
|
46
|
+
@mutex.lock if env['rack.multithread']
|
47
|
+
unless sid and session
|
48
|
+
env['rack.errors'].puts("Session '#{sid.inspect}' not found, initializing...") if $VERBOSE and not sid.nil?
|
49
|
+
session = {}
|
50
|
+
sid = generate_sid
|
51
|
+
@pool.store sid, session
|
47
52
|
end
|
48
|
-
|
53
|
+
session.instance_variable_set('@old', {}.merge(session))
|
54
|
+
return [sid, session]
|
55
|
+
ensure
|
56
|
+
@mutex.unlock if env['rack.multithread']
|
49
57
|
end
|
50
58
|
|
51
|
-
def set_session(env,
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
@pool
|
59
|
-
session.each do |k,v|
|
60
|
-
next unless old_session.has_key?(k) and v != old_session[k]
|
61
|
-
warn "session value assignment collision at #{k}: #{old_session[k]} <- #{v}"
|
62
|
-
end if $DEBUG and env['rack.multithread']
|
59
|
+
def set_session(env, session_id, new_session, options)
|
60
|
+
@mutex.lock if env['rack.multithread']
|
61
|
+
session = @pool[session_id]
|
62
|
+
if options[:renew] or options[:drop]
|
63
|
+
@pool.delete session_id
|
64
|
+
return false if options[:drop]
|
65
|
+
session_id = generate_sid
|
66
|
+
@pool.store session_id, 0
|
63
67
|
end
|
64
|
-
|
68
|
+
old_session = new_session.instance_variable_get('@old') || {}
|
69
|
+
session = merge_sessions session_id, old_session, new_session, session
|
70
|
+
@pool.store session_id, session
|
71
|
+
return session_id
|
65
72
|
rescue
|
66
|
-
warn "#{
|
67
|
-
warn "#{env['rack.session'].inspect} has been lost."
|
73
|
+
warn "#{new_session.inspect} has been lost."
|
68
74
|
warn $!.inspect
|
69
|
-
|
75
|
+
ensure
|
76
|
+
@mutex.unlock if env['rack.multithread']
|
77
|
+
end
|
78
|
+
|
79
|
+
private
|
80
|
+
|
81
|
+
def merge_sessions sid, old, new, cur=nil
|
82
|
+
cur ||= {}
|
83
|
+
unless Hash === old and Hash === new
|
84
|
+
warn 'Bad old or new sessions provided.'
|
85
|
+
return cur
|
86
|
+
end
|
87
|
+
|
88
|
+
delete = old.keys - new.keys
|
89
|
+
warn "//@#{sid}: dropping #{delete*','}" if $DEBUG and not delete.empty?
|
90
|
+
delete.each{|k| cur.delete k }
|
91
|
+
|
92
|
+
update = new.keys.select{|k| new[k] != old[k] }
|
93
|
+
warn "//@#{sid}: updating #{update*','}" if $DEBUG and not update.empty?
|
94
|
+
update.each{|k| cur[k] = new[k] }
|
95
|
+
|
96
|
+
cur
|
70
97
|
end
|
71
98
|
end
|
72
99
|
end
|