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