Skip to content

Instantly share code, notes, and snippets.

@gjtorikian
Created January 14, 2016 20:22
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save gjtorikian/6c879d8beb728c0cc259 to your computer and use it in GitHub Desktop.
Save gjtorikian/6c879d8beb728c0cc259 to your computer and use it in GitHub Desktop.
sinatra.diff
diff --git a/lib/sinatra/base.rb b/lib/sinatra/base.rb
index 3b07ab0..b76ef47 100644
--- a/lib/sinatra/base.rb
+++ b/lib/sinatra/base.rb
@@ -2,6 +2,7 @@
require 'rack'
require 'tilt'
require 'rack/protection'
+require 'forwardable'
# stdlib dependencies
require 'thread'
@@ -9,31 +10,43 @@ require 'time'
require 'uri'
# other files we need
-require 'sinatra/showexceptions'
+require 'sinatra/show_exceptions'
require 'sinatra/version'
module Sinatra
# The request object. See Rack::Request for more info:
- # http://rack.rubyforge.org/doc/classes/Rack/Request.html
+ # http://rubydoc.info/github/rack/rack/master/Rack/Request
class Request < Rack::Request
+ HEADER_PARAM = /\s*[\w.]+=(?:[\w.]+|"(?:[^"\\]|\\.)*")?\s*/
+ HEADER_VALUE_WITH_PARAMS = /(?:(?:\w+|\*)\/(?:\w+(?:\.|\-|\+)?|\*)*)\s*(?:;#{HEADER_PARAM})*/
+
# Returns an array of acceptable media types for the response
def accept
@env['sinatra.accept'] ||= begin
- entries = @env['HTTP_ACCEPT'].to_s.split(',')
- entries.map { |e| accept_entry(e) }.sort_by(&:last).map(&:first)
+ if @env.include? 'HTTP_ACCEPT' and @env['HTTP_ACCEPT'].to_s != ''
+ @env['HTTP_ACCEPT'].to_s.scan(HEADER_VALUE_WITH_PARAMS).
+ map! { |e| AcceptEntry.new(e) }.sort
+ else
+ [AcceptEntry.new('*/*')]
+ end
end
end
+ def accept?(type)
+ preferred_type(type).to_s.include?(type)
+ end
+
def preferred_type(*types)
- return accept.first if types.empty?
+ accepts = accept # just evaluate once
+ return accepts.first if types.empty?
types.flatten!
- accept.detect do |pattern|
+ return types.first if accepts.empty?
+ accepts.detect do |pattern|
type = types.detect { |t| File.fnmatch(pattern, t) }
return type if type
end
end
- alias accept? preferred_type
alias secure? ssl?
def forwarded?
@@ -45,24 +58,69 @@ module Sinatra
end
def idempotent?
- safe? or put? or delete?
+ safe? or put? or delete? or link? or unlink?
+ end
+
+ def link?
+ request_method == "LINK"
+ end
+
+ def unlink?
+ request_method == "UNLINK"
end
private
- def accept_entry(entry)
- type, *options = entry.delete(' ').split(';')
- quality = 0 # we sort smallest first
- options.delete_if { |e| quality = 1 - e[2..-1].to_f if e.start_with? 'q=' }
- [type, [quality, type.count('*'), 1 - options.size]]
+ class AcceptEntry
+ attr_accessor :params
+ attr_reader :entry
+
+ def initialize(entry)
+ params = entry.scan(HEADER_PARAM).map! do |s|
+ key, value = s.strip.split('=', 2)
+ value = value[1..-2].gsub(/\\(.)/, '\1') if value.start_with?('"')
+ [key, value]
+ end
+
+ @entry = entry
+ @type = entry[/[^;]+/].delete(' ')
+ @params = Hash[params]
+ @q = @params.delete('q') { 1.0 }.to_f
+ end
+
+ def <=>(other)
+ other.priority <=> self.priority
+ end
+
+ def priority
+ # We sort in descending order; better matches should be higher.
+ [ @q, -@type.count('*'), @params.size ]
+ end
+
+ def to_str
+ @type
+ end
+
+ def to_s(full = false)
+ full ? entry : to_str
+ end
+
+ def respond_to?(*args)
+ super or to_str.respond_to?(*args)
+ end
+
+ def method_missing(*args, &block)
+ to_str.send(*args, &block)
+ end
end
end
- # The response object. See Rack::Response and Rack::ResponseHelpers for
+ # The response object. See Rack::Response and Rack::Response::Helpers for
# more info:
- # http://rack.rubyforge.org/doc/classes/Rack/Response.html
- # http://rack.rubyforge.org/doc/classes/Rack/Response/Helpers.html
+ # http://rubydoc.info/github/rack/rack/master/Rack/Response
+ # http://rubydoc.info/github/rack/rack/master/Rack/Response/Helpers
class Response < Rack::Response
+ DROP_BODY_RESPONSES = [204, 205, 304]
def initialize(*)
super
headers['Content-Type'] ||= 'text/html'
@@ -110,7 +168,7 @@ module Sinatra
end
def drop_body?
- [204, 205, 304].include?(status.to_i)
+ DROP_BODY_RESPONSES.include?(status.to_i)
end
end
@@ -164,24 +222,25 @@ module Sinatra
end
class NotFound < NameError #:nodoc:
- def code ; 404 ; end
+ def http_status; 404 end
end
# Methods available to routes, before/after filters, and views.
module Helpers
# Set or retrieve the response status code.
- def status(value=nil)
+ def status(value = nil)
response.status = value if value
response.status
end
# Set or retrieve the response body. When a block is given,
# evaluation is deferred until the body is read with #each.
- def body(value=nil, &block)
+ def body(value = nil, &block)
if block_given?
def block.each; yield(call) end
response.body = block
elsif value
+ headers.delete 'Content-Length' unless request.head? || value.is_a?(Rack::File) || value.is_a?(Stream)
response.body = value
else
response.body
@@ -198,7 +257,7 @@ module Sinatra
# According to RFC 2616 section 14.30, "the field value consists of a
# single absolute URI"
- response['Location'] = uri(uri, settings.absolute_redirects?, settings.prefixed_redirects?)
+ response['Location'] = uri(uri.to_s, settings.absolute_redirects?, settings.prefixed_redirects?)
halt(*args)
end
@@ -224,32 +283,27 @@ module Sinatra
alias to uri
# Halt processing and return the error status provided.
- def error(code, body=nil)
+ def error(code, body = nil)
code, body = 500, code.to_str if code.respond_to? :to_str
response.body = body unless body.nil?
halt code
end
# Halt processing and return a 404 Not Found.
- def not_found(body=nil)
+ def not_found(body = nil)
error 404, body
end
# Set multiple response headers with Hash.
- def headers(hash=nil)
+ def headers(hash = nil)
response.headers.merge! hash if hash
response.headers
end
- # Access the underlying Rack session.
- def session
- request.session
- end
-
- # Access shared logger object.
- def logger
- request.logger
- end
+ extend Forwardable
+ def_delegators :request,
+ :session, # Access the underlying Rack session.
+ :logger # Access shared logger object.
# Look up a media type by file extension in Rack's mime registry.
def mime_type(type)
@@ -258,7 +312,7 @@ module Sinatra
# Set the Content-Type of the response body given a media type or file
# extension.
- def content_type(type = nil, params={})
+ def content_type(type = nil, params = {})
return response['Content-Type'] unless type
default = params.delete :default
mime_type = mime_type(type) || default
@@ -270,15 +324,18 @@ module Sinatra
params.delete :charset if mime_type.include? 'charset'
unless params.empty?
mime_type << (mime_type.include?(';') ? ', ' : ';')
- mime_type << params.map { |kv| kv.join('=') }.join(', ')
+ mime_type << params.map do |key, val|
+ val = val.inspect if val =~ /[";,]/
+ "#{key}=#{val}"
+ end.join(', ')
end
response['Content-Type'] = mime_type
end
# Set the Content-Disposition to "attachment" with the specified filename,
# instructing the user agents to prompt to save.
- def attachment(filename=nil)
- response['Content-Disposition'] = 'attachment'
+ def attachment(filename = nil, disposition = 'attachment')
+ response['Content-Disposition'] = disposition.to_s
if filename
params = '; filename="%s"' % File.basename(filename)
response['Content-Disposition'] << params
@@ -288,16 +345,16 @@ module Sinatra
end
# Use the contents of the file at +path+ as the response body.
- def send_file(path, opts={})
+ def send_file(path, opts = {})
if opts[:type] or not response['Content-Type']
content_type opts[:type] || File.extname(path), :default => 'application/octet-stream'
end
- if opts[:disposition] == 'attachment' || opts[:filename]
- attachment opts[:filename] || path
- elsif opts[:disposition] == 'inline'
- response['Content-Disposition'] = 'inline'
- end
+ disposition = opts[:disposition]
+ filename = opts[:filename]
+ disposition = 'attachment' if disposition.nil? and filename
+ filename = path if filename.nil?
+ attachment(filename, disposition) if disposition
last_modified opts[:last_modified] if opts[:last_modified]
@@ -305,7 +362,9 @@ module Sinatra
file.path = path
result = file.serving env
result[1].each { |k,v| headers[k] ||= v }
- halt result[0], result[2]
+ headers['Content-Length'] = result[1]['Content-Length']
+ opts[:status] &&= Integer(opts[:status])
+ halt opts[:status] || result[0], result[2]
rescue Errno::ENOENT
not_found
end
@@ -328,7 +387,7 @@ module Sinatra
end
def close
- return if @closed
+ return if closed?
@closed = true
@scheduler.schedule { @callbacks.each { |c| c.call }}
end
@@ -351,11 +410,15 @@ module Sinatra
end
def callback(&block)
- return yield if @closed
+ return yield if closed?
@callbacks << block
end
alias errback callback
+
+ def closed?
+ @closed
+ end
end
# Allows to start sending data to the client even though later parts of
@@ -393,7 +456,7 @@ module Sinatra
hash.each do |key, value|
key = key.to_s.tr('_', '-')
value = value.to_i if key == "max-age"
- values << [key, value].join('=')
+ values << "#{key}=#{value}"
end
response['Cache-Control'] = values.join(', ') if values.any?
@@ -452,6 +515,7 @@ module Sinatra
rescue ArgumentError
end
+ ETAG_KINDS = [:strong, :weak]
# Set the response entity tag (HTTP 'ETag' header) and halt if conditional
# GET matches. The +value+ argument is an identifier that uniquely
# identifies the current version of the resource. The +kind+ argument
@@ -467,12 +531,12 @@ module Sinatra
kind = options[:kind] || :strong
new_resource = options.fetch(:new_resource) { request.post? }
- unless [:strong, :weak].include?(kind)
+ unless ETAG_KINDS.include?(kind)
raise ArgumentError, ":strong or :weak expected"
end
value = '"%s"' % value
- value = 'W/' + value if kind == :weak
+ value = "W/#{value}" if kind == :weak
response['ETag'] = value
if success? or status == 304
@@ -575,7 +639,7 @@ module Sinatra
#
# Possible options are:
# :content_type The content type to use, same arguments as content_type.
- # :layout If set to false, no layout is rendered, otherwise
+ # :layout If set to something falsy, no layout is rendered, otherwise
# the specified layout is used (Ignored for `sass` and `less`)
# :layout_engine Engine to use for rendering the layout.
# :locals A hash with local variables that should be available
@@ -591,117 +655,160 @@ module Sinatra
def initialize
super
@default_layout = :layout
+ @preferred_extension = nil
end
- def erb(template, options={}, locals={})
- render :erb, template, options, locals
+ def erb(template, options = {}, locals = {}, &block)
+ render(:erb, template, options, locals, &block)
end
- def erubis(template, options={}, locals={})
+ def erubis(template, options = {}, locals = {})
warn "Sinatra::Templates#erubis is deprecated and will be removed, use #erb instead.\n" \
"If you have Erubis installed, it will be used automatically."
render :erubis, template, options, locals
end
- def haml(template, options={}, locals={})
- render :haml, template, options, locals
+ def haml(template, options = {}, locals = {}, &block)
+ render(:haml, template, options, locals, &block)
end
- def sass(template, options={}, locals={})
+ def sass(template, options = {}, locals = {})
options.merge! :layout => false, :default_content_type => :css
render :sass, template, options, locals
end
- def scss(template, options={}, locals={})
+ def scss(template, options = {}, locals = {})
options.merge! :layout => false, :default_content_type => :css
render :scss, template, options, locals
end
- def less(template, options={}, locals={})
+ def less(template, options = {}, locals = {})
options.merge! :layout => false, :default_content_type => :css
render :less, template, options, locals
end
- def builder(template=nil, options={}, locals={}, &block)
+ def stylus(template, options={}, locals={})
+ options.merge! :layout => false, :default_content_type => :css
+ render :styl, template, options, locals
+ end
+
+ def builder(template = nil, options = {}, locals = {}, &block)
options[:default_content_type] = :xml
render_ruby(:builder, template, options, locals, &block)
end
- def liquid(template, options={}, locals={})
- render :liquid, template, options, locals
+ def liquid(template, options = {}, locals = {}, &block)
+ render(:liquid, template, options, locals, &block)
end
- def markdown(template, options={}, locals={})
+ def markdown(template, options = {}, locals = {})
render :markdown, template, options, locals
end
- def textile(template, options={}, locals={})
+ def textile(template, options = {}, locals = {})
render :textile, template, options, locals
end
- def rdoc(template, options={}, locals={})
+ def rdoc(template, options = {}, locals = {})
render :rdoc, template, options, locals
end
- def radius(template, options={}, locals={})
+ def asciidoc(template, options = {}, locals = {})
+ render :asciidoc, template, options, locals
+ end
+
+ def radius(template, options = {}, locals = {})
render :radius, template, options, locals
end
- def markaby(template=nil, options={}, locals={}, &block)
+ def markaby(template = nil, options = {}, locals = {}, &block)
render_ruby(:mab, template, options, locals, &block)
end
- def coffee(template, options={}, locals={})
+ def coffee(template, options = {}, locals = {})
options.merge! :layout => false, :default_content_type => :js
render :coffee, template, options, locals
end
- def nokogiri(template=nil, options={}, locals={}, &block)
+ def nokogiri(template = nil, options = {}, locals = {}, &block)
options[:default_content_type] = :xml
render_ruby(:nokogiri, template, options, locals, &block)
end
- def slim(template, options={}, locals={})
- render :slim, template, options, locals
+ def slim(template, options = {}, locals = {}, &block)
+ render(:slim, template, options, locals, &block)
end
- def creole(template, options={}, locals={})
+ def creole(template, options = {}, locals = {})
render :creole, template, options, locals
end
+ def mediawiki(template, options = {}, locals = {})
+ render :mediawiki, template, options, locals
+ end
+
+ def wlang(template, options = {}, locals = {}, &block)
+ render(:wlang, template, options, locals, &block)
+ end
+
+ def yajl(template, options = {}, locals = {})
+ options[:default_content_type] = :json
+ render :yajl, template, options, locals
+ end
+
+ def rabl(template, options = {}, locals = {})
+ Rabl.register!
+ render :rabl, template, options, locals
+ end
+
# Calls the given block for every possible template file in views,
# named name.ext, where ext is registered on engine.
def find_template(views, name, engine)
yield ::File.join(views, "#{name}.#{@preferred_extension}")
- Tilt.mappings.each do |ext, engines|
- next unless ext != @preferred_extension and engines.include? engine
- yield ::File.join(views, "#{name}.#{ext}")
+
+ if Tilt.respond_to?(:mappings)
+ Tilt.mappings.each do |ext, engines|
+ next unless ext != @preferred_extension and engines.include? engine
+ yield ::File.join(views, "#{name}.#{ext}")
+ end
+ else
+ Tilt.default_mapping.extensions_for(engine).each do |ext|
+ yield ::File.join(views, "#{name}.#{ext}") unless ext == @preferred_extension
+ end
end
end
- private
+ private
+
# logic shared between builder and nokogiri
- def render_ruby(engine, template, options={}, locals={}, &block)
+ def render_ruby(engine, template, options = {}, locals = {}, &block)
options, template = template, nil if template.is_a?(Hash)
template = Proc.new { block } if template.nil?
render engine, template, options, locals
end
- def render(engine, data, options={}, locals={}, &block)
+ def render(engine, data, options = {}, locals = {}, &block)
# merge app-level options
- options = settings.send(engine).merge(options) if settings.respond_to?(engine)
- options[:outvar] ||= '@_out_buf'
- options[:default_encoding] ||= settings.default_encoding
+ engine_options = settings.respond_to?(engine) ? settings.send(engine) : {}
+ options.merge!(engine_options) { |key, v1, v2| v1 }
# extract generic options
locals = options.delete(:locals) || locals || {}
views = options.delete(:views) || settings.views || "./views"
- layout = options.delete(:layout)
+ layout = options[:layout]
+ layout = false if layout.nil? && options.include?(:layout)
eat_errors = layout.nil?
- layout = @default_layout if layout.nil? or layout == true
- content_type = options.delete(:content_type) || options.delete(:default_content_type)
- layout_engine = options.delete(:layout_engine) || engine
- scope = options.delete(:scope) || self
+ layout = engine_options[:layout] if layout.nil? or (layout == true && engine_options[:layout] != false)
+ layout = @default_layout if layout.nil? or layout == true
+ layout_options = options.delete(:layout_options) || {}
+ content_type = options.delete(:content_type) || options.delete(:default_content_type)
+ layout_engine = options.delete(:layout_engine) || engine
+ scope = options.delete(:scope) || self
+ options.delete(:layout)
+
+ # set some defaults
+ options[:outvar] ||= '@_out_buf'
+ options[:default_encoding] ||= settings.default_encoding
# compile and render template
begin
@@ -715,7 +822,8 @@ module Sinatra
# render layout
if layout
- options = options.merge(:views => views, :layout => false, :eat_errors => eat_errors, :scope => scope)
+ options = options.merge(:views => views, :layout => false, :eat_errors => eat_errors, :scope => scope).
+ merge!(layout_options)
catch(:layout_missing) { return render(layout_engine, layout, options, locals) { output } }
end
@@ -725,7 +833,7 @@ module Sinatra
def compile_template(engine, data, options, views)
eat_errors = options.delete :eat_errors
- template_cache.fetch engine, data, options do
+ template_cache.fetch engine, data, options, views do
template = Tilt[engine]
raise "Template engine not found: #{engine}" if template.nil?
@@ -740,7 +848,7 @@ module Sinatra
@preferred_extension = engine.to_s
find_template(views, data, template) do |file|
path ||= file # keep the initial path rather than the last one
- if found = File.exists?(file)
+ if found = File.exist?(file)
path = file
break
end
@@ -765,10 +873,12 @@ module Sinatra
include Helpers
include Templates
- attr_accessor :app
+ URI_INSTANCE = URI.const_defined?(:Parser) ? URI::Parser.new : URI
+
+ attr_accessor :app, :env, :request, :response, :params
attr_reader :template_cache
- def initialize(app=nil)
+ def initialize(app = nil)
super()
@app = app
@template_cache = Tilt::Cache.new
@@ -780,8 +890,6 @@ module Sinatra
dup.call!(env)
end
- attr_accessor :env, :request, :response, :params
-
def call!(env) # :nodoc:
@env = env
@request = Request.new(env)
@@ -792,7 +900,7 @@ module Sinatra
@response['Content-Type'] = nil
invoke { dispatch! }
- invoke { error_block!(response.status) }
+ invoke { error_block!(response.status) } unless @env['sinatra.error']
unless @response['Content-Type']
if Array === body and body[0].respond_to? :content_type
@@ -845,7 +953,8 @@ module Sinatra
nil
end
- private
+ private
+
# Run filters defined on the class and all superclasses.
def filter!(type, base = settings)
filter! type, base.superclass if base.superclass.respond_to?(:filters)
@@ -853,12 +962,16 @@ module Sinatra
end
# Run routes defined on the class and all superclasses.
- def route!(base = settings, pass_block=nil)
+ def route!(base = settings, pass_block = nil)
if routes = base.routes[@request.request_method]
routes.each do |pattern, keys, conditions, block|
- pass_block = process_route(pattern, keys, conditions) do |*args|
+ returned_pass_block = process_route(pattern, keys, conditions) do |*args|
+ env['sinatra.route'] = block.instance_variable_get(:@route_name)
route_eval { block[*args] }
end
+
+ # don't wipe out pass_block in superclass
+ pass_block = returned_pass_block if returned_pass_block
end
end
@@ -885,7 +998,7 @@ module Sinatra
route = @request.path_info
route = '/' if route.empty? and not settings.empty_path_info?
return unless match = pattern.match(route)
- values += match.captures.to_a.map { |v| force_encoding URI.decode_www_form_component(v) if v }
+ values += match.captures.map! { |v| force_encoding URI_INSTANCE.unescape(v) if v }
if values.any?
original, @params = params, params.merge('splat' => [], 'captures' => values)
@@ -915,24 +1028,27 @@ module Sinatra
# Attempt to serve static files from public directory. Throws :halt when
# a matching file is found, returns nil otherwise.
- def static!
+ def static!(options = {})
return if (public_dir = settings.public_folder).nil?
- public_dir = File.expand_path(public_dir)
-
- path = File.expand_path(public_dir + unescape(request.path_info))
- return unless path.start_with?(public_dir) and File.file?(path)
+ path = File.expand_path("#{public_dir}#{URI_INSTANCE.unescape(request.path_info)}" )
+ return unless File.file?(path)
env['sinatra.static_file'] = path
cache_control(*settings.static_cache_control) if settings.static_cache_control?
- send_file path, :disposition => nil
+ send_file path, options.merge(:disposition => nil)
end
# Enable string or symbol key access to the nested params hash.
- def indifferent_params(params)
- params = indifferent_hash.merge(params)
- params.each do |key, value|
- next unless value.is_a?(Hash)
- params[key] = indifferent_params(value)
+ def indifferent_params(object)
+ case object
+ when Hash
+ new_hash = indifferent_hash
+ object.each { |key, value| new_hash[key] = indifferent_params(value) }
+ new_hash
+ when Array
+ object.map { |item| indifferent_params(item) }
+ else
+ object
end
end
@@ -946,6 +1062,7 @@ module Sinatra
res = catch(:halt) { yield }
res = [res] if Fixnum === res or String === res
if Array === res and Fixnum === res.first
+ res = res.dup
status(res.shift)
body(res.pop)
headers(*res)
@@ -965,13 +1082,26 @@ module Sinatra
rescue ::Exception => boom
invoke { handle_exception!(boom) }
ensure
- filter! :after unless env['sinatra.static_file']
+ begin
+ filter! :after unless env['sinatra.static_file']
+ rescue ::Exception => boom
+ invoke { handle_exception!(boom) } unless @env['sinatra.error']
+ end
end
# Error handling during requests.
def handle_exception!(boom)
@env['sinatra.error'] = boom
- status boom.respond_to?(:code) ? Integer(boom.code) : 500
+
+ if boom.respond_to? :http_status
+ status(boom.http_status)
+ elsif settings.use_code? and boom.respond_to? :code and boom.code.between? 400, 599
+ status(boom.code)
+ else
+ status(500)
+ end
+
+ status(500) unless status.between? 400, 599
if server_error?
dump_errors! boom if settings.dump_errors?
@@ -979,7 +1109,7 @@ module Sinatra
end
if not_found?
- headers['X-Cascade'] = 'pass'
+ headers['X-Cascade'] = 'pass' if settings.x_cascade?
body '<h1>Not Found</h1>'
end
@@ -993,20 +1123,41 @@ module Sinatra
def error_block!(key, *block_params)
base = settings
while base.respond_to?(:errors)
- next base = base.superclass unless args = base.errors[key]
- args += [block_params]
- return process_route(*args)
+ next base = base.superclass unless args_array = base.errors[key]
+ args_array.reverse_each do |args|
+ first = args == args_array.first
+ args += [block_params]
+ resp = process_route(*args)
+ return resp unless resp.nil? && !first
+ end
end
return false unless key.respond_to? :superclass and key.superclass < Exception
error_block!(key.superclass, *block_params)
end
def dump_errors!(boom)
- msg = ["#{boom.class} - #{boom.message}:", *boom.backtrace].join("\n\t")
+ msg = ["#{Time.now.strftime("%Y-%m-%d %H:%M:%S")} - #{boom.class} - #{boom.message}:", *boom.backtrace].join("\n\t")
@env['rack.errors'].puts(msg)
end
class << self
+ CALLERS_TO_IGNORE = [ # :nodoc:
+ /\/sinatra(\/(base|main|show_exceptions))?\.rb$/, # all sinatra code
+ /lib\/tilt.*\.rb$/, # all tilt code
+ /^\(.*\)$/, # generated code
+ /rubygems\/(custom|core_ext\/kernel)_require\.rb$/, # rubygems require hacks
+ /active_support/, # active_support require hacks
+ /bundler(\/runtime)?\.rb/, # bundler require hacks
+ /<internal:/, # internal in ruby >= 1.9.2
+ /src\/kernel\/bootstrap\/[A-Z]/ # maglev kernel files
+ ]
+
+ # contrary to what the comment said previously, rubinius never supported this
+ if defined?(RUBY_IGNORE_CALLERS)
+ warn "RUBY_IGNORE_CALLERS is deprecated and will no longer be supported by Sinatra 2.0"
+ CALLERS_TO_IGNORE.concat(RUBY_IGNORE_CALLERS)
+ end
+
attr_reader :routes, :filters, :templates, :errors
# Removes all routes, filters, middleware and extension hooks from the
@@ -1099,12 +1250,13 @@ module Sinatra
args = compile! "ERROR", //, block
codes = codes.map { |c| Array(c) }.flatten
codes << Exception if codes.empty?
- codes.each { |c| @errors[c] = args }
+ codes.each { |c| (@errors[c] ||= []) << args }
end
# Sugar for `error(404) { ... }`
def not_found(&block)
- error 404, &block
+ error(404, &block)
+ error(Sinatra::NotFound, &block)
end
# Define a named template. The block must return the template source.
@@ -1114,13 +1266,13 @@ module Sinatra
end
# Define the layout template. The block must return the template source.
- def layout(name=:layout, &block)
+ def layout(name = :layout, &block)
template name, &block
end
- # Load embeded templates from the file; uses the caller's __FILE__
+ # Load embedded templates from the file; uses the caller's __FILE__
# when no file is specified.
- def inline_templates=(file=nil)
+ def inline_templates=(file = nil)
file = (file.nil? || file == true) ? (caller_files.first || File.expand_path($0)) : file
begin
@@ -1152,8 +1304,9 @@ module Sinatra
end
# Lookup or register a mime type in Rack's mime registry.
- def mime_type(type, value=nil)
- return type if type.nil? || type.to_s.include?('/')
+ def mime_type(type, value = nil)
+ return type if type.nil?
+ return type.to_s if type.to_s.include?('/')
type = ".#{type}" unless type.to_s[0] == ?.
return Rack::Mime.mime_type(type, nil) unless value
Rack::Mime::MIME_TYPES[type] = value
@@ -1194,11 +1347,186 @@ module Sinatra
end
def public=(value)
- warn ":public is no longer used to avoid overloading Module#public, use :public_folder instead"
+ warn ":public is no longer used to avoid overloading Module#public, use :public_folder or :public_dir instead"
set(:public_folder, value)
end
- private
+ def public_dir=(value)
+ self.public_folder = value
+ end
+
+ def public_dir
+ public_folder
+ end
+
+ # Defining a `GET` handler also automatically defines
+ # a `HEAD` handler.
+ def get(path, opts = {}, &block)
+ conditions = @conditions.dup
+ route('GET', path, opts, &block)
+
+ @conditions = conditions
+ route('HEAD', path, opts, &block)
+ end
+
+ def put(path, opts = {}, &bk) route 'PUT', path, opts, &bk end
+ def post(path, opts = {}, &bk) route 'POST', path, opts, &bk end
+ def delete(path, opts = {}, &bk) route 'DELETE', path, opts, &bk end
+ def head(path, opts = {}, &bk) route 'HEAD', path, opts, &bk end
+ def options(path, opts = {}, &bk) route 'OPTIONS', path, opts, &bk end
+ def patch(path, opts = {}, &bk) route 'PATCH', path, opts, &bk end
+ def link(path, opts = {}, &bk) route 'LINK', path, opts, &bk end
+ def unlink(path, opts = {}, &bk) route 'UNLINK', path, opts, &bk end
+
+ # Makes the methods defined in the block and in the Modules given
+ # in `extensions` available to the handlers and templates
+ def helpers(*extensions, &block)
+ class_eval(&block) if block_given?
+ include(*extensions) if extensions.any?
+ end
+
+ # Register an extension. Alternatively take a block from which an
+ # extension will be created and registered on the fly.
+ def register(*extensions, &block)
+ extensions << Module.new(&block) if block_given?
+ @extensions += extensions
+ extensions.each do |extension|
+ extend extension
+ extension.registered(self) if extension.respond_to?(:registered)
+ end
+ end
+
+ def development?; environment == :development end
+ def production?; environment == :production end
+ def test?; environment == :test end
+
+ # Set configuration options for Sinatra and/or the app.
+ # Allows scoping of settings for certain environments.
+ def configure(*envs)
+ yield self if envs.empty? || envs.include?(environment.to_sym)
+ end
+
+ # Use the specified Rack middleware
+ def use(middleware, *args, &block)
+ @prototype = nil
+ @middleware << [middleware, args, block]
+ end
+
+ # Stop the self-hosted server if running.
+ def quit!
+ return unless running?
+ # Use Thin's hard #stop! if available, otherwise just #stop.
+ running_server.respond_to?(:stop!) ? running_server.stop! : running_server.stop
+ $stderr.puts "== Sinatra has ended his set (crowd applauds)" unless handler_name =~/cgi/i
+ set :running_server, nil
+ set :handler_name, nil
+ end
+
+ alias_method :stop!, :quit!
+
+ # Run the Sinatra app as a self-hosted server using
+ # Thin, Puma, Mongrel, or WEBrick (in that order). If given a block, will call
+ # with the constructed handler once we have taken the stage.
+ def run!(options = {}, &block)
+ return if running?
+ set options
+ handler = detect_rack_handler
+ handler_name = handler.name.gsub(/.*::/, '')
+ server_settings = settings.respond_to?(:server_settings) ? settings.server_settings : {}
+ server_settings.merge!(:Port => port, :Host => bind)
+
+ begin
+ start_server(handler, server_settings, handler_name, &block)
+ rescue Errno::EADDRINUSE
+ $stderr.puts "== Someone is already performing on port #{port}!"
+ raise
+ ensure
+ quit!
+ end
+ end
+
+ alias_method :start!, :run!
+
+ # Check whether the self-hosted server is running or not.
+ def running?
+ running_server?
+ end
+
+ # The prototype instance used to process requests.
+ def prototype
+ @prototype ||= new
+ end
+
+ # Create a new instance without middleware in front of it.
+ alias new! new unless method_defined? :new!
+
+ # Create a new instance of the class fronted by its middleware
+ # pipeline. The object is guaranteed to respond to #call but may not be
+ # an instance of the class new was called on.
+ def new(*args, &bk)
+ instance = new!(*args, &bk)
+ Wrapper.new(build(instance).to_app, instance)
+ end
+
+ # Creates a Rack::Builder instance with all the middleware set up and
+ # the given +app+ as end point.
+ def build(app)
+ builder = Rack::Builder.new
+ setup_default_middleware builder
+ setup_middleware builder
+ builder.run app
+ builder
+ end
+
+ def call(env)
+ synchronize { prototype.call(env) }
+ end
+
+ # Like Kernel#caller but excluding certain magic entries and without
+ # line / method information; the resulting array contains filenames only.
+ def caller_files
+ cleaned_caller(1).flatten
+ end
+
+ # Like caller_files, but containing Arrays rather than strings with the
+ # first element being the file, and the second being the line.
+ def caller_locations
+ cleaned_caller 2
+ end
+
+ private
+
+ # Starts the server by running the Rack Handler.
+ def start_server(handler, server_settings, handler_name)
+ handler.run(self, server_settings) do |server|
+ unless handler_name =~ /cgi/i
+ $stderr.puts "== Sinatra (v#{Sinatra::VERSION}) has taken the stage on #{port} for #{environment} with backup from #{handler_name}"
+ end
+
+ setup_traps
+ set :running_server, server
+ set :handler_name, handler_name
+ server.threaded = settings.threaded if server.respond_to? :threaded=
+
+ yield server if block_given?
+ end
+ end
+
+ def setup_traps
+ if traps?
+ at_exit { quit! }
+
+ [:INT, :TERM].each do |signal|
+ old_handler = trap(signal) do
+ quit!
+ old_handler.call if old_handler.respond_to?(:call)
+ end
+ end
+
+ set :traps, false
+ end
+ end
+
# Dynamically defines a method on settings.
def define_singleton(name, content = Proc.new)
# replace with call to singleton_class once we're 1.9 only
@@ -1232,8 +1560,11 @@ module Sinatra
types.map! { |t| mime_types(t) }
types.flatten!
condition do
- if type = request.preferred_type(types)
- content_type(type)
+ if type = response['Content-Type']
+ types.include? type or types.include? type[/^[^;]+/]
+ elsif type = request.preferred_type(types)
+ params = (type.respond_to?(:params) ? type.params : {})
+ content_type(type, params)
true
else
false
@@ -1241,26 +1572,7 @@ module Sinatra
end
end
- public
- # Defining a `GET` handler also automatically defines
- # a `HEAD` handler.
- def get(path, opts={}, &block)
- conditions = @conditions.dup
- route('GET', path, opts, &block)
-
- @conditions = conditions
- route('HEAD', path, opts, &block)
- end
-
- def put(path, opts={}, &bk) route 'PUT', path, opts, &bk end
- def post(path, opts={}, &bk) route 'POST', path, opts, &bk end
- def delete(path, opts={}, &bk) route 'DELETE', path, opts, &bk end
- def head(path, opts={}, &bk) route 'HEAD', path, opts, &bk end
- def options(path, opts={}, &bk) route 'OPTIONS', path, opts, &bk end
- def patch(path, opts={}, &bk) route 'PATCH', path, opts, &bk end
-
- private
- def route(verb, path, options={}, &block)
+ def route(verb, path, options = {}, &block)
# Because of self.options.host
host_name(options.delete(:host)) if options.key?(:host)
enable :empty_path_info if path == "" and empty_path_info.nil?
@@ -1275,6 +1587,7 @@ module Sinatra
end
def generate_method(method_name, &block)
+ method_name = method_name.to_sym
define_method(method_name, &block)
method = instance_method method_name
remove_method method_name
@@ -1288,136 +1601,103 @@ module Sinatra
pattern, keys = compile path
conditions, @conditions = @conditions, []
- [ pattern, keys, conditions, block.arity != 0 ?
- proc { |a,p| unbound_method.bind(a).call(*p) } :
- proc { |a,p| unbound_method.bind(a).call } ]
+ wrapper = block.arity != 0 ?
+ proc { |a,p| unbound_method.bind(a).call(*p) } :
+ proc { |a,p| unbound_method.bind(a).call }
+ wrapper.instance_variable_set(:@route_name, method_name)
+
+ [ pattern, keys, conditions, wrapper ]
end
def compile(path)
- keys = []
if path.respond_to? :to_str
- pattern = path.to_str.gsub(/[^\?\%\\\/\:\*\w]/) { |c| encoded(c) }
- pattern.gsub!(/((:\w+)|\*)/) do |match|
- if match == "*"
- keys << 'splat'
- "(.*?)"
- else
- keys << $2[1..-1]
- "([^/?#]+)"
+ keys = []
+
+ # Split the path into pieces in between forward slashes.
+ # A negative number is given as the second argument of path.split
+ # because with this number, the method does not ignore / at the end
+ # and appends an empty string at the end of the return value.
+ #
+ segments = path.split('/', -1).map! do |segment|
+ ignore = []
+
+ # Special character handling.
+ #
+ pattern = segment.to_str.gsub(/[^\?\%\\\/\:\*\w]|:(?!\w)/) do |c|
+ ignore << escaped(c).join if c.match(/[\.@]/)
+ patt = encoded(c)
+ patt.gsub(/%[\da-fA-F]{2}/) do |match|
+ match.split(//).map! { |char| char == char.downcase ? char : "[#{char}#{char.downcase}]" }.join
+ end
+ end
+
+ ignore = ignore.uniq.join
+
+ # Key handling.
+ #
+ pattern.gsub(/((:\w+)|\*)/) do |match|
+ if match == "*"
+ keys << 'splat'
+ "(.*?)"
+ else
+ keys << $2[1..-1]
+ ignore_pattern = safe_ignore(ignore)
+
+ ignore_pattern
+ end
end
end
- [/^#{pattern}$/, keys]
+
+ # Special case handling.
+ #
+ if last_segment = segments[-1] and last_segment.match(/\[\^\\\./)
+ parts = last_segment.rpartition(/\[\^\\\./)
+ parts[1] = '[^'
+ segments[-1] = parts.join
+ end
+ [/\A#{segments.join('/')}\z/, keys]
elsif path.respond_to?(:keys) && path.respond_to?(:match)
[path, path.keys]
elsif path.respond_to?(:names) && path.respond_to?(:match)
[path, path.names]
elsif path.respond_to? :match
- [path, keys]
+ [path, []]
else
raise TypeError, path
end
end
- URI = ::URI.const_defined?(:Parser) ? ::URI::Parser.new : ::URI
-
def encoded(char)
- enc = URI.escape(char)
- enc = "(?:#{Regexp.escape enc}|#{URI.escape char, /./})" if enc == char
+ enc = URI_INSTANCE.escape(char)
+ enc = "(?:#{escaped(char, enc).join('|')})" if enc == char
enc = "(?:#{enc}|#{encoded('+')})" if char == " "
enc
end
- public
- # Makes the methods defined in the block and in the Modules given
- # in `extensions` available to the handlers and templates
- def helpers(*extensions, &block)
- class_eval(&block) if block_given?
- include(*extensions) if extensions.any?
+ def escaped(char, enc = URI_INSTANCE.escape(char))
+ [Regexp.escape(enc), URI_INSTANCE.escape(char, /./)]
end
- # Register an extension. Alternatively take a block from which an
- # extension will be created and registered on the fly.
- def register(*extensions, &block)
- extensions << Module.new(&block) if block_given?
- @extensions += extensions
- extensions.each do |extension|
- extend extension
- extension.registered(self) if extension.respond_to?(:registered)
+ def safe_ignore(ignore)
+ unsafe_ignore = []
+ ignore = ignore.gsub(/%[\da-fA-F]{2}/) do |hex|
+ unsafe_ignore << hex[1..2]
+ ''
end
- end
-
- def development?; environment == :development end
- def production?; environment == :production end
- def test?; environment == :test end
-
- # Set configuration options for Sinatra and/or the app.
- # Allows scoping of settings for certain environments.
- def configure(*envs, &block)
- yield self if envs.empty? || envs.include?(environment.to_sym)
- end
-
- # Use the specified Rack middleware
- def use(middleware, *args, &block)
- @prototype = nil
- @middleware << [middleware, args, block]
- end
-
- def quit!(server, handler_name)
- # Use Thin's hard #stop! if available, otherwise just #stop.
- server.respond_to?(:stop!) ? server.stop! : server.stop
- $stderr.puts "\n== Sinatra has ended his set (crowd applauds)" unless handler_name =~/cgi/i
- end
-
- # Run the Sinatra app as a self-hosted server using
- # Thin, Mongrel or WEBrick (in that order). If given a block, will call
- # with the constructed handler once we have taken the stage.
- def run!(options={})
- set options
- handler = detect_rack_handler
- handler_name = handler.name.gsub(/.*::/, '')
- handler.run self, :Host => bind, :Port => port do |server|
- unless handler_name =~ /cgi/i
- $stderr.puts "== Sinatra/#{Sinatra::VERSION} has taken the stage " +
- "on #{port} for #{environment} with backup from #{handler_name}"
+ unsafe_patterns = unsafe_ignore.map! do |unsafe|
+ chars = unsafe.split(//).map! do |char|
+ char == char.downcase ? char : char + char.downcase
end
- [:INT, :TERM].each { |sig| trap(sig) { quit!(server, handler_name) } }
- server.threaded = settings.threaded if server.respond_to? :threaded=
- set :running, true
- yield server if block_given?
- end
- rescue Errno::EADDRINUSE
- $stderr.puts "== Someone is already performing on port #{port}!"
- end
-
- # The prototype instance used to process requests.
- def prototype
- @prototype ||= new
- end
- # Create a new instance without middleware in front of it.
- alias new! new unless method_defined? :new!
-
- # Create a new instance of the class fronted by its middleware
- # pipeline. The object is guaranteed to respond to #call but may not be
- # an instance of the class new was called on.
- def new(*args, &bk)
- build(Rack::Builder.new, *args, &bk).to_app
- end
-
- # Creates a Rack::Builder instance with all the middleware set up and
- # an instance of this class as end point.
- def build(builder, *args, &bk)
- setup_default_middleware builder
- setup_middleware builder
- builder.run new!(*args, &bk)
- builder
- end
-
- def call(env)
- synchronize { prototype.call(env) }
+ "|(?:%[^#{chars[0]}].|%[#{chars[0]}][^#{chars[1]}])"
+ end
+ if unsafe_patterns.length > 0
+ "((?:[^#{ignore}/?#%]#{unsafe_patterns.join()})+)"
+ else
+ "([^#{ignore}/?#]+)"
+ end
end
- private
def setup_default_middleware(builder)
builder.use ExtendedRack
builder.use ShowExceptions if show_exceptions?
@@ -1460,8 +1740,9 @@ module Sinatra
def setup_protection(builder)
return unless protection?
options = Hash === protection ? protection.dup : {}
+ protect_session = options.fetch(:session) { sessions? }
options[:except] = Array options[:except]
- options[:except] += [:session_hijacking, :remote_token] unless sessions?
+ options[:except] += [:session_hijacking, :remote_token] unless protect_session
options[:reaction] ||= :drop_session
builder.use Rack::Protection, options
end
@@ -1500,37 +1781,6 @@ module Sinatra
end
end
- public
- CALLERS_TO_IGNORE = [ # :nodoc:
- /\/sinatra(\/(base|main|showexceptions))?\.rb$/, # all sinatra code
- /lib\/tilt.*\.rb$/, # all tilt code
- /^\(.*\)$/, # generated code
- /rubygems\/(custom|core_ext\/kernel)_require\.rb$/, # rubygems require hacks
- /active_support/, # active_support require hacks
- /bundler(\/runtime)?\.rb/, # bundler require hacks
- /<internal:/, # internal in ruby >= 1.9.2
- /src\/kernel\/bootstrap\/[A-Z]/ # maglev kernel files
- ]
-
- # contrary to what the comment said previously, rubinius never supported this
- if defined?(RUBY_IGNORE_CALLERS)
- warn "RUBY_IGNORE_CALLERS is deprecated and will no longer be supported by Sinatra 2.0"
- CALLERS_TO_IGNORE.concat(RUBY_IGNORE_CALLERS)
- end
-
- # Like Kernel#caller but excluding certain magic entries and without
- # line / method information; the resulting array contains filenames only.
- def caller_files
- cleaned_caller(1).flatten
- end
-
- # Like caller_files, but containing Arrays rather than strings with the
- # first element being the file, and the second being the line.
- def caller_locations
- cleaned_caller 2
- end
-
- private
# used for deprecation warnings
def warn(message)
super message + "\n\tfrom #{cleaned_caller.first.join(':')}"
@@ -1539,7 +1789,7 @@ module Sinatra
# Like Kernel#caller but excluding certain magic entries
def cleaned_caller(keep = 3)
caller(1).
- map { |line| line.split(/:(?=\d|in )/, 3)[0,keep] }.
+ map! { |line| line.split(/:(?=\d|in )/, 3)[0,keep] }.
reject { |file, *_| CALLERS_TO_IGNORE.any? { |pattern| file =~ pattern } }
end
end
@@ -1577,8 +1827,10 @@ module Sinatra
set :logging, false
set :protection, true
set :method_override, false
+ set :use_code, false
set :default_encoding, "utf-8"
- set :add_charset, %w[javascript xml xhtml+xml json].map { |t| "application/#{t}" }
+ set :x_cascade, true
+ set :add_charset, %w[javascript xml xhtml+xml].map { |t| "application/#{t}" }
settings.add_charset << /^text\//
# explicitly generating a session secret eagerly to play nice with preforking
@@ -1596,10 +1848,25 @@ module Sinatra
end
set :run, false # start server via at-exit hook?
- set :running, false # is the built-in server running now?
- set :server, %w[thin mongrel webrick]
- set :bind, '0.0.0.0'
- set :port, 4567
+ set :running_server, nil
+ set :handler_name, nil
+ set :traps, true
+ set :server, %w[HTTP webrick]
+ set :bind, Proc.new { development? ? 'localhost' : '0.0.0.0' }
+ set :port, Integer(ENV['PORT'] && !ENV['PORT'].empty? ? ENV['PORT'] : 4567)
+
+ ruby_engine = defined?(RUBY_ENGINE) && RUBY_ENGINE
+
+ if ruby_engine == 'macruby'
+ server.unshift 'control_tower'
+ else
+ server.unshift 'reel'
+ server.unshift 'mongrel' if ruby_engine.nil?
+ server.unshift 'puma' if ruby_engine != 'rbx'
+ server.unshift 'thin' if ruby_engine != 'jruby'
+ server.unshift 'puma' if ruby_engine == 'rbx'
+ server.unshift 'trinidad' if ruby_engine == 'jruby'
+ end
set :absolute_redirects, true
set :prefixed_redirects, false
@@ -1624,7 +1891,7 @@ module Sinatra
configure :development do
get '/__sinatra__/:image.png' do
- filename = File.dirname(__FILE__) + "/images/#{params[:image]}.png"
+ filename = File.dirname(__FILE__) + "/images/#{params[:image].to_i}.png"
content_type :png
send_file filename
end
@@ -1632,25 +1899,44 @@ module Sinatra
error NotFound do
content_type 'text/html'
- (<<-HTML).gsub(/^ {8}/, '')
- <!DOCTYPE html>
- <html>
- <head>
- <style type="text/css">
- body { text-align:center;font-family:helvetica,arial;font-size:22px;
- color:#888;margin:20px}
- #c {margin:0 auto;width:500px;text-align:left}
- </style>
- </head>
- <body>
- <h2>Sinatra doesn&rsquo;t know this ditty.</h2>
- <img src='#{uri "/__sinatra__/404.png"}'>
- <div id="c">
- Try this:
- <pre>#{request.request_method.downcase} '#{request.path_info}' do\n "Hello World"\nend</pre>
- </div>
- </body>
- </html>
+ if self.class == Sinatra::Application
+ code = <<-RUBY.gsub(/^ {12}/, '')
+ #{request.request_method.downcase} '#{request.path_info}' do
+ "Hello World"
+ end
+ RUBY
+ else
+ code = <<-RUBY.gsub(/^ {12}/, '')
+ class #{self.class}
+ #{request.request_method.downcase} '#{request.path_info}' do
+ "Hello World"
+ end
+ end
+ RUBY
+
+ file = settings.app_file.to_s.sub(settings.root.to_s, '').sub(/^\//, '')
+ code = "# in #{file}\n#{code}" unless file.empty?
+ end
+
+ (<<-HTML).gsub(/^ {10}/, '')
+ <!DOCTYPE html>
+ <html>
+ <head>
+ <style type="text/css">
+ body { text-align:center;font-family:helvetica,arial;font-size:22px;
+ color:#888;margin:20px}
+ #c {margin:0 auto;width:500px;text-align:left}
+ </style>
+ </head>
+ <body>
+ <h2>Sinatra doesn&rsquo;t know this ditty.</h2>
+ <img src='#{uri "/__sinatra__/404.png"}'>
+ <div id="c">
+ Try this:
+ <pre>#{Rack::Utils.escape_html(code)}</pre>
+ </div>
+ </body>
+ </html>
HTML
end
end
@@ -1691,10 +1977,10 @@ module Sinatra
end
end
- delegate :get, :patch, :put, :post, :delete, :head, :options, :template, :layout,
- :before, :after, :error, :not_found, :configure, :set, :mime_type,
- :enable, :disable, :use, :development?, :test?, :production?,
- :helpers, :settings
+ delegate :get, :patch, :put, :post, :delete, :head, :options, :link, :unlink,
+ :template, :layout, :before, :after, :error, :not_found, :configure,
+ :set, :mime_type, :enable, :disable, :use, :development?, :test?,
+ :production?, :helpers, :settings, :register
class << self
attr_accessor :target
@@ -1703,9 +1989,30 @@ module Sinatra
self.target = Application
end
- # Create a new Sinatra application. The block is evaluated in the new app's
- # class scope.
- def self.new(base=Base, options={}, &block)
+ class Wrapper
+ def initialize(stack, instance)
+ @stack, @instance = stack, instance
+ end
+
+ def settings
+ @instance.settings
+ end
+
+ def helpers
+ @instance
+ end
+
+ def call(env)
+ @stack.call(env)
+ end
+
+ def inspect
+ "#<#{@instance.class} app_file=#{settings.app_file.inspect}>"
+ end
+ end
+
+ # Create a new Sinatra application; the block is evaluated in the class scope.
+ def self.new(base = Base, &block)
base = Class.new(base)
base.class_eval(&block) if block_given?
base
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment