merb 0.3.4 → 0.3.7

Sign up to get free protection for your applications and to get access to all the features.
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