merb 0.3.4 → 0.3.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. data/README +206 -197
  2. data/Rakefile +12 -21
  3. data/bin/merb +1 -1
  4. data/examples/skeleton/Rakefile +6 -20
  5. data/examples/skeleton/dist/app/mailers/layout/application.erb +1 -0
  6. data/examples/skeleton/dist/conf/database.yml +23 -0
  7. data/examples/skeleton/dist/conf/environments/development.rb +1 -0
  8. data/examples/skeleton/dist/conf/environments/production.rb +1 -0
  9. data/examples/skeleton/dist/conf/environments/test.rb +1 -0
  10. data/examples/skeleton/dist/conf/merb.yml +32 -28
  11. data/examples/skeleton/dist/conf/merb_init.rb +16 -13
  12. data/examples/skeleton/dist/conf/router.rb +9 -9
  13. data/examples/skeleton/dist/schema/migrations/001_add_sessions_table.rb +2 -2
  14. data/lib/merb.rb +23 -18
  15. data/lib/merb/caching/fragment_cache.rb +3 -7
  16. data/lib/merb/caching/store/memcache.rb +20 -0
  17. data/lib/merb/core_ext/merb_array.rb +0 -0
  18. data/lib/merb/core_ext/merb_class.rb +44 -4
  19. data/lib/merb/core_ext/merb_enumerable.rb +43 -1
  20. data/lib/merb/core_ext/merb_hash.rb +200 -122
  21. data/lib/merb/core_ext/merb_kernel.rb +2 -0
  22. data/lib/merb/core_ext/merb_module.rb +41 -0
  23. data/lib/merb/core_ext/merb_numeric.rb +57 -5
  24. data/lib/merb/core_ext/merb_object.rb +172 -6
  25. data/lib/merb/generators/merb_app/merb_app.rb +15 -9
  26. data/lib/merb/merb_abstract_controller.rb +193 -0
  27. data/lib/merb/merb_constants.rb +26 -1
  28. data/lib/merb/merb_controller.rb +143 -234
  29. data/lib/merb/merb_dispatcher.rb +28 -20
  30. data/lib/merb/merb_drb_server.rb +2 -3
  31. data/lib/merb/merb_exceptions.rb +194 -49
  32. data/lib/merb/merb_handler.rb +34 -26
  33. data/lib/merb/merb_mail_controller.rb +200 -0
  34. data/lib/merb/merb_mailer.rb +33 -13
  35. data/lib/merb/merb_part_controller.rb +42 -0
  36. data/lib/merb/merb_plugins.rb +293 -0
  37. data/lib/merb/merb_request.rb +6 -4
  38. data/lib/merb/merb_router.rb +99 -65
  39. data/lib/merb/merb_server.rb +65 -21
  40. data/lib/merb/merb_upload_handler.rb +2 -1
  41. data/lib/merb/merb_view_context.rb +36 -15
  42. data/lib/merb/mixins/basic_authentication_mixin.rb +5 -5
  43. data/lib/merb/mixins/controller_mixin.rb +67 -28
  44. data/lib/merb/mixins/erubis_capture_mixin.rb +1 -8
  45. data/lib/merb/mixins/form_control_mixin.rb +280 -42
  46. data/lib/merb/mixins/render_mixin.rb +127 -45
  47. data/lib/merb/mixins/responder_mixin.rb +5 -7
  48. data/lib/merb/mixins/view_context_mixin.rb +260 -94
  49. data/lib/merb/session.rb +23 -0
  50. data/lib/merb/session/merb_ar_session.rb +28 -16
  51. data/lib/merb/session/merb_mem_cache_session.rb +108 -0
  52. data/lib/merb/session/merb_memory_session.rb +65 -20
  53. data/lib/merb/template/erubis.rb +22 -13
  54. data/lib/merb/template/haml.rb +5 -16
  55. data/lib/merb/template/markaby.rb +5 -3
  56. data/lib/merb/template/xml_builder.rb +17 -5
  57. data/lib/merb/test/merb_fake_request.rb +63 -0
  58. data/lib/merb/test/merb_multipart.rb +58 -0
  59. data/lib/tasks/db.rake +2 -0
  60. data/lib/tasks/merb.rake +20 -8
  61. metadata +24 -25
  62. data/examples/skeleton.tar +0 -0
@@ -21,5 +21,30 @@ module Merb
21
21
  HOUR = 60*60
22
22
  DAY = HOUR*24
23
23
  WEEK = DAY*7
24
+ MULTIPART_REGEXP = /\Amultipart\/form-data.*boundary=\"?([^\";,]+)/n.freeze
25
+ HTTP_COOKIE = 'HTTP_COOKIE'.freeze
26
+ QUERY_STRING = 'QUERY_STRING'.freeze
27
+ APPLICATION_JSON = 'application/json'.freeze
28
+ TEXT_JSON = 'text/x-json'.freeze
29
+ APPLICATION_XML = 'application/xml'.freeze
30
+ TEXT_XML = 'text/xml'.freeze
31
+ UPCASE_CONTENT_TYPE = 'CONTENT_TYPE'.freeze
32
+ CONTENT_TYPE = "Content-Type".freeze
33
+ LAST_MODIFIED = "Last-Modified".freeze
34
+ SLASH = "/".freeze
35
+ REQUEST_METHOD = "REQUEST_METHOD".freeze
36
+ GET = "GET".freeze
37
+ POST = "POST".freeze
38
+ HEAD = "HEAD".freeze
39
+ CONTENT_LENGTH = "CONTENT_LENGTH".freeze
40
+ HTTP_X_FORWARDED_FOR = "HTTP_X_FORWARDED_FOR".freeze
41
+ HTTP_IF_MODIFIED_SINCE = "HTTP_IF_MODIFIED_SINCE".freeze
42
+ HTTP_IF_NONE_MATCH = "HTTP_IF_NONE_MATCH".freeze
43
+ UPLOAD_ID = 'upload_id'.freeze
44
+ PATH_INFO="PATH_INFO".freeze
45
+ SCRIPT_NAME="SCRIPT_NAME".freeze
46
+ REQUEST_URI='REQUEST_URI'.freeze
47
+ REQUEST_PATH='REQUEST_PATH'.freeze
48
+ REMOTE_ADDR="REMOTE_ADDR".freeze
24
49
  end
25
- end
50
+ end
@@ -1,297 +1,206 @@
1
1
  require File.dirname(__FILE__)+'/mixins/controller_mixin'
2
- require File.dirname(__FILE__)+'/mixins/render_mixin'
3
2
  require File.dirname(__FILE__)+'/mixins/responder_mixin'
4
3
  require File.dirname(__FILE__)+'/merb_request'
5
-
4
+ require File.dirname(__FILE__)+'/merb_exceptions'
5
+ require 'set'
6
6
  module Merb
7
7
 
8
- # All of your controllers will inherit from Merb::Controller. This
8
+ # All of your web controllers will inherit from Merb::Controller. This
9
9
  # superclass takes care of parsing the incoming headers and body into
10
10
  # params and cookies and headers. If the request is a file upload it will
11
11
  # stream it into a tempfile and pass in the filename and tempfile object
12
12
  # to your controller via params. It also parses the ?query=string and
13
13
  # puts that into params as well.
14
- class Controller
15
-
16
- class_inheritable_accessor :_layout,
17
- :_session_id_key,
18
- :_template_extensions,
19
- :_template_root,
20
- :_layout_root
21
- self._layout = :application
14
+ class Controller < AbstractController
15
+
16
+ class_inheritable_accessor :_session_id_key, :_session_expiry
22
17
  self._session_id_key = :_session_id
23
- self._template_extensions = { }
24
- self._template_root = File.expand_path(MERB_VIEW_ROOT)
25
- self._layout_root = File.expand_path(MERB_VIEW_ROOT / "layout")
26
-
18
+ self._session_expiry = Time.now + Merb::Const::WEEK * 2
19
+
27
20
  include Merb::ControllerMixin
28
- include Merb::RenderMixin
29
21
  include Merb::ResponderMixin
30
-
31
- attr_accessor :status, :body, :request
22
+ include Merb::ControllerExceptions::HTTPErrors
23
+
24
+ class << self
25
+ def callable_actions
26
+ @callable_actions ||= Set.new(public_instance_methods - hidden_actions)
27
+ end
28
+
29
+ def hidden_actions
30
+ write_inheritable_attribute(:hidden_actions, Merb::Controller.public_instance_methods) unless read_inheritable_attribute(:hidden_actions)
31
+ read_inheritable_attribute(:hidden_actions)
32
+ end
33
+
34
+ # Hide each of the given methods from being callable as actions.
35
+ def hide_action(*names)
36
+ write_inheritable_attribute(:hidden_actions, hidden_actions | names.collect { |n| n.to_s })
37
+ end
38
+
39
+ def build(req, env, args, resp)
40
+ cont = new
41
+ cont.parse_request(req, env, args, resp)
42
+ cont
43
+ end
44
+ end
32
45
 
33
- MULTIPART_REGEXP = /\Amultipart\/form-data.*boundary=\"?([^\";,]+)/n.freeze
34
-
35
- # parses the http request into params, headers and cookies
36
- # that you can use in your controller classes. Also handles
37
- # file uploads by writing a tempfile and passing a reference
38
- # in params.
39
- def initialize(request, env, args, response)
40
- @env = MerbHash[env.to_hash]
41
- @status, @method, @response, @headers = 200, (env[Mongrel::Const::REQUEST_METHOD]||Mongrel::Const::GET).downcase.to_sym, response,
46
+ # Parses the http request into params, headers and cookies that you can use
47
+ # in your controller classes. Also handles file uploads by writing a
48
+ # tempfile and passing a reference in params.
49
+ def parse_request(req, env, args, resp)
50
+ env = env.to_hash
51
+ @_status, method, @_response, @_headers = 200, (env[Merb::Const::REQUEST_METHOD]||Merb::Const::GET).downcase.to_sym, resp,
42
52
  {'Content-Type' =>'text/html'}
43
- cookies = query_parse(@env[Mongrel::Const::HTTP_COOKIE], ';,')
44
- querystring = query_parse(@env[Mongrel::Const::QUERY_STRING])
53
+ cookies = query_parse(env[Merb::Const::HTTP_COOKIE], ';,')
54
+ querystring = query_parse(env[Merb::Const::QUERY_STRING])
45
55
 
46
- if MULTIPART_REGEXP =~ @env[Mongrel::Const::UPCASE_CONTENT_TYPE] && @method == :post
47
- querystring.update(parse_multipart(request, $1))
48
- elsif @method == :post
49
- if [Mongrel::Const::APPLICATION_JSON, Mongrel::Const::TEXT_JSON].include?(@env[Mongrel::Const::UPCASE_CONTENT_TYPE])
56
+ if Merb::Const::MULTIPART_REGEXP =~ env[Merb::Const::UPCASE_CONTENT_TYPE] && [:put,:post].include?(method)
57
+ querystring.update(parse_multipart(req, $1, env))
58
+ elsif [:post, :put].include?(method)
59
+ if [Merb::Const::APPLICATION_JSON, Merb::Const::TEXT_JSON].include?(env[Merb::Const::UPCASE_CONTENT_TYPE])
50
60
  MERB_LOGGER.info("JSON Request")
51
- json = JSON.parse(request.read || "") || {}
52
- json = MerbHash.new(json) if json.is_a? Hash
61
+ json = JSON.parse(req.read || "") || {}
53
62
  querystring.update(json)
54
- elsif [Mongrel::Const::APPLICATION_XML, Mongrel::Const::TEXT_XML].include?(@env[Mongrel::Const::UPCASE_CONTENT_TYPE])
55
- querystring.update(Hash.from_xml(request.read).with_indifferent_access)
63
+ elsif [Merb::Const::APPLICATION_XML, Merb::Const::TEXT_XML].include?(env[Merb::Const::UPCASE_CONTENT_TYPE])
64
+ querystring.update(Hash.from_xml(req.read).with_indifferent_access)
56
65
  else
57
- querystring.update(query_parse(request.read))
66
+ querystring.update(query_parse(req.read))
58
67
  end
59
68
  end
60
69
 
61
- @cookies, @params = cookies, querystring.update(args)
62
- @cookies[_session_id_key] = @params[_session_id_key] if @params.key?(_session_id_key)
70
+ @_cookies, @_params = cookies.symbolize_keys!, querystring.update(args).symbolize_keys!
71
+
72
+ if @_params.key?(_session_id_key) && !Merb::Server.config[:session_id_cookie_only]
73
+ @_cookies[_session_id_key] = @_params[_session_id_key]
74
+ elsif @_params.key?(_session_id_key) && Merb::Server.config[:session_id_cookie_only]
75
+ # This condition allows for certain controller/action paths to allow a
76
+ # session ID to be passed in a query string. This is needed for Flash
77
+ # Uploads to work since flash will not pass a Session Cookie Recommend
78
+ # running session.regenerate after any controller taking advantage of
79
+ # this in case someone is attempting a session fixation attack
80
+ @_cookies[_session_id_key] = @_params[_session_id_key] if Merb::Server.config[:query_string_whitelist].include?("#{params[:controller]}/#{params[:action]}")
81
+ end
63
82
 
83
+ # Handle alternate HTTP method passed as _method parameter. Doesn't allow
84
+ # method to be overridden for :get unless Merb is in development mode.
85
+ #
86
+ # i.e. You can pass _method=put on the querystring if you are in
87
+ # development mode.
64
88
  allow = [:post, :put, :delete]
65
89
  allow << :get if MERB_ENV == 'development'
66
- if @params.key?(:_method) && allow.include?(@method)
67
- @method = @params.delete(:_method).downcase.intern
90
+ if @_params.key?(:_method) && allow.include?(method)
91
+ method = @_params.delete(:_method).downcase.intern
68
92
  end
69
- @request = Request.new(@env, @method, request)
70
-
93
+ @_request = Request.new(env, method, req)
71
94
  MERB_LOGGER.info("Params: #{params.inspect}\nCookies: #{cookies.inspect}")
72
95
  end
96
+
73
97
 
74
- def dispatch(action=:to_s)
98
+
99
+ def dispatch(action=:index)
75
100
  start = Time.now
76
- setup_session if respond_to?:setup_session
77
- cought = catch(:halt) { call_filters(before_filters) }
78
- @body = case cought
79
- when :filter_chain_completed
80
- send(action)
81
- when String
82
- cought
83
- when nil
84
- filters_halted
85
- when Symbol
86
- send(cought)
87
- when Proc
88
- cought.call(self)
89
- else
90
- raise MerbControllerError, "The before filter chain is broken dude. wtf?"
101
+ begin
102
+ if !self.class.callable_actions.include?(action.to_s)
103
+ raise NotFound
104
+ MERB_LOGGER.info "Action: #{action} not in callable_actions: #{self.class.callable_actions}"
105
+ else
106
+ setup_session
107
+ super(action)
108
+ finalize_session
109
+ end
110
+ rescue ControllerExceptions::Base => e
111
+ e.set_controller(self) # for access to session, params, etc
112
+ @_body = e.call_action
113
+ set_status(e.status)
91
114
  end
92
- call_filters(after_filters)
93
- finalize_session if respond_to?:finalize_session
94
- MERB_LOGGER.info("Time spent in #{self.class}##{action} action: #{Time.now - start} seconds")
115
+
116
+ @_benchmarks[:action_time] = Time.now - start
117
+ MERB_LOGGER.info("Time spent in #{self.class}##{action} action: #{@_benchmarks[:action_time]} seconds")
118
+ end
119
+
120
+ # Accessor for @_body. Please use status and never @status directly.
121
+ def body
122
+ @_body
95
123
  end
96
124
 
97
- # override this method on your controller classes to specialize
98
- # the output when the filter chain is halted.
99
- def filters_halted
100
- "<html><body><h1>Filter Chain Halted!</h1></body></html>"
125
+ # Accessor for @_status. Please use status and never @_status directly.
126
+ def status
127
+ @_status
101
128
  end
102
129
 
103
- # accessor for @request. Please use request and
104
- # never @request directly.
130
+
131
+ # Accessor for @_request. Please use request and never @_request directly.
105
132
  def request
106
- @request
133
+ @_request
107
134
  end
108
-
109
- # accessor for @params. Please use params and
110
- # never @params directly.
135
+
136
+ # Accessor for @_params. Please use params and never @_params directly.
111
137
  def params
112
- @params
113
- end
138
+ @_params
139
+ end
114
140
 
115
- # accessor for @cookies. Please use cookies and
116
- # never @cookies directly.
141
+ # Accessor for @_cookies. Please use cookies and never @_cookies directly.
117
142
  def cookies
118
- @cookies
119
- end
120
-
121
- # accessor for @headers. Please use headers and
122
- # never @headers directly.
143
+ @_cookies
144
+ end
145
+
146
+ # Accessor for @_headers. Please use headers and never @_headers directly.
123
147
  def headers
124
- @headers
148
+ @_headers
125
149
  end
126
150
 
127
- # accessor for @session. Please use session and
128
- # never @session directly.
151
+ # Accessor for @_session. Please use session and never @_session directly.
129
152
  def session
130
- @session
153
+ @_session
131
154
  end
132
155
 
133
- # accessor for @response. Please use response and
134
- # never @response directly.
156
+ # Accessor for @_response. Please use response and never @_response directly.
135
157
  def response
136
- @response
158
+ @_response
137
159
  end
138
160
 
139
-
140
- # lookup the trait[:template_extensions] for the extname of the filename
141
- # you pass. Answers with the engine that matches the extension, Template::Erubis
142
- # is used if none matches.
143
- def engine_for(file)
144
- extension = File.extname(file)[1..-1]
145
- _template_extensions[extension]
146
- rescue
147
- ::Merb::Template::Erubis
148
- end
149
-
150
- # This method is called by templating-engines to register themselves with
151
- # a list of extensions that will be looked up on #render of an action.
152
- def self.register_engine(engine, *extensions)
153
- [extensions].flatten.uniq.each do |ext|
154
- _template_extensions[ext] = engine
155
- end
161
+ # Sends a mail from a MailController
162
+ #
163
+ # send_mail FooMailer, :bar, :from => "foo@bar.com", :to => "baz@bat.com"
164
+ #
165
+ # would send an email via the FooMailer's bar method.
166
+ #
167
+ # The mail_params hash would be sent to the mailer, and includes items
168
+ # like from, to subject, and cc. See
169
+ # Merb::MailController#dispatch_and_deliver for more details.
170
+ #
171
+ # The send_params hash would be sent to the MailController, and is
172
+ # available to methods in the MailController as <tt>params</tt>. If you do
173
+ # not send any send_params, this controller's params will be available to
174
+ # the MailController as <tt>params</tt>
175
+ def send_mail(klass, method, mail_params, send_params = nil)
176
+ klass.new(send_params || params, self).dispatch_and_deliver(method, mail_params)
156
177
  end
157
-
158
-
159
-
160
- # shared_accessor sets up a class instance variable that can
161
- # be unique for each class but also inherits the shared attrs
162
- # from its superclasses. Since @@class variables are almost
163
- # global vars within an inheritance tree, we use
164
- # @class_instance_variables instead
165
- class_inheritable_accessor :before_filters
166
- class_inheritable_accessor :after_filters
167
-
168
- # calls a filter chain according to rules.
169
- def call_filters(filter_set)
170
- (filter_set || []).each do |(filter, rule)|
171
- ok = false
172
- if rule.has_key?(:only)
173
- if rule[:only].include?(params[:action].intern)
174
- ok = true
175
- end
176
- elsif rule.has_key?(:exclude)
177
- if !rule[:exclude].include?(params[:action].intern)
178
- ok = true
179
- end
180
- else
181
- ok = true
182
- end
183
- if ok
184
- case filter
185
- when Symbol, String
186
- send(filter)
187
- when Proc
188
- filter.call(self)
189
- end
190
- end
191
- end
192
- return :filter_chain_completed
193
- end
194
178
 
195
- # #before is a class method that allows you to specify before
196
- # filters in your controllers. Filters can either be a symbol
197
- # or string that corresponds to a method name to call, or a
198
- # proc object. if it is a method name that method will be
199
- # called and if it is a proc it will be called with an argument
200
- # of self where self is the current controller object. When
201
- # you use a proc as a filter it needs to take one parameter.
179
+ # Dispatches a PartController. Use like:
202
180
  #
203
- # examples:
204
- # before :some_filter
205
- # before :authenticate, :exclude => [:login, :signup]
206
- # before Proc.new {|c| c.some_method }, :only => :foo
181
+ # <%= part TodoPart => :list %>
207
182
  #
208
- # You can use either :only => :actionname or :exclude => [:this, :that]
209
- # but not both at once. :only will only run before the listed actions
210
- # and :exclude will run for every action that is not listed.
183
+ # will instantiate a new TodoPart controller and call the :list action
184
+ # invoking the Part's before and after filters as part of the call.
211
185
  #
212
- # Merb's before filter chain is very flixible. To halt the
213
- # filter chain you use throw :halt . If throw is called with
214
- # only one argument of :halt the return of the method filters_halted
215
- # will be what is rendered to the view. You can overide filters_halted
216
- # in your own controllers to control what it outputs. But the throw
217
- # construct is much more powerful then just that. throw :halt can
218
- # also take a second argument. Here is what that second arg can be
219
- # and the behavior each type can have:
186
+ # returns a string containing the results of the Part controllers dispatch
220
187
  #
221
- # when the second arg is a string then that string will be what
222
- # is rendered to the browser. Since merb's render method returns
223
- # a string you can render a template or just use a plain string:
188
+ # You can compose parts easily as well, these two parts will stil be wrapped
189
+ # in the layout of the Foo controller:
224
190
  #
225
- # String:
226
- # throw :halt, "You don't have permissions to do that!"
227
- # throw :halt, render(:action => :access_denied)
191
+ # class Foo < Application
192
+ # def some_action
193
+ # wrap_layout(part(TodoPart => :new) + part(TodoPart => :list))
194
+ # end
195
+ #end
228
196
  #
229
- # if the second arg is a symbol then the method named after that
230
- # symbol will be called
231
- # Symbol:
232
- # throw :halt, :must_click_disclaimer
233
- #
234
- # If the second arg is a Proc, it will be called and its return
235
- # value will be what is rendered to the browser:
236
- # Proc:
237
- # throw :halt, Proc.new {|c| c.access_denied }
238
- # throw :halt, Proc.new {|c| Tidy.new(c.index) }
239
- #
240
- def self.before(filter, opts={})
241
- raise(ArgumentError,
242
- "You can specify either :only or :exclude but
243
- not both at the same time for the same filter."
244
- ) if opts.has_key?(:only) && opts.has_key?(:exclude)
245
-
246
- opts = shuffle_filters!(opts)
247
-
248
- case filter
249
- when Symbol, String, Proc
250
- (self.before_filters ||= []) << [filter, opts]
251
- else
252
- raise(ArgumentError,
253
- 'filters need to be either a Symbol, String or a Proc'
254
- )
255
- end
256
- end
257
-
258
- # #after is a class method that allows you to specify after
259
- # filters in your controllers. Filters can either be a symbol
260
- # or string that corresponds to a method name or a proc object.
261
- # if it is a method name that method will be called and if it
262
- # is a proc it will be called with an argument of self. When
263
- # you use a proc as a filter it needs to take one parameter.
264
- # you can gain access to the response body like so:
265
- # after Proc.new {|c| Tidy.new(c.body) }, :only => :index
266
- #
267
- def self.after(filter, opts={})
268
- raise(ArgumentError,
269
- "You can specify either :only or :exclude but
270
- not both at the same time for the same filter."
271
- ) if opts.has_key?(:only) && opts.has_key?(:exclude)
272
-
273
- opts = shuffle_filters!(opts)
274
-
275
- case filter
276
- when Symbol, Proc, String
277
- (self.after_filters ||= []) << [filter, opts]
278
- else
279
- raise(ArgumentError,
280
- 'After filters need to be either a Symbol, String or a Proc'
281
- )
197
+ def part(opts={})
198
+ res = opts.inject([]) do |memo,(klass,action)|
199
+ memo << klass.new(self).dispatch(action)
282
200
  end
201
+ res.size == 1 ? res[0] : res
283
202
  end
284
203
 
285
- def self.shuffle_filters!(opts={})
286
- if opts[:only] && opts[:only].is_a?(Symbol)
287
- opts[:only] = [opts[:only]]
288
- end
289
- if opts[:exclude] && opts[:exclude].is_a?(Symbol)
290
- opts[:exclude] = [opts[:exclude]]
291
- end
292
- return opts
293
- end
294
-
295
204
  end
296
205
 
297
206
  end