module Sinatra::Helpers

def attachment(filename=nil)

instructing the user agents to prompt to save.
Set the Content-Disposition to "attachment" with the specified filename,
def attachment(filename=nil)
  response['Content-Disposition'] = 'attachment'
  if filename
    params = '; filename="%s"' % File.basename(filename)
    response['Content-Disposition'] << params
  end
end

def back

Sugar for redirect (example: redirect back)
def back
  request.referer
end

def body(value=nil, &block)

evaluation is deferred until the body is read with #each.
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
    response.body = value
  else
    response.body
  end
end

def cache_control(*values)

http://tools.ietf.org/html/rfc2616#section-14.9.1
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, :min_stale, :s_max_age).
: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.kind_of?(Hash)
    hash = values.pop
    hash.reject! { |k,v| v == false }
    hash.reject! { |k,v| values << k if v == true }
  else
    hash = {}
  end
  values = values.map { |value| value.to_s.tr('_','-') }
  hash.each do |key, value|
    key = key.to_s.tr('_', '-')
    value = value.to_i if key == "max-age"
    values << [key, value].join('=')
  end
  response['Cache-Control'] = values.join(', ') if values.any?
end

def content_type(type = nil, params={})

extension.
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
  fail "Unknown media type: %p" % type if mime_type.nil?
  mime_type = mime_type.dup
  unless params.include? :charset or settings.add_charset.all? { |p| not 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 { |kv| kv.join('=') }.join(', ')
  end
  response['Content-Type'] = mime_type
end

def error(code, body=nil)

Halt processing and return the error status provided.
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

def etag(value, kind=:strong)

GET or HEAD, a '304 Not Modified' response is sent.
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, kind=:strong)
  raise TypeError, ":strong or :weak expected" if ![:strong,:weak].include?(kind)
  value = '"%s"' % value
  value = 'W/' + value if kind == :weak
  response['ETag'] = value
  # Conditional GET check
  if etags = env['HTTP_IF_NONE_MATCH']
    etags = etags.split(/\s*,\s*/)
    halt 304 if etags.include?(value) || etags.include?('*')
  end
end

def expires(amount, *values)


=> Expires: Mon, 08 Jun 2009 08:50:17 GMT
=> Cache-Control: public, must-revalidate, max-age=60
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.kind_of?(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)
  cache_control(*values)
  response['Expires'] = time.httpdate
end

def headers(hash=nil)

Set multiple response headers with Hash.
def headers(hash=nil)
  response.headers.merge! hash if hash
  response.headers
end

def last_modified(time)

with a '304 Not Modified' response.
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
  # compare based on seconds since epoch
  halt 304 if Time.httpdate(env['HTTP_IF_MODIFIED_SINCE']).to_i >= time.to_i
rescue ArgumentError
end

def mime_type(type)

Look up a media type by file extension in Rack's mime registry.
def mime_type(type)
  Base.mime_type(type)
end

def not_found(body=nil)

Halt processing and return a 404 Not Found.
def not_found(body=nil)
  error 404, body
end

def redirect(uri, *args)

Halt processing and redirect to the URI provided.
def redirect(uri, *args)
  status 302
  # 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?)
  halt(*args)
end

def send_file(path, opts={})

Use the contents of the file at +path+ as the response body.
def send_file(path, opts={})
  stat = File.stat(path)
  last_modified(opts[:last_modified] || stat.mtime)
  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
  file_length = opts[:length] || stat.size
  sf = StaticFile.open(path, 'rb')
  if ! sf.parse_ranges(env, file_length)
    response['Content-Range'] = "bytes */#{file_length}"
    halt 416
  elsif r=sf.range
    response['Content-Range'] = "bytes #{r.begin}-#{r.end}/#{file_length}"
    response['Content-Length'] = (r.end - r.begin + 1).to_s
    halt 206, sf
  else
    response['Content-Length'] ||= file_length.to_s
    halt sf
  end
rescue Errno::ENOENT
  not_found
end

def session

Access the underlying Rack session.
def session
  request.session
end

def status(value=nil)

Set or retrieve the response status code.
def status(value=nil)
  response.status = value if value
  response.status
end

def time_for(value)

if 1.8 support is dropped.
This can be removed and calls to it replaced with to_time,
Ruby 1.8 has no #to_time method.
def time_for(value)
  if value.respond_to? :to_time
    value.to_time
  elsif value.is_a? Time
    value
  elsif value.respond_to? :new_offset
    # DateTime#to_time does the same on 1.9
    d = value.new_offset 0
    t = Time.utc d.year, d.mon, d.mday, d.hour, d.min, d.sec + d.sec_fraction
    t.getlocal
  elsif value.respond_to? :mday
    # Date#to_time does the same on 1.9
    Time.local(value.year, value.mon, value.mday)
  elsif value.is_a? Numeric
    Time.at value
  else
    Time.parse value.to_s
  end
rescue ArgumentError => boom
  raise boom
rescue Exception
  raise ArgumentError, "unable to convert #{value.inspect} to a Time object"
end

def uri(addr = nil, absolute = true, add_script_name = true)

Takes Rack routers and reverse proxies into account.
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 =~ /\A[A-z][A-z0-9\+\.\-]*:/
  uri = [host = ""]
  if absolute
    host << "http#{'s' if request.secure?}://"
    if request.forwarded? or request.port != (request.secure? ? 443 : 80)
      host << request.host_with_port
    else
      host << request.host
    end
  end
  uri << request.script_name.to_s if add_script_name
  uri << (addr ? addr : request.path_info).to_s
  File.join uri
end