# Methods available to routes, before/after filters, and views. module Helpers # 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?('/') type = ".#{type}" unless type.to_s[0] == ?. return Rack::Mime.mime_type(type, nil) unless value Rack::Mime::MIME_TYPES[type] = value end # Set or retrieve the response status code. 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) if block_given? def block.each ; yield call ; end response.body = block else response.body = value end end # Halt processing and redirect to the URI provided. def redirect(uri, *args) status 302 response['Location'] = uri halt(*args) end # 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 # Halt processing and return a 404 Not Found. def not_found(body=nil) error 404, body end # Set multiple response headers with Hash. def headers(hash=nil) response.headers.merge! hash if hash response.headers end # Access the underlying Rack session. def session env['rack.session'] ||= {} end # Look up a media type by file extension in Rack's mime registry. def mime_type(type) Base.mime_type(type) end # Set the Content-Type of the response body given a media type or file # extension. def content_type(type, params={}) mime_type = mime_type(type) fail "Unknown media type: %p" % type if mime_type.nil? if params.any? params = params.collect { |kv| "%s=%s" % kv }.join(', ') response['Content-Type'] = [mime_type, params].join(";") else response['Content-Type'] = mime_type end 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' if filename params = '; filename="%s"' % File.basename(filename) response['Content-Disposition'] << params end end # Use the contents of the file at +path+ as the response body. def send_file(path, opts={}) stat = File.stat(path) last_modified stat.mtime content_type mime_type(opts[:type]) || mime_type(File.extname(path)) || response['Content-Type'] || 'application/octet-stream' response['Content-Length'] ||= (opts[:length] || stat.size).to_s if opts[:disposition] == 'attachment' || opts[:filename] attachment opts[:filename] || path elsif opts[:disposition] == 'inline' response['Content-Disposition'] = 'inline' end halt StaticFile.open(path, 'rb') rescue Errno::ENOENT not_found end # Rack response body used to deliver static files. The file contents are # generated iteratively in 8K chunks. class StaticFile < ::File #:nodoc: alias_method :to_path, :path def each rewind while buf = read(8192) yield buf end end end # Specify response freshness policy for HTTP caches (Cache-Control header). # Any number of non-value directives (:public, :private, :no_cache, # :no_store, :must_revalidate, :proxy_revalidate) may be passed along with # a Hash of value directives (:max_age, :min_stale, :s_max_age). # # cache_control :public, :must_revalidate, :max_age => 60 # => Cache-Control: public, must-revalidate, max-age=60 # # See RFC 2616 / 14.9 for more on standard cache control directives: # http://tools.ietf.org/html/rfc2616#section-14.9.1 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 { |k,v| values << [k.to_s.tr('_', '-'), v].join('=') } response['Cache-Control'] = values.join(', ') if values.any? end # Set the Expires header and Cache-Control/max-age directive. Amount # can be an integer number of seconds in the future or a Time object # indicating when the response should be considered "stale". The remaining # "values" arguments are passed to the #cache_control helper: # # expires 500, :public, :must_revalidate # => Cache-Control: public, must-revalidate, max-age=60 # => Expires: Mon, 08 Jun 2009 08:50:17 GMT # def expires(amount, *values) values << {} unless values.last.kind_of?(Hash) if amount.respond_to?(:to_time) max_age = amount.to_time - Time.now time = amount.to_time else max_age = amount time = Time.now + amount end values.last.merge!(:max_age => max_age) cache_control(*values) response['Expires'] = time.httpdate end # Set the last modified time of the resource (HTTP 'Last-Modified' header) # and halt if conditional GET matches. The +time+ argument is a Time, # DateTime, or other object that responds to +to_time+. # # When the current request includes an 'If-Modified-Since' header that # matches the time specified, execution is immediately halted with a # '304 Not Modified' response. def last_modified(time) return unless time time = time.to_time if time.respond_to?(:to_time) time = time.httpdate if time.respond_to?(:httpdate) response['Last-Modified'] = time halt 304 if time == request.env['HTTP_IF_MODIFIED_SINCE'] time end # 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 # indicates whether the etag should be used as a :strong (default) or :weak # cache validator. # # When the current request includes an 'If-None-Match' header with a # matching etag, execution is immediately halted. If the request method is # GET or HEAD, a '304 Not Modified' response is sent. 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 ## Sugar for redirect (example: redirect back) def back ; request.referer ; end end