require File.dirname(__FILE__)+'/mixins/controller_mixin'
require File.dirname(__FILE__)+'/mixins/render_mixin'
require File.dirname(__FILE__)+'/mixins/responder_mixin'
require File.dirname(__FILE__)+'/merb_request'
module Merb
# All of your controllers will inherit from Merb::Controller. This
# superclass takes care of parsing the incoming headers and body into
# params and cookies and headers. If the request is a file upload it will
# stream it into a tempfile and pass in the filename and tempfile object
# to your controller via params. It also parses the ?query=string and
# puts that into params as well.
class Controller
shared_accessor :layout, :session_id_key, :cache_templates
include Merb::ControllerMixin
include Merb::RenderMixin
include Merb::ResponderMixin
attr_accessor :status, :body
MULTIPART_REGEXP = /\Amultipart\/form-data.*boundary=\"?([^\";,]+)/n.freeze
CONTENT_DISPOSITION_REGEXP = /^Content-Disposition: form-data;/.freeze
FIELD_ATTRIBUTE_REGEXP = /(?:\s(\w+)="([^"]+)")/.freeze
CONTENT_TYPE_REGEXP = /^Content-Type: (.+?)(\r$|\Z)/m.freeze
# parses the http request into params, headers and cookies
# that you can use in your controller classes. Also handles
# file uploads by writing a tempfile and passing a reference
# in params.
def initialize(req, env, args, method=(env['REQUEST_METHOD']||'GET'))
env = ::MerbHash[env.to_hash]
@status, @method, @env, @headers, @root = 200, method.downcase.to_sym, env,
{'Content-Type' =>'text/html'}, env['SCRIPT_NAME'].sub(/\/$/,'')
cookies = query_parse(env['HTTP_COOKIE'], ';,')
querystring = query_parse(env['QUERY_STRING'])
self.layout ||= :application
self.session_id_key ||= :_session_id
@in = req
if MULTIPART_REGEXP =~ env['CONTENT_TYPE']
boundary_regexp = /(?:\r?\n|\A)#{Regexp::quote("--#$1")}(?:--)?\r$/
until @in.eof?
attrs=MerbHash[]
for line in @in
case line
when "\r\n" : break
when CONTENT_DISPOSITION_REGEXP
attrs.update ::MerbHash[*$'.scan(FIELD_ATTRIBUTE_REGEXP).flatten]
when CONTENT_TYPE_REGEXP
attrs[:type] = $1
end
end
name=attrs[:name]
io_buffer=if attrs[:filename]
io_buffer=attrs[:tempfile]=Tempfile.new(:Merb)
io_buffer.binmode
else
attrs=""
end
while chunk=@in.read(16384)
if chunk =~ boundary_regexp
io_buffer << $`.chomp
@in.seek(-$'.size, IO::SEEK_CUR)
break
end
io_buffer << chunk
end
if name =~ /(.*)?\[\]/
(querystring[$1] ||= []) << attrs
else
querystring[name]=attrs if name
end
attrs[:tempfile].rewind if attrs.is_a?MerbHash
end
elsif @method == :post
if ['application/json', 'text/x-json'].include?(env['CONTENT_TYPE'])
MERB_LOGGER.info("JSON Request")
json = JSON.parse(@in.read || "") || {}
json = ::MerbHash.new(json) if json.is_a? Hash
querystring.merge!(json)
else
querystring.merge!(query_parse(@in.read))
end
end
@cookies, @params = cookies.dup, querystring.dup.merge(args)
@cookies.merge!(:_session_id => @params[:_session_id]) if @params.has_key?(:_session_id)
@method = @params.delete(:_method).downcase.to_sym if @params.has_key?(:_method)
@request = Request.new(@env, @method)
MERB_LOGGER.info("Params: #{params.inspect}")
end
def dispatch(action=:to_s)
start = Time.now
setup_session if respond_to?:setup_session
cought = catch(:halt) { call_filters(before_filters) }
@body = case cought
when :filter_chain_completed
send(action)
when String
cought
when nil
filters_halted
when Symbol
send(cought)
when Proc
cought.call(self)
else
raise MerbControllerError, "The before filter chain is broken dude. wtf?"
end
call_filters(after_filters)
finalize_session if respond_to?:finalize_session
MERB_LOGGER.info("Time spent in #{action} action: #{Time.now - start} seconds")
end
# override this method on your controller classes to specialize
# the output when the filter chain is halted.
def filters_halted
"
Filter Chain Halted!
"
end
# accessor for @request. Please use request and
# never @request directly.
def request
@request
end
# accessor for @params. Please use params and
# never @params directly.
def params
@params
end
# accessor for @cookies. Please use cookies and
# never @cookies directly.
def cookies
@cookies
end
# accessor for @headers. Please use headers and
# never @headers directly.
def headers
@headers
end
# accessor for @session. Please use session and
# never @session directly.
def session
@session
end
# shared_accessor sets up a class instance variable that can
# be unique for each class but also inherits the meta attrs
# from its superclasses. Since @@class variables are almost
# global vars within an inheritance tree, we use
# @class_instance_variables instead
shared_accessor :before_filters
shared_accessor :after_filters
# calls a filter chain according to rules.
def call_filters(filter_set)
(filter_set || []).each do |(filter, rule)|
ok = false
if rule.has_key?(:only)
if rule[:only].include?(params[:action].intern)
ok = true
end
elsif rule.has_key?(:exclude)
if !rule[:exclude].include?(params[:action].intern)
ok = true
end
else
ok = true
end
case filter
when Symbol, String
send(filter) if ok
when Proc
filter.call(self) if ok
end
end
return :filter_chain_completed
end
# #before is a class method that allows you to specify before
# filters in your controllers. Filters can either be a symbol
# or string that corresponds to a method name to call, or a
# proc object. if it is a method name that method will be
# called and if it is a proc it will be called with an argument
# of self where self is the current controller object. When
# you use a proc as a filter it needs to take one parameter.
#
# examples:
# before :some_filter
# before :authenticate, :exclude => [:login, :signup]
# before Proc.new {|c| c.some_method }, :only => :foo
#
# You can use either :only => :actionname or :exclude => [:this, :that]
# but not both at once. :only will only run before the listed actions
# and :exclude will run for every action that is not listed.
#
# Merb's before filter chain is very flixible. To halt the
# filter chain you use throw :halt . If throw is called with
# only one argument of :halt the return of the method filters_halted
# will be what is rendered to the view. You can overide filters_halted
# in your own controllers to control what it outputs. But the throw
# construct is much more powerful then just that. throw :halt can
# also take a second argument. Here is what that second arg can be
# and the behavior each type can have:
#
# when the second arg is a string then that string will be what
# is rendered to the browser. Since merb's render method returns
# a string you can render a template or just use a plain string:
#
# String:
# throw :halt, "You don't have permissions to do that!"
# throw :halt, render(:action => :access_denied)
#
# if the second arg is a symbol then the method named after that
# symbol will be called
# Symbol:
# throw :halt, :must_click_disclaimer
#
# If the second arg is a Proc, it will be called and its return
# value will be what is rendered to the browser:
# Proc:
# throw :halt, Proc.new {|c| c.access_denied }
# throw :halt, Proc.new {|c| Tidy.new(c.index) }
#
def self.before(filter, opts={})
raise(ArgumentError,
"You can specify either :only or :exclude but
not both at the same time for the same filter."
) if opts.has_key?(:only) && opts.has_key?(:exclude)
opts = shuffle_filters!(opts)
case filter
when Symbol, String, Proc
(self.before_filters ||= []) << [filter, opts]
else
raise(ArgumentError,
'filters need to be either a Symbol, String or a Proc'
)
end
end
# #after is a class method that allows you to specify after
# filters in your controllers. Filters can either be a symbol
# or string that corresponds to a method name or a proc object.
# if it is a method name that method will be called and if it
# is a proc it will be called with an argument of self. When
# you use a proc as a filter it needs to take one parameter.
# you can gain access to the response body like so:
# after Proc.new {|c| Tidy.new(c.body) }, :only => :index
#
def self.after(filter, opts={})
raise(ArgumentError,
"You can specify either :only or :exclude but
not both at the same time for the same filter."
) if opts.has_key?(:only) && opts.has_key?(:exclude)
raise(ArgumentError,
'after filters can only be a Proc object'
) unless Proc === filter
opts = shuffle_filters!(opts)
(self.after_filters ||= []) << [filter, opts]
end
def self.shuffle_filters!(opts={})
if opts[:only] && opts[:only].is_a?(Symbol)
opts[:only] = [opts[:only]]
end
if opts[:exclude] && opts[:exclude].is_a?(Symbol)
opts[:exclude] = [opts[:exclude]]
end
return opts
end
end
end