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
class_inheritable_accessor :_layout,
:_session_id_key,
:_template_extensions,
:_template_root,
:_layout_root
self._layout = :application
self._session_id_key = :_session_id
self._template_extensions = { }
self._template_root = File.expand_path(MERB_ROOT / "dist/app/views")
self._layout_root = File.expand_path(MERB_ROOT / "dist/app/views/layout")
include Merb::ControllerMixin
include Merb::RenderMixin
include Merb::ResponderMixin
attr_accessor :status, :body, :request
MULTIPART_REGEXP = /\Amultipart\/form-data.*boundary=\"?([^\";,]+)/n.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(request, env, args, response)
@env = MerbHash[env.to_hash]
@status, @method, @response, @headers = 200, (env[Mongrel::Const::REQUEST_METHOD]||Mongrel::Const::GET).downcase.to_sym, response,
{'Content-Type' =>'text/html'}
cookies = query_parse(@env[Mongrel::Const::HTTP_COOKIE], ';,')
querystring = query_parse(@env[Mongrel::Const::QUERY_STRING])
if MULTIPART_REGEXP =~ @env[Mongrel::Const::UPCASE_CONTENT_TYPE] && @method == :post
querystring.update(parse_multipart(request, $1))
elsif @method == :post
if [Mongrel::Const::APPLICATION_JSON, Mongrel::Const::TEXT_JSON].include?(@env[Mongrel::Const::UPCASE_CONTENT_TYPE])
MERB_LOGGER.info("JSON Request")
json = JSON.parse(request.read || "") || {}
json = MerbHash.new(json) if json.is_a? Hash
querystring.update(json)
else
querystring.update(query_parse(request.read))
end
end
@cookies, @params = cookies, querystring.update(args)
@cookies[_session_id_key] = @params[_session_id_key] if @params.key?(_session_id_key)
allow = [:post, :put, :delete]
allow << :get if MERB_ENV == 'development'
if @params.key?(:_method) && allow.include?(@method)
@method = @params.delete(:_method).downcase.intern
end
@request = Request.new(@env, @method)
MERB_LOGGER.info("Params: #{params.inspect}\nCookies: #{cookies.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 #{self.class}##{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
# accessor for @response. Please use response and
# never @response directly.
def response
@response
end
# lookup the trait[:template_extensions] for the extname of the filename
# you pass. Answers with the engine that matches the extension, Template::Erubis
# is used if none matches.
def engine_for(file)
extension = File.extname(file)[1..-1]
_template_extensions[extension]
rescue
::Merb::Template::Erubis
end
# This method is called by templating-engines to register themselves with
# a list of extensions that will be looked up on #render of an action.
def self.register_engine(engine, *extensions)
[extensions].flatten.uniq.each do |ext|
_template_extensions[ext] = engine
end
end
# shared_accessor sets up a class instance variable that can
# be unique for each class but also inherits the shared attrs
# from its superclasses. Since @@class variables are almost
# global vars within an inheritance tree, we use
# @class_instance_variables instead
class_inheritable_accessor :before_filters
class_inheritable_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)
opts = shuffle_filters!(opts)
case filter
when Symbol, Proc, String
(self.after_filters ||= []) << [filter, opts]
else
raise(ArgumentError,
'After filters need to be either a Symbol, String or a Proc'
)
end
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