require File.dirname(__FILE__)+'/mixins/controller_mixin'
require File.dirname(__FILE__)+'/mixins/responder_mixin'
require File.dirname(__FILE__)+'/merb_request'
require File.dirname(__FILE__)+'/merb_exceptions'
require 'set'
module Merb
# All of your web 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 < AbstractController
class_inheritable_accessor :_session_id_key, :_session_expiry
self._session_id_key = :_session_id
self._session_expiry = Time.now + Merb::Const::WEEK * 2
include Merb::ControllerMixin
include Merb::ResponderMixin
include Merb::ControllerExceptions::HTTPErrors
class << self
def callable_actions
@callable_actions ||= Set.new(public_instance_methods - hidden_actions)
end
def hidden_actions
write_inheritable_attribute(:hidden_actions, Merb::Controller.public_instance_methods) unless read_inheritable_attribute(:hidden_actions)
read_inheritable_attribute(:hidden_actions)
end
# Hide each of the given methods from being callable as actions.
def hide_action(*names)
write_inheritable_attribute(:hidden_actions, hidden_actions | names.collect { |n| n.to_s })
end
def build(req, env, args, resp)
cont = new
cont.parse_request(req, env, args, resp)
cont
end
end
# 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 parse_request(req, env, args, resp)
env = env.to_hash
@_status, method, @_response, @_headers = 200, (env[Merb::Const::REQUEST_METHOD]||Merb::Const::GET).downcase.to_sym, resp,
{'Content-Type' =>'text/html'}
cookies = query_parse(env[Merb::Const::HTTP_COOKIE], ';,')
querystring = query_parse(env[Merb::Const::QUERY_STRING])
if Merb::Const::MULTIPART_REGEXP =~ env[Merb::Const::UPCASE_CONTENT_TYPE] && [:put,:post].include?(method)
querystring.update(parse_multipart(req, $1, env))
elsif [:post, :put].include?(method)
if [Merb::Const::APPLICATION_JSON, Merb::Const::TEXT_JSON].include?(env[Merb::Const::UPCASE_CONTENT_TYPE])
MERB_LOGGER.info("JSON Request")
json = JSON.parse(req.read || "") || {}
querystring.update(json)
elsif [Merb::Const::APPLICATION_XML, Merb::Const::TEXT_XML].include?(env[Merb::Const::UPCASE_CONTENT_TYPE])
querystring.update(Hash.from_xml(req.read).with_indifferent_access)
else
querystring.update(query_parse(req.read))
end
end
@_cookies, @_params = cookies.symbolize_keys!, querystring.update(args).symbolize_keys!
if @_params.key?(_session_id_key) && !Merb::Server.config[:session_id_cookie_only]
@_cookies[_session_id_key] = @_params[_session_id_key]
elsif @_params.key?(_session_id_key) && Merb::Server.config[:session_id_cookie_only]
# This condition allows for certain controller/action paths to allow a
# session ID to be passed in a query string. This is needed for Flash
# Uploads to work since flash will not pass a Session Cookie Recommend
# running session.regenerate after any controller taking advantage of
# this in case someone is attempting a session fixation attack
@_cookies[_session_id_key] = @_params[_session_id_key] if Merb::Server.config[:query_string_whitelist].include?("#{params[:controller]}/#{params[:action]}")
end
# Handle alternate HTTP method passed as _method parameter. Doesn't allow
# method to be overridden for :get unless Merb is in development mode.
#
# i.e. You can pass _method=put on the querystring if you are in
# development mode.
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, req)
MERB_LOGGER.info("Params: #{params.inspect}\nCookies: #{cookies.inspect}")
end
def dispatch(action=:index)
start = Time.now
begin
if !self.class.callable_actions.include?(action.to_s)
raise NotFound
MERB_LOGGER.info "Action: #{action} not in callable_actions: #{self.class.callable_actions}"
else
setup_session
super(action)
finalize_session
end
rescue ControllerExceptions::Base => e
e.set_controller(self) # for access to session, params, etc
@_body = e.call_action
set_status(e.status)
end
@_benchmarks[:action_time] = Time.now - start
MERB_LOGGER.info("Time spent in #{self.class}##{action} action: #{@_benchmarks[:action_time]} seconds")
end
# Accessor for @_body. Please use status and never @status directly.
def body
@_body
end
# Accessor for @_status. Please use status and never @_status directly.
def status
@_status
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
# Sends a mail from a MailController
#
# send_mail FooMailer, :bar, :from => "foo@bar.com", :to => "baz@bat.com"
#
# would send an email via the FooMailer's bar method.
#
# The mail_params hash would be sent to the mailer, and includes items
# like from, to subject, and cc. See
# Merb::MailController#dispatch_and_deliver for more details.
#
# The send_params hash would be sent to the MailController, and is
# available to methods in the MailController as params. If you do
# not send any send_params, this controller's params will be available to
# the MailController as params
def send_mail(klass, method, mail_params, send_params = nil)
klass.new(send_params || params, self).dispatch_and_deliver(method, mail_params)
end
# Dispatches a PartController. Use like:
#
# <%= part TodoPart => :list %>
#
# will instantiate a new TodoPart controller and call the :list action
# invoking the Part's before and after filters as part of the call.
#
# returns a string containing the results of the Part controllers dispatch
#
# You can compose parts easily as well, these two parts will stil be wrapped
# in the layout of the Foo controller:
#
# class Foo < Application
# def some_action
# wrap_layout(part(TodoPart => :new) + part(TodoPart => :list))
# end
#end
#
def part(opts={})
res = opts.inject([]) do |memo,(klass,action)|
memo << klass.new(self).dispatch(action)
end
res.size == 1 ? res[0] : res
end
end
end