sinatra-base 1.0 → 1.4.0

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