merb-core 0.9.5 → 0.9.6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (78) hide show
  1. data/CHANGELOG +925 -0
  2. data/CONTRIBUTORS +93 -0
  3. data/PUBLIC_CHANGELOG +85 -0
  4. data/Rakefile +18 -28
  5. data/bin/merb +34 -5
  6. data/lib/merb-core/autoload.rb +2 -3
  7. data/lib/merb-core/bootloader.rb +60 -66
  8. data/lib/merb-core/config.rb +7 -1
  9. data/lib/merb-core/controller/abstract_controller.rb +35 -21
  10. data/lib/merb-core/controller/merb_controller.rb +15 -42
  11. data/lib/merb-core/controller/mixins/authentication.rb +42 -6
  12. data/lib/merb-core/controller/mixins/conditional_get.rb +83 -0
  13. data/lib/merb-core/controller/mixins/render.rb +3 -3
  14. data/lib/merb-core/core_ext/kernel.rb +6 -19
  15. data/lib/merb-core/dispatch/cookies.rb +96 -80
  16. data/lib/merb-core/dispatch/default_exception/views/index.html.erb +2 -0
  17. data/lib/merb-core/dispatch/request.rb +18 -16
  18. data/lib/merb-core/dispatch/router/route.rb +6 -0
  19. data/lib/merb-core/dispatch/router.rb +4 -1
  20. data/lib/merb-core/dispatch/session/container.rb +64 -0
  21. data/lib/merb-core/dispatch/session/cookie.rb +91 -101
  22. data/lib/merb-core/dispatch/session/memcached.rb +38 -174
  23. data/lib/merb-core/dispatch/session/memory.rb +62 -208
  24. data/lib/merb-core/dispatch/session/store_container.rb +145 -0
  25. data/lib/merb-core/dispatch/session.rb +174 -48
  26. data/lib/merb-core/rack/middleware/conditional_get.rb +14 -8
  27. data/lib/merb-core/rack/middleware/csrf.rb +73 -0
  28. data/lib/merb-core/rack.rb +1 -0
  29. data/lib/merb-core/script.rb +112 -0
  30. data/lib/merb-core/server.rb +2 -0
  31. data/lib/merb-core/tasks/merb_rake_helper.rb +25 -0
  32. data/lib/merb-core/test/helpers/request_helper.rb +40 -3
  33. data/lib/merb-core/test/run_specs.rb +4 -3
  34. data/lib/merb-core/vendor/facets/inflect.rb +7 -10
  35. data/lib/merb-core/version.rb +1 -1
  36. data/lib/merb-core.rb +11 -40
  37. data/spec/private/core_ext/kernel_spec.rb +0 -11
  38. data/spec/private/dispatch/fixture/log/merb_test.log +893 -0
  39. data/spec/private/router/fixture/log/merb_test.log +12 -1728
  40. data/spec/private/router/route_spec.rb +4 -0
  41. data/spec/private/router/router_spec.rb +8 -0
  42. data/spec/private/vendor/facets/plural_spec.rb +1 -1
  43. data/spec/private/vendor/facets/singular_spec.rb +1 -1
  44. data/spec/public/abstract_controller/controllers/display.rb +8 -2
  45. data/spec/public/abstract_controller/controllers/filters.rb +18 -0
  46. data/spec/public/abstract_controller/display_spec.rb +6 -2
  47. data/spec/public/abstract_controller/filter_spec.rb +4 -0
  48. data/spec/public/controller/authentication_spec.rb +114 -43
  49. data/spec/public/controller/base_spec.rb +8 -0
  50. data/spec/public/controller/conditional_get_spec.rb +100 -0
  51. data/spec/public/controller/config/init.rb +1 -1
  52. data/spec/public/controller/controllers/authentication.rb +29 -0
  53. data/spec/public/controller/controllers/base.rb +13 -0
  54. data/spec/public/controller/controllers/conditional_get.rb +35 -0
  55. data/spec/public/controller/controllers/cookies.rb +10 -1
  56. data/spec/public/controller/cookies_spec.rb +38 -9
  57. data/spec/public/controller/spec_helper.rb +1 -0
  58. data/spec/public/controller/url_spec.rb +70 -1
  59. data/spec/public/directory_structure/directory/log/merb_test.log +461 -0
  60. data/spec/public/rack/conditinal_get_middleware_spec.rb +77 -89
  61. data/spec/public/rack/csrf_middleware_spec.rb +70 -0
  62. data/spec/public/reloading/directory/log/merb_test.log +52 -0
  63. data/spec/public/request/request_spec.rb +19 -1
  64. data/spec/public/router/fixation_spec.rb +26 -4
  65. data/spec/public/router/fixture/log/merb_test.log +234 -30332
  66. data/spec/public/session/controllers/sessions.rb +52 -0
  67. data/spec/public/session/cookie_session_spec.rb +73 -0
  68. data/spec/public/session/memcached_session_spec.rb +31 -0
  69. data/spec/public/session/memory_session_spec.rb +28 -0
  70. data/spec/public/session/multiple_sessions_spec.rb +74 -0
  71. data/spec/public/session/no_session_spec.rb +12 -0
  72. data/spec/public/session/session_spec.rb +91 -0
  73. data/spec/public/test/controllers/spec_helper_controller.rb +2 -1
  74. data/spec/public/test/request_helper_spec.rb +15 -0
  75. data/spec/spec_helper.rb +2 -2
  76. metadata +23 -5
  77. data/spec/private/dispatch/cookies_spec.rb +0 -219
  78. data/spec/private/dispatch/session_mixin_spec.rb +0 -47
@@ -27,7 +27,7 @@
27
27
  # before :some_filter
28
28
  # before :authenticate, :exclude => [:login, :signup]
29
29
  # before :has_role, :with => ["Admin"], :exclude => [:index, :show]
30
- # before Proc.new {|c| c.some_method }, :only => :foo
30
+ # before Proc.new { some_method }, :only => :foo
31
31
  # before :authorize, :unless => :logged_in?
32
32
  #
33
33
  # You can use either <code>:only => :actionname</code> or
@@ -64,8 +64,8 @@
64
64
  # If the second arg is a Proc, it will be called and its return
65
65
  # value will be what is rendered to the browser:
66
66
  #
67
- # throw :halt, proc {|c| c.access_denied }
68
- # throw :halt, proc {|c| Tidy.new(c.index) }
67
+ # throw :halt, proc { access_denied }
68
+ # throw :halt, proc { Tidy.new(c.index) }
69
69
  #
70
70
  # ===== Filter Options (.before, .after, .add_filter, .if, .unless)
71
71
  # :only<Symbol, Array[Symbol]>::
@@ -98,6 +98,7 @@ class Merb::AbstractController
98
98
 
99
99
  class_inheritable_accessor :_layout, :_template_root, :template_roots
100
100
  class_inheritable_accessor :_before_filters, :_after_filters
101
+ class_inheritable_accessor :_before_dispatch_callbacks, :_after_dispatch_callbacks
101
102
 
102
103
  cattr_accessor :_abstract_subclasses
103
104
 
@@ -113,6 +114,7 @@ class Merb::AbstractController
113
114
  FILTER_OPTIONS = [:only, :exclude, :if, :unless, :with]
114
115
 
115
116
  self._before_filters, self._after_filters = [], []
117
+ self._before_dispatch_callbacks, self._after_dispatch_callbacks = [], []
116
118
 
117
119
  #---
118
120
  # We're using abstract_subclasses so that Merb::Controller can have its
@@ -217,7 +219,7 @@ class Merb::AbstractController
217
219
  include Object.full_const_get("#{helper_module_name}") rescue nil
218
220
  HERE
219
221
  super
220
- end
222
+ end
221
223
  end
222
224
 
223
225
  # ==== Parameters
@@ -228,7 +230,7 @@ class Merb::AbstractController
228
230
  @_template_stack = []
229
231
  end
230
232
 
231
- # This will dispatch the request, calling setup_session and finalize_session
233
+ # This will dispatch the request, calling internal before/after dispatch_callbacks
232
234
  #
233
235
  # ==== Parameters
234
236
  # action<~to_s>::
@@ -238,7 +240,7 @@ class Merb::AbstractController
238
240
  # ==== Raises
239
241
  # MerbControllerError:: Invalid body content caught.
240
242
  def _dispatch(action)
241
- setup_session
243
+ self._before_dispatch_callbacks.each { |cb| cb.call(self) }
242
244
  self.action_name = action
243
245
 
244
246
  caught = catch(:halt) do
@@ -253,14 +255,16 @@ class Merb::AbstractController
253
255
  when String then caught
254
256
  when nil then _filters_halted
255
257
  when Symbol then __send__(caught)
256
- when Proc then caught.call(self)
258
+ when Proc then self.instance_eval(&caught)
257
259
  else
258
260
  raise ArgumentError, "Threw :halt, #{caught}. Expected String, nil, Symbol, Proc."
259
261
  end
260
262
  start = Time.now
261
263
  _call_filters(_after_filters)
262
264
  @_benchmarks[:after_filters_time] = Time.now - start if _after_filters
263
- finalize_session
265
+
266
+ self._after_dispatch_callbacks.each { |cb| cb.call(self) }
267
+
264
268
  @body
265
269
  end
266
270
 
@@ -298,7 +302,7 @@ class Merb::AbstractController
298
302
  else
299
303
  send(filter)
300
304
  end
301
- when Proc then self.instance_eval(&filter)
305
+ when Proc then self.instance_eval(&filter)
302
306
  end
303
307
  end
304
308
  end
@@ -360,7 +364,7 @@ class Merb::AbstractController
360
364
  def _evaluate_condition(condition)
361
365
  case condition
362
366
  when Symbol : self.send(condition)
363
- when Proc : condition.call(self)
367
+ when Proc : self.instance_eval(&condition)
364
368
  else
365
369
  raise ArgumentError,
366
370
  'Filter condtions need to be either a Symbol or a Proc'
@@ -412,14 +416,6 @@ class Merb::AbstractController
412
416
  #---
413
417
  # Defaults that can be overridden by plugins, other mixins, or subclasses
414
418
  def _filters_halted() "<html><body><h1>Filter Chain Halted!</h1></body></html>" end
415
-
416
- # Method stub for setting up the session. This will be overriden by session
417
- # modules.
418
- def setup_session() end
419
-
420
- # Method stub for finalizing up the session. This will be overriden by
421
- # session modules.
422
- def finalize_session() end
423
419
 
424
420
  # ==== Parameters
425
421
  # name<~to_sym, Hash>:: The name of the URL to generate.
@@ -453,11 +449,24 @@ class Merb::AbstractController
453
449
  # ==== Returns
454
450
  # String:: The generated url with protocol + hostname + URL.
455
451
  #
452
+ # ==== Options
453
+ #
454
+ # :protocol and :host options are special: use them to explicitly
455
+ # specify protocol and host of resulting url. If you omit them,
456
+ # protocol and host of request are used.
457
+ #
456
458
  # ==== Alternatives
457
459
  # If a hash is used as the first argument, a default route will be
458
460
  # generated based on it and rparams.
459
461
  def absolute_url(name, rparams={})
460
- request.protocol + request.host + url(name, rparams)
462
+ # FIXME: arrgh, why request.protocol returns http://?
463
+ # :// is not part of protocol name
464
+ protocol = rparams.delete(:protocol)
465
+ protocol << "://" if protocol
466
+
467
+ (protocol || request.protocol) +
468
+ (rparams.delete(:host) || request.host) +
469
+ url(name, rparams)
461
470
  end
462
471
 
463
472
  # Calls the capture method for the selected template engine.
@@ -506,9 +515,14 @@ class Merb::AbstractController
506
515
  end
507
516
 
508
517
  opts = normalize_filters!(opts)
509
-
518
+
510
519
  case filter
511
- when Symbol, Proc, String
520
+ when Proc
521
+ # filters with procs created via class methods have identical signature
522
+ # regardless if they handle content differently or not. So procs just
523
+ # get appended
524
+ filters << [filter, opts]
525
+ when Symbol, String
512
526
  if existing_filter = filters.find {|f| f.first.to_s[filter.to_s]}
513
527
  filters[ filters.index(existing_filter) ] = [filter, opts]
514
528
  else
@@ -1,24 +1,19 @@
1
1
  class Merb::Controller < Merb::AbstractController
2
2
 
3
- class_inheritable_accessor :_hidden_actions, :_shown_actions,
4
- :_session_id_key, :_session_secret_key, :_session_expiry, :_session_cookie_domain
3
+ class_inheritable_accessor :_hidden_actions, :_shown_actions
5
4
 
6
5
  self._hidden_actions ||= []
7
- self._shown_actions ||= []
8
-
6
+ self._shown_actions ||= []
7
+
9
8
  cattr_accessor :_subclasses
10
9
  self._subclasses = Set.new
11
10
 
12
11
  def self.subclasses_list() _subclasses end
13
12
 
14
- self._session_secret_key = nil
15
- self._session_id_key = Merb::Config[:session_id_key] || '_session_id'
16
- self._session_expiry = Merb::Config[:session_expiry] || Merb::Const::WEEK * 2
17
- self._session_cookie_domain = Merb::Config[:session_cookie_domain]
18
-
19
13
  include Merb::ResponderMixin
20
14
  include Merb::ControllerMixin
21
15
  include Merb::AuthenticationMixin
16
+ include Merb::ConditionalGetMixin
22
17
 
23
18
  class << self
24
19
 
@@ -102,7 +97,7 @@ class Merb::Controller < Merb::AbstractController
102
97
  end
103
98
 
104
99
  private
105
-
100
+
106
101
  # All methods that are callable as actions.
107
102
  #
108
103
  # ==== Returns
@@ -140,15 +135,15 @@ class Merb::Controller < Merb::AbstractController
140
135
  def _template_location(context, type, controller)
141
136
  _conditionally_append_extension(controller ? "#{controller}/#{context}" : "#{context}", type)
142
137
  end
143
-
144
- # The location to look for a template and mime-type. This is overridden
145
- # from AbstractController, which defines a version of this that does not
138
+
139
+ # The location to look for a template and mime-type. This is overridden
140
+ # from AbstractController, which defines a version of this that does not
146
141
  # involve mime-types.
147
142
  #
148
143
  # ==== Parameters
149
- # template<String>::
144
+ # template<String>::
150
145
  # The absolute path to a template - without mime and template extension.
151
- # The mime-type extension is optional - it will be appended from the
146
+ # The mime-type extension is optional - it will be appended from the
152
147
  # current content type if it hasn't been added already.
153
148
  # type<~to_s>::
154
149
  # The mime-type of the template that will be rendered. Defaults to nil.
@@ -163,11 +158,8 @@ class Merb::Controller < Merb::AbstractController
163
158
  # Sets the variables that came in through the dispatch as available to
164
159
  # the controller.
165
160
  #
166
- # This method uses the :session_id_cookie_only and :query_string_whitelist
167
- # configuration options. See CONFIG for more details.
168
- #
169
161
  # ==== Parameters
170
- # request<Merb::Request>:: The Merb::Request that came in from Mongrel.
162
+ # request<Merb::Request>:: The Merb::Request that came in from Rack.
171
163
  # status<Integer>:: An integer code for the status. Defaults to 200.
172
164
  # headers<Hash{header => value}>::
173
165
  # A hash of headers to start the controller with. These headers can be
@@ -204,7 +196,7 @@ class Merb::Controller < Merb::AbstractController
204
196
  end
205
197
 
206
198
  attr_reader :request, :headers
207
-
199
+
208
200
  def status
209
201
  @_status
210
202
  end
@@ -227,20 +219,6 @@ class Merb::Controller < Merb::AbstractController
227
219
  # Hash:: The parameters from the request object
228
220
  def params() request.params end
229
221
 
230
- # ==== Returns
231
- # Merb::Cookies::
232
- # A new Merb::Cookies instance representing the cookies that came in
233
- # from the request object
234
- #
235
- # ==== Notes
236
- # Headers are passed into the cookie object so that you can do:
237
- # cookies[:foo] = "bar"
238
- def cookies() @_cookies ||= _setup_cookies end
239
-
240
- # ==== Returns
241
- # Hash:: The session that was extracted from the request object.
242
- def session() request.session end
243
-
244
222
  # The results of the controller's render, to be returned to Rack.
245
223
  #
246
224
  # ==== Returns
@@ -249,19 +227,14 @@ class Merb::Controller < Merb::AbstractController
249
227
  def rack_response
250
228
  [status, headers, body]
251
229
  end
252
-
230
+
253
231
  # Hide any methods that may have been exposed as actions before.
254
232
  hide_action(*_callable_methods)
255
-
233
+
256
234
  private
257
235
 
258
236
  # If not already added, add the proper mime extension to the template path.
259
237
  def _conditionally_append_extension(template, type)
260
238
  type && !template.match(/\.#{type.to_s.escape_regexp}$/) ? "#{template}.#{type}" : template
261
239
  end
262
-
263
- # Create a default cookie jar, and pre-set a fixation cookie if fixation is enabled.
264
- def _setup_cookies
265
- ::Merb::Cookies.new(request.cookies, @headers)
266
- end
267
- end
240
+ end
@@ -45,10 +45,27 @@ module Merb::AuthenticationMixin
45
45
  #
46
46
  # end
47
47
  #
48
+ # If you need to request basic authentication inside an action you need to use the request! method.
49
+ #
50
+ # ====Example
51
+ #
52
+ # class Sessions < Application
53
+ #
54
+ # def new
55
+ # case content_type
56
+ # when :html
57
+ # render
58
+ # else
59
+ # basic_authentication.request!
60
+ # end
61
+ # end
62
+ #
63
+ # end
64
+ #
48
65
  #---
49
66
  # @public
50
67
  def basic_authentication(realm = "Application", &authenticator)
51
- BasicAuthentication.new(self, realm, &authenticator)
68
+ @_basic_authentication ||= BasicAuthentication.new(self, realm, &authenticator)
52
69
  end
53
70
 
54
71
  class BasicAuthentication
@@ -58,24 +75,43 @@ module Merb::AuthenticationMixin
58
75
  def initialize(controller, realm = "Application", &authenticator)
59
76
  @controller = controller
60
77
  @realm = realm
78
+ @auth = Rack::Auth::Basic::Request.new(@controller.request.env)
61
79
  authenticate_or_request(&authenticator) if authenticator
62
80
  end
63
81
 
64
82
  def authenticate(&authenticator)
65
- auth = Rack::Auth::Basic::Request.new(@controller.request.env)
66
-
67
- if auth.provided? and auth.basic?
68
- authenticator.call(*auth.credentials)
83
+ if @auth.provided? and @auth.basic?
84
+ authenticator.call(*@auth.credentials)
69
85
  else
70
86
  false
71
87
  end
72
88
  end
73
89
 
74
90
  def request
75
- @controller.headers['WWW-Authenticate'] = 'Basic realm="%s"' % @realm
91
+ request!
76
92
  throw :halt, @controller.render("HTTP Basic: Access denied.\n", :status => Unauthorized.status, :layout => false)
77
93
  end
78
94
 
95
+ # This is a special case for use outside a before filter. Use this if you need to
96
+ # request basic authenticaiton as part of an action
97
+ def request!
98
+ @controller.status = Unauthorized.status
99
+ @controller.headers['WWW-Authenticate'] = 'Basic realm="%s"' % @realm
100
+ end
101
+
102
+ # Checks to see if there has been any basic authentication credentials provided
103
+ def provided?
104
+ @auth.provided?
105
+ end
106
+
107
+ def username
108
+ provided? ? @auth.credentials.first : nil
109
+ end
110
+
111
+ def password
112
+ provided? ? @auth.credentials.last : nil
113
+ end
114
+
79
115
  protected
80
116
 
81
117
  def authenticate_or_request(&authenticator)
@@ -0,0 +1,83 @@
1
+ # Provides conditional get support in Merb core.
2
+ # Conditional get support is intentionally
3
+ # simple and does not do fancy stuff like making
4
+ # ETag value from Ruby objects for you.
5
+ #
6
+ # The most interesting method for end user is
7
+ # +request_fresh?+ that is used after setting of
8
+ # last modification time or ETag:
9
+ #
10
+ # ==== Example
11
+ #
12
+ # def show
13
+ # self.etag = Digest::SHA1.hexdigest(calculate_cache_key(params))
14
+ #
15
+ # if request_fresh?
16
+ # self.status = 304
17
+ # return ''
18
+ # else
19
+ # @product = Product.get(params[:id])
20
+ # display @product
21
+ # end
22
+ # end
23
+ module Merb::ConditionalGetMixin
24
+
25
+ # Sets ETag response header by calling
26
+ # #to_s on the argument.
27
+ #
28
+ # ==== Parameters
29
+ # tag<~to_s>::
30
+ # value of ETag header enclosed in double quotes
31
+ # as required by the RFC
32
+ def etag=(tag)
33
+ headers[Merb::Const::ETAG] = %("#{tag}")
34
+ end
35
+
36
+ # ==== Returns
37
+ # <String>::
38
+ # Value of ETag response header or nil if it's not set.
39
+ def etag
40
+ headers[Merb::Const::ETAG]
41
+ end
42
+
43
+ # ==== Returns
44
+ # <Boolean>::
45
+ # true if ETag response header equals If-None-Match request header,
46
+ # false otherwise
47
+ def etag_matches?(tag = self.etag)
48
+ tag == self.request.if_none_match
49
+ end
50
+
51
+ # Sets Last-Modified response header.
52
+ #
53
+ # ==== Parameters
54
+ # tag<Time>::
55
+ # resource modification timestamp converted into format
56
+ # required by the RFC
57
+ def last_modified=(time)
58
+ headers[Merb::Const::LAST_MODIFIED] = time.httpdate
59
+ end
60
+
61
+ # ==== Returns
62
+ # <String>::
63
+ # Value of Last-Modified response header or nil if it's not set.
64
+ def last_modified
65
+ Time.rfc2822(headers[Merb::Const::LAST_MODIFIED])
66
+ end
67
+
68
+ # ==== Returns
69
+ # <Boolean>::
70
+ # true if Last-Modified response header is < than
71
+ # If-Modified-Since request header value, false otherwise.
72
+ def not_modified?(time = self.last_modified)
73
+ request.if_modified_since && time && time <= request.if_modified_since
74
+ end
75
+
76
+ # ==== Returns
77
+ # <Boolean>::
78
+ # true if either ETag matches or entity is not modified,
79
+ # so request is fresh; false otherwise
80
+ def request_fresh?
81
+ etag_matches?(self.etag) || not_modified?(self.last_modified)
82
+ end
83
+ end
@@ -41,7 +41,7 @@ module Merb::RenderMixin
41
41
  # ==== Returns
42
42
  # Hash:: The default render options.
43
43
  def layout(layout)
44
- self.default_render_options.update(:layout => (layout ? layout : false))
44
+ self.default_render_options.update(:layout => (layout || false))
45
45
  end
46
46
 
47
47
  # Enable the default layout logic - reset the layout option.
@@ -95,7 +95,7 @@ module Merb::RenderMixin
95
95
  thing ||= action_name.to_sym
96
96
 
97
97
  # Content negotiation
98
- opts[:format] ? (self.content_type = opts[:format]) : content_type
98
+ self.content_type = opts[:format] if opts[:format]
99
99
 
100
100
  # Handle options (:status)
101
101
  _handle_options!(opts)
@@ -189,7 +189,7 @@ module Merb::RenderMixin
189
189
  # explicitly passed in the opts.
190
190
  #
191
191
  def display(object, thing = nil, opts = {})
192
- template_opt = opts.delete(:template)
192
+ template_opt = thing.is_a?(Hash) ? thing.delete(:template) : opts.delete(:template)
193
193
 
194
194
  case thing
195
195
  # display @object, "path/to/foo" means display @object, nil, :template => "path/to/foo"
@@ -37,27 +37,14 @@ module Kernel
37
37
  # If the gem cannot be found, the method will attempt to require the string
38
38
  # as a library.
39
39
  def load_dependency(name, *ver)
40
- try_framework = Merb.frozen?
41
40
  begin
42
- # If this is a piece of merb, and we're frozen, try to require
43
- # first, so we can pick it up from framework/,
44
- # otherwise try activating the gem
45
- if name =~ /^merb/ && try_framework
46
- require name
47
- else
48
- gem(name, *ver) if ver
49
- require name
50
- Merb.logger.info!("loading gem '#{name}' ...")
51
- end
41
+ gem(name, *ver) if ver
42
+ require name
43
+ Merb.logger.info!("loading gem '#{name}' ...")
52
44
  rescue LoadError
53
- if try_framework
54
- try_framework = false
55
- retry
56
- else
57
- Merb.logger.info!("loading gem '#{name}' ...")
58
- # Failed requiring as a gem, let's try loading with a normal require.
59
- require name
60
- end
45
+ Merb.logger.info!("loading gem '#{name}' ...")
46
+ # Failed requiring as a gem, let's try loading with a normal require.
47
+ require name
61
48
  end
62
49
  end
63
50