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