diff options
Diffstat (limited to 'actionpack/lib/action_controller')
9 files changed, 459 insertions, 25 deletions
diff --git a/actionpack/lib/action_controller/base.rb b/actionpack/lib/action_controller/base.rb index ca0dccf575..c03c77cb4a 100644 --- a/actionpack/lib/action_controller/base.rb +++ b/actionpack/lib/action_controller/base.rb @@ -206,13 +206,17 @@ module ActionController HttpAuthentication::Digest::ControllerMethods, HttpAuthentication::Token::ControllerMethods, + # Before callbacks should also be executed the earliest as possible, so + # also include them at the bottom. + AbstractController::Callbacks, + # Add instrumentations hooks at the bottom, to ensure they instrument # all the methods properly. Instrumentation, - # Before callbacks should also be executed the earliest as possible, so - # also include them at the bottom. - AbstractController::Callbacks, + # Params wrapper should come before instrumentation so they are + # properly showed in logs + ParamsWrapper, # The same with rescue, append it at the end to wrap as much as possible. Rescue diff --git a/actionpack/lib/action_controller/log_subscriber.rb b/actionpack/lib/action_controller/log_subscriber.rb index 3fae697cc3..8d813a8e38 100644 --- a/actionpack/lib/action_controller/log_subscriber.rb +++ b/actionpack/lib/action_controller/log_subscriber.rb @@ -7,8 +7,10 @@ module ActionController def start_processing(event) payload = event.payload params = payload[:params].except(*INTERNAL_PARAMS) + format = payload[:format] + format = format.to_s.upcase if format.is_a?(Symbol) - info " Processing by #{payload[:controller]}##{payload[:action]} as #{payload[:formats].first.to_s.upcase}" + info " Processing by #{payload[:controller]}##{payload[:action]} as #{format}" info " Parameters: #{params.inspect}" unless params.empty? end diff --git a/actionpack/lib/action_controller/metal/compatibility.rb b/actionpack/lib/action_controller/metal/compatibility.rb index 006b9fd456..05dca445a4 100644 --- a/actionpack/lib/action_controller/metal/compatibility.rb +++ b/actionpack/lib/action_controller/metal/compatibility.rb @@ -18,13 +18,10 @@ module ActionController delegate :default_charset=, :to => "ActionDispatch::Response" end - # TODO: Update protected instance variables list - config_accessor :protected_instance_variables - self.protected_instance_variables = %w(@assigns @performed_redirect @performed_render - @variables_added @request_origin @url - @parent_controller @action_name - @before_filter_chain_aborted @_headers @_params - @_response) + self.protected_instance_variables = %w( + @_status @_headers @_params @_env @_response @_request + @_view_runtime @_stream @_url_options @_action_has_layout + ) def rescue_action(env) raise env["action_dispatch.rescue.exception"] diff --git a/actionpack/lib/action_controller/metal/implicit_render.rb b/actionpack/lib/action_controller/metal/implicit_render.rb index 3ec0c4c6a4..e8e465d3ba 100644 --- a/actionpack/lib/action_controller/metal/implicit_render.rb +++ b/actionpack/lib/action_controller/metal/implicit_render.rb @@ -1,21 +1,19 @@ module ActionController module ImplicitRender def send_action(method, *args) - if respond_to?(method, true) - ret = super - default_render unless response_body - ret - else - default_render - end + ret = super + default_render unless response_body + ret end def default_render(*args) render(*args) end - def action_method?(action_name) - super || template_exists?(action_name.to_s, _prefixes) + def method_for_action(action_name) + super || if template_exists?(action_name.to_s, _prefixes) + "default_render" + end end end end diff --git a/actionpack/lib/action_controller/metal/instrumentation.rb b/actionpack/lib/action_controller/metal/instrumentation.rb index dc3ea939e6..16cbbce2fb 100644 --- a/actionpack/lib/action_controller/metal/instrumentation.rb +++ b/actionpack/lib/action_controller/metal/instrumentation.rb @@ -14,12 +14,12 @@ module ActionController attr_internal :view_runtime - def process_action(action, *args) + def process_action(*args) raw_payload = { :controller => self.class.name, :action => self.action_name, :params => request.filtered_parameters, - :formats => request.formats.map(&:to_sym), + :format => request.format.ref, :method => request.method, :path => (request.fullpath rescue "unknown") } diff --git a/actionpack/lib/action_controller/metal/params_wrapper.rb b/actionpack/lib/action_controller/metal/params_wrapper.rb new file mode 100644 index 0000000000..881af74147 --- /dev/null +++ b/actionpack/lib/action_controller/metal/params_wrapper.rb @@ -0,0 +1,224 @@ +require 'active_support/core_ext/class/attribute' +require 'active_support/core_ext/hash/slice' +require 'active_support/core_ext/hash/except' +require 'active_support/core_ext/array/wrap' +require 'action_dispatch/http/mime_types' + +module ActionController + # Wraps parameters hash into nested hash. This will allow client to submit + # POST request without having to specify a root element in it. + # + # By default this functionality won't be enabled. You can enable + # it globally by setting +ActionController::Base.wrap_parameters+: + # + # ActionController::Base.wrap_parameters = [:json] + # + # You could also turn it on per controller by setting the format array to + # non-empty array: + # + # class UsersController < ApplicationController + # wrap_parameters :format => [:json, :xml] + # end + # + # If you enable +ParamsWrapper+ for +:json+ format. Instead of having to + # send JSON parameters like this: + # + # {"user": {"name": "Konata"}} + # + # You can now just send a parameters like this: + # + # {"name": "Konata"} + # + # And it will be wrapped into a nested hash with the key name matching + # controller's name. For example, if you're posting to +UsersController+, + # your new +params+ hash will look like this: + # + # {"name" => "Konata", "user" => {"name" => "Konata"}} + # + # You can also specify the key in which the parameters should be wrapped to, + # and also the list of attributes it should wrap by using either +:only+ or + # +:except+ options like this: + # + # class UsersController < ApplicationController + # wrap_parameters :person, :only => [:username, :password] + # end + # + # If you're going to pass the parameters to an +ActiveModel+ object (such as + # +User.new(params[:user])+), you might consider passing the model class to + # the method instead. The +ParamsWrapper+ will actually try to determine the + # list of attribute names from the model and only wrap those attributes: + # + # class UsersController < ApplicationController + # wrap_parameters Person + # end + # + # You still could pass +:only+ and +:except+ to set the list of attributes + # you want to wrap. + # + # By default, if you don't specify the key in which the parameters would be + # wrapped to, +ParamsWrapper+ will actually try to determine if there's + # a model related to it or not. This controller, for example: + # + # class Admin::UsersController < ApplicationController + # end + # + # will try to check if +Admin::User+ or +User+ model exists, and use it to + # determine the wrapper key respectively. If both of the model doesn't exists, + # it will then fallback to use +user+ as the key. + module ParamsWrapper + extend ActiveSupport::Concern + + EXCLUDE_PARAMETERS = %w(authenticity_token _method utf8) + + included do + class_attribute :_wrapper_options + self._wrapper_options = {:format => []} + end + + module ClassMethods + # Sets the name of the wrapper key, or the model which +ParamsWrapper+ + # would use to determine the attribute names from. + # + # ==== Examples + # wrap_parameters :format => :xml + # # enables the parmeter wrapper for XML format + # + # wrap_parameters :person + # # wraps parameters into +params[:person]+ hash + # + # wrap_parameters Person + # # wraps parameters by determine the wrapper key from Person class + # (+person+, in this case) and the list of attribute names + # + # wrap_parameters :only => [:username, :title] + # # wraps only +:username+ and +:title+ attributes from parameters. + # + # wrap_parameters false + # # disable parameters wrapping for this controller altogether. + # + # ==== Options + # * <tt>:format</tt> - The list of formats in which the parameters wrapper + # will be enabled. + # * <tt>:only</tt> - The list of attribute names which parameters wrapper + # will wrap into a nested hash. + # * <tt>:except</tt> - The list of attribute names which parameters wrapper + # will exclude from a nested hash. + def wrap_parameters(name_or_model_or_options, options = {}) + model = nil + + case name_or_model_or_options + when Hash + options = name_or_model_or_options + when false + options = options.merge(:format => []) + when Symbol, String + options = options.merge(:name => name_or_model_or_options) + else + model = name_or_model_or_options + end + + _set_wrapper_defaults(_wrapper_options.slice(:format).merge(options), model) + end + + # Sets the default wrapper key or model which will be used to determine + # wrapper key and attribute names. Will be called automatically when the + # module is inherited. + def inherited(klass) + if klass._wrapper_options[:format].present? + klass._set_wrapper_defaults(klass._wrapper_options.slice(:format)) + end + super + end + + protected + + # Determine the wrapper model from the controller's name. By convention, + # this could be done by trying to find the defined model that has the + # same singularize name as the controller. For example, +UsersController+ + # will try to find if the +User+ model exists. + def _default_wrap_model + model_name = self.name.sub(/Controller$/, '').singularize + + begin + model_klass = model_name.constantize + rescue NameError => e + unscoped_model_name = model_name.split("::", 2).last + break if unscoped_model_name == model_name + model_name = unscoped_model_name + end until model_klass + + model_klass + end + + def _set_wrapper_defaults(options, model=nil) + options = options.dup + + unless options[:only] || options[:except] + model ||= _default_wrap_model + if model.respond_to?(:column_names) + options[:only] = model.column_names + end + end + + unless options[:name] + model ||= _default_wrap_model + options[:name] = model ? model.to_s.demodulize.underscore : + controller_name.singularize + end + + options[:only] = Array.wrap(options[:only]).collect(&:to_s) if options[:only] + options[:except] = Array.wrap(options[:except]).collect(&:to_s) if options[:except] + options[:format] = Array.wrap(options[:format]) + + self._wrapper_options = options + end + end + + # Performs parameters wrapping upon the request. Will be called automatically + # by the metal call stack. + def process_action(*args) + if _wrapper_enabled? + wrapped_hash = _wrap_parameters request.request_parameters + wrapped_filtered_hash = _wrap_parameters request.filtered_parameters + + # This will make the wrapped hash accessible from controller and view + request.parameters.merge! wrapped_hash + request.request_parameters.merge! wrapped_hash + + # This will make the wrapped hash displayed in the log file + request.filtered_parameters.merge! wrapped_filtered_hash + end + super + end + + private + + # Returns the wrapper key which will use to stored wrapped parameters. + def _wrapper_key + _wrapper_options[:name] + end + + # Returns the list of enabled formats. + def _wrapper_formats + _wrapper_options[:format] + end + + # Returns the list of parameters which will be selected for wrapped. + def _wrap_parameters(parameters) + value = if only = _wrapper_options[:only] + parameters.slice(*only) + else + except = _wrapper_options[:except] || [] + parameters.except(*(except + EXCLUDE_PARAMETERS)) + end + + { _wrapper_key => value } + end + + # Checks if we should perform parameters wrapping. + def _wrapper_enabled? + ref = request.content_mime_type.try(:ref) + _wrapper_formats.include?(ref) && !request.request_parameters[_wrapper_key] + end + end +end diff --git a/actionpack/lib/action_controller/metal/responder.rb b/actionpack/lib/action_controller/metal/responder.rb index 59a3621f72..ebadb29ea7 100644 --- a/actionpack/lib/action_controller/metal/responder.rb +++ b/actionpack/lib/action_controller/metal/responder.rb @@ -68,7 +68,7 @@ module ActionController #:nodoc: # respond_with(@project, @task) # end # - # Giving an array of resources, you ensure that the responder will redirect to + # Giving several resources ensures that the responder will redirect to # <code>project_task_url</code> instead of <code>task_url</code>. # # Namespaced and singleton resources require a symbol to be given, as in @@ -77,6 +77,11 @@ module ActionController #:nodoc: # # respond_with(@project, :manager, @task) # + # Note that if you give an array, it will be treated as a collection, + # so the following is not equivalent: + # + # respond_with [@project, :manager, @task] + # # === Custom options # # <code>respond_with</code> also allow you to pass options that are forwarded diff --git a/actionpack/lib/action_controller/metal/streaming.rb b/actionpack/lib/action_controller/metal/streaming.rb index b9bd49f670..3892a12407 100644 --- a/actionpack/lib/action_controller/metal/streaming.rb +++ b/actionpack/lib/action_controller/metal/streaming.rb @@ -2,7 +2,209 @@ require 'active_support/core_ext/file/path' require 'rack/chunked' module ActionController #:nodoc: - # Methods for sending streaming templates back to the client. + # Allows views to be streamed back to the client as they are rendered. + # + # The default way Rails renders views is by first rendering the template + # and then the layout. The response is sent to the client after the whole + # template is rendered, all queries are made, and the layout is processed. + # + # Streaming inverts the rendering flow by rendering the layout first and + # streaming each part of the layout as they are processed. This allows the + # header of the HTML (which is usually in the layout) to be streamed back + # to client very quickly, allowing JavaScripts and stylesheets to be loaded + # earlier than usual. + # + # This approach was introduced in Rails 3.1 and is still improving. Several + # Rack middlewares may not work and you need to be careful when streaming. + # Those points are going to be addressed soon. + # + # In order to use streaming, you will need to use a Ruby version that + # supports fibers (fibers are supported since version 1.9.2 of the main + # Ruby implementation). + # + # == Examples + # + # Streaming can be added to a controller easily, all you need to do is + # call +stream+ in the controller class: + # + # class PostsController + # stream + # end + # + # The +stream+ method accepts the same options as +before_filter+ and friends: + # + # class PostsController + # stream :only => :index + # end + # + # You can also selectively turn on streaming for specific actions: + # + # class PostsController + # def index + # @posts = Post.scoped + # render :stream => true + # end + # end + # + # == When to use streaming + # + # Streaming may be considered to be overkill for lightweight actions like + # +new+ or +edit+. The real benefit of streaming is on expensive actions + # that, for example, do a lot of queries on the database. + # + # In such actions, you want to delay queries execution as much as you can. + # For example, imagine the following +dashboard+ action: + # + # def dashboard + # @posts = Post.all + # @pages = Page.all + # @articles = Article.all + # end + # + # Most of the queries here are happening in the controller. In order to benefit + # from streaming you would want to rewrite it as: + # + # def dashboard + # # Allow lazy execution of the queries + # @posts = Post.scoped + # @pages = Page.scoped + # @articles = Article.scoped + # render :stream => true + # end + # + # == Communication between layout and template + # + # When streaming, rendering happens top-down instead of inside-out. + # Rails starts with the layout, and the template is rendered later, + # when its +yield+ is reached. + # + # This means that, if your application currently relies on instance + # variables set in the template to be used in the layout, they won't + # work once you move to streaming. The proper way to communicate + # between layout and template, regardless of whether you use streaming + # or not, is by using +content_for+, +provide+ and +yield+. + # + # Take a simple example where the layout expects the template to tell + # which title to use: + # + # <html> + # <head><title><%= yield :title %></title></head> + # <body><%= yield %></body> + # </html> + # + # You would use +content_for+ in your template to specify the title: + # + # <%= content_for :title, "Main" %> + # Hello + # + # And the final result would be: + # + # <html> + # <head><title>Main</title></head> + # <body>Hello</body> + # </html> + # + # However, if +content_for+ is called several times, the final result + # would have all calls concatenated. For instance, if we have the following + # template: + # + # <%= content_for :title, "Main" %> + # Hello + # <%= content_for :title, " page" %> + # + # The final result would be: + # + # <html> + # <head><title>Main page</title></head> + # <body>Hello</body> + # </html> + # + # This means that, if you have <code>yield :title</code> in your layout + # and you want to use streaming, you would have to render the whole template + # (and eventually trigger all queries) before streaming the title and all + # assets, which kills the purpose of streaming. For this reason Rails 3.1 + # introduces a new helper called +provide+ that does the same as +content_for+ + # but tells the layout to stop searching for other entries and continue rendering. + # + # For instance, the template above using +provide+ would be: + # + # <%= provide :title, "Main" %> + # Hello + # <%= content_for :title, " page" %> + # + # Giving: + # + # <html> + # <head><title>Main</title></head> + # <body>Hello</body> + # </html> + # + # That said, when streaming, you need to properly check your templates + # and choose when to use +provide+ and +content_for+. + # + # == Headers, cookies, session and flash + # + # When streaming, the HTTP headers are sent to the client right before + # it renders the first line. This means that, modifying headers, cookies, + # session or flash after the template starts rendering will not propagate + # to the client. + # + # If you try to modify cookies, session or flash, an +ActionDispatch::ClosedError+ + # will be raised, showing those objects are closed for modification. + # + # == Middlewares + # + # Middlewares that need to manipulate the body won't work with streaming. + # You should disable those middlewares whenever streaming in development + # or production. For instance, +Rack::Bug+ won't work when streaming as it + # needs to inject contents in the HTML body. + # + # Also +Rack::Cache+ won't work with streaming as it does not support + # streaming bodies yet. Whenever streaming Cache-Control is automatically + # set to "no-cache". + # + # == Errors + # + # When it comes to streaming, exceptions get a bit more complicated. This + # happens because part of the template was already rendered and streamed to + # the client, making it impossible to render a whole exception page. + # + # Currently, when an exception happens in development or production, Rails + # will automatically stream to the client: + # + # "><script type="text/javascript">window.location = "/500.html"</script></html> + # + # The first two characters (">) are required in case the exception happens + # while rendering attributes for a given tag. You can check the real cause + # for the exception in your logger. + # + # == Web server support + # + # Not all web servers support streaming out-of-the-box. You need to check + # the instructions for each of them. + # + # ==== Unicorn + # + # Unicorn supports streaming but it needs to be configured. For this, you + # need to create a config file as follow: + # + # # unicorn.config.rb + # listen 3000, :tcp_nopush => false + # + # And use it on initialization: + # + # unicorn_rails --config-file unicorn.config.rb + # + # You may also want to configure other parameters like <tt>:tcp_nodelay</tt>. + # Please check its documentation for more information: http://unicorn.bogomips.org/Unicorn/Configurator.html#method-i-listen + # + # If you are using Unicorn with Nginx, you may need to tweak Nginx. + # Streaming should work out of the box on Rainbows. + # + # ==== Passenger + # + # To be described. + # module Streaming extend ActiveSupport::Concern @@ -51,7 +253,7 @@ module ActionController #:nodoc: # Call render_to_body if we are streaming instead of usual +render+. def _render_template(options) #:nodoc: if options.delete(:stream) - Rack::Chunked::Body.new view_context.render_body(options) + Rack::Chunked::Body.new view_renderer.render_body(view_context, options) else super end diff --git a/actionpack/lib/action_controller/vendor/html-scanner/html/selector.rb b/actionpack/lib/action_controller/vendor/html-scanner/html/selector.rb index 0fe2e6d1a6..1eadfc0390 100644 --- a/actionpack/lib/action_controller/vendor/html-scanner/html/selector.rb +++ b/actionpack/lib/action_controller/vendor/html-scanner/html/selector.rb @@ -128,6 +128,8 @@ module HTML # (no parent element). # * <tt>:empty</tt> -- Match the element only if it has no child elements, # and no text content. + # * <tt>:content(string)</tt> -- Match the element only if it has <tt>string</tt> + # as its text content (ignoring leading and trailing whitespace). # * <tt>:only-child</tt> -- Match the element if it is the only child (element) # of its parent element. # * <tt>:only-of-type</tt> -- Match the element if it is the only child (element) |