merb 0.0.7 → 0.0.8

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 +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