sinatra-base 1.0 → 1.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (101) hide show
  1. data/.yardopts +4 -0
  2. data/AUTHORS +15 -0
  3. data/CHANGES +524 -1
  4. data/Gemfile +82 -0
  5. data/LICENSE +1 -1
  6. data/README.de.rdoc +2093 -0
  7. data/README.es.rdoc +2091 -0
  8. data/README.fr.rdoc +2116 -0
  9. data/README.hu.rdoc +607 -0
  10. data/README.jp.rdoc +514 -23
  11. data/README.pt-br.rdoc +647 -0
  12. data/README.pt-pt.rdoc +646 -0
  13. data/README.rdoc +1580 -205
  14. data/README.ru.rdoc +2015 -0
  15. data/README.zh.rdoc +1816 -0
  16. data/Rakefile +110 -44
  17. data/examples/chat.rb +61 -0
  18. data/examples/simple.rb +3 -0
  19. data/examples/stream.ru +26 -0
  20. data/lib/sinatra.rb +0 -3
  21. data/lib/sinatra/base.rb +923 -393
  22. data/lib/sinatra/main.rb +9 -7
  23. data/lib/sinatra/showexceptions.rb +37 -4
  24. data/lib/sinatra/version.rb +3 -0
  25. data/sinatra-base.gemspec +15 -91
  26. data/test/base_test.rb +2 -2
  27. data/test/builder_test.rb +32 -2
  28. data/test/coffee_test.rb +92 -0
  29. data/test/contest.rb +62 -28
  30. data/test/creole_test.rb +65 -0
  31. data/test/delegator_test.rb +162 -0
  32. data/test/encoding_test.rb +20 -0
  33. data/test/erb_test.rb +25 -2
  34. data/test/extensions_test.rb +1 -1
  35. data/test/filter_test.rb +226 -8
  36. data/test/haml_test.rb +8 -2
  37. data/test/helper.rb +47 -0
  38. data/test/helpers_test.rb +1287 -80
  39. data/test/integration/app.rb +62 -0
  40. data/test/integration_helper.rb +208 -0
  41. data/test/integration_test.rb +82 -0
  42. data/test/less_test.rb +36 -6
  43. data/test/liquid_test.rb +59 -0
  44. data/test/mapped_error_test.rb +84 -7
  45. data/test/markaby_test.rb +80 -0
  46. data/test/markdown_test.rb +81 -0
  47. data/test/middleware_test.rb +1 -1
  48. data/test/nokogiri_test.rb +69 -0
  49. data/test/rack_test.rb +45 -0
  50. data/test/radius_test.rb +59 -0
  51. data/test/rdoc_test.rb +66 -0
  52. data/test/readme_test.rb +136 -0
  53. data/test/request_test.rb +13 -1
  54. data/test/response_test.rb +21 -2
  55. data/test/result_test.rb +5 -5
  56. data/test/route_added_hook_test.rb +1 -1
  57. data/test/routing_test.rb +328 -13
  58. data/test/sass_test.rb +48 -18
  59. data/test/scss_test.rb +88 -0
  60. data/test/server_test.rb +4 -3
  61. data/test/settings_test.rb +191 -21
  62. data/test/sinatra_test.rb +5 -1
  63. data/test/slim_test.rb +88 -0
  64. data/test/static_test.rb +89 -5
  65. data/test/streaming_test.rb +140 -0
  66. data/test/templates_test.rb +143 -4
  67. data/test/textile_test.rb +65 -0
  68. data/test/views/a/in_a.str +1 -0
  69. data/test/views/ascii.erb +2 -0
  70. data/test/views/b/in_b.str +1 -0
  71. data/test/views/calc.html.erb +1 -0
  72. data/test/views/explicitly_nested.str +1 -0
  73. data/test/views/hello.coffee +1 -0
  74. data/test/views/hello.creole +1 -0
  75. data/test/views/hello.liquid +1 -0
  76. data/test/views/hello.mab +1 -0
  77. data/test/views/hello.md +1 -0
  78. data/test/views/hello.nokogiri +1 -0
  79. data/test/views/hello.radius +1 -0
  80. data/test/views/hello.rdoc +1 -0
  81. data/test/views/hello.sass +1 -1
  82. data/test/views/hello.scss +3 -0
  83. data/test/views/hello.slim +1 -0
  84. data/test/views/hello.str +1 -0
  85. data/test/views/hello.textile +1 -0
  86. data/test/views/hello.yajl +1 -0
  87. data/test/views/layout2.liquid +2 -0
  88. data/test/views/layout2.mab +2 -0
  89. data/test/views/layout2.nokogiri +3 -0
  90. data/test/views/layout2.radius +2 -0
  91. data/test/views/layout2.slim +3 -0
  92. data/test/views/layout2.str +2 -0
  93. data/test/views/nested.str +1 -0
  94. data/test/views/utf8.erb +2 -0
  95. data/test/yajl_test.rb +80 -0
  96. metadata +126 -91
  97. data/lib/sinatra/tilt.rb +0 -746
  98. data/test/erubis_test.rb +0 -82
  99. data/test/views/error.erubis +0 -3
  100. data/test/views/hello.erubis +0 -1
  101. data/test/views/layout2.erubis +0 -2
data/Rakefile CHANGED
@@ -1,58 +1,130 @@
1
1
  require 'rake/clean'
2
2
  require 'rake/testtask'
3
3
  require 'fileutils'
4
+ require 'date'
5
+
6
+ # CI Reporter is only needed for the CI
7
+ begin
8
+ require 'ci/reporter/rake/test_unit'
9
+ rescue LoadError
10
+ end
4
11
 
5
12
  task :default => :test
6
13
  task :spec => :test
7
14
 
15
+ CLEAN.include "**/*.rbc"
16
+
8
17
  def source_version
9
- line = File.read('lib/sinatra/base.rb')[/^\s*VERSION = .*/]
10
- line.match(/.*VERSION = '(.*)'/)[1]
18
+ @source_version ||= begin
19
+ load './lib/sinatra/version.rb'
20
+ Sinatra::VERSION
21
+ end
22
+ end
23
+
24
+ def prev_feature
25
+ source_version.gsub(/^(\d\.)(\d+)\..*$/) { $1 + ($2.to_i - 1).to_s }
26
+ end
27
+
28
+ def prev_version
29
+ return prev_feature + '.0' if source_version.end_with? '.0'
30
+ source_version.gsub(/\d+$/) { |s| s.to_i - 1 }
11
31
  end
12
32
 
13
33
  # SPECS ===============================================================
14
34
 
35
+ task :test do
36
+ ENV['LANG'] = 'C'
37
+ ENV.delete 'LC_CTYPE'
38
+ end
39
+
15
40
  Rake::TestTask.new(:test) do |t|
16
41
  t.test_files = FileList['test/*_test.rb']
17
- t.ruby_opts = ['-rubygems -I.'] if defined? Gem
42
+ t.ruby_opts = ['-rubygems'] if defined? Gem
43
+ t.ruby_opts << '-I.'
44
+ end
45
+
46
+ Rake::TestTask.new(:"test:core") do |t|
47
+ core_tests = %w[base delegator encoding extensions filter
48
+ helpers mapped_error middleware radius rdoc
49
+ readme request response result route_added_hook
50
+ routing server settings sinatra static templates]
51
+ t.test_files = core_tests.map {|n| "test/#{n}_test.rb"}
52
+ t.ruby_opts = ["-rubygems"] if defined? Gem
53
+ t.ruby_opts << "-I."
18
54
  end
19
55
 
20
56
  # Rcov ================================================================
57
+
21
58
  namespace :test do
22
59
  desc 'Mesures test coverage'
23
60
  task :coverage do
24
61
  rm_f "coverage"
25
- rcov = "rcov --text-summary --test-unit-only -Ilib"
26
- system("#{rcov} --no-html --no-color test/*_test.rb")
62
+ sh "rcov -Ilib test/*_test.rb"
27
63
  end
28
64
  end
29
65
 
30
66
  # Website =============================================================
31
- # Building docs requires HAML and the hanna gem:
32
- # gem install mislav-hanna --source=http://gems.github.com
33
67
 
34
68
  desc 'Generate RDoc under doc/api'
35
69
  task 'doc' => ['doc:api']
70
+ task('doc:api') { sh "yardoc -o doc/api" }
71
+ CLEAN.include 'doc/api'
36
72
 
37
- task 'doc:api' => ['doc/api/index.html']
38
-
39
- file 'doc/api/index.html' => FileList['lib/**/*.rb','README.rdoc'] do |f|
40
- require 'rbconfig'
41
- hanna = RbConfig::CONFIG['ruby_install_name'].sub('ruby', 'hanna')
42
- rb_files = f.prerequisites
43
- sh((<<-end).gsub(/\s+/, ' '))
44
- #{hanna}
45
- --charset utf8
46
- --fmt html
47
- --inline-source
48
- --line-numbers
49
- --main README.rdoc
50
- --op doc/api
51
- --title 'Sinatra API Documentation'
52
- #{rb_files.join(' ')}
73
+ # README ===============================================================
74
+
75
+ task :add_template, [:name] do |t, args|
76
+ Dir.glob('README.*') do |file|
77
+ code = File.read(file)
78
+ if code =~ /^===.*#{args.name.capitalize}/
79
+ puts "Already covered in #{file}"
80
+ else
81
+ template = code[/===[^\n]*Liquid.*index\.liquid<\/tt>[^\n]*/m]
82
+ if !template
83
+ puts "Liquid not found in #{file}"
84
+ else
85
+ puts "Adding section to #{file}"
86
+ template = template.gsub(/Liquid/, args.name.capitalize).gsub(/liquid/, args.name.downcase)
87
+ code.gsub! /^(\s*===.*CoffeeScript)/, "\n" << template << "\n\\1"
88
+ File.open(file, "w") { |f| f << code }
89
+ end
90
+ end
53
91
  end
54
92
  end
55
- CLEAN.include 'doc/api'
93
+
94
+ # Thanks in announcement ===============================================
95
+
96
+ team = ["Ryan Tomayko", "Blake Mizerany", "Simon Rozet", "Konstantin Haase"]
97
+ desc "list of contributors"
98
+ task :thanks, [:release,:backports] do |t, a|
99
+ a.with_defaults :release => "#{prev_version}..HEAD",
100
+ :backports => "#{prev_feature}.0..#{prev_feature}.x"
101
+ included = `git log --format=format:"%aN\t%s" #{a.release}`.lines.to_a
102
+ excluded = `git log --format=format:"%aN\t%s" #{a.backports}`.lines.to_a
103
+ commits = (included - excluded).group_by { |c| c[/^[^\t]+/] }
104
+ authors = commits.keys.sort_by { |n| - commits[n].size } - team
105
+ puts authors[0..-2].join(', ') << " and " << authors.last,
106
+ "(based on commits included in #{a.release}, but not in #{a.backports})"
107
+ end
108
+
109
+ desc "list of authors"
110
+ task :authors, [:commit_range, :format, :sep] do |t, a|
111
+ a.with_defaults :format => "%s (%d)", :sep => ", ", :commit_range => '--all'
112
+ authors = Hash.new { |h,k| h[k] = 0 }
113
+ blake = "Blake Mizerany"
114
+ overall = 0
115
+ mapping = {
116
+ "blake.mizerany@gmail.com" => blake, "bmizerany" => blake,
117
+ "a_user@mac.com" => blake, "ichverstehe" => "Harry Vangberg",
118
+ "Wu Jiang (nouse)" => "Wu Jiang" }
119
+ `git shortlog -s #{a.commit_range}`.lines.map do |line|
120
+ line = line.force_encoding 'binary' if line.respond_to? :force_encoding
121
+ num, name = line.split("\t", 2).map(&:strip)
122
+ authors[mapping[name] || name] += num.to_i
123
+ overall += num.to_i
124
+ end
125
+ puts "#{overall} commits by #{authors.count} authors:"
126
+ puts authors.sort_by { |n,c| -c }.map { |e| a.format % e }.join(a.sep)
127
+ end
56
128
 
57
129
  # PACKAGING ============================================================
58
130
 
@@ -92,25 +164,19 @@ if defined?(Gem)
92
164
  SH
93
165
  end
94
166
 
95
- task 'sinatra.gemspec' => FileList['{lib,test,compat}/**','Rakefile','CHANGES','*.rdoc'] do |f|
96
- # read spec file and split out manifest section
97
- spec = File.read(f.name)
98
- head, manifest, tail = spec.split(" # = MANIFEST =\n")
99
- # replace version and date
100
- head.sub!(/\.version = '.*'/, ".version = '#{source_version}'")
101
- head.sub!(/\.date = '.*'/, ".date = '#{Date.today.to_s}'")
102
- # determine file list from git ls-files
103
- files = `git ls-files`.
104
- split("\n").
105
- sort.
106
- reject{ |file| file =~ /^\./ }.
107
- reject { |file| file =~ /^doc/ }.
108
- map{ |file| " #{file}" }.
109
- join("\n")
110
- # piece file back together and write...
111
- manifest = " s.files = %w[\n#{files}\n ]\n"
112
- spec = [head,manifest,tail].join(" # = MANIFEST =\n")
113
- File.open(f.name, 'w') { |io| io.write(spec) }
114
- puts "updated #{f.name}"
167
+ task 'release' => ['test', package('.gem')] do
168
+ if File.read("CHANGES") =~ /= \d\.\d\.\d . not yet released$/i
169
+ fail 'please update changes first'
170
+ end
171
+
172
+ sh <<-SH
173
+ gem install #{package('.gem')} --local &&
174
+ gem push #{package('.gem')} &&
175
+ git commit --allow-empty -a -m '#{source_version} release' &&
176
+ git tag -s v#{source_version} -m '#{source_version} release' &&
177
+ git tag -s #{source_version} -m '#{source_version} release' &&
178
+ git push && (git push sinatra || true) &&
179
+ git push --tags && (git push sinatra --tags || true)
180
+ SH
115
181
  end
116
182
  end
@@ -0,0 +1,61 @@
1
+ #!/usr/bin/env ruby -I ../lib -I lib
2
+ # coding: utf-8
3
+ require 'sinatra'
4
+ set :server, 'thin'
5
+ connections = []
6
+
7
+ get '/' do
8
+ halt erb(:login) unless params[:user]
9
+ erb :chat, :locals => { :user => params[:user].gsub(/\W/, '') }
10
+ end
11
+
12
+ get '/stream', :provides => 'text/event-stream' do
13
+ stream :keep_open do |out|
14
+ connections << out
15
+ out.callback { connections.delete(out) }
16
+ end
17
+ end
18
+
19
+ post '/' do
20
+ connections.each { |out| out << "data: #{params[:msg]}\n\n" }
21
+ 204 # response without entity body
22
+ end
23
+
24
+ __END__
25
+
26
+ @@ layout
27
+ <html>
28
+ <head>
29
+ <title>Super Simple Chat with Sinatra</title>
30
+ <meta charset="utf-8" />
31
+ <script src="http://ajax.googleapis.com/ajax/libs/jquery/1/jquery.min.js"></script>
32
+ </head>
33
+ <body><%= yield %></body>
34
+ </html>
35
+
36
+ @@ login
37
+ <form action='/'>
38
+ <label for='user'>User Name:</label>
39
+ <input name='user' value='' />
40
+ <input type='submit' value="GO!" />
41
+ </form>
42
+
43
+ @@ chat
44
+ <pre id='chat'></pre>
45
+
46
+ <script>
47
+ // reading
48
+ var es = new EventSource('/stream');
49
+ es.onmessage = function(e) { $('#chat').append(e.data + "\n") };
50
+
51
+ // writing
52
+ $("form").live("submit", function(e) {
53
+ $.post('/', {msg: "<%= user %>: " + $('#msg').val()});
54
+ $('#msg').val(''); $('#msg').focus();
55
+ e.preventDefault();
56
+ });
57
+ </script>
58
+
59
+ <form>
60
+ <input id='msg' placeholder='type message here...' />
61
+ </form>
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env ruby -I ../lib -I lib
2
+ require 'sinatra'
3
+ get('/') { 'this is a simple app' }
@@ -0,0 +1,26 @@
1
+ # this example does *not* work properly with WEBrick
2
+ #
3
+ # run *one* of these:
4
+ #
5
+ # rackup -s mongrel stream.ru # gem install mongrel
6
+ # thin -R stream.ru start # gem install thin
7
+ # unicorn stream.ru # gem install unicorn
8
+ # puma stream.ru # gem install puma
9
+
10
+ require 'sinatra/base'
11
+
12
+ class Stream < Sinatra::Base
13
+ get '/' do
14
+ content_type :txt
15
+
16
+ stream do |out|
17
+ out << "It's gonna be legen -\n"
18
+ sleep 0.5
19
+ out << " (wait for it) \n"
20
+ sleep 1
21
+ out << "- dary!\n"
22
+ end
23
+ end
24
+ end
25
+
26
+ run Stream
@@ -1,6 +1,3 @@
1
- libdir = File.dirname(__FILE__)
2
- $LOAD_PATH.unshift(libdir) unless $LOAD_PATH.include?(libdir)
3
-
4
1
  require 'sinatra/base'
5
2
  #require 'sinatra/main'
6
3
 
@@ -1,51 +1,60 @@
1
+ # external dependencies
2
+ require 'rack'
3
+ require 'tilt'
4
+ require "rack/protection"
5
+
6
+ # stdlib dependencies
1
7
  require 'thread'
2
8
  require 'time'
3
9
  require 'uri'
4
- require 'rack'
5
- require 'rack/builder'
6
- require 'sinatra/showexceptions'
7
10
 
8
- # require tilt if available; fall back on bundled version.
9
- begin
10
- require 'tilt'
11
- if Tilt::VERSION < '0.8'
12
- warn "WARN: sinatra requires tilt >= 0.8; you have #{Tilt::VERSION}. " +
13
- "loading bundled version..."
14
- Object.send :remove_const, :Tilt
15
- raise LoadError
16
- end
17
- rescue LoadError
18
- require 'sinatra/tilt'
19
- end
11
+ # other files we need
12
+ require 'sinatra/showexceptions'
13
+ require 'sinatra/version'
20
14
 
21
15
  module Sinatra
22
- VERSION = '1.0'
23
-
24
16
  # The request object. See Rack::Request for more info:
25
17
  # http://rack.rubyforge.org/doc/classes/Rack/Request.html
26
18
  class Request < Rack::Request
27
19
  # Returns an array of acceptable media types for the response
28
20
  def accept
29
- @env['HTTP_ACCEPT'].to_s.split(',').map { |a| a.strip }
21
+ @env['sinatra.accept'] ||= begin
22
+ entries = @env['HTTP_ACCEPT'].to_s.split(',')
23
+ entries.map { |e| accept_entry(e) }.sort_by(&:last).map(&:first)
24
+ end
30
25
  end
31
26
 
32
- def secure?
33
- (@env['HTTP_X_FORWARDED_PROTO'] || @env['rack.url_scheme']) == 'https'
27
+ def preferred_type(*types)
28
+ return accept.first if types.empty?
29
+ types.flatten!
30
+ accept.detect do |pattern|
31
+ type = types.detect { |t| File.fnmatch(pattern, t) }
32
+ return type if type
33
+ end
34
34
  end
35
35
 
36
- # Override Rack < 1.1's Request#params implementation (see lh #72 for
37
- # more info) and add a Request#user_agent method.
38
- # XXX remove when we require rack > 1.1
39
- if Rack.release < '1.1'
40
- def params
41
- self.GET.update(self.POST)
42
- rescue EOFError, Errno::ESPIPE
43
- self.GET
44
- end
36
+ alias accept? preferred_type
37
+ alias secure? ssl?
45
38
 
46
- def user_agent
47
- @env['HTTP_USER_AGENT']
48
- end
39
+ def forwarded?
40
+ @env.include? "HTTP_X_FORWARDED_HOST"
41
+ end
42
+
43
+ def safe?
44
+ get? or head? or options? or trace?
45
+ end
46
+
47
+ def idempotent?
48
+ safe? or put? or delete?
49
+ end
50
+
51
+ private
52
+
53
+ def accept_entry(entry)
54
+ type, *options = entry.delete(' ').split(';')
55
+ quality = 0 # we sort smallest first
56
+ options.delete_if { |e| quality = 1 - e[2..-1].to_f if e.start_with? 'q=' }
57
+ [type, [quality, type.count('*'), 1 - options.size]]
49
58
  end
50
59
  end
51
60
 
@@ -54,25 +63,48 @@ module Sinatra
54
63
  # http://rack.rubyforge.org/doc/classes/Rack/Response.html
55
64
  # http://rack.rubyforge.org/doc/classes/Rack/Response/Helpers.html
56
65
  class Response < Rack::Response
66
+ def body=(value)
67
+ value = value.body while Rack::Response === value
68
+ @body = String === value ? [value.to_str] : value
69
+ end
70
+
71
+ def each
72
+ block_given? ? super : enum_for(:each)
73
+ end
74
+
57
75
  def finish
58
- @body = block if block_given?
59
- if [204, 304].include?(status.to_i)
60
- header.delete "Content-Type"
61
- [status.to_i, header.to_hash, []]
62
- else
63
- body = @body || []
64
- body = [body] if body.respond_to? :to_str
65
- if body.respond_to?(:to_ary)
66
- header["Content-Length"] = body.to_ary.
67
- inject(0) { |len, part| len + Rack::Utils.bytesize(part) }.to_s
68
- end
69
- [status.to_i, header.to_hash, body]
76
+ if status.to_i / 100 == 1
77
+ headers.delete "Content-Length"
78
+ headers.delete "Content-Type"
79
+ elsif Array === body and not [204, 304].include?(status.to_i)
80
+ headers["Content-Length"] = body.inject(0) { |l, p| l + Rack::Utils.bytesize(p) }.to_s
81
+ end
82
+
83
+ # Rack::Response#finish sometimes returns self as response body. We don't want that.
84
+ status, headers, result = super
85
+ result = body if result == self
86
+ [status, headers, result]
87
+ end
88
+ end
89
+
90
+ # Behaves exactly like Rack::CommonLogger with the notable exception that it does nothing,
91
+ # if another CommonLogger is already in the middleware chane.
92
+ class CommonLogger < Rack::CommonLogger
93
+ def call(env)
94
+ env['sinatra.commonlogger'] ? @app.call(env) : super
95
+ end
96
+
97
+ superclass.class_eval do
98
+ alias call_without_check call unless method_defined? :call_without_check
99
+ def call(env)
100
+ env['sinatra.commonlogger'] = true
101
+ call_without_check(env)
70
102
  end
71
103
  end
72
104
  end
73
105
 
74
106
  class NotFound < NameError #:nodoc:
75
- def code ; 404 ; end
107
+ def http_status; 404 end
76
108
  end
77
109
 
78
110
  # Methods available to routes, before/after filters, and views.
@@ -87,20 +119,50 @@ module Sinatra
87
119
  # evaluation is deferred until the body is read with #each.
88
120
  def body(value=nil, &block)
89
121
  if block_given?
90
- def block.each ; yield call ; end
122
+ def block.each; yield(call) end
91
123
  response.body = block
92
- else
124
+ elsif value
93
125
  response.body = value
126
+ else
127
+ response.body
94
128
  end
95
129
  end
96
130
 
97
131
  # Halt processing and redirect to the URI provided.
98
132
  def redirect(uri, *args)
99
- status 302
100
- response['Location'] = uri
133
+ if env['HTTP_VERSION'] == 'HTTP/1.1' and env["REQUEST_METHOD"] != 'GET'
134
+ status 303
135
+ else
136
+ status 302
137
+ end
138
+
139
+ # According to RFC 2616 section 14.30, "the field value consists of a
140
+ # single absolute URI"
141
+ response['Location'] = uri(uri, settings.absolute_redirects?, settings.prefixed_redirects?)
101
142
  halt(*args)
102
143
  end
103
144
 
145
+ # Generates the absolute URI for a given path in the app.
146
+ # Takes Rack routers and reverse proxies into account.
147
+ def uri(addr = nil, absolute = true, add_script_name = true)
148
+ return addr if addr =~ /\A[A-z][A-z0-9\+\.\-]*:/
149
+ uri = [host = ""]
150
+ if absolute
151
+ host << "http#{'s' if request.secure?}://"
152
+ if request.forwarded? or request.port != (request.secure? ? 443 : 80)
153
+ host << request.host_with_port
154
+ else
155
+ host << request.host
156
+ end
157
+ end
158
+ uri << request.script_name.to_s if add_script_name
159
+ uri << (addr ? addr : request.path_info).to_s
160
+ File.join uri
161
+ end
162
+
163
+ alias url uri
164
+ alias to uri
165
+
104
166
  # Halt processing and return the error status provided.
105
167
  def error(code, body=nil)
106
168
  code, body = 500, code.to_str if code.respond_to? :to_str
@@ -121,7 +183,12 @@ module Sinatra
121
183
 
122
184
  # Access the underlying Rack session.
123
185
  def session
124
- env['rack.session'] ||= {}
186
+ request.session
187
+ end
188
+
189
+ # Access shared logger object.
190
+ def logger
191
+ request.logger
125
192
  end
126
193
 
127
194
  # Look up a media type by file extension in Rack's mime registry.
@@ -131,15 +198,21 @@ module Sinatra
131
198
 
132
199
  # Set the Content-Type of the response body given a media type or file
133
200
  # extension.
134
- def content_type(type, params={})
135
- mime_type = self.mime_type(type)
201
+ def content_type(type = nil, params={})
202
+ return response['Content-Type'] unless type
203
+ default = params.delete :default
204
+ mime_type = mime_type(type) || default
136
205
  fail "Unknown media type: %p" % type if mime_type.nil?
137
- if params.any?
138
- params = params.collect { |kv| "%s=%s" % kv }.join(', ')
139
- response['Content-Type'] = [mime_type, params].join(";")
140
- else
141
- response['Content-Type'] = mime_type
206
+ mime_type = mime_type.dup
207
+ unless params.include? :charset or settings.add_charset.all? { |p| not p === mime_type }
208
+ params[:charset] = params.delete('charset') || settings.default_encoding
209
+ end
210
+ params.delete :charset if mime_type.include? 'charset'
211
+ unless params.empty?
212
+ mime_type << (mime_type.include?(';') ? ', ' : ';')
213
+ mime_type << params.map { |kv| kv.join('=') }.join(', ')
142
214
  end
215
+ response['Content-Type'] = mime_type
143
216
  end
144
217
 
145
218
  # Set the Content-Disposition to "attachment" with the specified filename,
@@ -149,20 +222,16 @@ module Sinatra
149
222
  if filename
150
223
  params = '; filename="%s"' % File.basename(filename)
151
224
  response['Content-Disposition'] << params
225
+ ext = File.extname(filename)
226
+ content_type(ext) unless response['Content-Type'] or ext.empty?
152
227
  end
153
228
  end
154
229
 
155
230
  # Use the contents of the file at +path+ as the response body.
156
231
  def send_file(path, opts={})
157
- stat = File.stat(path)
158
- last_modified stat.mtime
159
-
160
- content_type mime_type(opts[:type]) ||
161
- mime_type(File.extname(path)) ||
162
- response['Content-Type'] ||
163
- 'application/octet-stream'
164
-
165
- response['Content-Length'] ||= (opts[:length] || stat.size).to_s
232
+ if opts[:type] or not response['Content-Type']
233
+ content_type opts[:type] || File.extname(path), :default => 'application/octet-stream'
234
+ end
166
235
 
167
236
  if opts[:disposition] == 'attachment' || opts[:filename]
168
237
  attachment opts[:filename] || path
@@ -170,21 +239,93 @@ module Sinatra
170
239
  response['Content-Disposition'] = 'inline'
171
240
  end
172
241
 
173
- halt StaticFile.open(path, 'rb')
242
+ last_modified opts[:last_modified] if opts[:last_modified]
243
+
244
+ file = Rack::File.new nil
245
+ file.path = path
246
+ result = file.serving env
247
+ result[1].each { |k,v| headers[k] ||= v }
248
+ headers['Content-Length'] = result[1]['Content-Length']
249
+ halt opts[:status] || result[0], result[2]
174
250
  rescue Errno::ENOENT
175
251
  not_found
176
252
  end
177
253
 
178
- # Rack response body used to deliver static files. The file contents are
179
- # generated iteratively in 8K chunks.
180
- class StaticFile < ::File #:nodoc:
181
- alias_method :to_path, :path
182
- def each
183
- rewind
184
- while buf = read(8192)
185
- yield buf
254
+ # Class of the response body in case you use #stream.
255
+ #
256
+ # Three things really matter: The front and back block (back being the
257
+ # blog generating content, front the one sending it to the client) and
258
+ # the scheduler, integrating with whatever concurrency feature the Rack
259
+ # handler is using.
260
+ #
261
+ # Scheduler has to respond to defer and schedule.
262
+ class Stream
263
+ def self.schedule(*) yield end
264
+ def self.defer(*) yield end
265
+
266
+ def initialize(scheduler = self.class, keep_open = false, &back)
267
+ @back, @scheduler, @keep_open = back.to_proc, scheduler, keep_open
268
+ @callbacks, @closed = [], false
269
+ end
270
+
271
+ def close
272
+ return if @closed
273
+ @closed = true
274
+ @scheduler.schedule { @callbacks.each { |c| c.call }}
275
+ end
276
+
277
+ def each(&front)
278
+ @front = front
279
+ @scheduler.defer do
280
+ begin
281
+ @back.call(self)
282
+ rescue Exception => e
283
+ @scheduler.schedule { raise e }
284
+ end
285
+ close unless @keep_open
186
286
  end
187
287
  end
288
+
289
+ def <<(data)
290
+ @scheduler.schedule { @front.call(data.to_s) }
291
+ self
292
+ end
293
+
294
+ def callback(&block)
295
+ return yield if @closed
296
+ @callbacks << block
297
+ end
298
+
299
+ alias errback callback
300
+ end
301
+
302
+ # Allows to start sending data to the client even though later parts of
303
+ # the response body have not yet been generated.
304
+ #
305
+ # The close parameter specifies whether Stream#close should be called
306
+ # after the block has been executed. This is only relevant for evented
307
+ # servers like Thin or Rainbows.
308
+ def stream(keep_open = false)
309
+ scheduler = env['async.callback'] ? EventMachine : Stream
310
+ current = @params.dup
311
+
312
+ block = proc do |out|
313
+ begin
314
+ original, @params = @params, current
315
+ yield(out)
316
+ ensure
317
+ @params = original if original
318
+ end
319
+ end
320
+
321
+ out = Stream.new(scheduler, keep_open, &block)
322
+
323
+ if env['async.close']
324
+ env['async.close'].callback { out.close }
325
+ env['async.close'].errback { out.close }
326
+ end
327
+
328
+ body out
188
329
  end
189
330
 
190
331
  # Specify response freshness policy for HTTP caches (Cache-Control header).
@@ -206,8 +347,12 @@ module Sinatra
206
347
  hash = {}
207
348
  end
208
349
 
209
- values = values.map { |value| value.to_s.tr('_','-') }
210
- hash.each { |k,v| values << [k.to_s.tr('_', '-'), v].join('=') }
350
+ values.map! { |value| value.to_s.tr('_','-') }
351
+ hash.each do |key, value|
352
+ key = key.to_s.tr('_', '-')
353
+ value = value.to_i if key == "max-age"
354
+ values << [key, value].join('=')
355
+ end
211
356
 
212
357
  response['Cache-Control'] = values.join(', ') if values.any?
213
358
  end
@@ -224,12 +369,12 @@ module Sinatra
224
369
  def expires(amount, *values)
225
370
  values << {} unless values.last.kind_of?(Hash)
226
371
 
227
- if amount.respond_to?(:to_time)
228
- max_age = amount.to_time - Time.now
229
- time = amount.to_time
230
- else
372
+ if amount.is_a? Integer
373
+ time = Time.now + amount.to_i
231
374
  max_age = amount
232
- time = Time.now + amount
375
+ else
376
+ time = time_for amount
377
+ max_age = time - Time.now
233
378
  end
234
379
 
235
380
  values.last.merge!(:max_age => max_age)
@@ -242,16 +387,27 @@ module Sinatra
242
387
  # and halt if conditional GET matches. The +time+ argument is a Time,
243
388
  # DateTime, or other object that responds to +to_time+.
244
389
  #
245
- # When the current request includes an 'If-Modified-Since' header that
246
- # matches the time specified, execution is immediately halted with a
247
- # '304 Not Modified' response.
390
+ # When the current request includes an 'If-Modified-Since' header that is
391
+ # equal or later than the time specified, execution is immediately halted
392
+ # with a '304 Not Modified' response.
248
393
  def last_modified(time)
249
394
  return unless time
250
- time = time.to_time if time.respond_to?(:to_time)
251
- time = time.httpdate if time.respond_to?(:httpdate)
252
- response['Last-Modified'] = time
253
- halt 304 if time == request.env['HTTP_IF_MODIFIED_SINCE']
254
- time
395
+ time = time_for time
396
+ response['Last-Modified'] = time.httpdate
397
+ return if env['HTTP_IF_NONE_MATCH']
398
+
399
+ if status == 200 and env['HTTP_IF_MODIFIED_SINCE']
400
+ # compare based on seconds since epoch
401
+ since = Time.httpdate(env['HTTP_IF_MODIFIED_SINCE']).to_i
402
+ halt 304 if since >= time.to_i
403
+ end
404
+
405
+ if (success? or status == 412) and env['HTTP_IF_UNMODIFIED_SINCE']
406
+ # compare based on seconds since epoch
407
+ since = Time.httpdate(env['HTTP_IF_UNMODIFIED_SINCE']).to_i
408
+ halt 412 if since < time.to_i
409
+ end
410
+ rescue ArgumentError
255
411
  end
256
412
 
257
413
  # Set the response entity tag (HTTP 'ETag' header) and halt if conditional
@@ -263,24 +419,103 @@ module Sinatra
263
419
  # When the current request includes an 'If-None-Match' header with a
264
420
  # matching etag, execution is immediately halted. If the request method is
265
421
  # GET or HEAD, a '304 Not Modified' response is sent.
266
- def etag(value, kind=:strong)
267
- raise TypeError, ":strong or :weak expected" if ![:strong,:weak].include?(kind)
422
+ def etag(value, options = {})
423
+ # Before touching this code, please double check RFC 2616 14.24 and 14.26.
424
+ options = {:kind => options} unless Hash === options
425
+ kind = options[:kind] || :strong
426
+ new_resource = options.fetch(:new_resource) { request.post? }
427
+
428
+ unless [:strong, :weak].include?(kind)
429
+ raise ArgumentError, ":strong or :weak expected"
430
+ end
431
+
268
432
  value = '"%s"' % value
269
433
  value = 'W/' + value if kind == :weak
270
434
  response['ETag'] = value
271
435
 
272
- # Conditional GET check
273
- if etags = env['HTTP_IF_NONE_MATCH']
274
- etags = etags.split(/\s*,\s*/)
275
- halt 304 if etags.include?(value) || etags.include?('*')
436
+ if success? or status == 304
437
+ if etag_matches? env['HTTP_IF_NONE_MATCH'], new_resource
438
+ halt(request.safe? ? 304 : 412)
439
+ end
440
+
441
+ if env['HTTP_IF_MATCH']
442
+ halt 412 unless etag_matches? env['HTTP_IF_MATCH'], new_resource
443
+ end
444
+ end
445
+ end
446
+
447
+ # Sugar for redirect (example: redirect back)
448
+ def back
449
+ request.referer
450
+ end
451
+
452
+ # whether or not the status is set to 1xx
453
+ def informational?
454
+ status.between? 100, 199
455
+ end
456
+
457
+ # whether or not the status is set to 2xx
458
+ def success?
459
+ status.between? 200, 299
460
+ end
461
+
462
+ # whether or not the status is set to 3xx
463
+ def redirect?
464
+ status.between? 300, 399
465
+ end
466
+
467
+ # whether or not the status is set to 4xx
468
+ def client_error?
469
+ status.between? 400, 499
470
+ end
471
+
472
+ # whether or not the status is set to 5xx
473
+ def server_error?
474
+ status.between? 500, 599
475
+ end
476
+
477
+ # whether or not the status is set to 404
478
+ def not_found?
479
+ status == 404
480
+ end
481
+
482
+ # Generates a Time object from the given value.
483
+ # Used by #expires and #last_modified.
484
+ def time_for(value)
485
+ if value.respond_to? :to_time
486
+ value.to_time
487
+ elsif value.is_a? Time
488
+ value
489
+ elsif value.respond_to? :new_offset
490
+ # DateTime#to_time does the same on 1.9
491
+ d = value.new_offset 0
492
+ t = Time.utc d.year, d.mon, d.mday, d.hour, d.min, d.sec + d.sec_fraction
493
+ t.getlocal
494
+ elsif value.respond_to? :mday
495
+ # Date#to_time does the same on 1.9
496
+ Time.local(value.year, value.mon, value.mday)
497
+ elsif value.is_a? Numeric
498
+ Time.at value
499
+ else
500
+ Time.parse value.to_s
276
501
  end
502
+ rescue ArgumentError => boom
503
+ raise boom
504
+ rescue Exception
505
+ raise ArgumentError, "unable to convert #{value.inspect} to a Time object"
277
506
  end
278
507
 
279
- ## Sugar for redirect (example: redirect back)
280
- def back ; request.referer ; end
508
+ private
281
509
 
510
+ # Helper method checking if a ETag value list includes the current ETag.
511
+ def etag_matches?(list, new_resource = request.post?)
512
+ return !new_resource if list == '*'
513
+ list.to_s.split(/\s*,\s*/).include? response['ETag']
514
+ end
282
515
  end
283
516
 
517
+ private
518
+
284
519
  # Template rendering methods. Each method takes the name of a template
285
520
  # to render as a Symbol and returns a String with the rendered output,
286
521
  # as well as an optional hash with additional options.
@@ -290,20 +525,32 @@ module Sinatra
290
525
  # that will be rendered.
291
526
  #
292
527
  # Possible options are:
293
- # :layout If set to false, no layout is rendered, otherwise
294
- # the specified layout is used (Ignored for `sass` and `less`)
295
- # :locals A hash with local variables that should be available
296
- # in the template
528
+ # :content_type The content type to use, same arguments as content_type.
529
+ # :layout If set to false, no layout is rendered, otherwise
530
+ # the specified layout is used (Ignored for `sass` and `less`)
531
+ # :layout_engine Engine to use for rendering the layout.
532
+ # :locals A hash with local variables that should be available
533
+ # in the template
534
+ # :scope If set, template is evaluate with the binding of the given
535
+ # object rather than the application instance.
536
+ # :views Views directory to use.
297
537
  module Templates
298
- include Tilt::CompileSite
538
+ module ContentTyped
539
+ attr_accessor :content_type
540
+ end
541
+
542
+ def initialize
543
+ super
544
+ @default_layout = :layout
545
+ end
299
546
 
300
547
  def erb(template, options={}, locals={})
301
- options[:outvar] = '@_out_buf'
302
548
  render :erb, template, options, locals
303
549
  end
304
550
 
305
551
  def erubis(template, options={}, locals={})
306
- options[:outvar] = '@_out_buf'
552
+ warn "Sinatra::Templates#erubis is deprecated and will be removed, use #erb instead.\n" \
553
+ "If you have Erubis installed, it will be used automatically."
307
554
  render :erubis, template, options, locals
308
555
  end
309
556
 
@@ -312,69 +559,157 @@ module Sinatra
312
559
  end
313
560
 
314
561
  def sass(template, options={}, locals={})
315
- options[:layout] = false
562
+ options.merge! :layout => false, :default_content_type => :css
316
563
  render :sass, template, options, locals
317
564
  end
318
565
 
566
+ def scss(template, options={}, locals={})
567
+ options.merge! :layout => false, :default_content_type => :css
568
+ render :scss, template, options, locals
569
+ end
570
+
319
571
  def less(template, options={}, locals={})
320
- options[:layout] = false
572
+ options.merge! :layout => false, :default_content_type => :css
321
573
  render :less, template, options, locals
322
574
  end
323
575
 
324
576
  def builder(template=nil, options={}, locals={}, &block)
577
+ options[:default_content_type] = :xml
578
+ render_ruby(:builder, template, options, locals, &block)
579
+ end
580
+
581
+ def liquid(template, options={}, locals={})
582
+ render :liquid, template, options, locals
583
+ end
584
+
585
+ def markdown(template, options={}, locals={})
586
+ render :markdown, template, options, locals
587
+ end
588
+
589
+ def textile(template, options={}, locals={})
590
+ render :textile, template, options, locals
591
+ end
592
+
593
+ def rdoc(template, options={}, locals={})
594
+ render :rdoc, template, options, locals
595
+ end
596
+
597
+ def radius(template, options={}, locals={})
598
+ render :radius, template, options, locals
599
+ end
600
+
601
+ def markaby(template=nil, options={}, locals={}, &block)
602
+ render_ruby(:mab, template, options, locals, &block)
603
+ end
604
+
605
+ def coffee(template, options={}, locals={})
606
+ options.merge! :layout => false, :default_content_type => :js
607
+ render :coffee, template, options, locals
608
+ end
609
+
610
+ def nokogiri(template=nil, options={}, locals={}, &block)
611
+ options[:default_content_type] = :xml
612
+ render_ruby(:nokogiri, template, options, locals, &block)
613
+ end
614
+
615
+ def slim(template, options={}, locals={})
616
+ render :slim, template, options, locals
617
+ end
618
+
619
+ def creole(template, options={}, locals={})
620
+ render :creole, template, options, locals
621
+ end
622
+
623
+ def yajl(template, options={}, locals={})
624
+ options[:default_content_type] = :json
625
+ render :yajl, template, options, locals
626
+ end
627
+
628
+ # Calls the given block for every possible template file in views,
629
+ # named name.ext, where ext is registered on engine.
630
+ def find_template(views, name, engine)
631
+ yield ::File.join(views, "#{name}.#{@preferred_extension}")
632
+ Tilt.mappings.each do |ext, engines|
633
+ next unless ext != @preferred_extension and engines.include? engine
634
+ yield ::File.join(views, "#{name}.#{ext}")
635
+ end
636
+ end
637
+
638
+ private
639
+ # logic shared between builder and nokogiri
640
+ def render_ruby(engine, template, options={}, locals={}, &block)
325
641
  options, template = template, nil if template.is_a?(Hash)
326
642
  template = Proc.new { block } if template.nil?
327
- render :builder, template, options, locals
643
+ render engine, template, options, locals
328
644
  end
329
645
 
330
- private
331
646
  def render(engine, data, options={}, locals={}, &block)
332
647
  # merge app-level options
333
648
  options = settings.send(engine).merge(options) if settings.respond_to?(engine)
649
+ options[:outvar] ||= '@_out_buf'
650
+ options[:default_encoding] ||= settings.default_encoding
334
651
 
335
652
  # extract generic options
336
- locals = options.delete(:locals) || locals || {}
337
- views = options.delete(:views) || settings.views || "./views"
338
- layout = options.delete(:layout)
339
- layout = :layout if layout.nil? || layout == true
653
+ locals = options.delete(:locals) || locals || {}
654
+ views = options.delete(:views) || settings.views || "./views"
655
+ layout = options.delete(:layout)
656
+ eat_errors = layout.nil?
657
+ layout = @default_layout if layout.nil? or layout == true
658
+ content_type = options.delete(:content_type) || options.delete(:default_content_type)
659
+ layout_engine = options.delete(:layout_engine) || engine
660
+ scope = options.delete(:scope) || self
340
661
 
341
662
  # compile and render template
342
- template = compile_template(engine, data, options, views)
343
- output = template.render(self, locals, &block)
663
+ begin
664
+ layout_was = @default_layout
665
+ @default_layout = false
666
+ template = compile_template(engine, data, options, views)
667
+ output = template.render(scope, locals, &block)
668
+ ensure
669
+ @default_layout = layout_was
670
+ end
344
671
 
345
672
  # render layout
346
673
  if layout
347
- begin
348
- options = options.merge(:views => views, :layout => false)
349
- output = render(engine, layout, options, locals) { output }
350
- rescue Errno::ENOENT
351
- end
674
+ options = options.merge(:views => views, :layout => false, :eat_errors => eat_errors, :scope => scope)
675
+ catch(:layout_missing) { return render(layout_engine, layout, options, locals) { output } }
352
676
  end
353
677
 
678
+ output.extend(ContentTyped).content_type = content_type if content_type
354
679
  output
355
680
  end
356
681
 
357
682
  def compile_template(engine, data, options, views)
358
- @template_cache.fetch engine, data, options do
683
+ eat_errors = options.delete :eat_errors
684
+ template_cache.fetch engine, data, options do
359
685
  template = Tilt[engine]
360
686
  raise "Template engine not found: #{engine}" if template.nil?
361
687
 
362
- case
363
- when data.is_a?(Symbol)
364
- body, path, line = self.class.templates[data]
688
+ case data
689
+ when Symbol
690
+ body, path, line = settings.templates[data]
365
691
  if body
366
692
  body = body.call if body.respond_to?(:call)
367
693
  template.new(path, line.to_i, options) { body }
368
694
  else
369
- path = ::File.join(views, "#{data}.#{engine}")
695
+ found = false
696
+ @preferred_extension = engine.to_s
697
+ find_template(views, data, template) do |file|
698
+ path ||= file # keep the initial path rather than the last one
699
+ if found = File.exists?(file)
700
+ path = file
701
+ break
702
+ end
703
+ end
704
+ throw :layout_missing if eat_errors and not found
370
705
  template.new(path, 1, options)
371
706
  end
372
- when data.is_a?(Proc) || data.is_a?(String)
707
+ when Proc, String
373
708
  body = data.is_a?(String) ? Proc.new { data } : data
374
- path, line = self.class.caller_locations.first
709
+ path, line = settings.caller_locations.first
375
710
  template.new(path, line.to_i, options, &body)
376
711
  else
377
- raise ArgumentError
712
+ raise ArgumentError, "Sorry, don't know how to render #{data.inspect}."
378
713
  end
379
714
  end
380
715
  end
@@ -387,8 +722,10 @@ module Sinatra
387
722
  include Templates
388
723
 
389
724
  attr_accessor :app
725
+ attr_reader :template_cache
390
726
 
391
727
  def initialize(app=nil)
728
+ super()
392
729
  @app = app
393
730
  @template_cache = Tilt::Cache.new
394
731
  yield self if block_given?
@@ -401,34 +738,44 @@ module Sinatra
401
738
 
402
739
  attr_accessor :env, :request, :response, :params
403
740
 
404
- def call!(env)
741
+ def call!(env) # :nodoc:
405
742
  @env = env
406
743
  @request = Request.new(env)
407
744
  @response = Response.new
408
745
  @params = indifferent_params(@request.params)
409
- @template_cache.clear if settings.reload_templates
746
+ template_cache.clear if settings.reload_templates
747
+ force_encoding(@params)
410
748
 
749
+ @response['Content-Type'] = nil
411
750
  invoke { dispatch! }
412
751
  invoke { error_block!(response.status) }
413
752
 
414
- status, header, body = @response.finish
415
-
416
- # Never produce a body on HEAD requests. Do retain the Content-Length
417
- # unless it's "0", in which case we assume it was calculated erroneously
418
- # for a manual HEAD response and remove it entirely.
419
- if @env['REQUEST_METHOD'] == 'HEAD'
420
- body = []
421
- header.delete('Content-Length') if header['Content-Length'] == '0'
753
+ unless @response['Content-Type']
754
+ if Array === body and body[0].respond_to? :content_type
755
+ content_type body[0].content_type
756
+ else
757
+ content_type :html
758
+ end
422
759
  end
423
760
 
424
- [status, header, body]
761
+ @response.finish
762
+ end
763
+
764
+ # Access settings defined with Base.set.
765
+ def self.settings
766
+ self
425
767
  end
426
768
 
427
769
  # Access settings defined with Base.set.
428
770
  def settings
429
- self.class
771
+ self.class.settings
772
+ end
773
+
774
+ def options
775
+ warn "Sinatra::Base#options is deprecated and will be removed, " \
776
+ "use #settings instead."
777
+ settings
430
778
  end
431
- alias_method :options, :settings
432
779
 
433
780
  # Exit the current block, halts any further processing
434
781
  # of the request, and returns the specified response.
@@ -447,7 +794,7 @@ module Sinatra
447
794
  # Forward the request to the downstream app -- middleware only.
448
795
  def forward
449
796
  fail "downstream app not set" unless @app.respond_to? :call
450
- status, headers, body = @app.call(@request.env)
797
+ status, headers, body = @app.call env
451
798
  @response.status = status
452
799
  @response.body = body
453
800
  @response.headers.merge! headers
@@ -455,70 +802,58 @@ module Sinatra
455
802
  end
456
803
 
457
804
  private
458
- # Run before filters defined on the class and all superclasses.
459
- def before_filter!(base=self.class)
460
- before_filter!(base.superclass) if base.superclass.respond_to?(:before_filters)
461
- base.before_filters.each { |block| instance_eval(&block) }
462
- end
463
-
464
- # Run after filters defined on the class and all superclasses.
465
- def after_filter!(base=self.class)
466
- after_filter!(base.superclass) if base.superclass.respond_to?(:after_filters)
467
- base.after_filters.each { |block| instance_eval(&block) }
805
+ # Run filters defined on the class and all superclasses.
806
+ def filter!(type, base = settings)
807
+ filter! type, base.superclass if base.superclass.respond_to?(:filters)
808
+ base.filters[type].each { |args| process_route(*args) }
468
809
  end
469
810
 
470
811
  # Run routes defined on the class and all superclasses.
471
- def route!(base=self.class, pass_block=nil)
812
+ def route!(base = settings, pass_block=nil)
472
813
  if routes = base.routes[@request.request_method]
473
- original_params = @params
474
- path = unescape(@request.path_info)
475
-
476
814
  routes.each do |pattern, keys, conditions, block|
477
- if match = pattern.match(path)
478
- values = match.captures.to_a
479
- params =
480
- if keys.any?
481
- keys.zip(values).inject({}) do |hash,(k,v)|
482
- if k == 'splat'
483
- (hash[k] ||= []) << v
484
- else
485
- hash[k] = v
486
- end
487
- hash
488
- end
489
- elsif values.any?
490
- {'captures' => values}
491
- else
492
- {}
493
- end
494
- @params = original_params.merge(params)
495
- @block_params = values
496
-
497
- pass_block = catch(:pass) do
498
- conditions.each { |cond|
499
- throw :pass if instance_eval(&cond) == false }
500
- route_eval(&block)
501
- end
815
+ pass_block = process_route(pattern, keys, conditions) do |*args|
816
+ route_eval { block[*args] }
502
817
  end
503
818
  end
504
-
505
- @params = original_params
506
819
  end
507
820
 
508
821
  # Run routes defined in superclass.
509
822
  if base.superclass.respond_to?(:routes)
510
- route! base.superclass, pass_block
511
- return
823
+ return route!(base.superclass, pass_block)
512
824
  end
513
825
 
514
826
  route_eval(&pass_block) if pass_block
515
-
516
827
  route_missing
517
828
  end
518
829
 
519
830
  # Run a route block and throw :halt with the result.
520
- def route_eval(&block)
521
- throw :halt, instance_eval(&block)
831
+ def route_eval
832
+ throw :halt, yield
833
+ end
834
+
835
+ # If the current request matches pattern and conditions, fill params
836
+ # with keys and call the given block.
837
+ # Revert params afterwards.
838
+ #
839
+ # Returns pass block.
840
+ def process_route(pattern, keys, conditions, block = nil, values = [])
841
+ route = @request.path_info
842
+ route = '/' if route.empty? and not settings.empty_path_info?
843
+ return unless match = pattern.match(route)
844
+ values += match.captures.to_a.map { |v| force_encoding URI.decode_www_form_component(v) if v }
845
+
846
+ if values.any?
847
+ original, @params = params, params.merge('splat' => [], 'captures' => values)
848
+ keys.zip(values) { |k,v| Array === @params[k] ? @params[k] << v : @params[k] = v if v }
849
+ end
850
+
851
+ catch(:pass) do
852
+ conditions.each { |c| throw :pass if c.bind(self).call == false }
853
+ block ? block[self, values] : yield(self, values)
854
+ end
855
+ ensure
856
+ @params = original if original
522
857
  end
523
858
 
524
859
  # No matching route was found or all routes passed. The default
@@ -537,130 +872,122 @@ module Sinatra
537
872
  # Attempt to serve static files from public directory. Throws :halt when
538
873
  # a matching file is found, returns nil otherwise.
539
874
  def static!
540
- return if (public_dir = settings.public).nil?
875
+ return if (public_dir = settings.public_folder).nil?
541
876
  public_dir = File.expand_path(public_dir)
542
877
 
543
878
  path = File.expand_path(public_dir + unescape(request.path_info))
544
- return if path[0, public_dir.length] != public_dir
545
- return unless File.file?(path)
879
+ return unless path.start_with?(public_dir) and File.file?(path)
546
880
 
547
881
  env['sinatra.static_file'] = path
882
+ cache_control(*settings.static_cache_control) if settings.static_cache_control?
548
883
  send_file path, :disposition => nil
549
884
  end
550
885
 
551
886
  # Enable string or symbol key access to the nested params hash.
552
- def indifferent_params(params)
553
- params = indifferent_hash.merge(params)
554
- params.each do |key, value|
555
- next unless value.is_a?(Hash)
556
- params[key] = indifferent_params(value)
887
+ def indifferent_params(object)
888
+ case object
889
+ when Hash
890
+ new_hash = indifferent_hash
891
+ object.each { |key, value| new_hash[key] = indifferent_params(value) }
892
+ new_hash
893
+ when Array
894
+ object.map { |item| indifferent_params(item) }
895
+ else
896
+ object
557
897
  end
558
898
  end
559
899
 
900
+ # Creates a Hash with indifferent access.
560
901
  def indifferent_hash
561
902
  Hash.new {|hash,key| hash[key.to_s] if Symbol === key }
562
903
  end
563
904
 
564
905
  # Run the block with 'throw :halt' support and apply result to the response.
565
- def invoke(&block)
566
- res = catch(:halt) { instance_eval(&block) }
567
- return if res.nil?
568
-
569
- case
570
- when res.respond_to?(:to_str)
571
- @response.body = [res]
572
- when res.respond_to?(:to_ary)
573
- res = res.to_ary
574
- if Fixnum === res.first
575
- if res.length == 3
576
- @response.status, headers, body = res
577
- @response.body = body if body
578
- headers.each { |k, v| @response.headers[k] = v } if headers
579
- elsif res.length == 2
580
- @response.status = res.first
581
- @response.body = res.last
582
- else
583
- raise TypeError, "#{res.inspect} not supported"
584
- end
585
- else
586
- @response.body = res
587
- end
588
- when res.respond_to?(:each)
589
- @response.body = res
590
- when (100...599) === res
591
- @response.status = res
906
+ def invoke
907
+ res = catch(:halt) { yield }
908
+ res = [res] if Fixnum === res or String === res
909
+ if Array === res and Fixnum === res.first
910
+ status(res.shift)
911
+ body(res.pop)
912
+ headers(*res)
913
+ elsif res.respond_to? :each
914
+ body res
592
915
  end
593
-
594
- res
916
+ nil # avoid double setting the same response tuple twice
595
917
  end
596
918
 
597
919
  # Dispatch a request with error handling.
598
920
  def dispatch!
599
- static! if settings.static? && (request.get? || request.head?)
600
- before_filter!
601
- route!
602
- rescue NotFound => boom
603
- handle_not_found!(boom)
921
+ invoke do
922
+ static! if settings.static? && (request.get? || request.head?)
923
+ filter! :before
924
+ route!
925
+ end
604
926
  rescue ::Exception => boom
605
- handle_exception!(boom)
927
+ invoke { handle_exception!(boom) }
606
928
  ensure
607
- after_filter! unless env['sinatra.static_file']
608
- end
609
-
610
- def handle_not_found!(boom)
611
- @env['sinatra.error'] = boom
612
- @response.status = 404
613
- @response.headers['X-Cascade'] = 'pass'
614
- @response.body = ['<h1>Not Found</h1>']
615
- error_block! boom.class, NotFound
929
+ filter! :after unless env['sinatra.static_file']
616
930
  end
617
931
 
932
+ # Error handling during requests.
618
933
  def handle_exception!(boom)
619
934
  @env['sinatra.error'] = boom
620
935
 
621
- dump_errors!(boom) if settings.dump_errors?
622
- raise boom if settings.show_exceptions?
623
-
624
- @response.status = 500
625
- if res = error_block!(boom.class)
626
- res
627
- elsif settings.raise_errors?
628
- raise boom
936
+ if boom.respond_to? :http_status
937
+ status(boom.http_status)
938
+ elsif settings.use_code? and boom.respond_to? :code and boom.code.between? 400, 599
939
+ status(boom.code)
940
+ warn "Using #{boom.class}#code (#{status}) for setting status code. This is deprecated. " \
941
+ "Use #http_status instead. If this happens unintentional, please \`disable :use_code\`" \
942
+ " in your application."
629
943
  else
630
- error_block!(Exception)
944
+ status(500)
945
+ end
946
+
947
+ status(500) unless status.between? 400, 599
948
+
949
+ if server_error?
950
+ dump_errors! boom if settings.dump_errors?
951
+ raise boom if settings.show_exceptions? and settings.show_exceptions != :after_handler
631
952
  end
953
+
954
+ if not_found?
955
+ headers['X-Cascade'] = 'pass'
956
+ body '<h1>Not Found</h1>'
957
+ end
958
+
959
+ res = error_block!(boom.class, boom) || error_block!(status, boom)
960
+ return res if res or not server_error?
961
+ raise boom if settings.raise_errors? or settings.show_exceptions?
962
+ error_block! Exception, boom
632
963
  end
633
964
 
634
965
  # Find an custom error block for the key(s) specified.
635
- def error_block!(*keys)
636
- keys.each do |key|
637
- base = self.class
638
- while base.respond_to?(:errors)
639
- if block = base.errors[key]
640
- # found a handler, eval and return result
641
- return instance_eval(&block)
642
- else
643
- base = base.superclass
644
- end
645
- end
966
+ def error_block!(key, *block_params)
967
+ base = settings
968
+ while base.respond_to?(:errors)
969
+ next base = base.superclass unless args = base.errors[key]
970
+ args += [block_params]
971
+ return process_route(*args)
646
972
  end
647
- nil
973
+ return false unless key.respond_to? :superclass and key.superclass < Exception
974
+ error_block!(key.superclass, *block_params)
648
975
  end
649
976
 
650
977
  def dump_errors!(boom)
651
- msg = ["#{boom.class} - #{boom.message}:",
652
- *boom.backtrace].join("\n ")
978
+ msg = ["#{boom.class} - #{boom.message}:", *boom.backtrace].join("\n\t")
653
979
  @env['rack.errors'].puts(msg)
654
980
  end
655
981
 
656
982
  class << self
657
- attr_reader :routes, :before_filters, :after_filters, :templates, :errors
983
+ attr_reader :routes, :filters, :templates, :errors
658
984
 
985
+ # Removes all routes, filters, middleware and extension hooks from the
986
+ # current class (not routes/filters/... defined by its superclass).
659
987
  def reset!
660
988
  @conditions = []
661
989
  @routes = {}
662
- @before_filters = []
663
- @after_filters = []
990
+ @filters = {:before => [], :after => []}
664
991
  @errors = {}
665
992
  @middleware = []
666
993
  @prototype = nil
@@ -693,19 +1020,43 @@ module Sinatra
693
1020
 
694
1021
  # Sets an option to the given value. If the value is a proc,
695
1022
  # the proc will be called every time the option is accessed.
696
- def set(option, value=self, &block)
697
- raise ArgumentError if block && value != self
698
- value = block if block
699
- if value.kind_of?(Proc)
700
- metadef(option, &value)
701
- metadef("#{option}?") { !!__send__(option) }
702
- metadef("#{option}=") { |val| metadef(option, &Proc.new{val}) }
703
- elsif value == self && option.respond_to?(:to_hash)
704
- option.to_hash.each { |k,v| set(k, v) }
705
- elsif respond_to?("#{option}=")
706
- __send__ "#{option}=", value
707
- else
708
- set option, Proc.new{value}
1023
+ def set(option, value = (not_set = true), ignore_setter = false, &block)
1024
+ raise ArgumentError if block and !not_set
1025
+ value, not_set = block, false if block
1026
+
1027
+ if not_set
1028
+ raise ArgumentError unless option.respond_to?(:each)
1029
+ option.each { |k,v| set(k, v) }
1030
+ return self
1031
+ end
1032
+
1033
+ if respond_to?("#{option}=") and not ignore_setter
1034
+ return __send__("#{option}=", value)
1035
+ end
1036
+
1037
+ setter = proc { |val| set option, val, true }
1038
+ getter = proc { value }
1039
+
1040
+ case value
1041
+ when Proc
1042
+ getter = value
1043
+ when Symbol, Fixnum, FalseClass, TrueClass, NilClass
1044
+ # we have a lot of enable and disable calls, let's optimize those
1045
+ class_eval "def self.#{option}() #{value.inspect} end"
1046
+ getter = nil
1047
+ when Hash
1048
+ setter = proc do |val|
1049
+ val = value.merge val if Hash === val
1050
+ set option, val, true
1051
+ end
1052
+ end
1053
+
1054
+ (class << self; self; end).class_eval do
1055
+ define_method("#{option}=", &setter) if setter
1056
+ define_method(option, &getter) if getter
1057
+ unless method_defined? "#{option}?"
1058
+ class_eval "def #{option}?() !!#{option} end"
1059
+ end
709
1060
  end
710
1061
  self
711
1062
  end
@@ -723,8 +1074,11 @@ module Sinatra
723
1074
  # Define a custom error handler. Optionally takes either an Exception
724
1075
  # class, or an HTTP status code to specify which errors should be
725
1076
  # handled.
726
- def error(codes=Exception, &block)
727
- Array(codes).each { |code| @errors[code] = block }
1077
+ def error(*codes, &block)
1078
+ args = compile! "ERROR", //, block
1079
+ codes = codes.map { |c| Array(c) }.flatten
1080
+ codes << Exception if codes.empty?
1081
+ codes.each { |c| @errors[c] = args }
728
1082
  end
729
1083
 
730
1084
  # Sugar for `error(404) { ... }`
@@ -746,22 +1100,28 @@ module Sinatra
746
1100
  # Load embeded templates from the file; uses the caller's __FILE__
747
1101
  # when no file is specified.
748
1102
  def inline_templates=(file=nil)
749
- file = (file.nil? || file == true) ? caller_files.first : file
1103
+ file = (file.nil? || file == true) ? (caller_files.first || File.expand_path($0)) : file
750
1104
 
751
1105
  begin
752
- app, data =
753
- ::IO.read(file).gsub("\r\n", "\n").split(/^__END__$/, 2)
1106
+ io = ::IO.respond_to?(:binread) ? ::IO.binread(file) : ::IO.read(file)
1107
+ app, data = io.gsub("\r\n", "\n").split(/^__END__$/, 2)
754
1108
  rescue Errno::ENOENT
755
1109
  app, data = nil
756
1110
  end
757
1111
 
758
1112
  if data
1113
+ if app and app =~ /([^\n]*\n)?#[^\n]*coding: *(\S+)/m
1114
+ encoding = $2
1115
+ else
1116
+ encoding = settings.default_encoding
1117
+ end
759
1118
  lines = app.count("\n") + 1
760
1119
  template = nil
1120
+ force_encoding data, encoding
761
1121
  data.each_line do |line|
762
1122
  lines += 1
763
- if line =~ /^@@\s*(.*)/
764
- template = ''
1123
+ if line =~ /^@@\s*(.*\S)\s*$/
1124
+ template = force_encoding('', encoding)
765
1125
  templates[$1.to_sym] = [template, file, lines]
766
1126
  elsif template
767
1127
  template << line
@@ -778,56 +1138,79 @@ module Sinatra
778
1138
  Rack::Mime::MIME_TYPES[type] = value
779
1139
  end
780
1140
 
1141
+ # provides all mime types matching type, including deprecated types:
1142
+ # mime_types :html # => ['text/html']
1143
+ # mime_types :js # => ['application/javascript', 'text/javascript']
1144
+ def mime_types(type)
1145
+ type = mime_type type
1146
+ type =~ /^application\/(xml|javascript)$/ ? [type, "text/#$1"] : [type]
1147
+ end
1148
+
781
1149
  # Define a before filter; runs before all requests within the same
782
1150
  # context as route handlers and may access/modify the request and
783
1151
  # response.
784
- def before(&block)
785
- @before_filters << block
1152
+ def before(path = nil, options = {}, &block)
1153
+ add_filter(:before, path, options, &block)
786
1154
  end
787
1155
 
788
1156
  # Define an after filter; runs after all requests within the same
789
1157
  # context as route handlers and may access/modify the request and
790
1158
  # response.
791
- def after(&block)
792
- @after_filters << block
1159
+ def after(path = nil, options = {}, &block)
1160
+ add_filter(:after, path, options, &block)
1161
+ end
1162
+
1163
+ # add a filter
1164
+ def add_filter(type, path = nil, options = {}, &block)
1165
+ path, options = //, path if path.respond_to?(:each_pair)
1166
+ filters[type] << compile!(type, path || //, block, options)
793
1167
  end
794
1168
 
795
1169
  # Add a route condition. The route is considered non-matching when the
796
1170
  # block returns false.
797
- def condition(&block)
798
- @conditions << block
1171
+ def condition(name = "#{caller.first[/`.*'/]} condition", &block)
1172
+ @conditions << generate_method(name, &block)
1173
+ end
1174
+
1175
+ def public=(value)
1176
+ warn ":public is no longer used to avoid overloading Module#public, use :public_folder instead"
1177
+ set(:public_folder, value)
799
1178
  end
800
1179
 
801
1180
  private
1181
+ # Condition for matching host name. Parameter might be String or Regexp.
802
1182
  def host_name(pattern)
803
1183
  condition { pattern === request.host }
804
1184
  end
805
1185
 
1186
+ # Condition for matching user agent. Parameter should be Regexp.
1187
+ # Will set params[:agent].
806
1188
  def user_agent(pattern)
807
- condition {
808
- if request.user_agent =~ pattern
1189
+ condition do
1190
+ if request.user_agent.to_s =~ pattern
809
1191
  @params[:agent] = $~[1..-1]
810
1192
  true
811
1193
  else
812
1194
  false
813
1195
  end
814
- }
1196
+ end
815
1197
  end
816
1198
  alias_method :agent, :user_agent
817
1199
 
1200
+ # Condition for matching mimetypes. Accepts file extensions.
818
1201
  def provides(*types)
819
- types = [types] unless types.kind_of? Array
820
- types.map!{|t| mime_type(t)}
821
-
822
- condition {
823
- matching_types = (request.accept & types)
824
- unless matching_types.empty?
825
- response.headers['Content-Type'] = matching_types.first
1202
+ types.map! { |t| mime_types(t) }
1203
+ types.flatten!
1204
+ condition do
1205
+ if type = response['Content-Type']
1206
+ types.include? type or types.include? type[/^[^;]+/]
1207
+ elsif type = request.preferred_type(types)
1208
+ content_type(type)
826
1209
  true
827
1210
  else
828
1211
  false
829
1212
  end
830
- }
1213
+ end
831
1214
  end
832
1215
 
833
1216
  public
@@ -841,60 +1224,65 @@ module Sinatra
841
1224
  route('HEAD', path, opts, &block)
842
1225
  end
843
1226
 
844
- def put(path, opts={}, &bk); route 'PUT', path, opts, &bk end
845
- def post(path, opts={}, &bk); route 'POST', path, opts, &bk end
846
- def delete(path, opts={}, &bk); route 'DELETE', path, opts, &bk end
847
- def head(path, opts={}, &bk); route 'HEAD', path, opts, &bk end
1227
+ def put(path, opts={}, &bk) route 'PUT', path, opts, &bk end
1228
+ def post(path, opts={}, &bk) route 'POST', path, opts, &bk end
1229
+ def delete(path, opts={}, &bk) route 'DELETE', path, opts, &bk end
1230
+ def head(path, opts={}, &bk) route 'HEAD', path, opts, &bk end
1231
+ def options(path, opts={}, &bk) route 'OPTIONS', path, opts, &bk end
1232
+ def patch(path, opts={}, &bk) route 'PATCH', path, opts, &bk end
848
1233
 
849
1234
  private
850
1235
  def route(verb, path, options={}, &block)
851
1236
  # Because of self.options.host
852
- host_name(options.delete(:bind)) if options.key?(:host)
853
-
854
- options.each {|option, args| send(option, *args)}
855
-
856
- pattern, keys = compile(path)
857
- conditions, @conditions = @conditions, []
858
-
859
- define_method "#{verb} #{path}", &block
860
- unbound_method = instance_method("#{verb} #{path}")
861
- block =
862
- if block.arity != 0
863
- proc { unbound_method.bind(self).call(*@block_params) }
864
- else
865
- proc { unbound_method.bind(self).call }
866
- end
867
-
1237
+ host_name(options.delete(:host)) if options.key?(:host)
1238
+ enable :empty_path_info if path == "" and empty_path_info.nil?
1239
+ signature = compile!(verb, path, block, options)
1240
+ (@routes[verb] ||= []) << signature
868
1241
  invoke_hook(:route_added, verb, path, block)
869
-
870
- (@routes[verb] ||= []).
871
- push([pattern, keys, conditions, block]).last
1242
+ signature
872
1243
  end
873
1244
 
874
1245
  def invoke_hook(name, *args)
875
1246
  extensions.each { |e| e.send(name, *args) if e.respond_to?(name) }
876
1247
  end
877
1248
 
1249
+ def generate_method(method_name, &block)
1250
+ define_method(method_name, &block)
1251
+ method = instance_method method_name
1252
+ remove_method method_name
1253
+ method
1254
+ end
1255
+
1256
+ def compile!(verb, path, block, options = {})
1257
+ options.each_pair { |option, args| send(option, *args) }
1258
+ method_name = "#{verb} #{path}"
1259
+ unbound_method = generate_method(method_name, &block)
1260
+ pattern, keys = compile path
1261
+ conditions, @conditions = @conditions, []
1262
+
1263
+ [ pattern, keys, conditions, block.arity != 0 ?
1264
+ proc { |a,p| unbound_method.bind(a).call(*p) } :
1265
+ proc { |a,p| unbound_method.bind(a).call } ]
1266
+ end
1267
+
878
1268
  def compile(path)
879
1269
  keys = []
880
1270
  if path.respond_to? :to_str
881
- special_chars = %w{. + ( )}
882
- pattern =
883
- path.to_str.gsub(/((:\w+)|[\*#{special_chars.join}])/) do |match|
884
- case match
885
- when "*"
886
- keys << 'splat'
887
- "(.*?)"
888
- when *special_chars
889
- Regexp.escape(match)
890
- else
891
- keys << $2[1..-1]
892
- "([^/?&#]+)"
893
- end
1271
+ pattern = path.to_str.gsub(/[^\?\%\\\/\:\*\w]/) { |c| encoded(c) }
1272
+ pattern.gsub!(/((:\w+)|\*)/) do |match|
1273
+ if match == "*"
1274
+ keys << 'splat'
1275
+ "(.*?)"
1276
+ else
1277
+ keys << $2[1..-1]
1278
+ "([^/?#]+)"
894
1279
  end
1280
+ end
895
1281
  [/^#{pattern}$/, keys]
896
1282
  elsif path.respond_to?(:keys) && path.respond_to?(:match)
897
1283
  [path, path.keys]
1284
+ elsif path.respond_to?(:names) && path.respond_to?(:match)
1285
+ [path, path.names]
898
1286
  elsif path.respond_to? :match
899
1287
  [path, keys]
900
1288
  else
@@ -902,14 +1290,23 @@ module Sinatra
902
1290
  end
903
1291
  end
904
1292
 
1293
+ def encoded(char)
1294
+ enc = URI.encode(char)
1295
+ enc = "(?:#{Regexp.escape enc}|#{URI.encode char, /./})" if enc == char
1296
+ enc = "(?:#{enc}|#{encoded('+')})" if char == " "
1297
+ enc
1298
+ end
1299
+
905
1300
  public
906
1301
  # Makes the methods defined in the block and in the Modules given
907
1302
  # in `extensions` available to the handlers and templates
908
1303
  def helpers(*extensions, &block)
909
- class_eval(&block) if block_given?
1304
+ class_eval(&block) if block_given?
910
1305
  include(*extensions) if extensions.any?
911
1306
  end
912
1307
 
1308
+ # Register an extension. Alternatively take a block from which an
1309
+ # extension will be created and registered on the fly.
913
1310
  def register(*extensions, &block)
914
1311
  extensions << Module.new(&block) if block_given?
915
1312
  @extensions += extensions
@@ -935,24 +1332,31 @@ module Sinatra
935
1332
  @middleware << [middleware, args, block]
936
1333
  end
937
1334
 
1335
+ def quit!(server, handler_name)
1336
+ # Use Thin's hard #stop! if available, otherwise just #stop.
1337
+ server.respond_to?(:stop!) ? server.stop! : server.stop
1338
+ $stderr.puts "\n== Sinatra has ended his set (crowd applauds)" unless handler_name =~/cgi/i
1339
+ end
1340
+
938
1341
  # Run the Sinatra app as a self-hosted server using
939
- # Thin, Mongrel or WEBrick (in that order)
1342
+ # Thin, Mongrel or WEBrick (in that order). If given a block, will call
1343
+ # with the constructed handler once we have taken the stage.
940
1344
  def run!(options={})
941
1345
  set options
942
1346
  handler = detect_rack_handler
943
1347
  handler_name = handler.name.gsub(/.*::/, '')
944
- puts "== Sinatra/#{Sinatra::VERSION} has taken the stage " +
945
- "on #{port} for #{environment} with backup from #{handler_name}" unless handler_name =~/cgi/i
946
1348
  handler.run self, :Host => bind, :Port => port do |server|
947
- trap(:INT) do
948
- ## Use thins' hard #stop! if available, otherwise just #stop
949
- server.respond_to?(:stop!) ? server.stop! : server.stop
950
- puts "\n== Sinatra has ended his set (crowd applauds)" unless handler_name =~/cgi/i
1349
+ unless handler_name =~ /cgi/i
1350
+ $stderr.puts "== Sinatra/#{Sinatra::VERSION} has taken the stage " +
1351
+ "on #{port} for #{environment} with backup from #{handler_name}"
951
1352
  end
1353
+ [:INT, :TERM].each { |sig| trap(sig) { quit!(server, handler_name) } }
1354
+ server.threaded = settings.threaded if server.respond_to? :threaded=
952
1355
  set :running, true
1356
+ yield server if block_given?
953
1357
  end
954
- rescue Errno::EADDRINUSE => e
955
- puts "== Someone is already performing on port #{port}!"
1358
+ rescue Errno::EADDRINUSE
1359
+ $stderr.puts "== Someone is already performing on port #{port}!"
956
1360
  end
957
1361
 
958
1362
  # The prototype instance used to process requests.
@@ -960,19 +1364,23 @@ module Sinatra
960
1364
  @prototype ||= new
961
1365
  end
962
1366
 
1367
+ # Create a new instance without middleware in front of it.
1368
+ alias new! new unless method_defined? :new!
1369
+
963
1370
  # Create a new instance of the class fronted by its middleware
964
1371
  # pipeline. The object is guaranteed to respond to #call but may not be
965
1372
  # an instance of the class new was called on.
966
1373
  def new(*args, &bk)
967
- builder = Rack::Builder.new
968
- builder.use Rack::Session::Cookie if sessions?
969
- builder.use Rack::CommonLogger if logging?
970
- builder.use Rack::MethodOverride if method_override?
971
- builder.use ShowExceptions if show_exceptions?
972
- middleware.each { |c,a,b| builder.use(c, *a, &b) }
1374
+ build(Rack::Builder.new, *args, &bk).to_app
1375
+ end
973
1376
 
974
- builder.run super
975
- builder.to_app
1377
+ # Creates a Rack::Builder instance with all the middleware set up and
1378
+ # an instance of this class as end point.
1379
+ def build(builder, *args, &bk)
1380
+ setup_default_middleware builder
1381
+ setup_middleware builder
1382
+ builder.run new!(*args, &bk)
1383
+ builder
976
1384
  end
977
1385
 
978
1386
  def call(env)
@@ -980,13 +1388,66 @@ module Sinatra
980
1388
  end
981
1389
 
982
1390
  private
1391
+ def setup_default_middleware(builder)
1392
+ builder.use ShowExceptions if show_exceptions?
1393
+ builder.use Rack::MethodOverride if method_override?
1394
+ builder.use Rack::Head
1395
+ setup_logging builder
1396
+ setup_sessions builder
1397
+ setup_protection builder
1398
+ end
1399
+
1400
+ def setup_middleware(builder)
1401
+ middleware.each { |c,a,b| builder.use(c, *a, &b) }
1402
+ end
1403
+
1404
+ def setup_logging(builder)
1405
+ if logging?
1406
+ setup_common_logger(builder)
1407
+ setup_custom_logger(builder)
1408
+ elsif logging == false
1409
+ setup_null_logger(builder)
1410
+ end
1411
+ end
1412
+
1413
+ def setup_null_logger(builder)
1414
+ builder.use Rack::NullLogger
1415
+ end
1416
+
1417
+ def setup_common_logger(builder)
1418
+ builder.use Sinatra::CommonLogger
1419
+ end
1420
+
1421
+ def setup_custom_logger(builder)
1422
+ if logging.respond_to? :to_int
1423
+ builder.use Rack::Logger, logging
1424
+ else
1425
+ builder.use Rack::Logger
1426
+ end
1427
+ end
1428
+
1429
+ def setup_protection(builder)
1430
+ return unless protection?
1431
+ options = Hash === protection ? protection.dup : {}
1432
+ options[:except] = Array options[:except]
1433
+ options[:except] += [:session_hijacking, :remote_token] unless sessions?
1434
+ builder.use Rack::Protection, options
1435
+ end
1436
+
1437
+ def setup_sessions(builder)
1438
+ return unless sessions?
1439
+ options = {}
1440
+ options[:secret] = session_secret if session_secret?
1441
+ options.merge! sessions.to_hash if sessions.respond_to? :to_hash
1442
+ builder.use Rack::Session::Cookie, options
1443
+ end
1444
+
983
1445
  def detect_rack_handler
984
- servers = Array(self.server)
1446
+ servers = Array(server)
985
1447
  servers.each do |server_name|
986
1448
  begin
987
- return Rack::Handler.get(server_name.downcase)
988
- rescue LoadError
989
- rescue NameError
1449
+ return Rack::Handler.get(server_name.to_s)
1450
+ rescue LoadError, NameError
990
1451
  end
991
1452
  end
992
1453
  fail "Server handler (#{servers.join(',')}) not found."
@@ -994,6 +1455,7 @@ module Sinatra
994
1455
 
995
1456
  def inherited(subclass)
996
1457
  subclass.reset!
1458
+ subclass.set :app_file, caller_files.first unless subclass.app_file?
997
1459
  super
998
1460
  end
999
1461
 
@@ -1006,37 +1468,73 @@ module Sinatra
1006
1468
  end
1007
1469
  end
1008
1470
 
1009
- def metadef(message, &block)
1010
- (class << self; self; end).
1011
- send :define_method, message, &block
1012
- end
1013
-
1014
1471
  public
1015
- CALLERS_TO_IGNORE = [
1472
+ CALLERS_TO_IGNORE = [ # :nodoc:
1016
1473
  /\/sinatra(\/(base|main|showexceptions))?\.rb$/, # all sinatra code
1017
- /lib\/tilt.*\.rb$/, # all tilt code
1018
- /\(.*\)/, # generated code
1019
- /custom_require\.rb$/, # rubygems require hacks
1020
- /active_support/, # active_support require hacks
1474
+ /lib\/tilt.*\.rb$/, # all tilt code
1475
+ /^\(.*\)$/, # generated code
1476
+ /rubygems\/custom_require\.rb$/, # rubygems require hacks
1477
+ /active_support/, # active_support require hacks
1478
+ /bundler(\/runtime)?\.rb/, # bundler require hacks
1479
+ /<internal:/, # internal in ruby >= 1.9.2
1480
+ /src\/kernel\/bootstrap\/[A-Z]/ # maglev kernel files
1021
1481
  ]
1022
1482
 
1023
- # add rubinius (and hopefully other VM impls) ignore patterns ...
1024
- CALLERS_TO_IGNORE.concat(RUBY_IGNORE_CALLERS) if defined?(RUBY_IGNORE_CALLERS)
1483
+ # contrary to what the comment said previously, rubinius never supported this
1484
+ if defined?(RUBY_IGNORE_CALLERS)
1485
+ warn "RUBY_IGNORE_CALLERS is deprecated and will no longer be supported by Sinatra 2.0"
1486
+ CALLERS_TO_IGNORE.concat(RUBY_IGNORE_CALLERS)
1487
+ end
1025
1488
 
1026
1489
  # Like Kernel#caller but excluding certain magic entries and without
1027
1490
  # line / method information; the resulting array contains filenames only.
1028
1491
  def caller_files
1029
- caller_locations.
1030
- map { |file,line| file }
1492
+ cleaned_caller(1).flatten
1031
1493
  end
1032
1494
 
1495
+ # Like caller_files, but containing Arrays rather than strings with the
1496
+ # first element being the file, and the second being the line.
1033
1497
  def caller_locations
1498
+ cleaned_caller 2
1499
+ end
1500
+
1501
+ private
1502
+ # used for deprecation warnings
1503
+ def warn(message)
1504
+ super message + "\n\tfrom #{cleaned_caller.first.join(':')}"
1505
+ end
1506
+
1507
+ # Like Kernel#caller but excluding certain magic entries
1508
+ def cleaned_caller(keep = 3)
1034
1509
  caller(1).
1035
- map { |line| line.split(/:(?=\d|in )/)[0,2] }.
1036
- reject { |file,line| CALLERS_TO_IGNORE.any? { |pattern| file =~ pattern } }
1510
+ map { |line| line.split(/:(?=\d|in )/, 3)[0,keep] }.
1511
+ reject { |file, *_| CALLERS_TO_IGNORE.any? { |pattern| file =~ pattern } }
1037
1512
  end
1038
1513
  end
1039
1514
 
1515
+ # Fixes encoding issues by
1516
+ # * defaulting to UTF-8
1517
+ # * casting params to Encoding.default_external
1518
+ #
1519
+ # The latter might not be necessary if Rack handles it one day.
1520
+ # Keep an eye on Rack's LH #100.
1521
+ def force_encoding(*args) settings.force_encoding(*args) end
1522
+ if defined? Encoding
1523
+ def self.force_encoding(data, encoding = default_encoding)
1524
+ return if data == settings || data.is_a?(Tempfile)
1525
+ if data.respond_to? :force_encoding
1526
+ data.force_encoding(encoding).encode!
1527
+ elsif data.respond_to? :each_value
1528
+ data.each_value { |v| force_encoding(v, encoding) }
1529
+ elsif data.respond_to? :each
1530
+ data.each { |v| force_encoding(v, encoding) }
1531
+ end
1532
+ data
1533
+ end
1534
+ else
1535
+ def self.force_encoding(data, *) data end
1536
+ end
1537
+
1040
1538
  reset!
1041
1539
 
1042
1540
  set :environment, (ENV['RACK_ENV'] || :development).to_sym
@@ -1045,7 +1543,21 @@ module Sinatra
1045
1543
  set :show_exceptions, Proc.new { development? }
1046
1544
  set :sessions, false
1047
1545
  set :logging, false
1546
+ set :protection, true
1048
1547
  set :method_override, false
1548
+ set :use_code, true
1549
+ set :default_encoding, "utf-8"
1550
+ set :add_charset, %w[javascript xml xhtml+xml json].map { |t| "application/#{t}" }
1551
+ settings.add_charset << /^text\//
1552
+
1553
+ # explicitly generating a session secret eagerly to play nice with preforking
1554
+ begin
1555
+ require 'securerandom'
1556
+ set :session_secret, SecureRandom.hex(64)
1557
+ rescue LoadError, NotImplementedError
1558
+ # SecureRandom raises a NotImplementedError if no random device is available
1559
+ set :session_secret, "%064x" % Kernel.rand(2**256-1)
1560
+ end
1049
1561
 
1050
1562
  class << self
1051
1563
  alias_method :methodoverride?, :method_override?
@@ -1054,18 +1566,24 @@ module Sinatra
1054
1566
 
1055
1567
  set :run, false # start server via at-exit hook?
1056
1568
  set :running, false # is the built-in server running now?
1057
- set :server, %w[thin mongrel webrick]
1569
+ set :server, %w[thin puma mongrel webrick]
1058
1570
  set :bind, '0.0.0.0'
1059
1571
  set :port, 4567
1060
1572
 
1573
+ set :absolute_redirects, true
1574
+ set :prefixed_redirects, false
1575
+ set :empty_path_info, nil
1576
+
1061
1577
  set :app_file, nil
1062
1578
  set :root, Proc.new { app_file && File.expand_path(File.dirname(app_file)) }
1063
1579
  set :views, Proc.new { root && File.join(root, 'views') }
1064
1580
  set :reload_templates, Proc.new { development? }
1065
1581
  set :lock, false
1582
+ set :threaded, true
1066
1583
 
1067
- set :public, Proc.new { root && File.join(root, 'public') }
1068
- set :static, Proc.new { self.public && File.exist?(self.public) }
1584
+ set :public_folder, Proc.new { root && File.join(root, 'public') }
1585
+ set :static, Proc.new { public_folder && File.exist?(public_folder) }
1586
+ set :static_cache_control, false
1069
1587
 
1070
1588
  error ::Exception do
1071
1589
  response.status = 500
@@ -1094,8 +1612,8 @@ module Sinatra
1094
1612
  </style>
1095
1613
  </head>
1096
1614
  <body>
1097
- <h2>Sinatra doesn't know this ditty.</h2>
1098
- <img src='/__sinatra__/404.png'>
1615
+ <h2>Sinatra doesn&rsquo;t know this ditty.</h2>
1616
+ <img src='#{uri "/__sinatra__/404.png"}'>
1099
1617
  <div id="c">
1100
1618
  Try this:
1101
1619
  <pre>#{request.request_method.downcase} '#{request.path_info}' do\n "Hello World"\nend</pre>
@@ -1112,12 +1630,14 @@ module Sinatra
1112
1630
  #
1113
1631
  # The Application class should not be subclassed, unless you want to
1114
1632
  # inherit all settings, routes, handlers, and error pages from the
1115
- # top-level. Subclassing Sinatra::Base is heavily recommended for
1633
+ # top-level. Subclassing Sinatra::Base is highly recommended for
1116
1634
  # modular applications.
1117
1635
  class Application < Base
1118
1636
  set :logging, Proc.new { ! test? }
1119
1637
  set :method_override, true
1120
1638
  set :run, Proc.new { ! test? }
1639
+ set :session_secret, Proc.new { super() unless development? }
1640
+ set :app_file, nil
1121
1641
 
1122
1642
  def self.register(*extensions, &block) #:nodoc:
1123
1643
  added_methods = extensions.map {|m| m.public_instance_methods }.flatten
@@ -1132,36 +1652,46 @@ module Sinatra
1132
1652
  module Delegator #:nodoc:
1133
1653
  def self.delegate(*methods)
1134
1654
  methods.each do |method_name|
1135
- eval <<-RUBY, binding, '(__DELEGATE__)', 1
1136
- def #{method_name}(*args, &b)
1137
- ::Sinatra::Application.send(#{method_name.inspect}, *args, &b)
1138
- end
1139
- private #{method_name.inspect}
1140
- RUBY
1655
+ define_method(method_name) do |*args, &block|
1656
+ return super(*args, &block) if respond_to? method_name
1657
+ Delegator.target.send(method_name, *args, &block)
1658
+ end
1659
+ private method_name
1141
1660
  end
1142
1661
  end
1143
1662
 
1144
- delegate :get, :put, :post, :delete, :head, :template, :layout,
1663
+ delegate :get, :patch, :put, :post, :delete, :head, :options, :template, :layout,
1145
1664
  :before, :after, :error, :not_found, :configure, :set, :mime_type,
1146
1665
  :enable, :disable, :use, :development?, :test?, :production?,
1147
1666
  :helpers, :settings
1667
+
1668
+ class << self
1669
+ attr_accessor :target
1670
+ end
1671
+
1672
+ self.target = Application
1148
1673
  end
1149
1674
 
1150
1675
  # Create a new Sinatra application. The block is evaluated in the new app's
1151
1676
  # class scope.
1152
1677
  def self.new(base=Base, options={}, &block)
1153
1678
  base = Class.new(base)
1154
- base.send :class_eval, &block if block_given?
1679
+ base.class_eval(&block) if block_given?
1155
1680
  base
1156
1681
  end
1157
1682
 
1158
1683
  # Extend the top-level DSL with the modules provided.
1159
1684
  def self.register(*extensions, &block)
1160
- Application.register(*extensions, &block)
1685
+ Delegator.target.register(*extensions, &block)
1161
1686
  end
1162
1687
 
1163
1688
  # Include the helper modules provided in Sinatra's request context.
1164
1689
  def self.helpers(*extensions, &block)
1165
- Application.helpers(*extensions, &block)
1690
+ Delegator.target.helpers(*extensions, &block)
1691
+ end
1692
+
1693
+ # Use the middleware for classic applications.
1694
+ def self.use(*args, &block)
1695
+ Delegator.target.use(*args, &block)
1166
1696
  end
1167
1697
  end