module Sinatra::Helpers
def attachment(filename = nil, disposition = :attachment)
Set the Content-Disposition to "attachment" with the specified filename,
def attachment(filename = nil, disposition = :attachment) response['Content-Disposition'] = disposition.to_s.dup return unless filename params = format('; filename="%s"', File.basename(filename).gsub(/["\r\n]/, MULTIPART_FORM_DATA_REPLACEMENT_TABLE)) response['Content-Disposition'] << params ext = File.extname(filename) content_type(ext) unless response['content-type'] || ext.empty? end
def back
def back request.referer end
def bad_request?
def bad_request? status == 400 end
def body(value = nil, &block)
Set or retrieve the response body. When a block is given,
def body(value = nil, &block) if block_given? def block.each; yield(call) end response.body = block elsif value unless request.head? || value.is_a?(Rack::Files::BaseIterator) || value.is_a?(Stream) headers.delete 'content-length' end response.body = value else response.body end end
def cache_control(*values)
See RFC 2616 / 14.9 for more on standard cache control directives:
=> Cache-Control: public, must-revalidate, max-age=60
cache_control :public, :must_revalidate, :max_age => 60
a Hash of value directives (:max_age, :s_maxage).
:no_store, :must_revalidate, :proxy_revalidate) may be passed along with
Any number of non-value directives (:public, :private, :no_cache,
Specify response freshness policy for HTTP caches (Cache-Control header).
def cache_control(*values) if values.last.is_a?(Hash) hash = values.pop hash.reject! { |_k, v| v == false } hash.reject! { |k, v| values << k if v == true } else hash = {} end values.map! { |value| value.to_s.tr('_', '-') } hash.each do |key, value| key = key.to_s.tr('_', '-') value = value.to_i if %w[max-age s-maxage].include? key values << "#{key}=#{value}" end response['Cache-Control'] = values.join(', ') if values.any? end
def client_error?
def client_error? status.between? 400, 499 end
def content_type(type = nil, params = {})
Set the content-type of the response body given a media type or file
def content_type(type = nil, params = {}) return response['content-type'] unless type default = params.delete :default mime_type = mime_type(type) || default raise format('Unknown media type: %p', type) if mime_type.nil? mime_type = mime_type.dup unless params.include?(:charset) || settings.add_charset.all? { |p| !(p === mime_type) } params[:charset] = params.delete('charset') || settings.default_encoding end params.delete :charset if mime_type.include? 'charset' unless params.empty? mime_type << (mime_type.include?(';') ? ', ' : ';') mime_type << params.map do |key, val| val = val.inspect if val =~ /[";,]/ "#{key}=#{val}" end.join(', ') end response['content-type'] = mime_type end
def error(code, body = nil)
def error(code, body = nil) if code.respond_to? :to_str body = code.to_str code = 500 end response.body = body unless body.nil? halt code end
def etag(value, options = {})
matching etag, execution is immediately halted. If the request method is
When the current request includes an 'If-None-Match' header with a
cache validator.
indicates whether the etag should be used as a :strong (default) or :weak
identifies the current version of the resource. The +kind+ argument
GET matches. The +value+ argument is an identifier that uniquely
Set the response entity tag (HTTP 'ETag' header) and halt if conditional
def etag(value, options = {}) # Before touching this code, please double check RFC 2616 14.24 and 14.26. options = { kind: options } unless Hash === options kind = options[:kind] || :strong new_resource = options.fetch(:new_resource) { request.post? } unless ETAG_KINDS.include?(kind) raise ArgumentError, ':strong or :weak expected' end value = format('"%s"', value) value = "W/#{value}" if kind == :weak response['ETag'] = value return unless success? || status == 304 if etag_matches?(env['HTTP_IF_NONE_MATCH'], new_resource) halt(request.safe? ? 304 : 412) end if env['HTTP_IF_MATCH'] return if etag_matches?(env['HTTP_IF_MATCH'], new_resource) halt 412 end nil end
def etag_matches?(list, new_resource = request.post?)
def etag_matches?(list, new_resource = request.post?) return !new_resource if list == '*' list.to_s.split(/\s*,\s*/).include? response['ETag'] end
def expires(amount, *values)
=> Expires: Mon, 08 Jun 2009 08:50:17 GMT
=> Cache-Control: public, must-revalidate, max-age=500
expires 500, :public, :must_revalidate
"values" arguments are passed to the #cache_control helper:
indicating when the response should be considered "stale". The remaining
can be an integer number of seconds in the future or a Time object
Set the Expires header and Cache-Control/max-age directive. Amount
def expires(amount, *values) values << {} unless values.last.is_a?(Hash) if amount.is_a? Integer time = Time.now + amount.to_i max_age = amount else time = time_for amount max_age = time - Time.now end values.last.merge!(max_age: max_age) { |_key, v1, v2| v1 || v2 } cache_control(*values) response['Expires'] = time.httpdate end
def headers(hash = nil)
def headers(hash = nil) response.headers.merge! hash if hash response.headers end
def informational?
def informational? status.between? 100, 199 end
def last_modified(time)
equal or later than the time specified, execution is immediately halted
When the current request includes an 'If-Modified-Since' header that is
DateTime, or other object that responds to +to_time+.
and halt if conditional GET matches. The +time+ argument is a Time,
Set the last modified time of the resource (HTTP 'Last-Modified' header)
def last_modified(time) return unless time time = time_for time response['Last-Modified'] = time.httpdate return if env['HTTP_IF_NONE_MATCH'] if (status == 200) && env['HTTP_IF_MODIFIED_SINCE'] # compare based on seconds since epoch since = Time.httpdate(env['HTTP_IF_MODIFIED_SINCE']).to_i halt 304 if since >= time.to_i end if (success? || (status == 412)) && env['HTTP_IF_UNMODIFIED_SINCE'] # compare based on seconds since epoch since = Time.httpdate(env['HTTP_IF_UNMODIFIED_SINCE']).to_i halt 412 if since < time.to_i end rescue ArgumentError end
def logger
def logger request.logger end
def mime_type(type)
def mime_type(type) Base.mime_type(type) end
def not_found(body = nil)
def not_found(body = nil) error 404, body end
def not_found?
def not_found? status == 404 end
def redirect(uri, *args)
def redirect(uri, *args) # SERVER_PROTOCOL is required in Rack 3, fall back to HTTP_VERSION # for servers not updated for Rack 3 (like Puma 5) http_version = env['SERVER_PROTOCOL'] || env['HTTP_VERSION'] if (http_version == 'HTTP/1.1') && (env['REQUEST_METHOD'] != 'GET') status 303 else status 302 end # According to RFC 2616 section 14.30, "the field value consists of a # single absolute URI" response['Location'] = uri(uri.to_s, settings.absolute_redirects?, settings.prefixed_redirects?) halt(*args) end
def redirect?
def redirect? status.between? 300, 399 end
def send_file(path, opts = {})
def send_file(path, opts = {}) if opts[:type] || !response['content-type'] content_type opts[:type] || File.extname(path), default: 'application/octet-stream' end disposition = opts[:disposition] filename = opts[:filename] disposition = :attachment if disposition.nil? && filename filename = path if filename.nil? attachment(filename, disposition) if disposition last_modified opts[:last_modified] if opts[:last_modified] file = Rack::Files.new(File.dirname(settings.app_file)) result = file.serving(request, path) result[1].each { |k, v| headers[k] ||= v } 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
def server_error?
def server_error? status.between? 500, 599 end
def session
def session request.session end
def status(value = nil)
def status(value = nil) response.status = Rack::Utils.status_code(value) if value response.status end
def stream(keep_open = false)
The close parameter specifies whether Stream#close should be called
the response body have not yet been generated.
Allows to start sending data to the client even though later parts of
def stream(keep_open = false) scheduler = env['async.callback'] ? EventMachine : Stream current = @params.dup stream = if scheduler == Stream && keep_open Stream.new(scheduler, false) do |out| until out.closed? with_params(current) { yield(out) } end end else Stream.new(scheduler, keep_open) { |out| with_params(current) { yield(out) } } end body stream end
def success?
def success? status.between? 200, 299 end
def time_for(value)
Generates a Time object from the given value.
def time_for(value) if value.is_a? Numeric Time.at value elsif value.respond_to? :to_s Time.parse value.to_s else value.to_time end rescue ArgumentError => e raise e rescue Exception raise ArgumentError, "unable to convert #{value.inspect} to a Time object" end
def uri(addr = nil, absolute = true, add_script_name = true)
Generates the absolute URI for a given path in the app.
def uri(addr = nil, absolute = true, add_script_name = true) return addr if addr.to_s =~ /\A[a-z][a-z0-9+.\-]*:/i uri = [host = String.new] if absolute host << "http#{'s' if request.secure?}://" host << if request.forwarded? || (request.port != (request.secure? ? 443 : 80)) request.host_with_port else request.host end end uri << request.script_name.to_s if add_script_name uri << (addr || request.path_info).to_s File.join uri end
def with_params(temp_params)
def with_params(temp_params) original = @params @params = temp_params yield ensure @params = original if original end