nano-max-rb 0.0.1

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