sinatra-rack-3-commonlit 3.1.0

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