sinatra-rack-3-commonlit 3.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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