omniauth 0.3.2 → 1.0.0.beta1

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of omniauth might be problematic. Click here for more details.

@@ -0,0 +1,51 @@
1
+ require 'hashie/mash'
2
+
3
+ module OmniAuth
4
+ # The AuthHash is a normalized schema returned by all OmniAuth
5
+ # strategies. It maps as much user information as the provider
6
+ # is able to provide into the InfoHash (stored as the `'info'`
7
+ # key).
8
+ class AuthHash < Hashie::Mash
9
+ def self.subkey_class; Hashie::Mash end
10
+
11
+ # Tells you if this is considered to be a valid
12
+ # OmniAuth AuthHash. The requirements for that
13
+ # are that it has a provider name, a uid, and a
14
+ # valid info hash. See InfoHash#valid? for
15
+ # more details there.
16
+ def valid?
17
+ uid? && provider? && info? && info.valid?
18
+ end
19
+
20
+ def regular_writer(key, value)
21
+ if key.to_s == 'info' && !value.is_a?(InfoHash)
22
+ value = InfoHash.new(value)
23
+ end
24
+ super
25
+ end
26
+
27
+ class InfoHash < Hashie::Mash
28
+ def self.subkey_class; Hashie::Mash end
29
+
30
+ def name
31
+ return self[:name] if self[:name]
32
+ return "#{first_name} #{last_name}".strip if first_name? || last_name?
33
+ return nickname if nickname?
34
+ return email if email?
35
+ nil
36
+ end
37
+
38
+ def name?; !!name end
39
+
40
+ def valid?
41
+ name?
42
+ end
43
+
44
+ def to_hash
45
+ hash = super
46
+ hash['name'] ||= name
47
+ hash
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,33 @@
1
+ require 'omniauth'
2
+
3
+ module OmniAuth
4
+ class Builder < ::Rack::Builder
5
+ def initialize(app, &block)
6
+ @app = app
7
+ super(&block)
8
+ end
9
+
10
+ def on_failure(&block)
11
+ OmniAuth.config.on_failure = block
12
+ end
13
+
14
+ def configure(&block)
15
+ OmniAuth.configure(&block)
16
+ end
17
+
18
+ def provider(klass, *args, &block)
19
+ if klass.is_a?(Class)
20
+ middleware = klass
21
+ else
22
+ middleware = OmniAuth::Strategies.const_get("#{OmniAuth::Utils.camelize(klass.to_s)}")
23
+ end
24
+
25
+ use middleware, *args, &block
26
+ end
27
+
28
+ def call(env)
29
+ @ins << @app unless @ins.include?(@app)
30
+ to_app.call(env)
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,191 @@
1
+ require 'omniauth'
2
+
3
+ module OmniAuth
4
+ class Form
5
+ DEFAULT_CSS = <<-CSS
6
+ body {
7
+ background: #ccc;
8
+ font-family: "Lucida Grande", "Lucida Sans", Helvetica, Arial, sans-serif;
9
+ }
10
+
11
+ h1 {
12
+ text-align: center;
13
+ margin: 30px auto 0px;
14
+ font-size: 18px;
15
+ padding: 10px 10px 15px;
16
+ background: #555;
17
+ color: white;
18
+ width: 320px;
19
+ border: 10px solid #444;
20
+ border-bottom: 0;
21
+ -moz-border-radius-topleft: 10px;
22
+ -moz-border-radius-topright: 10px;
23
+ -webkit-border-top-left-radius: 10px;
24
+ -webkit-border-top-right-radius: 10px;
25
+ border-top-left-radius: 10px;
26
+ border-top-right-radius: 10px;
27
+ }
28
+
29
+ h1, form {
30
+ -moz-box-shadow: 2px 2px 7px rgba(0,0,0,0.3);
31
+ -webkit-box-shadow: 2px 2px 7px rgba(0,0,0,0.3);
32
+ }
33
+
34
+ form {
35
+ background: white;
36
+ border: 10px solid #eee;
37
+ border-top: 0;
38
+ padding: 20px;
39
+ margin: 0px auto 40px;
40
+ width: 300px;
41
+ -moz-border-radius-bottomleft: 10px;
42
+ -moz-border-radius-bottomright: 10px;
43
+ -webkit-border-bottom-left-radius: 10px;
44
+ -webkit-border-bottom-right-radius: 10px;
45
+ border-bottom-left-radius: 10px;
46
+ border-bottom-right-radius: 10px;
47
+ }
48
+
49
+ label {
50
+ display: block;
51
+ font-weight: bold;
52
+ margin-bottom: 5px;
53
+ }
54
+
55
+ input {
56
+ font-size: 18px;
57
+ padding: 4px 8px;
58
+ display: block;
59
+ margin-bottom: 10px;
60
+ width: 280px;
61
+ }
62
+
63
+ input#identifier, input#openid_url {
64
+ background: url(http://openid.net/login-bg.gif) no-repeat;
65
+ background-position: 0 50%;
66
+ padding-left: 18px;
67
+ }
68
+
69
+ button {
70
+ font-size: 22px;
71
+ padding: 4px 8px;
72
+ display: block;
73
+ margin: 20px auto 0;
74
+ }
75
+
76
+ fieldset {
77
+ border: 1px solid #ccc;
78
+ border-left: 0;
79
+ border-right: 0;
80
+ padding: 10px 0;
81
+ }
82
+
83
+ fieldset input {
84
+ width: 260px;
85
+ font-size: 16px;
86
+ }
87
+ CSS
88
+
89
+ attr_accessor :options
90
+
91
+ def initialize(options = {})
92
+ options[:title] ||= "Authentication Info Required"
93
+ options[:header_info] ||= ""
94
+ self.options = options
95
+
96
+ @html = ""
97
+ header(options[:title],options[:header_info])
98
+ end
99
+
100
+ def self.build(title=nil,&block)
101
+ form = OmniAuth::Form.new(:title => title)
102
+ if block.arity > 0
103
+ yield form
104
+ else
105
+ form.instance_eval(&block)
106
+ end
107
+ form
108
+ end
109
+
110
+ def label_field(text, target)
111
+ @html << "\n<label for='#{target}'>#{text}:</label>"
112
+ self
113
+ end
114
+
115
+ def input_field(type, name)
116
+ @html << "\n<input type='#{type}' id='#{name}' name='#{name}'/>"
117
+ self
118
+ end
119
+
120
+ def text_field(label, name)
121
+ label_field(label, name)
122
+ input_field('text', name)
123
+ self
124
+ end
125
+
126
+ def password_field(label, name)
127
+ label_field(label, name)
128
+ input_field('password', name)
129
+ self
130
+ end
131
+
132
+ def button(text)
133
+ @html << "\n<button type='submit'>#{text}</button>"
134
+ end
135
+
136
+ def html(html)
137
+ @html << html
138
+ end
139
+
140
+ def fieldset(legend, options = {}, &block)
141
+ @html << "\n<fieldset#{" style='#{options[:style]}'" if options[:style]}#{" id='#{options[:id]}'" if options[:id]}>\n <legend>#{legend}</legend>\n"
142
+ self.instance_eval &block
143
+ @html << "\n</fieldset>"
144
+ self
145
+ end
146
+
147
+ def header(title,header_info)
148
+ @html << <<-HTML
149
+ <!DOCTYPE html>
150
+ <html>
151
+ <head>
152
+ <title>#{title}</title>
153
+ #{css}
154
+ #{header_info}
155
+ </head>
156
+ <body>
157
+ <h1>#{title}</h1>
158
+ <form method='post' #{"action='#{options[:url]}' " if options[:url]}noValidate='noValidate'>
159
+ HTML
160
+ self
161
+ end
162
+
163
+ def footer
164
+ return self if @footer
165
+ @html << <<-HTML
166
+ <button type='submit'>Connect</button>
167
+ </form>
168
+ </body>
169
+ </html>
170
+ HTML
171
+ @footer = true
172
+ self
173
+ end
174
+
175
+ def to_html
176
+ footer
177
+ @html
178
+ end
179
+
180
+ def to_response
181
+ footer
182
+ Rack::Response.new(@html).finish
183
+ end
184
+
185
+ protected
186
+
187
+ def css
188
+ "\n<style type='text/css'>#{OmniAuth.config.form_css}</style>"
189
+ end
190
+ end
191
+ end
@@ -0,0 +1,60 @@
1
+ require 'omniauth'
2
+
3
+ module OmniAuth
4
+ module Strategies
5
+ # The Developer strategy is a very simple strategy that can be used as a
6
+ # placeholder in your application until a different authentication strategy
7
+ # is swapped in. It has zero security and should *never* be used in a
8
+ # production setting.
9
+ #
10
+ # ## Usage
11
+ #
12
+ # To use the Developer strategy, all you need to do is put it in like any
13
+ # other strategy:
14
+ #
15
+ # @example Basic Usage
16
+ #
17
+ # use OmniAuth::Builder do
18
+ # provider :developer
19
+ # end
20
+ #
21
+ # @example Custom Fields
22
+ #
23
+ # use OmniAuth::Builder do
24
+ # provider :developer,
25
+ # :fields => [:first_name, :last_name],
26
+ # :uid_field => :last_name
27
+ # end
28
+ #
29
+ # This will create a strategy that, when the user visits `/auth/developer`
30
+ # they will be presented a form that prompts for (by default) their name
31
+ # and email address. The auth hash will be populated with these fields and
32
+ # the `uid` will simply be set to the provided email.
33
+ class Developer
34
+ include OmniAuth::Strategy
35
+
36
+ option :fields, [:name, :email]
37
+ option :uid_field, :email
38
+
39
+ def request_phase
40
+ form = OmniAuth::Form.new(:title => "User Info", :url => callback_path)
41
+ options.fields.each do |field|
42
+ form.text_field field.to_s.capitalize.gsub("_", " "), field.to_s
43
+ end
44
+ form.button "Sign In"
45
+ form.to_response
46
+ end
47
+
48
+ uid do
49
+ request.params[options.uid_field.to_s]
50
+ end
51
+
52
+ info do
53
+ options.fields.inject({}) do |hash, field|
54
+ hash[field] = request.params[field.to_s]
55
+ hash
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,429 @@
1
+ require 'omniauth'
2
+ require 'hashie/mash'
3
+
4
+ module OmniAuth
5
+ class NoSessionError < StandardError; end
6
+ # The Strategy is the base unit of OmniAuth's ability to
7
+ # wrangle multiple providers. Each strategy provided by
8
+ # OmniAuth includes this mixin to gain the default functionality
9
+ # necessary to be compatible with the OmniAuth library.
10
+ module Strategy
11
+ def self.included(base)
12
+ OmniAuth.strategies << base
13
+
14
+ base.extend ClassMethods
15
+ base.class_eval do
16
+ attr_reader :app, :env, :options, :response
17
+
18
+ option :setup, false
19
+ option :skip_info, false
20
+ end
21
+ end
22
+
23
+ module ClassMethods
24
+ # Returns an inherited set of default options set at the class-level
25
+ # for each strategy.
26
+ def default_options
27
+ return @default_options if @default_options
28
+ existing = superclass.respond_to?(:default_options) ? superclass.default_options : {}
29
+ @default_options = OmniAuth::Strategy::Options.new(existing)
30
+ end
31
+
32
+ # This allows for more declarative subclassing of strategies by allowing
33
+ # default options to be set using a simple configure call.
34
+ #
35
+ # @param options [Hash] If supplied, these will be the default options (deep-merged into the superclass's default options).
36
+ # @yield [Options] The options Mash that allows you to set your defaults as you'd like.
37
+ #
38
+ # @example Using a yield to configure the default options.
39
+ #
40
+ # class MyStrategy
41
+ # include OmniAuth::Strategy
42
+ #
43
+ # configure do |c|
44
+ # c.foo = 'bar'
45
+ # end
46
+ # end
47
+ #
48
+ # @example Using a hash to configure the default options.
49
+ #
50
+ # class MyStrategy
51
+ # include OmniAuth::Strategy
52
+ # configure foo: 'bar'
53
+ # end
54
+ def configure(options = nil)
55
+ yield default_options and return unless options
56
+ default_options.deep_merge!(options)
57
+ end
58
+
59
+ # Directly declare a default option for your class. This is a useful from
60
+ # a documentation perspective as it provides a simple line-by-line analysis
61
+ # of the kinds of options your strategy provides by default.
62
+ #
63
+ # @param name [Symbol] The key of the default option in your configuration hash.
64
+ # @param value [Object] The value your object defaults to. Nil if not provided.
65
+ #
66
+ # @example
67
+ #
68
+ # class MyStrategy
69
+ # include OmniAuth::Strategy
70
+ #
71
+ # option :foo, 'bar'
72
+ # option
73
+ # end
74
+ def option(name, value = nil)
75
+ default_options[name] = value
76
+ end
77
+
78
+ # Sets (and retrieves) option key names for initializer arguments to be
79
+ # recorded as. This takes care of 90% of the use cases for overriding
80
+ # the initializer in OmniAuth Strategies.
81
+ def args(args = nil)
82
+ @args = Array(args) and return if args
83
+ existing = superclass.respond_to?(:args) ? superclass.args : []
84
+ return @args || existing
85
+ end
86
+
87
+ %w(uid info extra credentials).each do |fetcher|
88
+ class_eval <<-RUBY
89
+ def #{fetcher}(&block)
90
+ return @#{fetcher}_proc unless block_given?
91
+ @#{fetcher}_proc = block
92
+ end
93
+
94
+ def #{fetcher}_stack(context)
95
+ compile_stack(self.ancestors, :#{fetcher}, context)
96
+ end
97
+ RUBY
98
+ end
99
+
100
+ def compile_stack(ancestors, method, context)
101
+ stack = ancestors.inject([]) do |a, ancestor|
102
+ a << context.instance_eval(&ancestor.send(method)) if ancestor.respond_to?(method) && ancestor.send(method)
103
+ a
104
+ end
105
+ stack.reverse!
106
+ end
107
+ end
108
+
109
+ # Initializes the strategy by passing in the Rack endpoint,
110
+ # the unique URL segment name for this strategy, and any
111
+ # additional arguments. An `options` hash is automatically
112
+ # created from the last argument if it is a hash.
113
+ #
114
+ # @param app [Rack application] The application on which this middleware is applied.
115
+ #
116
+ # @overload new(app, options = {})
117
+ # If nothing but a hash is supplied, initialized with the supplied options
118
+ # overriding the strategy's default options via a deep merge.
119
+ # @overload new(app, *args, options = {})
120
+ # If the strategy has supplied custom arguments that it accepts, they may
121
+ # will be passed through and set to the appropriate values.
122
+ #
123
+ # @yield [Options] Yields options to block for further configuration.
124
+ def initialize(app, *args, &block)
125
+ @app = app
126
+ @options = self.class.default_options.dup
127
+
128
+ options.deep_merge!(args.pop) if args.last.is_a?(Hash)
129
+ options.name ||= self.class.to_s.split('::').last.downcase
130
+
131
+ self.class.args.each do |arg|
132
+ options[arg] = args.shift
133
+ end
134
+
135
+ # Make sure that all of the args have been dealt with, otherwise error out.
136
+ raise ArgumentError, "Received wrong number of arguments. #{args.inspect}" unless args.empty?
137
+
138
+ yield options if block_given?
139
+ end
140
+
141
+ def inspect
142
+ "#<#{self.class.to_s}>"
143
+ end
144
+
145
+ # Duplicates this instance and runs #call! on it.
146
+ # @param [Hash] The Rack environment.
147
+ def call(env)
148
+ dup.call!(env)
149
+ end
150
+
151
+ # The logic for dispatching any additional actions that need
152
+ # to be taken. For instance, calling the request phase if
153
+ # the request path is recognized.
154
+ #
155
+ # @param env [Hash] The Rack environment.
156
+ def call!(env)
157
+ raise OmniAuth::NoSessionError.new("You must provide a session to use OmniAuth.") unless env['rack.session']
158
+
159
+ @env = env
160
+ @env['omniauth.strategy'] = self if on_auth_path?
161
+
162
+ return mock_call!(env) if OmniAuth.config.test_mode
163
+
164
+ return options_call if on_auth_path? && options_request?
165
+ return request_call if on_request_path? && OmniAuth.config.allowed_request_methods.include?(request.request_method.downcase.to_sym)
166
+ return callback_call if on_callback_path?
167
+ return other_phase if respond_to?(:other_phase)
168
+ @app.call(env)
169
+ end
170
+
171
+ # Responds to an OPTIONS request.
172
+ def options_call
173
+ verbs = OmniAuth.config.allowed_request_methods.map(&:to_s).map(&:upcase).join(', ')
174
+ return [ 200, { 'Allow' => verbs }, [] ]
175
+ end
176
+
177
+ # Performs the steps necessary to run the request phase of a strategy.
178
+ def request_call
179
+ setup_phase
180
+ if options.form.respond_to?(:call)
181
+ options.form.call(env)
182
+ elsif options.form
183
+ call_app!
184
+ else
185
+ if request.params['origin']
186
+ env['rack.session']['omniauth.origin'] = request.params['origin']
187
+ elsif env['HTTP_REFERER'] && !env['HTTP_REFERER'].match(/#{request_path}$/)
188
+ env['rack.session']['omniauth.origin'] = env['HTTP_REFERER']
189
+ end
190
+ request_phase
191
+ end
192
+ end
193
+
194
+ # Performs the steps necessary to run the callback phase of a strategy.
195
+ def callback_call
196
+ setup_phase
197
+ @env['omniauth.origin'] = session.delete('omniauth.origin')
198
+ @env['omniauth.origin'] = nil if env['omniauth.origin'] == ''
199
+ @env['omniauth.params'] = session.delete('query_params') || {}
200
+ callback_phase
201
+ end
202
+
203
+ # Returns true if the environment recognizes either the
204
+ # request or callback path.
205
+ def on_auth_path?
206
+ on_request_path? || on_callback_path?
207
+ end
208
+
209
+ def on_request_path?
210
+ on_path?(request_path)
211
+ end
212
+
213
+ def on_callback_path?
214
+ on_path?(callback_path)
215
+ end
216
+
217
+ def on_path?(path)
218
+ current_path.casecmp(path) == 0
219
+ end
220
+
221
+ def options_request?
222
+ request.request_method == 'OPTIONS'
223
+ end
224
+
225
+ # This is called in lieu of the normal request process
226
+ # in the event that OmniAuth has been configured to be
227
+ # in test mode.
228
+ def mock_call!(env)
229
+ return mock_request_call if on_request_path?
230
+ return mock_callback_call if on_callback_path?
231
+ call_app!
232
+ end
233
+
234
+ def mock_request_call
235
+ setup_phase
236
+ return response if response = call_through_to_app
237
+
238
+ if request.params['origin']
239
+ @env['rack.session']['omniauth.origin'] = request.params['origin']
240
+ elsif env['HTTP_REFERER'] && !env['HTTP_REFERER'].match(/#{request_path}$/)
241
+ @env['rack.session']['omniauth.origin'] = env['HTTP_REFERER']
242
+ end
243
+ redirect(script_name + callback_path + query_string)
244
+ end
245
+
246
+ def mock_callback_call
247
+ setup_phase
248
+ mocked_auth = OmniAuth.mock_auth_for(name.to_s)
249
+ if mocked_auth.is_a?(Symbol)
250
+ fail!(mocked_auth)
251
+ else
252
+ @env['omniauth.auth'] = mocked_auth
253
+ @env['omniauth.origin'] = session.delete('omniauth.origin')
254
+ @env['omniauth.origin'] = nil if env['omniauth.origin'] == ''
255
+ call_app!
256
+ end
257
+ end
258
+
259
+ # The setup phase looks for the `:setup` option to exist and,
260
+ # if it is, will call either the Rack endpoint supplied to the
261
+ # `:setup` option or it will call out to the setup path of the
262
+ # underlying application. This will default to `/auth/:provider/setup`.
263
+ def setup_phase
264
+ if options[:setup].respond_to?(:call)
265
+ options[:setup].call(env)
266
+ elsif options.setup?
267
+ setup_env = env.merge('PATH_INFO' => setup_path, 'REQUEST_METHOD' => 'GET')
268
+ call_app!(setup_env)
269
+ end
270
+ end
271
+
272
+ # @abstract This method is called when the user is on the request path. You should
273
+ # perform any information gathering you need to be able to authenticate
274
+ # the user in this phase.
275
+ def request_phase
276
+ raise NotImplementedError
277
+ end
278
+
279
+ def uid
280
+ self.class.uid_stack(self).last
281
+ end
282
+
283
+ def info
284
+ merge_stack(self.class.info_stack(self))
285
+ end
286
+
287
+ def credentials
288
+ merge_stack(self.class.credentials_stack(self))
289
+ end
290
+
291
+ def extra
292
+ merge_stack(self.class.extra_stack(self))
293
+ end
294
+
295
+ def auth_hash
296
+ hash = AuthHash.new(:provider => name, :uid => uid)
297
+ hash.info = info unless skip_info?
298
+ hash.credentials = credentials if credentials
299
+ hash.extra = extra if extra
300
+ hash
301
+ end
302
+
303
+ # Determines whether or not user info should be retrieved. This
304
+ # allows some strategies to save a call to an external API service
305
+ # for existing users. You can use it either by setting the `:skip_info`
306
+ # to true or by setting `:skip_info` to a Proc that takes a uid and
307
+ # evaluates to true when you would like to skip info.
308
+ #
309
+ # @example
310
+ #
311
+ # use MyStrategy, :skip_info => lambda{|uid| User.find_by_uid(uid)}
312
+ def skip_info?
313
+ if options.skip_info?
314
+ if options.skip_info.respond_to?(:call)
315
+ return options.skip_info.call(uid)
316
+ else
317
+ return true
318
+ end
319
+ end
320
+ false
321
+ end
322
+
323
+ def callback_phase
324
+ self.env['omniauth.auth'] = auth_hash
325
+ call_app!
326
+ end
327
+
328
+ def path_prefix
329
+ options[:path_prefix] || OmniAuth.config.path_prefix
330
+ end
331
+
332
+ def request_path
333
+ options[:request_path] || "#{path_prefix}/#{name}"
334
+ end
335
+
336
+ def callback_path
337
+ options[:callback_path] || "#{path_prefix}/#{name}/callback"
338
+ end
339
+
340
+ def setup_path
341
+ options[:setup_path] || "#{path_prefix}/#{name}/setup"
342
+ end
343
+
344
+ def current_path
345
+ request.path_info.downcase.sub(/\/$/,'')
346
+ end
347
+
348
+ def query_string
349
+ request.query_string.empty? ? "" : "?#{request.query_string}"
350
+ end
351
+
352
+ def call_through_to_app
353
+ status, headers, body = *call_app!
354
+ session['query_params'] = Rack::Request.new(env).params
355
+ @response = Rack::Response.new(body, status, headers)
356
+
357
+ status == 404 ? nil : @response.finish
358
+ end
359
+
360
+ def call_app!(env = @env)
361
+ @app.call(env)
362
+ end
363
+
364
+ def full_host
365
+ case OmniAuth.config.full_host
366
+ when String
367
+ OmniAuth.config.full_host
368
+ when Proc
369
+ OmniAuth.config.full_host.call(env)
370
+ else
371
+ uri = URI.parse(request.url.gsub(/\?.*$/,''))
372
+ uri.path = ''
373
+ uri.query = nil
374
+ uri.to_s
375
+ end
376
+ end
377
+
378
+ def callback_url
379
+ full_host + script_name + callback_path + query_string
380
+ end
381
+
382
+ def script_name
383
+ @env['SCRIPT_NAME'] || ''
384
+ end
385
+
386
+ def session
387
+ @env['rack.session']
388
+ end
389
+
390
+ def request
391
+ @request ||= Rack::Request.new(@env)
392
+ end
393
+
394
+ def name
395
+ options.name
396
+ end
397
+
398
+ def redirect(uri)
399
+ r = Rack::Response.new
400
+
401
+ if options[:iframe]
402
+ r.write("<script type='text/javascript' charset='utf-8'>top.location.href = '#{uri}';</script>")
403
+ else
404
+ r.write("Redirecting to #{uri}...")
405
+ r.redirect(uri)
406
+ end
407
+
408
+ r.finish
409
+ end
410
+
411
+ def user_info; {} end
412
+
413
+ def fail!(message_key, exception = nil)
414
+ self.env['omniauth.error'] = exception
415
+ self.env['omniauth.error.type'] = message_key.to_sym
416
+ self.env['omniauth.error.strategy'] = self
417
+
418
+ OmniAuth.config.on_failure.call(self.env)
419
+ end
420
+
421
+ class Options < Hashie::Mash; end
422
+
423
+ protected
424
+
425
+ def merge_stack(stack)
426
+ stack.inject({}){|c,h| c.merge!(h); c}
427
+ end
428
+ end
429
+ end