merb 0.0.7 → 0.0.8

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 +66 -31
  2. data/Rakefile +3 -1
  3. data/bin/merb +47 -13
  4. data/examples/app_skeleton/Rakefile +4 -3
  5. data/examples/app_skeleton/dist/app/helpers/global_helper.rb +6 -0
  6. data/examples/app_skeleton/dist/conf/merb.yml +11 -0
  7. data/examples/app_skeleton/dist/conf/mup.conf +5 -0
  8. data/examples/app_skeleton/dist/conf/router.rb +1 -3
  9. data/examples/app_skeleton/scripts/merb_stop +10 -2
  10. data/examples/sample_app/Rakefile +3 -3
  11. data/examples/sample_app/dist/app/controllers/files.rb +3 -3
  12. data/examples/sample_app/dist/app/controllers/posts.rb +25 -23
  13. data/examples/sample_app/dist/app/controllers/test.rb +7 -3
  14. data/examples/sample_app/dist/app/helpers/global_helper.rb +7 -0
  15. data/examples/sample_app/dist/app/helpers/posts_helper.rb +4 -0
  16. data/examples/sample_app/dist/app/views/layout/application.herb +5 -4
  17. data/examples/sample_app/dist/app/views/layout/foo.herb +1 -1
  18. data/examples/sample_app/dist/app/views/posts/new.herb +9 -2
  19. data/examples/sample_app/dist/app/views/shared/_test.herb +1 -0
  20. data/examples/sample_app/dist/conf/merb.yml +7 -7
  21. data/examples/sample_app/dist/conf/merb_init.rb +8 -1
  22. data/examples/sample_app/dist/conf/mup.conf +5 -11
  23. data/examples/sample_app/dist/conf/router.rb +1 -1
  24. data/examples/sample_app/dist/public/test.html +5 -0
  25. data/examples/sample_app/dist/schema/migrations/002_add_sessions_table.rb +1 -1
  26. data/examples/sample_app/dist/schema/schema.rb +1 -1
  27. data/examples/sample_app/log/merb.4000.pid +1 -0
  28. data/lib/merb.rb +35 -17
  29. data/lib/merb/core_ext.rb +2 -0
  30. data/lib/merb/{merb_class_extensions.rb → core_ext/merb_class.rb} +42 -0
  31. data/lib/merb/core_ext/merb_enumerable.rb +7 -0
  32. data/lib/merb/{merb_utils.rb → core_ext/merb_hash.rb} +1 -78
  33. data/lib/merb/core_ext/merb_kernel.rb +16 -0
  34. data/lib/merb/core_ext/merb_module.rb +10 -0
  35. data/lib/merb/core_ext/merb_numeric.rb +20 -0
  36. data/lib/merb/core_ext/merb_object.rb +6 -0
  37. data/lib/merb/core_ext/merb_string.rb +40 -0
  38. data/lib/merb/core_ext/merb_symbol.rb +12 -0
  39. data/lib/merb/merb_constants.rb +18 -0
  40. data/lib/merb/merb_controller.rb +150 -76
  41. data/lib/merb/{session/merb_drb_server.rb → merb_drb_server.rb} +13 -46
  42. data/lib/merb/merb_exceptions.rb +4 -0
  43. data/lib/merb/merb_handler.rb +29 -17
  44. data/lib/merb/merb_request.rb +95 -0
  45. data/lib/merb/merb_upload_handler.rb +46 -0
  46. data/lib/merb/merb_upload_progress.rb +48 -0
  47. data/lib/merb/merb_view_context.rb +46 -0
  48. data/lib/merb/merb_yaml_store.rb +31 -0
  49. data/lib/merb/mixins/basic_authentication_mixin.rb +2 -2
  50. data/lib/merb/mixins/controller_mixin.rb +24 -75
  51. data/lib/merb/mixins/erubis_capture_mixin.rb +84 -0
  52. data/lib/merb/mixins/javascript_mixin.rb +103 -19
  53. data/lib/merb/mixins/merb_status_codes.rb +59 -0
  54. data/lib/merb/mixins/render_mixin.rb +114 -40
  55. data/lib/merb/mixins/responder_mixin.rb +2 -1
  56. data/lib/merb/session/merb_ar_session.rb +120 -0
  57. data/lib/merb/session/merb_drb_session.rb +0 -6
  58. data/lib/merb/vendor/paginator/paginator.rb +102 -99
  59. metadata +44 -8
  60. data/examples/sample_app/script/startdrb +0 -8
  61. data/lib/merb/session/merb_session.rb +0 -64
  62. data/lib/mutex_hotfix.rb +0 -34
@@ -0,0 +1,4 @@
1
+ module Merb
2
+ class Noroutefound < RuntimeError; end
3
+ class MissingControllerFile < RuntimeError; end
4
+ end
@@ -1,4 +1,3 @@
1
-
2
1
  class MerbHandler < Mongrel::HttpHandler
3
2
  @@file_only_methods = ["GET","HEAD"]
4
3
 
@@ -31,7 +30,7 @@ class MerbHandler < Mongrel::HttpHandler
31
30
  return
32
31
  end
33
32
 
34
- MERB_LOGGER.info("Request: PATH_INFO: #{request.params[Mongrel::Const::PATH_INFO]}")
33
+ MERB_LOGGER.info("\nRequest: PATH_INFO: #{request.params[Mongrel::Const::PATH_INFO]} (#{Time.now.strftime("%Y-%m-%d %H:%M:%S")})")
35
34
 
36
35
  # Rails style page caching. Check the public dir first for
37
36
  # .html pages and serve directly. Otherwise fall back to Merb
@@ -58,18 +57,16 @@ class MerbHandler < Mongrel::HttpHandler
58
57
  controller, action = handle(request)
59
58
  MERB_LOGGER.info("Routing to controller: #{controller.class} action: #{action}\nParsing HTTP Input took: #{Time.now - start} seconds")
60
59
 
61
- # special case allows the progress action of a Files controller
62
- # to be handled without locking since no db access is required.
63
- # but we do need to synchronize whenever you use ActiveRecord
64
- # in your controllers.
65
- if (action == 'progress') && (Files === controller)
66
- puts 'skip mutex'
60
+ # We need a mutex here because ActiveRecord code can be run
61
+ # in your controller actions. AR performs much better in single
62
+ # threaded mode so we lock here for the shortest amount of time
63
+ # possible. Route recognition and mime parsing has already occured
64
+ # at this point because those processes are thread safe. This
65
+ # gives us the best trade off for multi threaded performance
66
+ # of thread safe, and a lock around calls to your controller actions.
67
+ @guard.synchronize {
67
68
  controller.dispatch(action)
68
- else
69
- @guard.synchronize {
70
- controller.dispatch(action)
71
- }
72
- end
69
+ }
73
70
  rescue Exception => e
74
71
  response.start(500) do |head,out|
75
72
  head["Content-Type"] = "text/html"
@@ -98,10 +95,25 @@ class MerbHandler < Mongrel::HttpHandler
98
95
 
99
96
  if sendfile
100
97
  MERB_LOGGER.info("X-SENDFILE: #{sendfile}\nComplete Request took: #{Time.now - start} seconds")
101
- # send X-SENDFILE header to mongrel
102
- response.send_status(File.size(sendfile))
98
+ file_status = File.stat(sendfile)
99
+ response.status = 200
100
+ # Set the last modified times as well and etag for all files
101
+ response.header[Mongrel::Const::LAST_MODIFIED] = file_status.mtime.httpdate
102
+ # Calculated the same as apache, not sure how well the works on win32
103
+ response.header[Mongrel::Const::ETAG] = Mongrel::Const::ETAG_FORMAT % [file_status.mtime.to_i, file_status.size, file_status.ino]
104
+ # send a status with out content length
105
+ response.send_status(file_status.size)
103
106
  response.send_header
104
107
  response.send_file(sendfile)
108
+ elsif controller.body.respond_to? :read
109
+ response.send_status(clength)
110
+ response.send_header
111
+ while chunk = controller.body.read(16384)
112
+ response.write(chunk)
113
+ end
114
+ if controller.body.respond_to? :close
115
+ controller.body.close
116
+ end
105
117
  else
106
118
  MERB_LOGGER.info("Response status: #{response.status}\nComplete Request took: #{Time.now - start} seconds\n\n")
107
119
  # render response from successful controller
@@ -131,12 +143,12 @@ class MerbHandler < Mongrel::HttpHandler
131
143
  # this is where your Merb::Controller is instantiated.
132
144
  def instantiate_controller(controller_name, req, env, params)
133
145
  if !File.exist?(Merb::Server.config[:dist_root]+"/app/controllers/#{controller_name.snake_case}.rb")
134
- return Object.const_get(:Noroutefound).new(req, env, params)
146
+ raise Merb::MissingControllerFile
135
147
  end
136
148
  begin
137
149
  controller_name.import
138
150
  return Object.const_get( controller_name.camel_case ).new(req, env, params)
139
- rescue Exception
151
+ rescue RuntimeError
140
152
  warn "Error getting instance of '#{controller_name.camel_case}': #{$!}"
141
153
  raise $!
142
154
  end
@@ -0,0 +1,95 @@
1
+ module Merb
2
+
3
+ class Request
4
+
5
+ def initialize(env, method)
6
+ @env = env
7
+ @method = method
8
+ end
9
+
10
+ # returns true if the request is an ajax request.
11
+ def xml_http_request?
12
+ not /XMLHttpRequest/i.match(@env['HTTP_X_REQUESTED_WITH']).nil?
13
+ end
14
+ alias xhr? :xml_http_request?
15
+ alias ajax? :xml_http_request?
16
+
17
+ # returns the remote IP address if it can find it.
18
+ def remote_ip
19
+ return @env['HTTP_CLIENT_IP'] if @env.include?('HTTP_CLIENT_IP')
20
+
21
+ if @env.include?(Mongrel::Const::HTTP_X_FORWARDED_FOR) then
22
+ remote_ips = @env[Mongrel::Const::HTTP_X_FORWARDED_FOR].split(',').reject do |ip|
23
+ ip =~ /^unknown$|^(127|10|172\.16|192\.168)\./i
24
+ end
25
+
26
+ return remote_ips.first.strip unless remote_ips.empty?
27
+ end
28
+
29
+ return @env[Mongrel::Const::REMOTE_ADDR]
30
+ end
31
+
32
+ # returns either 'https://' or 'http://' depending on
33
+ # the HTTPS header
34
+ def protocol
35
+ ssl? ? 'https://' : 'http://'
36
+ end
37
+
38
+ # returns true if the request is an SSL request
39
+ def ssl?
40
+ @env['HTTPS'] == 'on' || @env['HTTP_X_FORWARDED_PROTO'] == 'https'
41
+ end
42
+
43
+ # returns he request uri.
44
+ def uri
45
+ @env['REQUEST_URI']
46
+ end
47
+
48
+ # returns the uri without the query string.
49
+ def path
50
+ uri ? uri.split('?').first : ''
51
+ end
52
+
53
+ # returns the PATH_INFO
54
+ def path_info
55
+ @env['PATH_INFO']
56
+ end
57
+
58
+ # returns the port the server is running on
59
+ def port
60
+ @env['SERVER_PORT'].to_i
61
+ end
62
+
63
+ # returns the full hostname including port
64
+ def host
65
+ @env['HTTP_X_FORWARDED_HOST'] || @env['HTTP_HOST']
66
+ end
67
+
68
+ # returns an array of all the subdomain parts of the host.
69
+ def subdomains(tld_length = 1)
70
+ parts = host.split('.')
71
+ parts[0..-(tld_length+2)]
72
+ end
73
+
74
+ # returns the full domain name without the port number.
75
+ def domain(tld_length = 1)
76
+ host.split('.').last(1 + tld_length).join('.').sub(/:\d+$/,'')
77
+ end
78
+
79
+ # returns the REQUEST_METHOD
80
+ def method
81
+ @method ||= @env['REQUEST_METHOD']
82
+ end
83
+
84
+ # create predicate methods for querying the REQUEST_METHOD
85
+ [:get, :post, :put, :delete, :head].each do |m|
86
+ eval %{
87
+ def #{m}?; method == :#{m}; end
88
+ }
89
+ end
90
+
91
+ end
92
+
93
+ end
94
+
95
+
@@ -0,0 +1,46 @@
1
+ require 'mongrel'
2
+
3
+
4
+ class MerbUploadHandler < Mongrel::HttpHandler
5
+
6
+ def initialize(options = {})
7
+ @path_info = Array(options[:path_info])
8
+ @frequency = options[:frequency] || 3
9
+ @request_notify = true
10
+ if options[:drb]
11
+ require 'drb'
12
+ DRb.start_service
13
+ Mongrel.const_set :Uploads, DRbObject.new(nil, "druby://#{Merb::Server.config[:host]}:#{Merb::Server.config[:session]}").upload_progress
14
+ else
15
+ require File.dirname(__FILE__)+'/merb_upload_progress'
16
+ Mongrel.const_set :Uploads, Merb::UploadProgress.new
17
+ end
18
+ Mongrel::Uploads.debug = true if options[:debug]
19
+ end
20
+
21
+ def request_begins(params)
22
+ upload_notify(:add, params, params[Mongrel::Const::CONTENT_LENGTH].to_i)
23
+ end
24
+
25
+ def request_progress(params, clen, total)
26
+ upload_notify(:mark, params, clen)
27
+ end
28
+
29
+ def process(request, response)
30
+ upload_notify(:finish, request.params)
31
+ end
32
+
33
+ private
34
+ def upload_notify(action, params, *args)
35
+ return unless @path_info.include?(params['PATH_INFO']) &&
36
+ params[Mongrel::Const::REQUEST_METHOD] == 'POST' &&
37
+ upload_id = Mongrel::HttpRequest.query_parse(params['QUERY_STRING'])['upload_id']
38
+ if action == :mark
39
+ last_checked_time = Mongrel::Uploads.last_checked(upload_id)
40
+ return unless last_checked_time && Time.now - last_checked_time > @frequency
41
+ end
42
+ Mongrel::Uploads.send(action, upload_id, *args)
43
+ Mongrel::Uploads.update_checked_time(upload_id) unless action == :finish
44
+ end
45
+ end
46
+
@@ -0,0 +1,48 @@
1
+ module Merb
2
+ # Keeps track of the status of all currently processing uploads
3
+ class UploadProgress
4
+ include DRbUndumped
5
+ attr_accessor :debug
6
+ def initialize
7
+ @guard = Mutex.new
8
+ @counters = {}
9
+ end
10
+
11
+ def check(upid)
12
+ @counters[upid].last rescue nil
13
+ end
14
+
15
+ def last_checked(upid)
16
+ @counters[upid].first rescue nil
17
+ end
18
+
19
+ def update_checked_time(upid)
20
+ @guard.synchronize { @counters[upid][0] = Time.now }
21
+ end
22
+
23
+ def add(upid, size)
24
+ @guard.synchronize do
25
+ @counters[upid] = [Time.now, {:size => size, :received => 0}]
26
+ puts "#{upid}: Added" if @debug
27
+ end
28
+ end
29
+
30
+ def mark(upid, len)
31
+ return unless status = check(upid)
32
+ puts "#{upid}: Marking" if @debug
33
+ @guard.synchronize { status[:received] = status[:size] - len }
34
+ end
35
+
36
+ def finish(upid)
37
+ @guard.synchronize do
38
+ puts "#{upid}: Finished" if @debug
39
+ @counters.delete(upid)
40
+ end
41
+ end
42
+
43
+ def list
44
+ @counters.keys.sort
45
+ end
46
+ end
47
+
48
+ end
@@ -0,0 +1,46 @@
1
+ require File.dirname(__FILE__)+'/mixins/erubis_capture_mixin'
2
+ require File.dirname(__FILE__)+'/mixins/javascript_mixin'
3
+
4
+ module Merb
5
+
6
+ # the ViewContext is really
7
+ # just an empty container for us to fill with instance
8
+ # variables from the controller, include helpers into
9
+ # and then use as the context object passed to Erubis
10
+ # when evaluating the templates.
11
+ class ViewContext
12
+ include Merb::ErubisCaptureMixin
13
+ include Merb::JavascriptMixin
14
+
15
+ def initialize(controller)
16
+ @controller = controller
17
+ (@controller.instance_variables - %w[@cookies @session @headers @params
18
+ @env @in @status @root @method @request @fingerprint_before
19
+ @controller @k]).each do |ivar|
20
+ self.instance_variable_set(ivar, @controller.instance_variable_get(ivar))
21
+ end
22
+ begin
23
+ self.class.class_eval("include Merb::#{@controller.class.name}Helper")
24
+ rescue NameError
25
+ MERB_LOGGER.info("Missing Helper: Merb::#{@controller.class.name}Helper")
26
+ end
27
+ end
28
+
29
+ # accessor for the view. refers to the current @controller object
30
+ def controller
31
+ @controller
32
+ end
33
+
34
+ # catch any method calls that the controller responds to
35
+ # and delegate them back to the controller.
36
+ def method_missing(sym, *args, &blk)
37
+ if @controller.respond_to? sym
38
+ @controller.send(sym, *args, &blk)
39
+ else
40
+ super
41
+ end
42
+ end
43
+
44
+ end
45
+
46
+ end
@@ -0,0 +1,31 @@
1
+ require 'yaml/store'
2
+
3
+ module Merb
4
+ # thanks to Michael Fellinger from Ramaze
5
+ class SimpleModel
6
+ attr_accessor :db
7
+
8
+ def initialize(filename = "#{DIST_ROOT}/schema/db.yaml")
9
+ FileUtils.touch(filename)
10
+ @db = YAML::Store.new(filename)
11
+ end
12
+
13
+ def method_missing(meth, *args, &block)
14
+ @db.transaction do
15
+ @db.send(meth, *args, &block)
16
+ end
17
+ end
18
+
19
+ def [](key)
20
+ @db.transaction do
21
+ @db[key]
22
+ end
23
+ end
24
+
25
+ def []=(key, value)
26
+ @db.transaction do
27
+ @db[key] = value
28
+ end
29
+ end
30
+ end
31
+ end
@@ -18,13 +18,13 @@ module Merb
18
18
 
19
19
  def authenticate
20
20
  if !authenticated?
21
- throw :halt
21
+ throw :halt, :access_denied
22
22
  end
23
23
  end
24
24
 
25
25
  def self.included(base)
26
26
  base.class_eval do
27
- def filters_halted
27
+ def access_denied
28
28
  @status = 401
29
29
  @headers['Content-type'] = 'text/plain'
30
30
  @headers['Status'] = 'Unauthorized'
@@ -14,8 +14,22 @@ module Merb
14
14
  # pass in a path to a file and this will set the
15
15
  # right headers and let mongrel do its thang and
16
16
  # serve the static file directly.
17
- def send_file(file)
18
- headers['X-SENDFILE'] = file
17
+ def send_file(file, opts={})
18
+ opts.update(Merb::Const::DEFAULT_SEND_FILE_OPTIONS.merge(opts))
19
+ [:type, :disposition].each do |arg|
20
+ raise ArgumentError, ":#{arg} option required" if opts[arg].nil?
21
+ end
22
+
23
+ disposition = opts[:disposition].dup || 'attachment'
24
+
25
+ disposition << %(; filename="#{opts[:filename] ? opts[:filename] : File.basename(file)}")
26
+
27
+ headers.update(
28
+ 'Content-Type' => opts[:type].strip, # fixes a problem with extra '\r' with some browsers
29
+ 'Content-Disposition' => disposition,
30
+ 'Content-Transfer-Encoding' => 'binary',
31
+ 'X-SENDFILE' => file
32
+ )
19
33
  return
20
34
  end
21
35
 
@@ -41,10 +55,18 @@ module Merb
41
55
  }
42
56
  end
43
57
 
58
+ # creates a random token like:
59
+ # "b9a82e011694cc13a4249731b9e83cea"
44
60
  def make_token
45
61
  require 'digest/md5'
46
62
  Digest::MD5.hexdigest("#{inspect}#{Time.now}#{rand}")
47
63
  end
64
+
65
+ def escape_xml(obj)
66
+ obj.to_s.gsub(/[&<>"']/) { |s| Merb::Const::ESCAPE_TABLE[s] }
67
+ end
68
+ alias h escape_xml
69
+ alias html_escape escape_xml
48
70
 
49
71
  # does url escaping
50
72
  def escape(s)
@@ -56,78 +78,5 @@ module Merb
56
78
  Mongrel::HttpRequest.unescape(s)
57
79
  end
58
80
 
59
- # returns true if the request is an ajax request.
60
- def xml_http_request?
61
- not /XMLHttpRequest/i.match(@env['HTTP_X_REQUESTED_WITH']).nil?
62
- end
63
- alias xhr? :xml_http_request?
64
- alias ajax? :xml_http_request?
65
-
66
- # returns the remote IP address if it can find it.
67
- def remote_ip
68
- return @env['HTTP_CLIENT_IP'] if @env.include?('HTTP_CLIENT_IP')
69
-
70
- if @env.include?(Mongrel::Const::HTTP_X_FORWARDED_FOR) then
71
- remote_ips = @env[Mongrel::Const::HTTP_X_FORWARDED_FOR].split(',').reject do |ip|
72
- ip =~ /^unknown$|^(127|10|172\.16|192\.168)\./i
73
- end
74
-
75
- return remote_ips.first.strip unless remote_ips.empty?
76
- end
77
-
78
- return @env[Mongrel::Const::REMOTE_ADDR]
79
- end
80
-
81
- # returns either 'https://' or 'http://' depending on
82
- # the HTTPS header
83
- def protocol
84
- @env['HTTPS'] == 'on' ? 'https://' : 'http://'
85
- end
86
-
87
- # returns true if the request is an SSL request
88
- def ssl?
89
- @env['HTTPS'] == 'on'
90
- end
91
-
92
- # The request uri.
93
- def uri
94
- @env['REQUEST_URI']
95
- end
96
-
97
- # The path is the uri without the query string.
98
- def path
99
- uri ? uri.split('?').first : ''
100
- end
101
-
102
- def path_info
103
- @env['PATH_INFO']
104
- end
105
-
106
- def port
107
- @env['SERVER_PORT'].to_i
108
- end
109
-
110
- def host
111
- @env['HTTP_X_FORWARDED_HOST'] || @env['HTTP_HOST']
112
- end
113
-
114
- def subdomains(tld_length = 1)
115
- parts = host.split('.')
116
- parts[0..-(tld_length+2)]
117
- end
118
-
119
- def domain(tld_length = 1)
120
- host.split('.').last(1 + tld_length).join('.')
121
- end
122
-
123
- def method
124
- @method ||= @env['REQUEST_METHOD']
125
- end
126
-
127
- [:get, :post, :put, :delete, :head].each do |m|
128
- eval %{
129
- def #{m}?; method == :#{m}; end
130
- }
131
- end
132
81
  end
133
82
  end