sinatra 2.0.7 → 3.0.6
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +216 -0
- data/CONTRIBUTING.md +11 -11
- data/Gemfile +44 -61
- data/MAINTENANCE.md +3 -16
- data/README.md +205 -470
- data/Rakefile +66 -75
- data/VERSION +1 -1
- data/examples/chat.rb +25 -11
- data/examples/rainbows.conf +3 -0
- data/examples/rainbows.rb +22 -0
- data/examples/simple.rb +2 -0
- data/examples/stream.ru +6 -4
- data/lib/sinatra/base.rb +466 -398
- data/lib/sinatra/indifferent_hash.rb +51 -41
- data/lib/sinatra/main.rb +19 -17
- data/lib/sinatra/show_exceptions.rb +19 -52
- data/lib/sinatra/version.rb +3 -1
- data/lib/sinatra.rb +2 -0
- data/sinatra.gemspec +40 -41
- metadata +41 -43
- data/README.de.md +0 -3239
- data/README.es.md +0 -3202
- data/README.fr.md +0 -3014
- data/README.hu.md +0 -728
- data/README.ja.md +0 -2814
- data/README.ko.md +0 -2967
- data/README.malayalam.md +0 -3141
- data/README.pt-br.md +0 -1760
- data/README.pt-pt.md +0 -791
- data/README.ru.md +0 -3207
- data/README.zh.md +0 -2934
data/lib/sinatra/base.rb
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
# coding: utf-8
|
|
2
1
|
# frozen_string_literal: true
|
|
3
2
|
|
|
4
3
|
# external dependencies
|
|
@@ -10,7 +9,6 @@ require 'mustermann/sinatra'
|
|
|
10
9
|
require 'mustermann/regular'
|
|
11
10
|
|
|
12
11
|
# stdlib dependencies
|
|
13
|
-
require 'thread'
|
|
14
12
|
require 'time'
|
|
15
13
|
require 'uri'
|
|
16
14
|
|
|
@@ -21,21 +19,22 @@ require 'sinatra/version'
|
|
|
21
19
|
|
|
22
20
|
module Sinatra
|
|
23
21
|
# The request object. See Rack::Request for more info:
|
|
24
|
-
#
|
|
22
|
+
# https://rubydoc.info/github/rack/rack/main/Rack/Request
|
|
25
23
|
class Request < Rack::Request
|
|
26
|
-
HEADER_PARAM = /\s*[\w.]+=(?:[\w.]+|"(?:[^"\\]|\\.)*")?\s
|
|
27
|
-
HEADER_VALUE_WITH_PARAMS =
|
|
24
|
+
HEADER_PARAM = /\s*[\w.]+=(?:[\w.]+|"(?:[^"\\]|\\.)*")?\s*/.freeze
|
|
25
|
+
HEADER_VALUE_WITH_PARAMS = %r{(?:(?:\w+|\*)/(?:\w+(?:\.|-|\+)?|\*)*)\s*(?:;#{HEADER_PARAM})*}.freeze
|
|
28
26
|
|
|
29
27
|
# Returns an array of acceptable media types for the response
|
|
30
28
|
def accept
|
|
31
|
-
@env['sinatra.accept'] ||=
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
29
|
+
@env['sinatra.accept'] ||= if @env.include?('HTTP_ACCEPT') && (@env['HTTP_ACCEPT'].to_s != '')
|
|
30
|
+
@env['HTTP_ACCEPT']
|
|
31
|
+
.to_s
|
|
32
|
+
.scan(HEADER_VALUE_WITH_PARAMS)
|
|
33
|
+
.map! { |e| AcceptEntry.new(e) }
|
|
34
|
+
.sort
|
|
35
|
+
else
|
|
36
|
+
[AcceptEntry.new('*/*')]
|
|
37
|
+
end
|
|
39
38
|
end
|
|
40
39
|
|
|
41
40
|
def accept?(type)
|
|
@@ -43,12 +42,13 @@ module Sinatra
|
|
|
43
42
|
end
|
|
44
43
|
|
|
45
44
|
def preferred_type(*types)
|
|
46
|
-
|
|
47
|
-
|
|
45
|
+
return accept.first if types.empty?
|
|
46
|
+
|
|
48
47
|
types.flatten!
|
|
49
|
-
return types.first if
|
|
50
|
-
|
|
51
|
-
|
|
48
|
+
return types.first if accept.empty?
|
|
49
|
+
|
|
50
|
+
accept.detect do |accept_header|
|
|
51
|
+
type = types.detect { |t| MimeTypeEntry.new(t).accepts?(accept_header) }
|
|
52
52
|
return type if type
|
|
53
53
|
end
|
|
54
54
|
end
|
|
@@ -56,33 +56,33 @@ module Sinatra
|
|
|
56
56
|
alias secure? ssl?
|
|
57
57
|
|
|
58
58
|
def forwarded?
|
|
59
|
-
@env.include?
|
|
59
|
+
@env.include? 'HTTP_X_FORWARDED_HOST'
|
|
60
60
|
end
|
|
61
61
|
|
|
62
62
|
def safe?
|
|
63
|
-
get?
|
|
63
|
+
get? || head? || options? || trace?
|
|
64
64
|
end
|
|
65
65
|
|
|
66
66
|
def idempotent?
|
|
67
|
-
safe?
|
|
67
|
+
safe? || put? || delete? || link? || unlink?
|
|
68
68
|
end
|
|
69
69
|
|
|
70
70
|
def link?
|
|
71
|
-
request_method ==
|
|
71
|
+
request_method == 'LINK'
|
|
72
72
|
end
|
|
73
73
|
|
|
74
74
|
def unlink?
|
|
75
|
-
request_method ==
|
|
75
|
+
request_method == 'UNLINK'
|
|
76
76
|
end
|
|
77
77
|
|
|
78
78
|
def params
|
|
79
79
|
super
|
|
80
80
|
rescue Rack::Utils::ParameterTypeError, Rack::Utils::InvalidParameterError => e
|
|
81
81
|
raise BadRequest, "Invalid query parameters: #{Rack::Utils.escape_html(e.message)}"
|
|
82
|
+
rescue EOFError => e
|
|
83
|
+
raise BadRequest, "Invalid multipart/form-data: #{Rack::Utils.escape_html(e.message)}"
|
|
82
84
|
end
|
|
83
85
|
|
|
84
|
-
private
|
|
85
|
-
|
|
86
86
|
class AcceptEntry
|
|
87
87
|
attr_accessor :params
|
|
88
88
|
attr_reader :entry
|
|
@@ -96,17 +96,17 @@ module Sinatra
|
|
|
96
96
|
|
|
97
97
|
@entry = entry
|
|
98
98
|
@type = entry[/[^;]+/].delete(' ')
|
|
99
|
-
@params =
|
|
99
|
+
@params = params.to_h
|
|
100
100
|
@q = @params.delete('q') { 1.0 }.to_f
|
|
101
101
|
end
|
|
102
102
|
|
|
103
103
|
def <=>(other)
|
|
104
|
-
other.priority <=>
|
|
104
|
+
other.priority <=> priority
|
|
105
105
|
end
|
|
106
106
|
|
|
107
107
|
def priority
|
|
108
108
|
# We sort in descending order; better matches should be higher.
|
|
109
|
-
[
|
|
109
|
+
[@q, -@type.count('*'), @params.size]
|
|
110
110
|
end
|
|
111
111
|
|
|
112
112
|
def to_str
|
|
@@ -118,25 +118,50 @@ module Sinatra
|
|
|
118
118
|
end
|
|
119
119
|
|
|
120
120
|
def respond_to?(*args)
|
|
121
|
-
super
|
|
121
|
+
super || to_str.respond_to?(*args)
|
|
122
122
|
end
|
|
123
123
|
|
|
124
124
|
def method_missing(*args, &block)
|
|
125
125
|
to_str.send(*args, &block)
|
|
126
126
|
end
|
|
127
127
|
end
|
|
128
|
+
|
|
129
|
+
class MimeTypeEntry
|
|
130
|
+
attr_reader :params
|
|
131
|
+
|
|
132
|
+
def initialize(entry)
|
|
133
|
+
params = entry.scan(HEADER_PARAM).map! do |s|
|
|
134
|
+
key, value = s.strip.split('=', 2)
|
|
135
|
+
value = value[1..-2].gsub(/\\(.)/, '\1') if value.start_with?('"')
|
|
136
|
+
[key, value]
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
@type = entry[/[^;]+/].delete(' ')
|
|
140
|
+
@params = params.to_h
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def accepts?(entry)
|
|
144
|
+
File.fnmatch(entry, self) && matches_params?(entry.params)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def to_str
|
|
148
|
+
@type
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def matches_params?(params)
|
|
152
|
+
return true if @params.empty?
|
|
153
|
+
|
|
154
|
+
params.all? { |k, v| !@params.key?(k) || @params[k] == v }
|
|
155
|
+
end
|
|
156
|
+
end
|
|
128
157
|
end
|
|
129
158
|
|
|
130
159
|
# The response object. See Rack::Response and Rack::Response::Helpers for
|
|
131
160
|
# more info:
|
|
132
|
-
#
|
|
133
|
-
#
|
|
161
|
+
# https://rubydoc.info/github/rack/rack/main/Rack/Response
|
|
162
|
+
# https://rubydoc.info/github/rack/rack/main/Rack/Response/Helpers
|
|
134
163
|
class Response < Rack::Response
|
|
135
|
-
DROP_BODY_RESPONSES = [204, 304]
|
|
136
|
-
def initialize(*)
|
|
137
|
-
super
|
|
138
|
-
headers['Content-Type'] ||= 'text/html'
|
|
139
|
-
end
|
|
164
|
+
DROP_BODY_RESPONSES = [204, 304].freeze
|
|
140
165
|
|
|
141
166
|
def body=(value)
|
|
142
167
|
value = value.body while Rack::Response === value
|
|
@@ -151,8 +176,8 @@ module Sinatra
|
|
|
151
176
|
result = body
|
|
152
177
|
|
|
153
178
|
if drop_content_info?
|
|
154
|
-
headers.delete
|
|
155
|
-
headers.delete
|
|
179
|
+
headers.delete 'Content-Length'
|
|
180
|
+
headers.delete 'Content-Type'
|
|
156
181
|
end
|
|
157
182
|
|
|
158
183
|
if drop_body?
|
|
@@ -163,36 +188,38 @@ module Sinatra
|
|
|
163
188
|
if calculate_content_length?
|
|
164
189
|
# if some other code has already set Content-Length, don't muck with it
|
|
165
190
|
# currently, this would be the static file-handler
|
|
166
|
-
headers[
|
|
191
|
+
headers['Content-Length'] = body.map(&:bytesize).reduce(0, :+).to_s
|
|
167
192
|
end
|
|
168
193
|
|
|
169
|
-
[status
|
|
194
|
+
[status, headers, result]
|
|
170
195
|
end
|
|
171
196
|
|
|
172
197
|
private
|
|
173
198
|
|
|
174
199
|
def calculate_content_length?
|
|
175
|
-
headers[
|
|
200
|
+
headers['Content-Type'] && !headers['Content-Length'] && (Array === body)
|
|
176
201
|
end
|
|
177
202
|
|
|
178
203
|
def drop_content_info?
|
|
179
|
-
|
|
204
|
+
informational? || drop_body?
|
|
180
205
|
end
|
|
181
206
|
|
|
182
207
|
def drop_body?
|
|
183
|
-
DROP_BODY_RESPONSES.include?(status
|
|
208
|
+
DROP_BODY_RESPONSES.include?(status)
|
|
184
209
|
end
|
|
185
210
|
end
|
|
186
211
|
|
|
187
|
-
# Some Rack handlers (
|
|
212
|
+
# Some Rack handlers (Rainbows!) implement an extended body object protocol, however,
|
|
188
213
|
# some middleware (namely Rack::Lint) will break it by not mirroring the methods in question.
|
|
189
214
|
# This middleware will detect an extended body object and will make sure it reaches the
|
|
190
215
|
# handler directly. We do this here, so our middleware and middleware set up by the app will
|
|
191
216
|
# still be able to run.
|
|
192
217
|
class ExtendedRack < Struct.new(:app)
|
|
193
218
|
def call(env)
|
|
194
|
-
result
|
|
195
|
-
|
|
219
|
+
result = app.call(env)
|
|
220
|
+
callback = env['async.callback']
|
|
221
|
+
return result unless callback && async?(*result)
|
|
222
|
+
|
|
196
223
|
after_response { callback.call result }
|
|
197
224
|
setup_close(env, *result)
|
|
198
225
|
throw :async
|
|
@@ -200,20 +227,23 @@ module Sinatra
|
|
|
200
227
|
|
|
201
228
|
private
|
|
202
229
|
|
|
203
|
-
def setup_close(env,
|
|
204
|
-
return unless body.respond_to?
|
|
230
|
+
def setup_close(env, _status, _headers, body)
|
|
231
|
+
return unless body.respond_to?(:close) && env.include?('async.close')
|
|
232
|
+
|
|
205
233
|
env['async.close'].callback { body.close }
|
|
206
234
|
env['async.close'].errback { body.close }
|
|
207
235
|
end
|
|
208
236
|
|
|
209
237
|
def after_response(&block)
|
|
210
|
-
raise NotImplementedError,
|
|
238
|
+
raise NotImplementedError, 'only supports EventMachine at the moment' unless defined? EventMachine
|
|
239
|
+
|
|
211
240
|
EventMachine.next_tick(&block)
|
|
212
241
|
end
|
|
213
242
|
|
|
214
|
-
def async?(status,
|
|
243
|
+
def async?(status, _headers, body)
|
|
215
244
|
return true if status == -1
|
|
216
|
-
|
|
245
|
+
|
|
246
|
+
body.respond_to?(:callback) && body.respond_to?(:errback)
|
|
217
247
|
end
|
|
218
248
|
end
|
|
219
249
|
|
|
@@ -225,7 +255,7 @@ module Sinatra
|
|
|
225
255
|
end
|
|
226
256
|
|
|
227
257
|
superclass.class_eval do
|
|
228
|
-
|
|
258
|
+
alias_method :call_without_check, :call unless method_defined? :call_without_check
|
|
229
259
|
def call(env)
|
|
230
260
|
env['sinatra.commonlogger'] = true
|
|
231
261
|
call_without_check(env)
|
|
@@ -233,11 +263,14 @@ module Sinatra
|
|
|
233
263
|
end
|
|
234
264
|
end
|
|
235
265
|
|
|
236
|
-
class
|
|
266
|
+
class Error < StandardError # :nodoc:
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
class BadRequest < Error # :nodoc:
|
|
237
270
|
def http_status; 400 end
|
|
238
271
|
end
|
|
239
272
|
|
|
240
|
-
class NotFound <
|
|
273
|
+
class NotFound < Error # :nodoc:
|
|
241
274
|
def http_status; 404 end
|
|
242
275
|
end
|
|
243
276
|
|
|
@@ -258,7 +291,7 @@ module Sinatra
|
|
|
258
291
|
elsif value
|
|
259
292
|
# Rack 2.0 returns a Rack::File::Iterator here instead of
|
|
260
293
|
# Rack::File as it was in the previous API.
|
|
261
|
-
unless request.head? || value.is_a?(Rack::
|
|
294
|
+
unless request.head? || value.is_a?(Rack::Files::Iterator) || value.is_a?(Stream)
|
|
262
295
|
headers.delete 'Content-Length'
|
|
263
296
|
end
|
|
264
297
|
response.body = value
|
|
@@ -269,7 +302,7 @@ module Sinatra
|
|
|
269
302
|
|
|
270
303
|
# Halt processing and redirect to the URI provided.
|
|
271
304
|
def redirect(uri, *args)
|
|
272
|
-
if env['HTTP_VERSION'] == 'HTTP/1.1'
|
|
305
|
+
if (env['HTTP_VERSION'] == 'HTTP/1.1') && (env['REQUEST_METHOD'] != 'GET')
|
|
273
306
|
status 303
|
|
274
307
|
else
|
|
275
308
|
status 302
|
|
@@ -284,18 +317,19 @@ module Sinatra
|
|
|
284
317
|
# Generates the absolute URI for a given path in the app.
|
|
285
318
|
# Takes Rack routers and reverse proxies into account.
|
|
286
319
|
def uri(addr = nil, absolute = true, add_script_name = true)
|
|
287
|
-
return addr if addr =~ /\A[a-z][a-z0-9
|
|
320
|
+
return addr if addr.to_s =~ /\A[a-z][a-z0-9+.\-]*:/i
|
|
321
|
+
|
|
288
322
|
uri = [host = String.new]
|
|
289
323
|
if absolute
|
|
290
324
|
host << "http#{'s' if request.secure?}://"
|
|
291
|
-
if request.forwarded?
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
325
|
+
host << if request.forwarded? || (request.port != (request.secure? ? 443 : 80))
|
|
326
|
+
request.host_with_port
|
|
327
|
+
else
|
|
328
|
+
request.host
|
|
329
|
+
end
|
|
296
330
|
end
|
|
297
331
|
uri << request.script_name.to_s if add_script_name
|
|
298
|
-
uri << (addr
|
|
332
|
+
uri << (addr || request.path_info).to_s
|
|
299
333
|
File.join uri
|
|
300
334
|
end
|
|
301
335
|
|
|
@@ -304,7 +338,10 @@ module Sinatra
|
|
|
304
338
|
|
|
305
339
|
# Halt processing and return the error status provided.
|
|
306
340
|
def error(code, body = nil)
|
|
307
|
-
|
|
341
|
+
if code.respond_to? :to_str
|
|
342
|
+
body = code.to_str
|
|
343
|
+
code = 500
|
|
344
|
+
end
|
|
308
345
|
response.body = body unless body.nil?
|
|
309
346
|
halt code
|
|
310
347
|
end
|
|
@@ -339,11 +376,13 @@ module Sinatra
|
|
|
339
376
|
# extension.
|
|
340
377
|
def content_type(type = nil, params = {})
|
|
341
378
|
return response['Content-Type'] unless type
|
|
379
|
+
|
|
342
380
|
default = params.delete :default
|
|
343
381
|
mime_type = mime_type(type) || default
|
|
344
|
-
|
|
382
|
+
raise format('Unknown media type: %p', type) if mime_type.nil?
|
|
383
|
+
|
|
345
384
|
mime_type = mime_type.dup
|
|
346
|
-
unless params.include?
|
|
385
|
+
unless params.include?(:charset) || settings.add_charset.all? { |p| !(p === mime_type) }
|
|
347
386
|
params[:charset] = params.delete('charset') || settings.default_encoding
|
|
348
387
|
end
|
|
349
388
|
params.delete :charset if mime_type.include? 'charset'
|
|
@@ -357,36 +396,43 @@ module Sinatra
|
|
|
357
396
|
response['Content-Type'] = mime_type
|
|
358
397
|
end
|
|
359
398
|
|
|
399
|
+
# https://html.spec.whatwg.org/#multipart-form-data
|
|
400
|
+
MULTIPART_FORM_DATA_REPLACEMENT_TABLE = {
|
|
401
|
+
'"' => '%22',
|
|
402
|
+
"\r" => '%0D',
|
|
403
|
+
"\n" => '%0A'
|
|
404
|
+
}.freeze
|
|
405
|
+
|
|
360
406
|
# Set the Content-Disposition to "attachment" with the specified filename,
|
|
361
407
|
# instructing the user agents to prompt to save.
|
|
362
408
|
def attachment(filename = nil, disposition = :attachment)
|
|
363
409
|
response['Content-Disposition'] = disposition.to_s.dup
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
410
|
+
return unless filename
|
|
411
|
+
|
|
412
|
+
params = format('; filename="%s"', File.basename(filename).gsub(/["\r\n]/, MULTIPART_FORM_DATA_REPLACEMENT_TABLE))
|
|
413
|
+
response['Content-Disposition'] << params
|
|
414
|
+
ext = File.extname(filename)
|
|
415
|
+
content_type(ext) unless response['Content-Type'] || ext.empty?
|
|
370
416
|
end
|
|
371
417
|
|
|
372
418
|
# Use the contents of the file at +path+ as the response body.
|
|
373
419
|
def send_file(path, opts = {})
|
|
374
|
-
if opts[:type]
|
|
375
|
-
content_type opts[:type] || File.extname(path), :
|
|
420
|
+
if opts[:type] || !response['Content-Type']
|
|
421
|
+
content_type opts[:type] || File.extname(path), default: 'application/octet-stream'
|
|
376
422
|
end
|
|
377
423
|
|
|
378
424
|
disposition = opts[:disposition]
|
|
379
425
|
filename = opts[:filename]
|
|
380
|
-
disposition = :attachment if disposition.nil?
|
|
426
|
+
disposition = :attachment if disposition.nil? && filename
|
|
381
427
|
filename = path if filename.nil?
|
|
382
428
|
attachment(filename, disposition) if disposition
|
|
383
429
|
|
|
384
430
|
last_modified opts[:last_modified] if opts[:last_modified]
|
|
385
431
|
|
|
386
|
-
file = Rack::
|
|
432
|
+
file = Rack::Files.new(File.dirname(settings.app_file))
|
|
387
433
|
result = file.serving(request, path)
|
|
388
434
|
|
|
389
|
-
result[1].each { |k,v| headers[k] ||= v }
|
|
435
|
+
result[1].each { |k, v| headers[k] ||= v }
|
|
390
436
|
headers['Content-Length'] = result[1]['Content-Length']
|
|
391
437
|
opts[:status] &&= Integer(opts[:status])
|
|
392
438
|
halt (opts[:status] || result[0]), result[2]
|
|
@@ -407,12 +453,16 @@ module Sinatra
|
|
|
407
453
|
def self.defer(*) yield end
|
|
408
454
|
|
|
409
455
|
def initialize(scheduler = self.class, keep_open = false, &back)
|
|
410
|
-
@back
|
|
411
|
-
@
|
|
456
|
+
@back = back.to_proc
|
|
457
|
+
@scheduler = scheduler
|
|
458
|
+
@keep_open = keep_open
|
|
459
|
+
@callbacks = []
|
|
460
|
+
@closed = false
|
|
412
461
|
end
|
|
413
462
|
|
|
414
463
|
def close
|
|
415
464
|
return if closed?
|
|
465
|
+
|
|
416
466
|
@closed = true
|
|
417
467
|
@scheduler.schedule { @callbacks.each { |c| c.call } }
|
|
418
468
|
end
|
|
@@ -424,8 +474,9 @@ module Sinatra
|
|
|
424
474
|
@back.call(self)
|
|
425
475
|
rescue Exception => e
|
|
426
476
|
@scheduler.schedule { raise e }
|
|
477
|
+
ensure
|
|
478
|
+
close unless @keep_open
|
|
427
479
|
end
|
|
428
|
-
close unless @keep_open
|
|
429
480
|
end
|
|
430
481
|
end
|
|
431
482
|
|
|
@@ -436,6 +487,7 @@ module Sinatra
|
|
|
436
487
|
|
|
437
488
|
def callback(&block)
|
|
438
489
|
return yield if closed?
|
|
490
|
+
|
|
439
491
|
@callbacks << block
|
|
440
492
|
end
|
|
441
493
|
|
|
@@ -451,11 +503,20 @@ module Sinatra
|
|
|
451
503
|
#
|
|
452
504
|
# The close parameter specifies whether Stream#close should be called
|
|
453
505
|
# after the block has been executed. This is only relevant for evented
|
|
454
|
-
# servers like
|
|
506
|
+
# servers like Rainbows.
|
|
455
507
|
def stream(keep_open = false)
|
|
456
508
|
scheduler = env['async.callback'] ? EventMachine : Stream
|
|
457
509
|
current = @params.dup
|
|
458
|
-
|
|
510
|
+
stream = if scheduler == Stream && keep_open
|
|
511
|
+
Stream.new(scheduler, false) do |out|
|
|
512
|
+
until out.closed?
|
|
513
|
+
with_params(current) { yield(out) }
|
|
514
|
+
end
|
|
515
|
+
end
|
|
516
|
+
else
|
|
517
|
+
Stream.new(scheduler, keep_open) { |out| with_params(current) { yield(out) } }
|
|
518
|
+
end
|
|
519
|
+
body stream
|
|
459
520
|
end
|
|
460
521
|
|
|
461
522
|
# Specify response freshness policy for HTTP caches (Cache-Control header).
|
|
@@ -469,18 +530,18 @@ module Sinatra
|
|
|
469
530
|
# See RFC 2616 / 14.9 for more on standard cache control directives:
|
|
470
531
|
# http://tools.ietf.org/html/rfc2616#section-14.9.1
|
|
471
532
|
def cache_control(*values)
|
|
472
|
-
if values.last.
|
|
533
|
+
if values.last.is_a?(Hash)
|
|
473
534
|
hash = values.pop
|
|
474
|
-
hash.reject! { |
|
|
535
|
+
hash.reject! { |_k, v| v == false }
|
|
475
536
|
hash.reject! { |k, v| values << k if v == true }
|
|
476
537
|
else
|
|
477
538
|
hash = {}
|
|
478
539
|
end
|
|
479
540
|
|
|
480
|
-
values.map! { |value| value.to_s.tr('_','-') }
|
|
541
|
+
values.map! { |value| value.to_s.tr('_', '-') }
|
|
481
542
|
hash.each do |key, value|
|
|
482
543
|
key = key.to_s.tr('_', '-')
|
|
483
|
-
value = value.to_i if [
|
|
544
|
+
value = value.to_i if %w[max-age s-maxage].include? key
|
|
484
545
|
values << "#{key}=#{value}"
|
|
485
546
|
end
|
|
486
547
|
|
|
@@ -497,7 +558,7 @@ module Sinatra
|
|
|
497
558
|
# => Expires: Mon, 08 Jun 2009 08:50:17 GMT
|
|
498
559
|
#
|
|
499
560
|
def expires(amount, *values)
|
|
500
|
-
values << {} unless values.last.
|
|
561
|
+
values << {} unless values.last.is_a?(Hash)
|
|
501
562
|
|
|
502
563
|
if amount.is_a? Integer
|
|
503
564
|
time = Time.now + amount.to_i
|
|
@@ -507,7 +568,7 @@ module Sinatra
|
|
|
507
568
|
max_age = time - Time.now
|
|
508
569
|
end
|
|
509
570
|
|
|
510
|
-
values.last.merge!(:max_age
|
|
571
|
+
values.last.merge!(max_age: max_age) { |_key, v1, v2| v1 || v2 }
|
|
511
572
|
cache_control(*values)
|
|
512
573
|
|
|
513
574
|
response['Expires'] = time.httpdate
|
|
@@ -522,17 +583,18 @@ module Sinatra
|
|
|
522
583
|
# with a '304 Not Modified' response.
|
|
523
584
|
def last_modified(time)
|
|
524
585
|
return unless time
|
|
586
|
+
|
|
525
587
|
time = time_for time
|
|
526
588
|
response['Last-Modified'] = time.httpdate
|
|
527
589
|
return if env['HTTP_IF_NONE_MATCH']
|
|
528
590
|
|
|
529
|
-
if status == 200
|
|
591
|
+
if (status == 200) && env['HTTP_IF_MODIFIED_SINCE']
|
|
530
592
|
# compare based on seconds since epoch
|
|
531
593
|
since = Time.httpdate(env['HTTP_IF_MODIFIED_SINCE']).to_i
|
|
532
594
|
halt 304 if since >= time.to_i
|
|
533
595
|
end
|
|
534
596
|
|
|
535
|
-
if (success?
|
|
597
|
+
if (success? || (status == 412)) && env['HTTP_IF_UNMODIFIED_SINCE']
|
|
536
598
|
# compare based on seconds since epoch
|
|
537
599
|
since = Time.httpdate(env['HTTP_IF_UNMODIFIED_SINCE']).to_i
|
|
538
600
|
halt 412 if since < time.to_i
|
|
@@ -540,7 +602,7 @@ module Sinatra
|
|
|
540
602
|
rescue ArgumentError
|
|
541
603
|
end
|
|
542
604
|
|
|
543
|
-
ETAG_KINDS = [
|
|
605
|
+
ETAG_KINDS = %i[strong weak].freeze
|
|
544
606
|
# Set the response entity tag (HTTP 'ETag' header) and halt if conditional
|
|
545
607
|
# GET matches. The +value+ argument is an identifier that uniquely
|
|
546
608
|
# identifies the current version of the resource. The +kind+ argument
|
|
@@ -552,27 +614,31 @@ module Sinatra
|
|
|
552
614
|
# GET or HEAD, a '304 Not Modified' response is sent.
|
|
553
615
|
def etag(value, options = {})
|
|
554
616
|
# Before touching this code, please double check RFC 2616 14.24 and 14.26.
|
|
555
|
-
options = {:
|
|
617
|
+
options = { kind: options } unless Hash === options
|
|
556
618
|
kind = options[:kind] || :strong
|
|
557
619
|
new_resource = options.fetch(:new_resource) { request.post? }
|
|
558
620
|
|
|
559
621
|
unless ETAG_KINDS.include?(kind)
|
|
560
|
-
raise ArgumentError,
|
|
622
|
+
raise ArgumentError, ':strong or :weak expected'
|
|
561
623
|
end
|
|
562
624
|
|
|
563
|
-
value = '"%s"'
|
|
625
|
+
value = format('"%s"', value)
|
|
564
626
|
value = "W/#{value}" if kind == :weak
|
|
565
627
|
response['ETag'] = value
|
|
566
628
|
|
|
567
|
-
|
|
568
|
-
if etag_matches? env['HTTP_IF_NONE_MATCH'], new_resource
|
|
569
|
-
halt(request.safe? ? 304 : 412)
|
|
570
|
-
end
|
|
629
|
+
return unless success? || status == 304
|
|
571
630
|
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
631
|
+
if etag_matches?(env['HTTP_IF_NONE_MATCH'], new_resource)
|
|
632
|
+
halt(request.safe? ? 304 : 412)
|
|
633
|
+
end
|
|
634
|
+
|
|
635
|
+
if env['HTTP_IF_MATCH']
|
|
636
|
+
return if etag_matches?(env['HTTP_IF_MATCH'], new_resource)
|
|
637
|
+
|
|
638
|
+
halt 412
|
|
575
639
|
end
|
|
640
|
+
|
|
641
|
+
nil
|
|
576
642
|
end
|
|
577
643
|
|
|
578
644
|
# Sugar for redirect (example: redirect back)
|
|
@@ -625,8 +691,8 @@ module Sinatra
|
|
|
625
691
|
else
|
|
626
692
|
value.to_time
|
|
627
693
|
end
|
|
628
|
-
rescue ArgumentError =>
|
|
629
|
-
raise
|
|
694
|
+
rescue ArgumentError => e
|
|
695
|
+
raise e
|
|
630
696
|
rescue Exception
|
|
631
697
|
raise ArgumentError, "unable to convert #{value.inspect} to a Time object"
|
|
632
698
|
end
|
|
@@ -636,19 +702,19 @@ module Sinatra
|
|
|
636
702
|
# Helper method checking if a ETag value list includes the current ETag.
|
|
637
703
|
def etag_matches?(list, new_resource = request.post?)
|
|
638
704
|
return !new_resource if list == '*'
|
|
705
|
+
|
|
639
706
|
list.to_s.split(/\s*,\s*/).include? response['ETag']
|
|
640
707
|
end
|
|
641
708
|
|
|
642
709
|
def with_params(temp_params)
|
|
643
|
-
original
|
|
710
|
+
original = @params
|
|
711
|
+
@params = temp_params
|
|
644
712
|
yield
|
|
645
713
|
ensure
|
|
646
714
|
@params = original if original
|
|
647
715
|
end
|
|
648
716
|
end
|
|
649
717
|
|
|
650
|
-
private
|
|
651
|
-
|
|
652
718
|
# Template rendering methods. Each method takes the name of a template
|
|
653
719
|
# to render as a Symbol and returns a String with the rendered output,
|
|
654
720
|
# as well as an optional hash with additional options.
|
|
@@ -660,7 +726,7 @@ module Sinatra
|
|
|
660
726
|
# Possible options are:
|
|
661
727
|
# :content_type The content type to use, same arguments as content_type.
|
|
662
728
|
# :layout If set to something falsy, no layout is rendered, otherwise
|
|
663
|
-
# the specified layout is used
|
|
729
|
+
# the specified layout is used
|
|
664
730
|
# :layout_engine Engine to use for rendering the layout.
|
|
665
731
|
# :locals A hash with local variables that should be available
|
|
666
732
|
# in the template
|
|
@@ -682,36 +748,10 @@ module Sinatra
|
|
|
682
748
|
render(:erb, template, options, locals, &block)
|
|
683
749
|
end
|
|
684
750
|
|
|
685
|
-
def erubis(template, options = {}, locals = {})
|
|
686
|
-
warn "Sinatra::Templates#erubis is deprecated and will be removed, use #erb instead.\n" \
|
|
687
|
-
"If you have Erubis installed, it will be used automatically."
|
|
688
|
-
render :erubis, template, options, locals
|
|
689
|
-
end
|
|
690
|
-
|
|
691
751
|
def haml(template, options = {}, locals = {}, &block)
|
|
692
752
|
render(:haml, template, options, locals, &block)
|
|
693
753
|
end
|
|
694
754
|
|
|
695
|
-
def sass(template, options = {}, locals = {})
|
|
696
|
-
options.merge! :layout => false, :default_content_type => :css
|
|
697
|
-
render :sass, template, options, locals
|
|
698
|
-
end
|
|
699
|
-
|
|
700
|
-
def scss(template, options = {}, locals = {})
|
|
701
|
-
options.merge! :layout => false, :default_content_type => :css
|
|
702
|
-
render :scss, template, options, locals
|
|
703
|
-
end
|
|
704
|
-
|
|
705
|
-
def less(template, options = {}, locals = {})
|
|
706
|
-
options.merge! :layout => false, :default_content_type => :css
|
|
707
|
-
render :less, template, options, locals
|
|
708
|
-
end
|
|
709
|
-
|
|
710
|
-
def stylus(template, options = {}, locals = {})
|
|
711
|
-
options.merge! :layout => false, :default_content_type => :css
|
|
712
|
-
render :styl, template, options, locals
|
|
713
|
-
end
|
|
714
|
-
|
|
715
755
|
def builder(template = nil, options = {}, locals = {}, &block)
|
|
716
756
|
options[:default_content_type] = :xml
|
|
717
757
|
render_ruby(:builder, template, options, locals, &block)
|
|
@@ -726,10 +766,6 @@ module Sinatra
|
|
|
726
766
|
render :markdown, template, options, locals
|
|
727
767
|
end
|
|
728
768
|
|
|
729
|
-
def textile(template, options = {}, locals = {})
|
|
730
|
-
render :textile, template, options, locals
|
|
731
|
-
end
|
|
732
|
-
|
|
733
769
|
def rdoc(template, options = {}, locals = {})
|
|
734
770
|
render :rdoc, template, options, locals
|
|
735
771
|
end
|
|
@@ -738,19 +774,10 @@ module Sinatra
|
|
|
738
774
|
render :asciidoc, template, options, locals
|
|
739
775
|
end
|
|
740
776
|
|
|
741
|
-
def radius(template, options = {}, locals = {})
|
|
742
|
-
render :radius, template, options, locals
|
|
743
|
-
end
|
|
744
|
-
|
|
745
777
|
def markaby(template = nil, options = {}, locals = {}, &block)
|
|
746
778
|
render_ruby(:mab, template, options, locals, &block)
|
|
747
779
|
end
|
|
748
780
|
|
|
749
|
-
def coffee(template, options = {}, locals = {})
|
|
750
|
-
options.merge! :layout => false, :default_content_type => :js
|
|
751
|
-
render :coffee, template, options, locals
|
|
752
|
-
end
|
|
753
|
-
|
|
754
781
|
def nokogiri(template = nil, options = {}, locals = {}, &block)
|
|
755
782
|
options[:default_content_type] = :xml
|
|
756
783
|
render_ruby(:nokogiri, template, options, locals, &block)
|
|
@@ -760,18 +787,6 @@ module Sinatra
|
|
|
760
787
|
render(:slim, template, options, locals, &block)
|
|
761
788
|
end
|
|
762
789
|
|
|
763
|
-
def creole(template, options = {}, locals = {})
|
|
764
|
-
render :creole, template, options, locals
|
|
765
|
-
end
|
|
766
|
-
|
|
767
|
-
def mediawiki(template, options = {}, locals = {})
|
|
768
|
-
render :mediawiki, template, options, locals
|
|
769
|
-
end
|
|
770
|
-
|
|
771
|
-
def wlang(template, options = {}, locals = {}, &block)
|
|
772
|
-
render(:wlang, template, options, locals, &block)
|
|
773
|
-
end
|
|
774
|
-
|
|
775
790
|
def yajl(template, options = {}, locals = {})
|
|
776
791
|
options[:default_content_type] = :json
|
|
777
792
|
render :yajl, template, options, locals
|
|
@@ -796,24 +811,27 @@ module Sinatra
|
|
|
796
811
|
|
|
797
812
|
# logic shared between builder and nokogiri
|
|
798
813
|
def render_ruby(engine, template, options = {}, locals = {}, &block)
|
|
799
|
-
|
|
800
|
-
|
|
814
|
+
if template.is_a?(Hash)
|
|
815
|
+
options = template
|
|
816
|
+
template = nil
|
|
817
|
+
end
|
|
818
|
+
template = proc { block } if template.nil?
|
|
801
819
|
render engine, template, options, locals
|
|
802
820
|
end
|
|
803
821
|
|
|
804
822
|
def render(engine, data, options = {}, locals = {}, &block)
|
|
805
823
|
# merge app-level options
|
|
806
824
|
engine_options = settings.respond_to?(engine) ? settings.send(engine) : {}
|
|
807
|
-
options.merge!(engine_options) { |
|
|
825
|
+
options.merge!(engine_options) { |_key, v1, _v2| v1 }
|
|
808
826
|
|
|
809
827
|
# extract generic options
|
|
810
828
|
locals = options.delete(:locals) || locals || {}
|
|
811
|
-
views = options.delete(:views) || settings.views ||
|
|
829
|
+
views = options.delete(:views) || settings.views || './views'
|
|
812
830
|
layout = options[:layout]
|
|
813
831
|
layout = false if layout.nil? && options.include?(:layout)
|
|
814
832
|
eat_errors = layout.nil?
|
|
815
|
-
layout = engine_options[:layout] if layout.nil?
|
|
816
|
-
layout = @default_layout if layout.nil?
|
|
833
|
+
layout = engine_options[:layout] if layout.nil? || (layout == true && engine_options[:layout] != false)
|
|
834
|
+
layout = @default_layout if layout.nil? || (layout == true)
|
|
817
835
|
layout_options = options.delete(:layout_options) || {}
|
|
818
836
|
content_type = options.delete(:default_content_type)
|
|
819
837
|
content_type = options.delete(:content_type) || content_type
|
|
@@ -838,8 +856,9 @@ module Sinatra
|
|
|
838
856
|
|
|
839
857
|
# render layout
|
|
840
858
|
if layout
|
|
841
|
-
|
|
842
|
-
|
|
859
|
+
extra_options = { views: views, layout: false, eat_errors: eat_errors, scope: scope }
|
|
860
|
+
options = options.merge(extra_options).merge!(layout_options)
|
|
861
|
+
|
|
843
862
|
catch(:layout_missing) { return render(layout_engine, layout, options, locals) { output } }
|
|
844
863
|
end
|
|
845
864
|
|
|
@@ -849,12 +868,12 @@ module Sinatra
|
|
|
849
868
|
|
|
850
869
|
def compile_template(engine, data, options, views)
|
|
851
870
|
eat_errors = options.delete :eat_errors
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
raise "Template engine not found: #{engine}" if template.nil?
|
|
871
|
+
template = Tilt[engine]
|
|
872
|
+
raise "Template engine not found: #{engine}" if template.nil?
|
|
855
873
|
|
|
856
|
-
|
|
857
|
-
|
|
874
|
+
case data
|
|
875
|
+
when Symbol
|
|
876
|
+
template_cache.fetch engine, data, options, views do
|
|
858
877
|
body, path, line = settings.templates[data]
|
|
859
878
|
if body
|
|
860
879
|
body = body.call if body.respond_to?(:call)
|
|
@@ -864,25 +883,35 @@ module Sinatra
|
|
|
864
883
|
@preferred_extension = engine.to_s
|
|
865
884
|
find_template(views, data, template) do |file|
|
|
866
885
|
path ||= file # keep the initial path rather than the last one
|
|
867
|
-
|
|
886
|
+
found = File.exist?(file)
|
|
887
|
+
if found
|
|
868
888
|
path = file
|
|
869
889
|
break
|
|
870
890
|
end
|
|
871
891
|
end
|
|
872
|
-
throw :layout_missing if eat_errors
|
|
892
|
+
throw :layout_missing if eat_errors && !found
|
|
873
893
|
template.new(path, 1, options)
|
|
874
894
|
end
|
|
875
|
-
when Proc, String
|
|
876
|
-
body = data.is_a?(String) ? Proc.new { data } : data
|
|
877
|
-
caller = settings.caller_locations.first
|
|
878
|
-
path = options[:path] || caller[0]
|
|
879
|
-
line = options[:line] || caller[1]
|
|
880
|
-
template.new(path, line.to_i, options, &body)
|
|
881
|
-
else
|
|
882
|
-
raise ArgumentError, "Sorry, don't know how to render #{data.inspect}."
|
|
883
895
|
end
|
|
896
|
+
when Proc
|
|
897
|
+
compile_block_template(template, options, &data)
|
|
898
|
+
when String
|
|
899
|
+
template_cache.fetch engine, data, options, views do
|
|
900
|
+
compile_block_template(template, options) { data }
|
|
901
|
+
end
|
|
902
|
+
else
|
|
903
|
+
raise ArgumentError, "Sorry, don't know how to render #{data.inspect}."
|
|
884
904
|
end
|
|
885
905
|
end
|
|
906
|
+
|
|
907
|
+
def compile_block_template(template, options, &body)
|
|
908
|
+
first_location = caller_locations.first
|
|
909
|
+
path = first_location.path
|
|
910
|
+
line = first_location.lineno
|
|
911
|
+
path = options[:path] || path
|
|
912
|
+
line = options[:line] || line
|
|
913
|
+
template.new(path, line.to_i, options, &body)
|
|
914
|
+
end
|
|
886
915
|
end
|
|
887
916
|
|
|
888
917
|
# Base class for all Sinatra applications and middleware.
|
|
@@ -896,10 +925,11 @@ module Sinatra
|
|
|
896
925
|
attr_accessor :app, :env, :request, :response, :params
|
|
897
926
|
attr_reader :template_cache
|
|
898
927
|
|
|
899
|
-
def initialize(app = nil)
|
|
928
|
+
def initialize(app = nil, **_kwargs)
|
|
900
929
|
super()
|
|
901
930
|
@app = app
|
|
902
931
|
@template_cache = Tilt::Cache.new
|
|
932
|
+
@pinned_response = nil # whether a before! filter pinned the content-type
|
|
903
933
|
yield self if block_given?
|
|
904
934
|
end
|
|
905
935
|
|
|
@@ -913,17 +943,17 @@ module Sinatra
|
|
|
913
943
|
@params = IndifferentHash.new
|
|
914
944
|
@request = Request.new(env)
|
|
915
945
|
@response = Response.new
|
|
946
|
+
@pinned_response = nil
|
|
916
947
|
template_cache.clear if settings.reload_templates
|
|
917
948
|
|
|
918
|
-
@response['Content-Type'] = nil
|
|
919
949
|
invoke { dispatch! }
|
|
920
950
|
invoke { error_block!(response.status) } unless @env['sinatra.error']
|
|
921
951
|
|
|
922
952
|
unless @response['Content-Type']
|
|
923
|
-
if Array === body
|
|
953
|
+
if Array === body && body[0].respond_to?(:content_type)
|
|
924
954
|
content_type body[0].content_type
|
|
925
|
-
|
|
926
|
-
content_type
|
|
955
|
+
elsif (default = settings.default_content_type)
|
|
956
|
+
content_type default
|
|
927
957
|
end
|
|
928
958
|
end
|
|
929
959
|
|
|
@@ -940,12 +970,6 @@ module Sinatra
|
|
|
940
970
|
self.class.settings
|
|
941
971
|
end
|
|
942
972
|
|
|
943
|
-
def options
|
|
944
|
-
warn "Sinatra::Base#options is deprecated and will be removed, " \
|
|
945
|
-
"use #settings instead."
|
|
946
|
-
settings
|
|
947
|
-
end
|
|
948
|
-
|
|
949
973
|
# Exit the current block, halts any further processing
|
|
950
974
|
# of the request, and returns the specified response.
|
|
951
975
|
def halt(*response)
|
|
@@ -962,7 +986,8 @@ module Sinatra
|
|
|
962
986
|
|
|
963
987
|
# Forward the request to the downstream app -- middleware only.
|
|
964
988
|
def forward
|
|
965
|
-
|
|
989
|
+
raise 'downstream app not set' unless @app.respond_to? :call
|
|
990
|
+
|
|
966
991
|
status, headers, body = @app.call env
|
|
967
992
|
@response.status = status
|
|
968
993
|
@response.body = body
|
|
@@ -973,23 +998,29 @@ module Sinatra
|
|
|
973
998
|
private
|
|
974
999
|
|
|
975
1000
|
# Run filters defined on the class and all superclasses.
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
base.
|
|
1001
|
+
# Accepts an optional block to call after each filter is applied.
|
|
1002
|
+
def filter!(type, base = settings, &block)
|
|
1003
|
+
filter!(type, base.superclass, &block) if base.superclass.respond_to?(:filters)
|
|
1004
|
+
base.filters[type].each do |args|
|
|
1005
|
+
result = process_route(*args)
|
|
1006
|
+
block.call(result) if block_given?
|
|
1007
|
+
end
|
|
979
1008
|
end
|
|
980
1009
|
|
|
981
1010
|
# Run routes defined on the class and all superclasses.
|
|
982
1011
|
def route!(base = settings, pass_block = nil)
|
|
983
|
-
|
|
984
|
-
routes.each do |pattern, conditions, block|
|
|
985
|
-
returned_pass_block = process_route(pattern, conditions) do |*args|
|
|
986
|
-
env['sinatra.route'] = "#{@request.request_method} #{pattern}"
|
|
987
|
-
route_eval { block[*args] }
|
|
988
|
-
end
|
|
1012
|
+
routes = base.routes[@request.request_method]
|
|
989
1013
|
|
|
990
|
-
|
|
991
|
-
|
|
1014
|
+
routes&.each do |pattern, conditions, block|
|
|
1015
|
+
response.delete_header('Content-Type') unless @pinned_response
|
|
1016
|
+
|
|
1017
|
+
returned_pass_block = process_route(pattern, conditions) do |*args|
|
|
1018
|
+
env['sinatra.route'] = "#{@request.request_method} #{pattern}"
|
|
1019
|
+
route_eval { block[*args] }
|
|
992
1020
|
end
|
|
1021
|
+
|
|
1022
|
+
# don't wipe out pass_block in superclass
|
|
1023
|
+
pass_block = returned_pass_block if returned_pass_block
|
|
993
1024
|
end
|
|
994
1025
|
|
|
995
1026
|
# Run routes defined in superclass.
|
|
@@ -1013,15 +1044,17 @@ module Sinatra
|
|
|
1013
1044
|
# Returns pass block.
|
|
1014
1045
|
def process_route(pattern, conditions, block = nil, values = [])
|
|
1015
1046
|
route = @request.path_info
|
|
1016
|
-
route = '/' if route.empty?
|
|
1047
|
+
route = '/' if route.empty? && !settings.empty_path_info?
|
|
1017
1048
|
route = route[0..-2] if !settings.strict_paths? && route != '/' && route.end_with?('/')
|
|
1018
|
-
return unless params = pattern.params(route)
|
|
1019
1049
|
|
|
1020
|
-
params.
|
|
1050
|
+
params = pattern.params(route)
|
|
1051
|
+
return unless params
|
|
1052
|
+
|
|
1053
|
+
params.delete('ignore') # TODO: better params handling, maybe turn it into "smart" object or detect changes
|
|
1021
1054
|
force_encoding(params)
|
|
1022
|
-
|
|
1055
|
+
@params = @params.merge(params) { |_k, v1, v2| v2 || v1 } if params.any?
|
|
1023
1056
|
|
|
1024
|
-
regexp_exists = pattern.is_a?(Mustermann::Regular) || (pattern.respond_to?(:patterns) && pattern.patterns.any? {|subpattern| subpattern.is_a?(Mustermann::Regular)}
|
|
1057
|
+
regexp_exists = pattern.is_a?(Mustermann::Regular) || (pattern.respond_to?(:patterns) && pattern.patterns.any? { |subpattern| subpattern.is_a?(Mustermann::Regular) })
|
|
1025
1058
|
if regexp_exists
|
|
1026
1059
|
captures = pattern.match(route).captures.map { |c| URI_INSTANCE.unescape(c) if c }
|
|
1027
1060
|
values += captures
|
|
@@ -1034,11 +1067,12 @@ module Sinatra
|
|
|
1034
1067
|
conditions.each { |c| throw :pass if c.bind(self).call == false }
|
|
1035
1068
|
block ? block[self, values] : yield(self, values)
|
|
1036
1069
|
end
|
|
1037
|
-
rescue
|
|
1070
|
+
rescue StandardError
|
|
1038
1071
|
@env['sinatra.error.params'] = @params
|
|
1039
1072
|
raise
|
|
1040
1073
|
ensure
|
|
1041
|
-
|
|
1074
|
+
params ||= {}
|
|
1075
|
+
params.each { |k, _| @params.delete(k) } unless @env['sinatra.error.params']
|
|
1042
1076
|
end
|
|
1043
1077
|
|
|
1044
1078
|
# No matching route was found or all routes passed. The default
|
|
@@ -1047,31 +1081,35 @@ module Sinatra
|
|
|
1047
1081
|
# a NotFound exception. Subclasses can override this method to perform
|
|
1048
1082
|
# custom route miss logic.
|
|
1049
1083
|
def route_missing
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
raise NotFound
|
|
1054
|
-
end
|
|
1084
|
+
raise NotFound unless @app
|
|
1085
|
+
|
|
1086
|
+
forward
|
|
1055
1087
|
end
|
|
1056
1088
|
|
|
1057
1089
|
# Attempt to serve static files from public directory. Throws :halt when
|
|
1058
1090
|
# a matching file is found, returns nil otherwise.
|
|
1059
1091
|
def static!(options = {})
|
|
1060
1092
|
return if (public_dir = settings.public_folder).nil?
|
|
1061
|
-
|
|
1093
|
+
|
|
1094
|
+
path = "#{public_dir}#{URI_INSTANCE.unescape(request.path_info)}"
|
|
1095
|
+
return unless valid_path?(path)
|
|
1096
|
+
|
|
1097
|
+
path = File.expand_path(path)
|
|
1098
|
+
return unless path.start_with?("#{File.expand_path(public_dir)}/")
|
|
1099
|
+
|
|
1062
1100
|
return unless File.file?(path)
|
|
1063
1101
|
|
|
1064
1102
|
env['sinatra.static_file'] = path
|
|
1065
1103
|
cache_control(*settings.static_cache_control) if settings.static_cache_control?
|
|
1066
|
-
send_file path, options.merge(:
|
|
1104
|
+
send_file path, options.merge(disposition: nil)
|
|
1067
1105
|
end
|
|
1068
1106
|
|
|
1069
1107
|
# Run the block with 'throw :halt' support and apply result to the response.
|
|
1070
|
-
def invoke
|
|
1071
|
-
res = catch(:halt)
|
|
1108
|
+
def invoke(&block)
|
|
1109
|
+
res = catch(:halt, &block)
|
|
1072
1110
|
|
|
1073
|
-
res = [res] if Integer === res
|
|
1074
|
-
if Array === res
|
|
1111
|
+
res = [res] if (Integer === res) || (String === res)
|
|
1112
|
+
if (Array === res) && (Integer === res.first)
|
|
1075
1113
|
res = res.dup
|
|
1076
1114
|
status(res.shift)
|
|
1077
1115
|
body(res.pop)
|
|
@@ -1087,56 +1125,71 @@ module Sinatra
|
|
|
1087
1125
|
# Avoid passing frozen string in force_encoding
|
|
1088
1126
|
@params.merge!(@request.params).each do |key, val|
|
|
1089
1127
|
next unless val.respond_to?(:force_encoding)
|
|
1128
|
+
|
|
1090
1129
|
val = val.dup if val.frozen?
|
|
1091
1130
|
@params[key] = force_encoding(val)
|
|
1092
1131
|
end
|
|
1093
1132
|
|
|
1094
1133
|
invoke do
|
|
1095
1134
|
static! if settings.static? && (request.get? || request.head?)
|
|
1096
|
-
filter! :before
|
|
1135
|
+
filter! :before do
|
|
1136
|
+
@pinned_response = !response['Content-Type'].nil?
|
|
1137
|
+
end
|
|
1097
1138
|
route!
|
|
1098
1139
|
end
|
|
1099
|
-
rescue ::Exception =>
|
|
1100
|
-
invoke { handle_exception!(
|
|
1140
|
+
rescue ::Exception => e
|
|
1141
|
+
invoke { handle_exception!(e) }
|
|
1101
1142
|
ensure
|
|
1102
1143
|
begin
|
|
1103
1144
|
filter! :after unless env['sinatra.static_file']
|
|
1104
|
-
rescue ::Exception =>
|
|
1105
|
-
invoke { handle_exception!(
|
|
1145
|
+
rescue ::Exception => e
|
|
1146
|
+
invoke { handle_exception!(e) } unless @env['sinatra.error']
|
|
1106
1147
|
end
|
|
1107
1148
|
end
|
|
1108
1149
|
|
|
1109
1150
|
# Error handling during requests.
|
|
1110
1151
|
def handle_exception!(boom)
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1152
|
+
error_params = @env['sinatra.error.params']
|
|
1153
|
+
|
|
1154
|
+
@params = @params.merge(error_params) if error_params
|
|
1155
|
+
|
|
1114
1156
|
@env['sinatra.error'] = boom
|
|
1115
1157
|
|
|
1116
|
-
if boom.
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1158
|
+
http_status = if boom.is_a? Sinatra::Error
|
|
1159
|
+
if boom.respond_to? :http_status
|
|
1160
|
+
boom.http_status
|
|
1161
|
+
elsif settings.use_code? && boom.respond_to?(:code)
|
|
1162
|
+
boom.code
|
|
1163
|
+
end
|
|
1164
|
+
end
|
|
1123
1165
|
|
|
1124
|
-
|
|
1166
|
+
http_status = 500 unless http_status&.between?(400, 599)
|
|
1167
|
+
status(http_status)
|
|
1125
1168
|
|
|
1126
|
-
boom_message = boom.message if boom.message && boom.message != boom.class.name
|
|
1127
1169
|
if server_error?
|
|
1128
1170
|
dump_errors! boom if settings.dump_errors?
|
|
1129
|
-
raise boom if settings.show_exceptions?
|
|
1171
|
+
raise boom if settings.show_exceptions? && (settings.show_exceptions != :after_handler)
|
|
1130
1172
|
elsif not_found?
|
|
1131
1173
|
headers['X-Cascade'] = 'pass' if settings.x_cascade?
|
|
1132
|
-
body boom_message || '<h1>Not Found</h1>'
|
|
1133
|
-
elsif bad_request?
|
|
1134
|
-
body boom_message || '<h1>Bad Request</h1>'
|
|
1135
1174
|
end
|
|
1136
1175
|
|
|
1137
|
-
res = error_block!(boom.class, boom) || error_block!(status, boom)
|
|
1138
|
-
|
|
1139
|
-
|
|
1176
|
+
if (res = error_block!(boom.class, boom) || error_block!(status, boom))
|
|
1177
|
+
return res
|
|
1178
|
+
end
|
|
1179
|
+
|
|
1180
|
+
if not_found? || bad_request?
|
|
1181
|
+
if boom.message && boom.message != boom.class.name
|
|
1182
|
+
body Rack::Utils.escape_html(boom.message)
|
|
1183
|
+
else
|
|
1184
|
+
content_type 'text/html'
|
|
1185
|
+
body "<h1>#{not_found? ? 'Not Found' : 'Bad Request'}</h1>"
|
|
1186
|
+
end
|
|
1187
|
+
end
|
|
1188
|
+
|
|
1189
|
+
return unless server_error?
|
|
1190
|
+
|
|
1191
|
+
raise boom if settings.raise_errors? || settings.show_exceptions?
|
|
1192
|
+
|
|
1140
1193
|
error_block! Exception, boom
|
|
1141
1194
|
end
|
|
1142
1195
|
|
|
@@ -1144,7 +1197,10 @@ module Sinatra
|
|
|
1144
1197
|
def error_block!(key, *block_params)
|
|
1145
1198
|
base = settings
|
|
1146
1199
|
while base.respond_to?(:errors)
|
|
1147
|
-
|
|
1200
|
+
args_array = base.errors[key]
|
|
1201
|
+
|
|
1202
|
+
next base = base.superclass unless args_array
|
|
1203
|
+
|
|
1148
1204
|
args_array.reverse_each do |args|
|
|
1149
1205
|
first = args == args_array.first
|
|
1150
1206
|
args += [block_params]
|
|
@@ -1152,51 +1208,50 @@ module Sinatra
|
|
|
1152
1208
|
return resp unless resp.nil? && !first
|
|
1153
1209
|
end
|
|
1154
1210
|
end
|
|
1155
|
-
return false unless key.respond_to?
|
|
1211
|
+
return false unless key.respond_to?(:superclass) && (key.superclass < Exception)
|
|
1212
|
+
|
|
1156
1213
|
error_block!(key.superclass, *block_params)
|
|
1157
1214
|
end
|
|
1158
1215
|
|
|
1159
1216
|
def dump_errors!(boom)
|
|
1160
|
-
msg = ["#{Time.now.strftime(
|
|
1217
|
+
msg = ["#{Time.now.strftime('%Y-%m-%d %H:%M:%S')} - #{boom.class} - #{boom.message}:", *boom.backtrace].join("\n\t")
|
|
1161
1218
|
@env['rack.errors'].puts(msg)
|
|
1162
1219
|
end
|
|
1163
1220
|
|
|
1164
1221
|
class << self
|
|
1165
1222
|
CALLERS_TO_IGNORE = [ # :nodoc:
|
|
1166
|
-
|
|
1167
|
-
/
|
|
1223
|
+
%r{/sinatra(/(base|main|show_exceptions))?\.rb$}, # all sinatra code
|
|
1224
|
+
%r{lib/tilt.*\.rb$}, # all tilt code
|
|
1168
1225
|
/^\(.*\)$/, # generated code
|
|
1169
|
-
/
|
|
1226
|
+
%r{rubygems/(custom|core_ext/kernel)_require\.rb$}, # rubygems require hacks
|
|
1170
1227
|
/active_support/, # active_support require hacks
|
|
1171
|
-
|
|
1228
|
+
%r{bundler(/(?:runtime|inline))?\.rb}, # bundler require hacks
|
|
1172
1229
|
/<internal:/, # internal in ruby >= 1.9.2
|
|
1173
|
-
/
|
|
1174
|
-
]
|
|
1175
|
-
|
|
1176
|
-
# contrary to what the comment said previously, rubinius never supported this
|
|
1177
|
-
if defined?(RUBY_IGNORE_CALLERS)
|
|
1178
|
-
warn "RUBY_IGNORE_CALLERS is deprecated and will no longer be supported by Sinatra 2.0"
|
|
1179
|
-
CALLERS_TO_IGNORE.concat(RUBY_IGNORE_CALLERS)
|
|
1180
|
-
end
|
|
1230
|
+
%r{zeitwerk/kernel\.rb} # Zeitwerk kernel#require decorator
|
|
1231
|
+
].freeze
|
|
1181
1232
|
|
|
1182
1233
|
attr_reader :routes, :filters, :templates, :errors
|
|
1183
1234
|
|
|
1235
|
+
def callers_to_ignore
|
|
1236
|
+
CALLERS_TO_IGNORE
|
|
1237
|
+
end
|
|
1238
|
+
|
|
1184
1239
|
# Removes all routes, filters, middleware and extension hooks from the
|
|
1185
1240
|
# current class (not routes/filters/... defined by its superclass).
|
|
1186
1241
|
def reset!
|
|
1187
1242
|
@conditions = []
|
|
1188
1243
|
@routes = {}
|
|
1189
|
-
@filters = {:
|
|
1244
|
+
@filters = { before: [], after: [] }
|
|
1190
1245
|
@errors = {}
|
|
1191
1246
|
@middleware = []
|
|
1192
1247
|
@prototype = nil
|
|
1193
1248
|
@extensions = []
|
|
1194
1249
|
|
|
1195
|
-
if superclass.respond_to?(:templates)
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1250
|
+
@templates = if superclass.respond_to?(:templates)
|
|
1251
|
+
Hash.new { |_hash, key| superclass.templates[key] }
|
|
1252
|
+
else
|
|
1253
|
+
{}
|
|
1254
|
+
end
|
|
1200
1255
|
end
|
|
1201
1256
|
|
|
1202
1257
|
# Extension modules registered on this class and all superclasses.
|
|
@@ -1220,16 +1275,21 @@ module Sinatra
|
|
|
1220
1275
|
# Sets an option to the given value. If the value is a proc,
|
|
1221
1276
|
# the proc will be called every time the option is accessed.
|
|
1222
1277
|
def set(option, value = (not_set = true), ignore_setter = false, &block)
|
|
1223
|
-
raise ArgumentError if block
|
|
1224
|
-
|
|
1278
|
+
raise ArgumentError if block && !not_set
|
|
1279
|
+
|
|
1280
|
+
if block
|
|
1281
|
+
value = block
|
|
1282
|
+
not_set = false
|
|
1283
|
+
end
|
|
1225
1284
|
|
|
1226
1285
|
if not_set
|
|
1227
1286
|
raise ArgumentError unless option.respond_to?(:each)
|
|
1228
|
-
|
|
1287
|
+
|
|
1288
|
+
option.each { |k, v| set(k, v) }
|
|
1229
1289
|
return self
|
|
1230
1290
|
end
|
|
1231
1291
|
|
|
1232
|
-
if respond_to?("#{option}=")
|
|
1292
|
+
if respond_to?("#{option}=") && !ignore_setter
|
|
1233
1293
|
return __send__("#{option}=", value)
|
|
1234
1294
|
end
|
|
1235
1295
|
|
|
@@ -1268,7 +1328,7 @@ module Sinatra
|
|
|
1268
1328
|
# class, or an HTTP status code to specify which errors should be
|
|
1269
1329
|
# handled.
|
|
1270
1330
|
def error(*codes, &block)
|
|
1271
|
-
args = compile!
|
|
1331
|
+
args = compile! 'ERROR', /.*/, block
|
|
1272
1332
|
codes = codes.flat_map(&method(:Array))
|
|
1273
1333
|
codes << Exception if codes.empty?
|
|
1274
1334
|
codes << Sinatra::NotFound if codes.include?(404)
|
|
@@ -1294,7 +1354,7 @@ module Sinatra
|
|
|
1294
1354
|
# Load embedded templates from the file; uses the caller's __FILE__
|
|
1295
1355
|
# when no file is specified.
|
|
1296
1356
|
def inline_templates=(file = nil)
|
|
1297
|
-
file = (
|
|
1357
|
+
file = (caller_files.first || File.expand_path($0)) if file.nil? || file == true
|
|
1298
1358
|
|
|
1299
1359
|
begin
|
|
1300
1360
|
io = ::IO.respond_to?(:binread) ? ::IO.binread(file) : ::IO.read(file)
|
|
@@ -1303,23 +1363,24 @@ module Sinatra
|
|
|
1303
1363
|
app, data = nil
|
|
1304
1364
|
end
|
|
1305
1365
|
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1366
|
+
return unless data
|
|
1367
|
+
|
|
1368
|
+
encoding = if app && app =~ /([^\n]*\n)?#[^\n]*coding: *(\S+)/m
|
|
1369
|
+
$2
|
|
1370
|
+
else
|
|
1371
|
+
settings.default_encoding
|
|
1372
|
+
end
|
|
1373
|
+
|
|
1374
|
+
lines = app.count("\n") + 1
|
|
1375
|
+
template = nil
|
|
1376
|
+
force_encoding data, encoding
|
|
1377
|
+
data.each_line do |line|
|
|
1378
|
+
lines += 1
|
|
1379
|
+
if line =~ /^@@\s*(.*\S)\s*$/
|
|
1380
|
+
template = force_encoding(String.new, encoding)
|
|
1381
|
+
templates[$1.to_sym] = [template, file, lines]
|
|
1382
|
+
elsif template
|
|
1383
|
+
template << line
|
|
1323
1384
|
end
|
|
1324
1385
|
end
|
|
1325
1386
|
end
|
|
@@ -1328,8 +1389,10 @@ module Sinatra
|
|
|
1328
1389
|
def mime_type(type, value = nil)
|
|
1329
1390
|
return type if type.nil?
|
|
1330
1391
|
return type.to_s if type.to_s.include?('/')
|
|
1331
|
-
|
|
1392
|
+
|
|
1393
|
+
type = ".#{type}" unless type.to_s[0] == '.'
|
|
1332
1394
|
return Rack::Mime.mime_type(type, nil) unless value
|
|
1395
|
+
|
|
1333
1396
|
Rack::Mime::MIME_TYPES[type] = value
|
|
1334
1397
|
end
|
|
1335
1398
|
|
|
@@ -1338,26 +1401,26 @@ module Sinatra
|
|
|
1338
1401
|
# mime_types :js # => ['application/javascript', 'text/javascript']
|
|
1339
1402
|
def mime_types(type)
|
|
1340
1403
|
type = mime_type type
|
|
1341
|
-
type =~
|
|
1404
|
+
type =~ %r{^application/(xml|javascript)$} ? [type, "text/#{$1}"] : [type]
|
|
1342
1405
|
end
|
|
1343
1406
|
|
|
1344
1407
|
# Define a before filter; runs before all requests within the same
|
|
1345
1408
|
# context as route handlers and may access/modify the request and
|
|
1346
1409
|
# response.
|
|
1347
1410
|
def before(path = /.*/, **options, &block)
|
|
1348
|
-
add_filter(:before, path, options, &block)
|
|
1411
|
+
add_filter(:before, path, **options, &block)
|
|
1349
1412
|
end
|
|
1350
1413
|
|
|
1351
1414
|
# Define an after filter; runs after all requests within the same
|
|
1352
1415
|
# context as route handlers and may access/modify the request and
|
|
1353
1416
|
# response.
|
|
1354
1417
|
def after(path = /.*/, **options, &block)
|
|
1355
|
-
add_filter(:after, path, options, &block)
|
|
1418
|
+
add_filter(:after, path, **options, &block)
|
|
1356
1419
|
end
|
|
1357
1420
|
|
|
1358
1421
|
# add a filter
|
|
1359
1422
|
def add_filter(type, path = /.*/, **options, &block)
|
|
1360
|
-
filters[type] << compile!(type, path, block, options)
|
|
1423
|
+
filters[type] << compile!(type, path, block, **options)
|
|
1361
1424
|
end
|
|
1362
1425
|
|
|
1363
1426
|
# Add a route condition. The route is considered non-matching when the
|
|
@@ -1367,7 +1430,7 @@ module Sinatra
|
|
|
1367
1430
|
end
|
|
1368
1431
|
|
|
1369
1432
|
def public=(value)
|
|
1370
|
-
|
|
1433
|
+
warn_for_deprecation ':public is no longer used to avoid overloading Module#public, use :public_folder or :public_dir instead'
|
|
1371
1434
|
set(:public_folder, value)
|
|
1372
1435
|
end
|
|
1373
1436
|
|
|
@@ -1389,14 +1452,21 @@ module Sinatra
|
|
|
1389
1452
|
route('HEAD', path, opts, &block)
|
|
1390
1453
|
end
|
|
1391
1454
|
|
|
1392
|
-
def put(path, opts = {}, &
|
|
1393
|
-
|
|
1394
|
-
def
|
|
1395
|
-
|
|
1396
|
-
def
|
|
1397
|
-
|
|
1398
|
-
def
|
|
1399
|
-
|
|
1455
|
+
def put(path, opts = {}, &block) route 'PUT', path, opts, &block end
|
|
1456
|
+
|
|
1457
|
+
def post(path, opts = {}, &block) route 'POST', path, opts, &block end
|
|
1458
|
+
|
|
1459
|
+
def delete(path, opts = {}, &block) route 'DELETE', path, opts, &block end
|
|
1460
|
+
|
|
1461
|
+
def head(path, opts = {}, &block) route 'HEAD', path, opts, &block end
|
|
1462
|
+
|
|
1463
|
+
def options(path, opts = {}, &block) route 'OPTIONS', path, opts, &block end
|
|
1464
|
+
|
|
1465
|
+
def patch(path, opts = {}, &block) route 'PATCH', path, opts, &block end
|
|
1466
|
+
|
|
1467
|
+
def link(path, opts = {}, &block) route 'LINK', path, opts, &block end
|
|
1468
|
+
|
|
1469
|
+
def unlink(path, opts = {}, &block) route 'UNLINK', path, opts, &block end
|
|
1400
1470
|
|
|
1401
1471
|
# Makes the methods defined in the block and in the Modules given
|
|
1402
1472
|
# in `extensions` available to the handlers and templates
|
|
@@ -1431,41 +1501,44 @@ module Sinatra
|
|
|
1431
1501
|
@prototype = nil
|
|
1432
1502
|
@middleware << [middleware, args, block]
|
|
1433
1503
|
end
|
|
1504
|
+
ruby2_keywords(:use) if respond_to?(:ruby2_keywords, true)
|
|
1434
1505
|
|
|
1435
1506
|
# Stop the self-hosted server if running.
|
|
1436
1507
|
def quit!
|
|
1437
1508
|
return unless running?
|
|
1509
|
+
|
|
1438
1510
|
# Use Thin's hard #stop! if available, otherwise just #stop.
|
|
1439
1511
|
running_server.respond_to?(:stop!) ? running_server.stop! : running_server.stop
|
|
1440
|
-
|
|
1512
|
+
warn '== Sinatra has ended his set (crowd applauds)' unless suppress_messages?
|
|
1441
1513
|
set :running_server, nil
|
|
1442
1514
|
set :handler_name, nil
|
|
1443
1515
|
end
|
|
1444
1516
|
|
|
1445
|
-
|
|
1517
|
+
alias stop! quit!
|
|
1446
1518
|
|
|
1447
1519
|
# Run the Sinatra app as a self-hosted server using
|
|
1448
|
-
#
|
|
1520
|
+
# Puma, Falcon, Mongrel, or WEBrick (in that order). If given a block, will call
|
|
1449
1521
|
# with the constructed handler once we have taken the stage.
|
|
1450
1522
|
def run!(options = {}, &block)
|
|
1451
1523
|
return if running?
|
|
1524
|
+
|
|
1452
1525
|
set options
|
|
1453
|
-
handler =
|
|
1526
|
+
handler = Rack::Handler.pick(server)
|
|
1454
1527
|
handler_name = handler.name.gsub(/.*::/, '')
|
|
1455
1528
|
server_settings = settings.respond_to?(:server_settings) ? settings.server_settings : {}
|
|
1456
|
-
server_settings.merge!(:
|
|
1529
|
+
server_settings.merge!(Port: port, Host: bind)
|
|
1457
1530
|
|
|
1458
1531
|
begin
|
|
1459
1532
|
start_server(handler, server_settings, handler_name, &block)
|
|
1460
1533
|
rescue Errno::EADDRINUSE
|
|
1461
|
-
|
|
1534
|
+
warn "== Someone is already performing on port #{port}!"
|
|
1462
1535
|
raise
|
|
1463
1536
|
ensure
|
|
1464
1537
|
quit!
|
|
1465
1538
|
end
|
|
1466
1539
|
end
|
|
1467
1540
|
|
|
1468
|
-
|
|
1541
|
+
alias start! run!
|
|
1469
1542
|
|
|
1470
1543
|
# Check whether the self-hosted server is running or not.
|
|
1471
1544
|
def running?
|
|
@@ -1483,10 +1556,11 @@ module Sinatra
|
|
|
1483
1556
|
# Create a new instance of the class fronted by its middleware
|
|
1484
1557
|
# pipeline. The object is guaranteed to respond to #call but may not be
|
|
1485
1558
|
# an instance of the class new was called on.
|
|
1486
|
-
def new(*args, &
|
|
1487
|
-
instance = new!(*args, &
|
|
1559
|
+
def new(*args, &block)
|
|
1560
|
+
instance = new!(*args, &block)
|
|
1488
1561
|
Wrapper.new(build(instance).to_app, instance)
|
|
1489
1562
|
end
|
|
1563
|
+
ruby2_keywords :new if respond_to?(:ruby2_keywords, true)
|
|
1490
1564
|
|
|
1491
1565
|
# Creates a Rack::Builder instance with all the middleware set up and
|
|
1492
1566
|
# the given +app+ as end point.
|
|
@@ -1508,12 +1582,6 @@ module Sinatra
|
|
|
1508
1582
|
cleaned_caller(1).flatten
|
|
1509
1583
|
end
|
|
1510
1584
|
|
|
1511
|
-
# Like caller_files, but containing Arrays rather than strings with the
|
|
1512
|
-
# first element being the file, and the second being the line.
|
|
1513
|
-
def caller_locations
|
|
1514
|
-
cleaned_caller 2
|
|
1515
|
-
end
|
|
1516
|
-
|
|
1517
1585
|
private
|
|
1518
1586
|
|
|
1519
1587
|
# Starts the server by running the Rack Handler.
|
|
@@ -1522,9 +1590,9 @@ module Sinatra
|
|
|
1522
1590
|
# behavior, by ensuring an instance exists:
|
|
1523
1591
|
prototype
|
|
1524
1592
|
# Run the instance we created:
|
|
1525
|
-
handler.run(self, server_settings) do |server|
|
|
1593
|
+
handler.run(self, **server_settings) do |server|
|
|
1526
1594
|
unless suppress_messages?
|
|
1527
|
-
|
|
1595
|
+
warn "== Sinatra (v#{Sinatra::VERSION}) has taken the stage on #{port} for #{environment} with backup from #{handler_name}"
|
|
1528
1596
|
end
|
|
1529
1597
|
|
|
1530
1598
|
setup_traps
|
|
@@ -1541,18 +1609,18 @@ module Sinatra
|
|
|
1541
1609
|
end
|
|
1542
1610
|
|
|
1543
1611
|
def setup_traps
|
|
1544
|
-
|
|
1545
|
-
at_exit { quit! }
|
|
1612
|
+
return unless traps?
|
|
1546
1613
|
|
|
1547
|
-
|
|
1548
|
-
old_handler = trap(signal) do
|
|
1549
|
-
quit!
|
|
1550
|
-
old_handler.call if old_handler.respond_to?(:call)
|
|
1551
|
-
end
|
|
1552
|
-
end
|
|
1614
|
+
at_exit { quit! }
|
|
1553
1615
|
|
|
1554
|
-
|
|
1616
|
+
%i[INT TERM].each do |signal|
|
|
1617
|
+
old_handler = trap(signal) do
|
|
1618
|
+
quit!
|
|
1619
|
+
old_handler.call if old_handler.respond_to?(:call)
|
|
1620
|
+
end
|
|
1555
1621
|
end
|
|
1622
|
+
|
|
1623
|
+
set :traps, false
|
|
1556
1624
|
end
|
|
1557
1625
|
|
|
1558
1626
|
# Dynamically defines a method on settings.
|
|
@@ -1580,18 +1648,21 @@ module Sinatra
|
|
|
1580
1648
|
end
|
|
1581
1649
|
end
|
|
1582
1650
|
end
|
|
1583
|
-
|
|
1651
|
+
alias agent user_agent
|
|
1584
1652
|
|
|
1585
1653
|
# Condition for matching mimetypes. Accepts file extensions.
|
|
1586
1654
|
def provides(*types)
|
|
1587
1655
|
types.map! { |t| mime_types(t) }
|
|
1588
1656
|
types.flatten!
|
|
1589
1657
|
condition do
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1658
|
+
response_content_type = response['Content-Type']
|
|
1659
|
+
preferred_type = request.preferred_type(types)
|
|
1660
|
+
|
|
1661
|
+
if response_content_type
|
|
1662
|
+
types.include?(response_content_type) || types.include?(response_content_type[/^[^;]+/])
|
|
1663
|
+
elsif preferred_type
|
|
1664
|
+
params = (preferred_type.respond_to?(:params) ? preferred_type.params : {})
|
|
1665
|
+
content_type(preferred_type, params)
|
|
1595
1666
|
true
|
|
1596
1667
|
else
|
|
1597
1668
|
false
|
|
@@ -1600,8 +1671,8 @@ module Sinatra
|
|
|
1600
1671
|
end
|
|
1601
1672
|
|
|
1602
1673
|
def route(verb, path, options = {}, &block)
|
|
1603
|
-
enable :empty_path_info if path ==
|
|
1604
|
-
signature = compile!(verb, path, block, options)
|
|
1674
|
+
enable :empty_path_info if path == '' && empty_path_info.nil?
|
|
1675
|
+
signature = compile!(verb, path, block, **options)
|
|
1605
1676
|
(@routes[verb] ||= []) << signature
|
|
1606
1677
|
invoke_hook(:route_added, verb, path, block)
|
|
1607
1678
|
signature
|
|
@@ -1629,16 +1700,17 @@ module Sinatra
|
|
|
1629
1700
|
pattern = compile(path, route_mustermann_opts)
|
|
1630
1701
|
method_name = "#{verb} #{path}"
|
|
1631
1702
|
unbound_method = generate_method(method_name, &block)
|
|
1632
|
-
conditions
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
proc { |a,
|
|
1703
|
+
conditions = @conditions
|
|
1704
|
+
@conditions = []
|
|
1705
|
+
wrapper = block.arity.zero? ?
|
|
1706
|
+
proc { |a, _p| unbound_method.bind(a).call } :
|
|
1707
|
+
proc { |a, p| unbound_method.bind(a).call(*p) }
|
|
1636
1708
|
|
|
1637
|
-
[
|
|
1709
|
+
[pattern, conditions, wrapper]
|
|
1638
1710
|
end
|
|
1639
1711
|
|
|
1640
1712
|
def compile(path, route_mustermann_opts = {})
|
|
1641
|
-
Mustermann.new(path, mustermann_opts.merge(route_mustermann_opts))
|
|
1713
|
+
Mustermann.new(path, **mustermann_opts.merge(route_mustermann_opts))
|
|
1642
1714
|
end
|
|
1643
1715
|
|
|
1644
1716
|
def setup_default_middleware(builder)
|
|
@@ -1652,7 +1724,7 @@ module Sinatra
|
|
|
1652
1724
|
end
|
|
1653
1725
|
|
|
1654
1726
|
def setup_middleware(builder)
|
|
1655
|
-
middleware.each { |c,a,b| builder.use(c, *a, &b) }
|
|
1727
|
+
middleware.each { |c, a, b| builder.use(c, *a, &b) }
|
|
1656
1728
|
end
|
|
1657
1729
|
|
|
1658
1730
|
def setup_logging(builder)
|
|
@@ -1682,9 +1754,10 @@ module Sinatra
|
|
|
1682
1754
|
|
|
1683
1755
|
def setup_protection(builder)
|
|
1684
1756
|
return unless protection?
|
|
1757
|
+
|
|
1685
1758
|
options = Hash === protection ? protection.dup : {}
|
|
1686
1759
|
options = {
|
|
1687
|
-
img_src:
|
|
1760
|
+
img_src: "'self' data:",
|
|
1688
1761
|
font_src: "'self'"
|
|
1689
1762
|
}.merge options
|
|
1690
1763
|
|
|
@@ -1698,23 +1771,13 @@ module Sinatra
|
|
|
1698
1771
|
|
|
1699
1772
|
def setup_sessions(builder)
|
|
1700
1773
|
return unless sessions?
|
|
1774
|
+
|
|
1701
1775
|
options = {}
|
|
1702
1776
|
options[:secret] = session_secret if session_secret?
|
|
1703
1777
|
options.merge! sessions.to_hash if sessions.respond_to? :to_hash
|
|
1704
1778
|
builder.use session_store, options
|
|
1705
1779
|
end
|
|
1706
1780
|
|
|
1707
|
-
def detect_rack_handler
|
|
1708
|
-
servers = Array(server)
|
|
1709
|
-
servers.each do |server_name|
|
|
1710
|
-
begin
|
|
1711
|
-
return Rack::Handler.get(server_name.to_s)
|
|
1712
|
-
rescue LoadError, NameError
|
|
1713
|
-
end
|
|
1714
|
-
end
|
|
1715
|
-
fail "Server handler (#{servers.join(',')}) not found."
|
|
1716
|
-
end
|
|
1717
|
-
|
|
1718
1781
|
def inherited(subclass)
|
|
1719
1782
|
subclass.reset!
|
|
1720
1783
|
subclass.set :app_file, caller_files.first unless subclass.app_file?
|
|
@@ -1731,15 +1794,15 @@ module Sinatra
|
|
|
1731
1794
|
end
|
|
1732
1795
|
|
|
1733
1796
|
# used for deprecation warnings
|
|
1734
|
-
def
|
|
1735
|
-
|
|
1797
|
+
def warn_for_deprecation(message)
|
|
1798
|
+
warn message + "\n\tfrom #{cleaned_caller.first.join(':')}"
|
|
1736
1799
|
end
|
|
1737
1800
|
|
|
1738
1801
|
# Like Kernel#caller but excluding certain magic entries
|
|
1739
1802
|
def cleaned_caller(keep = 3)
|
|
1740
|
-
caller(1)
|
|
1741
|
-
map!
|
|
1742
|
-
reject { |file, *_|
|
|
1803
|
+
caller(1)
|
|
1804
|
+
.map! { |line| line.split(/:(?=\d|in )/, 3)[0, keep] }
|
|
1805
|
+
.reject { |file, *_| callers_to_ignore.any? { |pattern| file =~ pattern } }
|
|
1743
1806
|
end
|
|
1744
1807
|
end
|
|
1745
1808
|
|
|
@@ -1747,6 +1810,7 @@ module Sinatra
|
|
|
1747
1810
|
# which is UTF-8 by default
|
|
1748
1811
|
def self.force_encoding(data, encoding = default_encoding)
|
|
1749
1812
|
return if data == settings || data.is_a?(Tempfile)
|
|
1813
|
+
|
|
1750
1814
|
if data.respond_to? :force_encoding
|
|
1751
1815
|
data.force_encoding(encoding).encode!
|
|
1752
1816
|
elsif data.respond_to? :each_value
|
|
@@ -1757,38 +1821,42 @@ module Sinatra
|
|
|
1757
1821
|
data
|
|
1758
1822
|
end
|
|
1759
1823
|
|
|
1760
|
-
def force_encoding(*args)
|
|
1824
|
+
def force_encoding(*args)
|
|
1825
|
+
settings.force_encoding(*args)
|
|
1826
|
+
end
|
|
1761
1827
|
|
|
1762
1828
|
reset!
|
|
1763
1829
|
|
|
1764
1830
|
set :environment, (ENV['APP_ENV'] || ENV['RACK_ENV'] || :development).to_sym
|
|
1765
|
-
set :raise_errors,
|
|
1766
|
-
set :dump_errors,
|
|
1767
|
-
set :show_exceptions,
|
|
1831
|
+
set :raise_errors, proc { test? }
|
|
1832
|
+
set :dump_errors, proc { !test? }
|
|
1833
|
+
set :show_exceptions, proc { development? }
|
|
1768
1834
|
set :sessions, false
|
|
1769
|
-
set :session_store, Rack::
|
|
1835
|
+
set :session_store, Rack::Protection::EncryptedCookie
|
|
1770
1836
|
set :logging, false
|
|
1771
1837
|
set :protection, true
|
|
1772
1838
|
set :method_override, false
|
|
1773
1839
|
set :use_code, false
|
|
1774
|
-
set :default_encoding,
|
|
1840
|
+
set :default_encoding, 'utf-8'
|
|
1775
1841
|
set :x_cascade, true
|
|
1776
1842
|
set :add_charset, %w[javascript xml xhtml+xml].map { |t| "application/#{t}" }
|
|
1777
|
-
settings.add_charset <<
|
|
1843
|
+
settings.add_charset << %r{^text/}
|
|
1778
1844
|
set :mustermann_opts, {}
|
|
1845
|
+
set :default_content_type, 'text/html'
|
|
1779
1846
|
|
|
1780
1847
|
# explicitly generating a session secret eagerly to play nice with preforking
|
|
1781
1848
|
begin
|
|
1782
1849
|
require 'securerandom'
|
|
1783
1850
|
set :session_secret, SecureRandom.hex(64)
|
|
1784
|
-
rescue LoadError, NotImplementedError
|
|
1851
|
+
rescue LoadError, NotImplementedError, RuntimeError
|
|
1785
1852
|
# SecureRandom raises a NotImplementedError if no random device is available
|
|
1786
|
-
|
|
1853
|
+
# RuntimeError raised due to broken openssl backend: https://bugs.ruby-lang.org/issues/19230
|
|
1854
|
+
set :session_secret, format('%064x', Kernel.rand((2**256) - 1))
|
|
1787
1855
|
end
|
|
1788
1856
|
|
|
1789
1857
|
class << self
|
|
1790
|
-
|
|
1791
|
-
|
|
1858
|
+
alias methodoverride? method_override?
|
|
1859
|
+
alias methodoverride= method_override=
|
|
1792
1860
|
end
|
|
1793
1861
|
|
|
1794
1862
|
set :run, false # start server via at-exit hook?
|
|
@@ -1796,21 +1864,17 @@ module Sinatra
|
|
|
1796
1864
|
set :handler_name, nil
|
|
1797
1865
|
set :traps, true
|
|
1798
1866
|
set :server, %w[HTTP webrick]
|
|
1799
|
-
set :bind,
|
|
1867
|
+
set :bind, proc { development? ? 'localhost' : '0.0.0.0' }
|
|
1800
1868
|
set :port, Integer(ENV['PORT'] && !ENV['PORT'].empty? ? ENV['PORT'] : 4567)
|
|
1801
1869
|
set :quiet, false
|
|
1802
1870
|
|
|
1803
1871
|
ruby_engine = defined?(RUBY_ENGINE) && RUBY_ENGINE
|
|
1804
1872
|
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
server.unshift 'mongrel' if ruby_engine.nil?
|
|
1811
|
-
server.unshift 'thin' if ruby_engine != 'jruby'
|
|
1812
|
-
server.unshift 'trinidad' if ruby_engine == 'jruby'
|
|
1813
|
-
end
|
|
1873
|
+
server.unshift 'puma'
|
|
1874
|
+
server.unshift 'falcon' if ruby_engine != 'jruby'
|
|
1875
|
+
server.unshift 'mongrel' if ruby_engine.nil?
|
|
1876
|
+
server.unshift 'thin' if ruby_engine != 'jruby'
|
|
1877
|
+
server.unshift 'trinidad' if ruby_engine == 'jruby'
|
|
1814
1878
|
|
|
1815
1879
|
set :absolute_redirects, true
|
|
1816
1880
|
set :prefixed_redirects, false
|
|
@@ -1818,14 +1882,14 @@ module Sinatra
|
|
|
1818
1882
|
set :strict_paths, true
|
|
1819
1883
|
|
|
1820
1884
|
set :app_file, nil
|
|
1821
|
-
set :root,
|
|
1822
|
-
set :views,
|
|
1823
|
-
set :reload_templates,
|
|
1885
|
+
set :root, proc { app_file && File.expand_path(File.dirname(app_file)) }
|
|
1886
|
+
set :views, proc { root && File.join(root, 'views') }
|
|
1887
|
+
set :reload_templates, proc { development? }
|
|
1824
1888
|
set :lock, false
|
|
1825
1889
|
set :threaded, true
|
|
1826
1890
|
|
|
1827
|
-
set :public_folder,
|
|
1828
|
-
set :static,
|
|
1891
|
+
set :public_folder, proc { root && File.join(root, 'public') }
|
|
1892
|
+
set :static, proc { public_folder && File.exist?(public_folder) }
|
|
1829
1893
|
set :static_cache_control, false
|
|
1830
1894
|
|
|
1831
1895
|
error ::Exception do
|
|
@@ -1836,7 +1900,7 @@ module Sinatra
|
|
|
1836
1900
|
|
|
1837
1901
|
configure :development do
|
|
1838
1902
|
get '/__sinatra__/:image.png' do
|
|
1839
|
-
filename =
|
|
1903
|
+
filename = __dir__ + "/images/#{params[:image].to_i}.png"
|
|
1840
1904
|
content_type :png
|
|
1841
1905
|
send_file filename
|
|
1842
1906
|
end
|
|
@@ -1844,7 +1908,7 @@ module Sinatra
|
|
|
1844
1908
|
error NotFound do
|
|
1845
1909
|
content_type 'text/html'
|
|
1846
1910
|
|
|
1847
|
-
if
|
|
1911
|
+
if instance_of?(Sinatra::Application)
|
|
1848
1912
|
code = <<-RUBY.gsub(/^ {12}/, '')
|
|
1849
1913
|
#{request.request_method.downcase} '#{request.path_info}' do
|
|
1850
1914
|
"Hello World"
|
|
@@ -1859,11 +1923,11 @@ module Sinatra
|
|
|
1859
1923
|
end
|
|
1860
1924
|
RUBY
|
|
1861
1925
|
|
|
1862
|
-
file = settings.app_file.to_s.sub(settings.root.to_s, '').sub(
|
|
1926
|
+
file = settings.app_file.to_s.sub(settings.root.to_s, '').sub(%r{^/}, '')
|
|
1863
1927
|
code = "# in #{file}\n#{code}" unless file.empty?
|
|
1864
1928
|
end
|
|
1865
1929
|
|
|
1866
|
-
|
|
1930
|
+
<<-HTML.gsub(/^ {10}/, '')
|
|
1867
1931
|
<!DOCTYPE html>
|
|
1868
1932
|
<html>
|
|
1869
1933
|
<head>
|
|
@@ -1875,7 +1939,7 @@ module Sinatra
|
|
|
1875
1939
|
</head>
|
|
1876
1940
|
<body>
|
|
1877
1941
|
<h2>Sinatra doesn’t know this ditty.</h2>
|
|
1878
|
-
<img src='#{uri
|
|
1942
|
+
<img src='#{uri '/__sinatra__/404.png'}'>
|
|
1879
1943
|
<div id="c">
|
|
1880
1944
|
Try this:
|
|
1881
1945
|
<pre>#{Rack::Utils.escape_html(code)}</pre>
|
|
@@ -1895,12 +1959,12 @@ module Sinatra
|
|
|
1895
1959
|
# top-level. Subclassing Sinatra::Base is highly recommended for
|
|
1896
1960
|
# modular applications.
|
|
1897
1961
|
class Application < Base
|
|
1898
|
-
set :logging,
|
|
1962
|
+
set :logging, proc { !test? }
|
|
1899
1963
|
set :method_override, true
|
|
1900
|
-
set :run,
|
|
1964
|
+
set :run, proc { !test? }
|
|
1901
1965
|
set :app_file, nil
|
|
1902
1966
|
|
|
1903
|
-
def self.register(*extensions, &block)
|
|
1967
|
+
def self.register(*extensions, &block) # :nodoc:
|
|
1904
1968
|
added_methods = extensions.flat_map(&:public_instance_methods)
|
|
1905
1969
|
Delegator.delegate(*added_methods)
|
|
1906
1970
|
super(*extensions, &block)
|
|
@@ -1910,13 +1974,16 @@ module Sinatra
|
|
|
1910
1974
|
# Sinatra delegation mixin. Mixing this module into an object causes all
|
|
1911
1975
|
# methods to be delegated to the Sinatra::Application class. Used primarily
|
|
1912
1976
|
# at the top-level.
|
|
1913
|
-
module Delegator
|
|
1977
|
+
module Delegator # :nodoc:
|
|
1914
1978
|
def self.delegate(*methods)
|
|
1915
1979
|
methods.each do |method_name|
|
|
1916
1980
|
define_method(method_name) do |*args, &block|
|
|
1917
1981
|
return super(*args, &block) if respond_to? method_name
|
|
1982
|
+
|
|
1918
1983
|
Delegator.target.send(method_name, *args, &block)
|
|
1919
1984
|
end
|
|
1985
|
+
# ensure keyword argument passing is compatible with ruby >= 2.7
|
|
1986
|
+
ruby2_keywords(method_name) if respond_to?(:ruby2_keywords, true)
|
|
1920
1987
|
private method_name
|
|
1921
1988
|
end
|
|
1922
1989
|
end
|
|
@@ -1935,7 +2002,8 @@ module Sinatra
|
|
|
1935
2002
|
|
|
1936
2003
|
class Wrapper
|
|
1937
2004
|
def initialize(stack, instance)
|
|
1938
|
-
@stack
|
|
2005
|
+
@stack = stack
|
|
2006
|
+
@instance = instance
|
|
1939
2007
|
end
|
|
1940
2008
|
|
|
1941
2009
|
def settings
|