diff options
Diffstat (limited to 'actionpack/lib/action_controller/metal')
22 files changed, 610 insertions, 406 deletions
diff --git a/actionpack/lib/action_controller/metal/compatibility.rb b/actionpack/lib/action_controller/metal/compatibility.rb deleted file mode 100644 index 05dca445a4..0000000000 --- a/actionpack/lib/action_controller/metal/compatibility.rb +++ /dev/null @@ -1,58 +0,0 @@ -module ActionController - module Compatibility - extend ActiveSupport::Concern - - class ::ActionController::ActionControllerError < StandardError #:nodoc: - end - - # Temporary hax - included do - ::ActionController::UnknownAction = ::AbstractController::ActionNotFound - ::ActionController::DoubleRenderError = ::AbstractController::DoubleRenderError - - # ROUTES TODO: This should be handled by a middleware and route generation - # should be able to handle SCRIPT_NAME - self.config.relative_url_root = ENV['RAILS_RELATIVE_URL_ROOT'] - - class << self - delegate :default_charset=, :to => "ActionDispatch::Response" - end - - 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"] - end - end - - # For old tests - def initialize_template_class(*) end - def assign_shortcuts(*) end - - def _normalize_options(options) - options[:text] = nil if options.delete(:nothing) == true - options[:text] = " " if options.key?(:text) && options[:text].nil? - super - end - - def render_to_body(options) - options[:template].sub!(/^\//, '') if options.key?(:template) - super || " " - end - - def _handle_method_missing - method_missing(@_action_name.to_sym) - end - - def method_for_action(action_name) - super || (respond_to?(:method_missing) && "_handle_method_missing") - end - - def performed? - response_body - end - end -end diff --git a/actionpack/lib/action_controller/metal/conditional_get.rb b/actionpack/lib/action_controller/metal/conditional_get.rb index a5e37172c9..2193dde667 100644 --- a/actionpack/lib/action_controller/metal/conditional_get.rb +++ b/actionpack/lib/action_controller/metal/conditional_get.rb @@ -23,8 +23,27 @@ module ActionController # This will render the show template if the request isn't sending a matching etag or # If-Modified-Since header and just a <tt>304 Not Modified</tt> response if there's a match. # - def fresh_when(options) - options.assert_valid_keys(:etag, :last_modified, :public) + # You can also just pass a record where last_modified will be set by calling updated_at and the etag by passing the object itself. Example: + # + # def show + # @article = Article.find(params[:id]) + # fresh_when(@article) + # end + # + # When passing a record, you can still set whether the public header: + # + # def show + # @article = Article.find(params[:id]) + # fresh_when(@article, :public => true) + # end + def fresh_when(record_or_options, additional_options = {}) + if record_or_options.is_a? Hash + options = record_or_options + options.assert_valid_keys(:etag, :last_modified, :public) + else + record = record_or_options + options = { :etag => record, :last_modified => record.try(:updated_at) }.merge(additional_options) + end response.etag = options[:etag] if options[:etag] response.last_modified = options[:last_modified] if options[:last_modified] @@ -55,26 +74,58 @@ module ActionController # end # end # end - def stale?(options) - fresh_when(options) + # + # You can also just pass a record where last_modified will be set by calling updated_at and the etag by passing the object itself. Example: + # + # def show + # @article = Article.find(params[:id]) + # + # if stale?(@article) + # @statistics = @article.really_expensive_call + # respond_to do |format| + # # all the supported formats + # end + # end + # end + # + # When passing a record, you can still set whether the public header: + # + # def show + # @article = Article.find(params[:id]) + # + # if stale?(@article, :public => true) + # @statistics = @article.really_expensive_call + # respond_to do |format| + # # all the supported formats + # end + # end + # end + def stale?(record_or_options, additional_options = {}) + fresh_when(record_or_options, additional_options) !request.fresh?(response) end # Sets a HTTP 1.1 Cache-Control header. Defaults to issuing a <tt>private</tt> instruction, so that # intermediate caches must not cache the response. # - # Examples: # expires_in 20.minutes # expires_in 3.hours, :public => true - # expires_in 3.hours, 'max-stale' => 5.hours, :public => true + # expires_in 3.hours, :public => true, :must_revalidate => true # # This method will overwrite an existing Cache-Control header. # See http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html for more possibilities. + # + # The method will also ensure a HTTP Date header for client compatibility. def expires_in(seconds, options = {}) #:doc: - response.cache_control.merge!(:max_age => seconds, :public => options.delete(:public)) + response.cache_control.merge!( + :max_age => seconds, + :public => options.delete(:public), + :must_revalidate => options.delete(:must_revalidate) + ) options.delete(:private) response.cache_control[:extras] = options.map {|k,v| "#{k}=#{v}"} + response.date = Time.now unless response.date? end # Sets a HTTP 1.1 Cache-Control header of <tt>no-cache</tt> so no caching should occur by the browser or diff --git a/actionpack/lib/action_controller/metal/data_streaming.rb b/actionpack/lib/action_controller/metal/data_streaming.rb index 997bc6e958..379ff97048 100644 --- a/actionpack/lib/action_controller/metal/data_streaming.rb +++ b/actionpack/lib/action_controller/metal/data_streaming.rb @@ -1,4 +1,4 @@ -require 'active_support/core_ext/file/path' +require 'action_controller/metal/exceptions' module ActionController #:nodoc: # Methods for sending arbitrary data and for streaming files to the browser, @@ -8,15 +8,13 @@ module ActionController #:nodoc: include ActionController::Rendering - DEFAULT_SEND_FILE_OPTIONS = { - :type => 'application/octet-stream'.freeze, - :disposition => 'attachment'.freeze, - }.freeze + DEFAULT_SEND_FILE_TYPE = 'application/octet-stream'.freeze #:nodoc: + DEFAULT_SEND_FILE_DISPOSITION = 'attachment'.freeze #:nodoc: protected # Sends the file. This uses a server-appropriate method (such as X-Sendfile) # via the Rack::Sendfile middleware. The header to use is set via - # config.action_dispatch.x_sendfile_header, and defaults to "X-Sendfile". + # +config.action_dispatch.x_sendfile_header+. # Your server can also configure this for you by setting the X-Sendfile-Type header. # # Be careful to sanitize the path parameter if it is coming from a web @@ -26,18 +24,21 @@ module ActionController #:nodoc: # Options: # * <tt>:filename</tt> - suggests a filename for the browser to use. # Defaults to <tt>File.basename(path)</tt>. - # * <tt>:type</tt> - specifies an HTTP content type. Defaults to 'application/octet-stream'. You can specify - # either a string or a symbol for a registered type register with <tt>Mime::Type.register</tt>, for example :json + # * <tt>:type</tt> - specifies an HTTP content type. + # You can specify either a string or a symbol for a registered type register with + # <tt>Mime::Type.register</tt>, for example :json + # If omitted, type will be guessed from the file extension specified in <tt>:filename</tt>. + # If no content type is registered for the extension, default type 'application/octet-stream' will be used. # * <tt>:disposition</tt> - specifies whether the file will be shown inline or downloaded. # Valid values are 'inline' and 'attachment' (default). - # * <tt>:status</tt> - specifies the status code to send with the response. Defaults to '200 OK'. + # * <tt>:status</tt> - specifies the status code to send with the response. Defaults to 200. # * <tt>:url_based_filename</tt> - set to +true+ if you want the browser guess the filename from # the URL, which is necessary for i18n filenames on certain browsers # (setting <tt>:filename</tt> overrides this option). # # The default Content-Type and Content-Disposition headers are # set to download arbitrary binary files in as many browsers as - # possible. IE versions 4, 5, 5.5, and 6 are all known to have + # possible. IE versions 4, 5, 5.5, and 6 are all known to have # a variety of quirks (especially when downloading over SSL). # # Simple download: @@ -58,8 +59,8 @@ module ActionController #:nodoc: # # Also be aware that the document may be cached by proxies and browsers. # The Pragma and Cache-Control headers declare how the file may be cached - # by intermediaries. They default to require clients to validate with - # the server before releasing cached responses. See + # by intermediaries. They default to require clients to validate with + # the server before releasing cached responses. See # http://www.mnot.net/cache_docs/ for an overview of web caching and # http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9 # for the Cache-Control header spec. @@ -71,7 +72,27 @@ module ActionController #:nodoc: self.status = options[:status] || 200 self.content_type = options[:content_type] if options.key?(:content_type) - self.response_body = File.open(path, "rb") + self.response_body = FileBody.new(path) + end + + # Avoid having to pass an open file handle as the response body. + # Rack::Sendfile will usually intercepts the response and just uses + # the path directly, so no reason to open the file. + class FileBody #:nodoc: + attr_reader :to_path + + def initialize(path) + @to_path = path + end + + # Stream the file's contents if Rack::Sendfile isn't present. + def each + File.open(to_path, 'rb') do |file| + while chunk = file.read(16384) + yield chunk + end + end + end end # Sends the given binary data to the browser. This method is similar to @@ -84,9 +105,11 @@ module ActionController #:nodoc: # * <tt>:filename</tt> - suggests a filename for the browser to use. # * <tt>:type</tt> - specifies an HTTP content type. Defaults to 'application/octet-stream'. You can specify # either a string or a symbol for a registered type register with <tt>Mime::Type.register</tt>, for example :json + # If omitted, type will be guessed from the file extension specified in <tt>:filename</tt>. + # If no content type is registered for the extension, default type 'application/octet-stream' will be used. # * <tt>:disposition</tt> - specifies whether the file will be shown inline or downloaded. # Valid values are 'inline' and 'attachment' (default). - # * <tt>:status</tt> - specifies the status code to send with the response. Defaults to '200 OK'. + # * <tt>:status</tt> - specifies the status code to send with the response. Defaults to 200. # # Generic data download: # @@ -102,34 +125,36 @@ module ActionController #:nodoc: # # See +send_file+ for more information on HTTP Content-* headers and caching. def send_data(data, options = {}) #:doc: - send_file_headers! options.dup + send_file_headers! options render options.slice(:status, :content_type).merge(:text => data) end private def send_file_headers!(options) - options.update(DEFAULT_SEND_FILE_OPTIONS.merge(options)) - [:type, :disposition].each do |arg| - raise ArgumentError, ":#{arg} option required" if options[arg].nil? - end + type_provided = options.has_key?(:type) - disposition = options[:disposition] - disposition += %(; filename="#{options[:filename]}") if options[:filename] - - content_type = options[:type] + content_type = options.fetch(:type, DEFAULT_SEND_FILE_TYPE) + raise ArgumentError, ":type option required" if content_type.nil? if content_type.is_a?(Symbol) extension = Mime[content_type] raise ArgumentError, "Unknown MIME type #{options[:type]}" unless extension self.content_type = extension else + if !type_provided && options[:filename] + # If type wasn't provided, try guessing from file extension. + content_type = Mime::Type.lookup_by_extension(File.extname(options[:filename]).downcase.delete('.')) || content_type + end self.content_type = content_type end - headers.merge!( - 'Content-Disposition' => disposition, - 'Content-Transfer-Encoding' => 'binary' - ) + disposition = options.fetch(:disposition, DEFAULT_SEND_FILE_DISPOSITION) + unless disposition.nil? + disposition += %(; filename="#{options[:filename]}") if options[:filename] + headers['Content-Disposition'] = disposition + end + + headers['Content-Transfer-Encoding'] = 'binary' response.sending_file = true diff --git a/actionpack/lib/action_controller/metal/exceptions.rb b/actionpack/lib/action_controller/metal/exceptions.rb index 07024d0a9a..8fd8f4797c 100644 --- a/actionpack/lib/action_controller/metal/exceptions.rb +++ b/actionpack/lib/action_controller/metal/exceptions.rb @@ -2,6 +2,9 @@ module ActionController class ActionControllerError < StandardError #:nodoc: end + class BadRequest < ActionControllerError #:nodoc: + end + class RenderError < ActionControllerError #:nodoc: end @@ -14,8 +17,6 @@ module ActionController end class MethodNotAllowed < ActionControllerError #:nodoc: - attr_reader :allowed_methods - def initialize(*allowed_methods) super("Only #{allowed_methods.to_sentence(:locale => :en)} requests are allowed.") end @@ -30,9 +31,6 @@ module ActionController class MissingFile < ActionControllerError #:nodoc: end - class RenderError < ActionControllerError #:nodoc: - end - class SessionOverflowError < ActionControllerError #:nodoc: DEFAULT_MESSAGE = 'Your session data is larger than the data column in which it is to be stored. You must increase the size of your data column if you intend to store large data.' @@ -43,4 +41,7 @@ module ActionController class UnknownHttpMethod < ActionControllerError #:nodoc: end -end
\ No newline at end of file + + class UnknownFormat < ActionControllerError #:nodoc: + end +end diff --git a/actionpack/lib/action_controller/metal/force_ssl.rb b/actionpack/lib/action_controller/metal/force_ssl.rb index eb8ed7dfbd..ac12cbb625 100644 --- a/actionpack/lib/action_controller/metal/force_ssl.rb +++ b/actionpack/lib/action_controller/metal/force_ssl.rb @@ -1,12 +1,12 @@ module ActionController - # This module provides a method which will redirects browser to use HTTPS + # This module provides a method which will redirect browser to use HTTPS # protocol. This will ensure that user's sensitive information will be # transferred safely over the internet. You _should_ always force browser # to use HTTPS when you're transferring sensitive information such as # user authentication, account information, or credit card information. # - # Note that if you really concern about your application safety, you might - # consider using +config.force_ssl+ in your configuration config file instead. + # Note that if you are really concerned about your application security, + # you might consider using +config.force_ssl+ in your config file instead. # That will ensure all the data transferred via HTTPS protocol and prevent # user from getting session hijacked when accessing the site under unsecured # HTTP protocol. @@ -18,18 +18,37 @@ module ActionController # Force the request to this particular controller or specified actions to be # under HTTPS protocol. # - # Note that this method will not be effective on development environment. + # If you need to disable this for any reason (e.g. development) then you can use + # an +:if+ or +:unless+ condition. + # + # class AccountsController < ApplicationController + # force_ssl :if => :ssl_configured? + # + # def ssl_configured? + # !Rails.env.development? + # end + # end # # ==== Options + # * <tt>host</tt> - Redirect to a different host name # * <tt>only</tt> - The callback should be run only for this action # * <tt>except<tt> - The callback should be run for all actions except this action + # * <tt>if</tt> - A symbol naming an instance method or a proc; the callback + # will be called only when it returns a true value. + # * <tt>unless</tt> - A symbol naming an instance method or a proc; the callback + # will be called only when it returns a false value. def force_ssl(options = {}) + host = options.delete(:host) before_filter(options) do - if !request.ssl? && !Rails.env.development? - redirect_to :protocol => 'https://', :status => :moved_permanently + unless request.ssl? + redirect_options = {:protocol => 'https://', :status => :moved_permanently} + redirect_options.merge!(:host => host) if host + redirect_options.merge!(:params => request.query_parameters) + flash.keep if respond_to?(:flash) + redirect_to redirect_options end end end end end -end
\ No newline at end of file +end diff --git a/actionpack/lib/action_controller/metal/head.rb b/actionpack/lib/action_controller/metal/head.rb index 8abcad55a2..2fcd933d32 100644 --- a/actionpack/lib/action_controller/metal/head.rb +++ b/actionpack/lib/action_controller/metal/head.rb @@ -9,6 +9,8 @@ module ActionController # # head :created, :location => person_path(@person) # + # head :created, :location => @person + # # It can also be used to return exceptional conditions: # # return head(:method_not_allowed) unless request.post? @@ -18,6 +20,7 @@ module ActionController options, status = status, nil if status.is_a?(Hash) status ||= options.delete(:status) || :ok location = options.delete(:location) + content_type = options.delete(:content_type) options.each do |key, value| headers[key.to_s.dasherize.split('-').each { |v| v[0] = v[0].chr.upcase }.join('-')] = value.to_s @@ -25,8 +28,28 @@ module ActionController self.status = status self.location = url_for(location) if location - self.content_type = Mime[formats.first] if formats + + if include_content_headers?(self.status) + self.content_type = content_type || (Mime[formats.first] if formats) + else + headers.delete('Content-Type') + headers.delete('Content-Length') + end + self.response_body = " " end + + private + # :nodoc: + def include_content_headers?(status) + case status + when 100..199 + false + when 204, 205, 304 + false + else + true + end + end end end diff --git a/actionpack/lib/action_controller/metal/helpers.rb b/actionpack/lib/action_controller/metal/helpers.rb index 75757db564..86d061e3b7 100644 --- a/actionpack/lib/action_controller/metal/helpers.rb +++ b/actionpack/lib/action_controller/metal/helpers.rb @@ -1,4 +1,3 @@ -require 'active_support/core_ext/array/wrap' require 'active_support/core_ext/class/attribute' module ActionController @@ -7,14 +6,16 @@ module ActionController # by default. # # In addition to using the standard template helpers provided, creating custom helpers to - # extract complicated logic or reusable functionality is strongly encouraged. By default, the controller will - # include a helper whose name matches that of the controller, e.g., <tt>MyController</tt> will automatically - # include <tt>MyHelper</tt>. + # extract complicated logic or reusable functionality is strongly encouraged. By default, each controller + # will include all helpers. + # + # In previous versions of \Rails the controller will include a helper whose + # name matches that of the controller, e.g., <tt>MyController</tt> will automatically + # include <tt>MyHelper</tt>. To return old behavior set +config.action_controller.include_all_helpers+ to +false+. # # Additional helpers can be specified using the +helper+ class method in ActionController::Base or any # controller which inherits from it. # - # ==== Examples # The +to_s+ method from the \Time class can be wrapped in a helper method to display a custom message if # a \Time object is blank: # @@ -29,7 +30,7 @@ module ActionController # class EventsController < ActionController::Base # helper FormattedTimeHelper # def index - # @events = Event.find(:all) + # @events = Event.all # end # end # @@ -50,10 +51,11 @@ module ActionController module Helpers extend ActiveSupport::Concern + class << self; attr_accessor :helpers_path; end include AbstractController::Helpers included do - config_accessor :helpers_path, :include_all_helpers + class_attribute :helpers_path, :include_all_helpers self.helpers_path ||= [] self.include_all_helpers = true end @@ -91,7 +93,7 @@ module ActionController def all_helpers_from_path(path) helpers = [] - Array.wrap(path).each do |_path| + Array(path).each do |_path| extract = /^#{Regexp.quote(_path.to_s)}\/?(.*)_helper.rb$/ helpers += Dir["#{_path}/**/*_helper.rb"].map { |file| file.sub(extract, '\1') } end diff --git a/actionpack/lib/action_controller/metal/http_authentication.rb b/actionpack/lib/action_controller/metal/http_authentication.rb index 1d6df89007..57bb0e2a32 100644 --- a/actionpack/lib/action_controller/metal/http_authentication.rb +++ b/actionpack/lib/action_controller/metal/http_authentication.rb @@ -1,9 +1,10 @@ -require 'active_support/base64' +require 'base64' require 'active_support/core_ext/object/blank' module ActionController + # Makes it dead easy to do HTTP Basic, Digest and Token authentication. module HttpAuthentication - # Makes it dead easy to do HTTP \Basic and \Digest authentication. + # Makes it dead easy to do HTTP \Basic authentication. # # === Simple \Basic example # @@ -60,53 +61,12 @@ module ActionController # # assert_equal 200, status # end - # - # === Simple \Digest example - # - # require 'digest/md5' - # class PostsController < ApplicationController - # REALM = "SuperSecret" - # USERS = {"dhh" => "secret", #plain text password - # "dap" => Digest::MD5.hexdigest(["dap",REALM,"secret"].join(":")) #ha1 digest password - # - # before_filter :authenticate, :except => [:index] - # - # def index - # render :text => "Everyone can see me!" - # end - # - # def edit - # render :text => "I'm only accessible if you know the password" - # end - # - # private - # def authenticate - # authenticate_or_request_with_http_digest(REALM) do |username| - # USERS[username] - # end - # end - # end - # - # === Notes - # - # The +authenticate_or_request_with_http_digest+ block must return the user's password - # or the ha1 digest hash so the framework can appropriately hash to check the user's - # credentials. Returning +nil+ will cause authentication to fail. - # - # Storing the ha1 hash: MD5(username:realm:password), is better than storing a plain password. If - # the password file or database is compromised, the attacker would be able to use the ha1 hash to - # authenticate as the user at this +realm+, but would not have the user's password to try using at - # other sites. - # - # In rare instances, web servers or front proxies strip authorization headers before - # they reach your application. You can debug this situation by logging all environment - # variables, and check for HTTP_AUTHORIZATION, amongst others. module Basic extend self module ControllerMethods extend ActiveSupport::Concern - + module ClassMethods def http_basic_authenticate_with(options = {}) before_filter(options.except(:name, :password, :realm)) do @@ -116,7 +76,7 @@ module ActionController end end end - + def authenticate_or_request_with_http_basic(realm = "Application", &login_procedure) authenticate_with_http_basic(&login_procedure) || request_http_basic_authentication(realm) end @@ -141,11 +101,11 @@ module ActionController end def decode_credentials(request) - ActiveSupport::Base64.decode64(request.authorization.split(' ', 2).last || '') + ::Base64.decode64(request.authorization.split(' ', 2).last || '') end def encode_credentials(user_name, password) - "Basic #{ActiveSupport::Base64.encode64("#{user_name}:#{password}")}" + "Basic #{::Base64.strict_encode64("#{user_name}:#{password}")}" end def authentication_request(controller, realm) @@ -155,6 +115,48 @@ module ActionController end end + # Makes it dead easy to do HTTP \Digest authentication. + # + # === Simple \Digest example + # + # require 'digest/md5' + # class PostsController < ApplicationController + # REALM = "SuperSecret" + # USERS = {"dhh" => "secret", #plain text password + # "dap" => Digest::MD5.hexdigest(["dap",REALM,"secret"].join(":"))} #ha1 digest password + # + # before_filter :authenticate, :except => [:index] + # + # def index + # render :text => "Everyone can see me!" + # end + # + # def edit + # render :text => "I'm only accessible if you know the password" + # end + # + # private + # def authenticate + # authenticate_or_request_with_http_digest(REALM) do |username| + # USERS[username] + # end + # end + # end + # + # === Notes + # + # The +authenticate_or_request_with_http_digest+ block must return the user's password + # or the ha1 digest hash so the framework can appropriately hash to check the user's + # credentials. Returning +nil+ will cause authentication to fail. + # + # Storing the ha1 hash: MD5(username:realm:password), is better than storing a plain password. If + # the password file or database is compromised, the attacker would be able to use the ha1 hash to + # authenticate as the user at this +realm+, but would not have the user's password to try using at + # other sites. + # + # In rare instances, web servers or front proxies strip authorization headers before + # they reach your application. You can debug this situation by logging all environment + # variables, and check for HTTP_AUTHORIZATION, amongst others. module Digest extend self @@ -192,12 +194,15 @@ module ActionController return false unless password method = request.env['rack.methodoverride.original_method'] || request.env['REQUEST_METHOD'] - uri = credentials[:uri][0,1] == '/' ? request.fullpath : request.url + uri = credentials[:uri][0,1] == '/' ? request.original_fullpath : request.original_url - [true, false].any? do |password_is_ha1| - expected = expected_response(method, uri, credentials, password, password_is_ha1) - expected == credentials[:response] - end + [true, false].any? do |trailing_question_mark| + [true, false].any? do |password_is_ha1| + _uri = trailing_question_mark ? uri + "?" : uri + expected = expected_response(method, _uri, credentials, password, password_is_ha1) + expected == credentials[:response] + end + end end end @@ -226,7 +231,7 @@ module ActionController def decode_credentials(header) Hash[header.to_s.gsub(/^Digest\s+/,'').split(',').map do |pair| key, value = pair.split('=', 2) - [key.strip.to_sym, value.to_s.gsub(/^"|"$/,'').gsub(/'/, '')] + [key.strip.to_sym, value.to_s.gsub(/^"|"$/,'').delete('\'')] end] end @@ -260,7 +265,7 @@ module ActionController # The quality of the implementation depends on a good choice. # A nonce might, for example, be constructed as the base 64 encoding of # - # => time-stamp H(time-stamp ":" ETag ":" private-key) + # time-stamp H(time-stamp ":" ETag ":" private-key) # # where time-stamp is a server-generated time or other non-repeating value, # ETag is the value of the HTTP ETag header associated with the requested entity, @@ -276,7 +281,7 @@ module ActionController # # An implementation might choose not to accept a previously used nonce or a previously used digest, in order to # protect against a replay attack. Or, an implementation might choose to use one-time nonces or digests for - # POST or PUT requests and a time-stamp for GET requests. For more details on the issues involved see Section 4 + # POST, PUT, or PATCH requests and a time-stamp for GET requests. For more details on the issues involved see Section 4 # of this document. # # The nonce is opaque to the client. Composed of Time, and hash of Time with secret @@ -286,16 +291,16 @@ module ActionController t = time.to_i hashed = [t, secret_key] digest = ::Digest::MD5.hexdigest(hashed.join(":")) - ActiveSupport::Base64.encode64("#{t}:#{digest}").gsub("\n", '') + ::Base64.strict_encode64("#{t}:#{digest}") end # Might want a shorter timeout depending on whether the request - # is a PUT or POST, and if client is browser or web service. + # is a PATCH, PUT, or POST, and if client is browser or web service. # Can be much shorter if the Stale directive is implemented. This would # allow a user to use new nonce without prompting user again for their # username and password. def validate_nonce(secret_key, request, value, seconds_to_timeout=5*60) - t = ActiveSupport::Base64.decode64(value).split(":").first.to_i + t = ::Base64.decode64(value).split(":").first.to_i nonce(secret_key, t) == value && (t - Time.now.to_i).abs <= seconds_to_timeout end @@ -400,7 +405,7 @@ module ActionController # the present token and options. # # controller - ActionController::Base instance for the current request. - # login_procedure - Proc to call if a token is present. The Proc should + # login_procedure - Proc to call if a token is present. The Proc should # take 2 arguments: # authenticate(controller) { |token, options| ... } # @@ -413,7 +418,7 @@ module ActionController end end - # Parses the token and options out of the token authorization header. If + # Parses the token and options out of the token authorization header. If # the header looks like this: # Authorization: Token token="abc", nonce="def" # Then the returned token is "abc", and the options is {:nonce => "def"} @@ -423,7 +428,7 @@ module ActionController # Returns an Array of [String, Hash] if a token is present. # Returns nil if no token is found. def token_and_options(request) - if header = request.authorization.to_s[/^Token (.*)/] + if request.authorization.to_s[/^Token (.*)/] values = Hash[$1.split(',').map do |value| value.strip! # remove any spaces between commas and values key, value = value.split(/\=\"?/) # split key=value pairs diff --git a/actionpack/lib/action_controller/metal/implicit_render.rb b/actionpack/lib/action_controller/metal/implicit_render.rb index e8e465d3ba..ae04b53825 100644 --- a/actionpack/lib/action_controller/metal/implicit_render.rb +++ b/actionpack/lib/action_controller/metal/implicit_render.rb @@ -2,7 +2,7 @@ module ActionController module ImplicitRender def send_action(method, *args) ret = super - default_render unless response_body + default_render unless performed? ret end diff --git a/actionpack/lib/action_controller/metal/instrumentation.rb b/actionpack/lib/action_controller/metal/instrumentation.rb index 16cbbce2fb..640ebf5f00 100644 --- a/actionpack/lib/action_controller/metal/instrumentation.rb +++ b/actionpack/lib/action_controller/metal/instrumentation.rb @@ -19,7 +19,7 @@ module ActionController :controller => self.class.name, :action => self.action_name, :params => request.filtered_parameters, - :format => request.format.ref, + :format => request.format.try(:ref), :method => request.method, :path => (request.fullpath rescue "unknown") } @@ -58,13 +58,18 @@ module ActionController def redirect_to(*args) ActiveSupport::Notifications.instrument("redirect_to.action_controller") do |payload| result = super - payload[:status] = self.status - payload[:location] = self.location + payload[:status] = response.status + payload[:location] = response.location result end end - protected + private + + # A hook invoked everytime a before callback is halted. + def halted_callback_hook(filter) + ActiveSupport::Notifications.instrument("halted_callback.action_controller", :filter => filter) + end # A hook which allows you to clean up any time taken into account in # views wrongly, like database querying time. @@ -97,4 +102,4 @@ module ActionController end end end -end
\ No newline at end of file +end diff --git a/actionpack/lib/action_controller/metal/mime_responds.rb b/actionpack/lib/action_controller/metal/mime_responds.rb index f10287afb4..0b800c3c62 100644 --- a/actionpack/lib/action_controller/metal/mime_responds.rb +++ b/actionpack/lib/action_controller/metal/mime_responds.rb @@ -6,8 +6,6 @@ module ActionController #:nodoc: module MimeResponds extend ActiveSupport::Concern - include ActionController::ImplicitRender - included do class_attribute :responder, :mimes_for_respond_to self.responder = ActionController::Responder @@ -18,8 +16,6 @@ module ActionController #:nodoc: # Defines mime types that are rendered by default when invoking # <tt>respond_with</tt>. # - # Examples: - # # respond_to :html, :xml, :json # # Specifies that all actions in the controller respond to requests @@ -42,8 +38,8 @@ module ActionController #:nodoc: def respond_to(*mimes) options = mimes.extract_options! - only_actions = Array(options.delete(:only)) - except_actions = Array(options.delete(:except)) + only_actions = Array(options.delete(:only)).map(&:to_s) + except_actions = Array(options.delete(:except)).map(&:to_s) new = mimes_for_respond_to.dup mimes.each do |mime| @@ -58,7 +54,7 @@ module ActionController #:nodoc: # Clear all mime types in <tt>respond_to</tt>. # def clear_respond_to - self.mimes_for_respond_to = ActiveSupport::OrderedHash.new.freeze + self.mimes_for_respond_to = Hash.new.freeze end end @@ -76,7 +72,7 @@ module ActionController #:nodoc: # # respond_to do |format| # format.html - # format.xml { render :xml => @people.to_xml } + # format.xml { render :xml => @people } # end # end # @@ -182,34 +178,120 @@ module ActionController #:nodoc: # # def index # @people = Person.all - # respond_with(@person) + # respond_with(@people) # end # end # # Be sure to check respond_with and respond_to documentation for more examples. - # def respond_to(*mimes, &block) raise ArgumentError, "respond_to takes either types or a block, never both" if mimes.any? && block_given? - if response = retrieve_response_from_mimes(mimes, &block) - response.call(nil) + if collector = retrieve_collector_from_mimes(mimes, &block) + response = collector.response + response ? response.call : render({}) end end - # respond_with wraps a resource around a responder for default representation. - # First it invokes respond_to, if a response cannot be found (ie. no block - # for the request was given and template was not available), it instantiates - # an ActionController::Responder with the controller and resource. + # For a given controller action, respond_with generates an appropriate + # response based on the mime-type requested by the client. # - # ==== Example + # If the method is called with just a resource, as in this example - # - # def index - # @users = User.all - # respond_with(@users) + # class PeopleController < ApplicationController + # respond_to :html, :xml, :json + # + # def index + # @people = Person.all + # respond_with @people + # end + # end + # + # then the mime-type of the response is typically selected based on the + # request's Accept header and the set of available formats declared + # by previous calls to the controller's class method +respond_to+. Alternatively + # the mime-type can be selected by explicitly setting <tt>request.format</tt> in + # the controller. + # + # If an acceptable format is not identified, the application returns a + # '406 - not acceptable' status. Otherwise, the default response is to render + # a template named after the current action and the selected format, + # e.g. <tt>index.html.erb</tt>. If no template is available, the behavior + # depends on the selected format: + # + # * for an html response - if the request method is +get+, an exception + # is raised but for other requests such as +post+ the response + # depends on whether the resource has any validation errors (i.e. + # assuming that an attempt has been made to save the resource, + # e.g. by a +create+ action) - + # 1. If there are no errors, i.e. the resource + # was saved successfully, the response +redirect+'s to the resource + # i.e. its +show+ action. + # 2. If there are validation errors, the response + # renders a default action, which is <tt>:new</tt> for a + # +post+ request or <tt>:edit</tt> for +put+. + # Thus an example like this - + # + # respond_to :html, :xml + # + # def create + # @user = User.new(params[:user]) + # flash[:notice] = 'User was successfully created.' if @user.save + # respond_with(@user) + # end + # + # is equivalent, in the absence of <tt>create.html.erb</tt>, to - + # + # def create + # @user = User.new(params[:user]) + # respond_to do |format| + # if @user.save + # flash[:notice] = 'User was successfully created.' + # format.html { redirect_to(@user) } + # format.xml { render :xml => @user } + # else + # format.html { render :action => "new" } + # format.xml { render :xml => @user } + # end + # end + # end + # + # * for a javascript request - if the template isn't found, an exception is + # raised. + # * for other requests - i.e. data formats such as xml, json, csv etc, if + # the resource passed to +respond_with+ responds to <code>to_<format></code>, + # the method attempts to render the resource in the requested format + # directly, e.g. for an xml request, the response is equivalent to calling + # <code>render :xml => resource</code>. + # + # === Nested resources + # + # As outlined above, the +resources+ argument passed to +respond_with+ + # can play two roles. It can be used to generate the redirect url + # for successful html requests (e.g. for +create+ actions when + # no template exists), while for formats other than html and javascript + # it is the object that gets rendered, by being converted directly to the + # required format (again assuming no template exists). + # + # For redirecting successful html requests, +respond_with+ also supports + # the use of nested resources, which are supplied in the same way as + # in <code>form_for</code> and <code>polymorphic_url</code>. For example - + # + # def create + # @project = Project.find(params[:project_id]) + # @task = @project.comments.build(params[:task]) + # flash[:notice] = 'Task was successfully created.' if @task.save + # respond_with(@project, @task) # end # - # It also accepts a block to be given. It's used to overwrite a default - # response: + # This would cause +respond_with+ to redirect to <code>project_task_url</code> + # instead of <code>task_url</code>. For request formats other than html or + # javascript, if multiple resources are passed in this way, it is the last + # one specified that is rendered. + # + # === Customizing response behavior + # + # Like +respond_to+, +respond_with+ may also be called with a block that + # can be used to overwrite any of the default responses, e.g. - # # def create # @user = User.new(params[:user]) @@ -220,21 +302,31 @@ module ActionController #:nodoc: # end # end # - # All options given to respond_with are sent to the underlying responder, - # except for the option :responder itself. Since the responder interface - # is quite simple (it just needs to respond to call), you can even give - # a proc to it. - # - # In order to use respond_with, first you need to declare the formats your - # controller responds to in the class level with a call to <tt>respond_to</tt>. - # + # The argument passed to the block is an ActionController::MimeResponds::Collector + # object which stores the responses for the formats defined within the + # block. Note that formats with responses defined explicitly in this way + # do not have to first be declared using the class method +respond_to+. + # + # Also, a hash passed to +respond_with+ immediately after the specified + # resource(s) is interpreted as a set of options relevant to all + # formats. Any option accepted by +render+ can be used, e.g. + # respond_with @people, :status => 200 + # However, note that these options are ignored after an unsuccessful attempt + # to save a resource, e.g. when automatically rendering <tt>:new</tt> + # after a post request. + # + # Two additional options are relevant specifically to +respond_with+ - + # 1. <tt>:location</tt> - overwrites the default redirect location used after + # a successful html +post+ request. + # 2. <tt>:action</tt> - overwrites the default render action used after an + # unsuccessful html +post+ request. def respond_with(*resources, &block) raise "In order to use respond_with, first you need to declare the formats your " << "controller responds to in the class level" if self.class.mimes_for_respond_to.empty? - if response = retrieve_response_from_mimes(&block) + if collector = retrieve_collector_from_mimes(&block) options = resources.size == 1 ? {} : resources.extract_options! - options.merge!(:default_response => response) + options[:default_response] = collector.response (options.delete(:responder) || self.class.responder).call(self, resources, options) end end @@ -243,9 +335,8 @@ module ActionController #:nodoc: # Collect mimes declared in the class method respond_to valid for the # current action. - # def collect_mimes_from_class_level #:nodoc: - action = action_name.to_sym + action = action_name.to_s self.class.mimes_for_respond_to.keys.select do |mime| config = self.class.mimes_for_respond_to[mime] @@ -260,30 +351,56 @@ module ActionController #:nodoc: end end - # Collects mimes and return the response for the negotiated format. Returns - # nil if :not_acceptable was sent to the client. + # Returns a Collector object containing the appropriate mime-type response + # for the current request, based on the available responses defined by a block. + # In typical usage this is the block passed to +respond_with+ or +respond_to+. # - def retrieve_response_from_mimes(mimes=nil, &block) #:nodoc: + # Sends :not_acceptable to the client and returns nil if no suitable format + # is available. + def retrieve_collector_from_mimes(mimes=nil, &block) #:nodoc: mimes ||= collect_mimes_from_class_level - collector = Collector.new(mimes) { |options| default_render(options || {}) } + collector = Collector.new(mimes) block.call(collector) if block_given? + format = collector.negotiate_format(request) - if format = request.negotiate_mime(collector.order) + if format self.content_type ||= format.to_s - lookup_context.freeze_formats([format.to_sym]) - collector.response_for(format) + lookup_context.formats = [format.to_sym] + lookup_context.rendered_format = lookup_context.formats.first + collector else - head :not_acceptable - nil + raise ActionController::UnknownFormat end end - class Collector #:nodoc: + # A container for responses available from the current controller for + # requests for different mime-types sent to a particular action. + # + # The public controller methods +respond_with+ and +respond_to+ may be called + # with a block that is used to define responses to different mime-types, e.g. + # for +respond_to+ : + # + # respond_to do |format| + # format.html + # format.xml { render :xml => @people } + # end + # + # In this usage, the argument passed to the block (+format+ above) is an + # instance of the ActionController::MimeResponds::Collector class. This + # object serves as a container in which available responses can be stored by + # calling any of the dynamically generated, mime-type-specific methods such + # as +html+, +xml+ etc on the Collector. Each response is represented by a + # corresponding block if present. + # + # A subsequent call to #negotiate_format(request) will enable the Collector + # to determine which specific mime-type it should respond with for the current + # request, with this response then being accessible by calling #response. + class Collector include AbstractController::Collector - attr_accessor :order + attr_accessor :order, :format - def initialize(mimes, &block) - @order, @responses, @default_response = [], {}, block + def initialize(mimes) + @order, @responses = [], {} mimes.each { |mime| send(mime) } end @@ -302,8 +419,12 @@ module ActionController #:nodoc: @responses[mime_type] ||= block end - def response_for(mime) - @responses[mime] || @responses[Mime::ALL] || @default_response + def response + @responses[format] || @responses[Mime::ALL] + end + + def negotiate_format(request) + @format = request.negotiate_mime(order) end end end diff --git a/actionpack/lib/action_controller/metal/params_wrapper.rb b/actionpack/lib/action_controller/metal/params_wrapper.rb index 5500f88930..aa67fa7f23 100644 --- a/actionpack/lib/action_controller/metal/params_wrapper.rb +++ b/actionpack/lib/action_controller/metal/params_wrapper.rb @@ -1,36 +1,34 @@ 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 'active_support/core_ext/module/anonymous' 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. + # Wraps the parameters hash into a nested hash. This will allow clients to submit + # POST requests without having to specify any root elements. # - # By default this functionality won't be enabled. You can enable - # it globally by setting +ActionController::Base.wrap_parameters+: - # - # ActionController::Base.wrap_parameters = [:json] + # This functionality is enabled in +config/initializers/wrap_parameters.rb+ + # and can be customized. If you are upgrading to \Rails 3.1, this file will + # need to be created for the functionality to be enabled. # # You could also turn it on per controller by setting the format array to - # non-empty array: + # a non-empty array: # # class UsersController < ApplicationController # wrap_parameters :format => [:json, :xml] # end # - # If you enable +ParamsWrapper+ for +:json+ format. Instead of having to + # 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: + # You can send parameters like this: # # {"name": "Konata"} # - # And it will be wrapped into a nested hash with the key name matching + # And it will be wrapped into a nested hash with the key name matching the # controller's name. For example, if you're posting to +UsersController+, # your new +params+ hash will look like this: # @@ -44,8 +42,13 @@ module ActionController # wrap_parameters :person, :include => [:username, :password] # end # + # On ActiveRecord models with no +:include+ or +:exclude+ option set, + # if attr_accessible is set on that model, it will only wrap the accessible + # parameters, else it will only wrap the parameters returned by the class + # method attribute_names. + # # 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 + # <tt>User.new(params[:user])</tt>), 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: # @@ -63,8 +66,8 @@ module ActionController # 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, + # will try to check if <tt>Admin::User</tt> or +User+ model exists, and use it to + # determine the wrapper key respectively. If both models don't exist, # it will then fallback to use +user+ as the key. module ParamsWrapper extend ActiveSupport::Concern @@ -82,20 +85,20 @@ module ActionController # # ==== Examples # wrap_parameters :format => :xml - # # enables the parmeter wrapper for XML format + # # enables the parameter 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 + # # wraps parameters by determining the wrapper key from Person class # (+person+, in this case) and the list of attribute names # # wrap_parameters :include => [:username, :title] # # wraps only +:username+ and +:title+ attributes from parameters. # # wrap_parameters false - # # disable parameters wrapping for this controller altogether. + # # disables parameters wrapping for this controller altogether. # # ==== Options # * <tt>:format</tt> - The list of formats in which the parameters wrapper @@ -142,19 +145,16 @@ module ActionController # try to find Foo::Bar::User, Foo::User and finally User. def _default_wrap_model #:nodoc: return nil if self.anonymous? - - model_name = self.name.sub(/Controller$/, '').singularize + model_name = self.name.sub(/Controller$/, '').classify begin - model_klass = model_name.constantize - rescue NameError, ArgumentError => e - if e.message =~ /is not missing constant|uninitialized constant #{model_name}/ + if model_klass = model_name.safe_constantize + model_klass + else namespaces = model_name.split("::") namespaces.delete_at(-2) break if namespaces.last == model_name model_name = namespaces.join("::") - else - raise end end until model_klass @@ -166,7 +166,10 @@ module ActionController unless options[:include] || options[:exclude] model ||= _default_wrap_model - if model.respond_to?(:attribute_names) && model.attribute_names.present? + role = options.fetch(:as, :default) + if model.respond_to?(:accessible_attributes) && model.accessible_attributes(role).present? + options[:include] = model.accessible_attributes(role).to_a + elsif model.respond_to?(:attribute_names) && model.attribute_names.present? options[:include] = model.attribute_names end end @@ -177,9 +180,9 @@ module ActionController controller_name.singularize end - options[:include] = Array.wrap(options[:include]).collect(&:to_s) if options[:include] - options[:exclude] = Array.wrap(options[:exclude]).collect(&:to_s) if options[:exclude] - options[:format] = Array.wrap(options[:format]) + options[:include] = Array(options[:include]).collect(&:to_s) if options[:include] + options[:exclude] = Array(options[:exclude]).collect(&:to_s) if options[:exclude] + options[:format] = Array(options[:format]) self._wrapper_options = options end @@ -190,7 +193,8 @@ module ActionController def process_action(*args) if _wrapper_enabled? wrapped_hash = _wrap_parameters request.request_parameters - wrapped_filtered_hash = _wrap_parameters request.filtered_parameters + wrapped_keys = request.request_parameters.keys + wrapped_filtered_hash = _wrap_parameters request.filtered_parameters.slice(*wrapped_keys) # This will make the wrapped hash accessible from controller and view request.parameters.merge! wrapped_hash diff --git a/actionpack/lib/action_controller/metal/redirecting.rb b/actionpack/lib/action_controller/metal/redirecting.rb index 55c650df6c..ee0e69d87c 100644 --- a/actionpack/lib/action_controller/metal/redirecting.rb +++ b/actionpack/lib/action_controller/metal/redirecting.rb @@ -18,13 +18,12 @@ module ActionController # # * <tt>Hash</tt> - The URL will be generated by calling url_for with the +options+. # * <tt>Record</tt> - The URL will be generated by calling url_for with the +options+, which will reference a named URL for that record. - # * <tt>String</tt> starting with <tt>protocol://</tt> (like <tt>http://</tt>) - Is passed straight through as the target for redirection. + # * <tt>String</tt> starting with <tt>protocol://</tt> (like <tt>http://</tt>) or a protocol relative reference (like <tt>//</tt>) - Is passed straight through as the target for redirection. # * <tt>String</tt> not containing a protocol - The current protocol and host is prepended to the string. # * <tt>Proc</tt> - A block that will be executed in the controller's context. Should return any option accepted by +redirect_to+. # * <tt>:back</tt> - Back to the page that issued the request. Useful for forms that are triggered from multiple places. # Short-hand for <tt>redirect_to(request.env["HTTP_REFERER"])</tt> # - # Examples: # redirect_to :action => "show", :id => 5 # redirect_to post # redirect_to "http://www.rubyonrails.org" @@ -35,7 +34,6 @@ module ActionController # # The redirection happens as a "302 Moved" header unless otherwise specified. # - # Examples: # redirect_to post_url(@post), :status => :found # redirect_to :action=>'atom', :status => :moved_permanently # redirect_to post_url(@post), :status => 301 @@ -43,21 +41,31 @@ module ActionController # # The status code can either be a standard {HTTP Status code}[http://www.iana.org/assignments/http-status-codes] as an # integer, or a symbol representing the downcased, underscored and symbolized description. + # Note that the status code must be a 3xx HTTP code, or redirection will not occur. # - # It is also possible to assign a flash message as part of the redirection. There are two special accessors for commonly used the flash names + # If you are using XHR requests other than GET or POST and redirecting after the + # request then some browsers will follow the redirect using the original request + # method. This may lead to undesirable behavior such as a double DELETE. To work + # around this you can return a <tt>303 See Other</tt> status code which will be + # followed using a GET request. + # + # redirect_to posts_url, :status => :see_other + # redirect_to :action => 'index', :status => 303 + # + # It is also possible to assign a flash message as part of the redirection. There are two special accessors for the commonly used flash names # +alert+ and +notice+ as well as a general purpose +flash+ bucket. # - # Examples: # redirect_to post_url(@post), :alert => "Watch it, mister!" # redirect_to post_url(@post), :status=> :found, :notice => "Pay attention to the road" # redirect_to post_url(@post), :status => 301, :flash => { :updated_post_id => @post.id } # redirect_to { :action=>'atom' }, :alert => "Something serious happened" # - # When using <tt>redirect_to :back</tt>, if there is no referrer, RedirectBackError will be raised. You may specify some fallback - # behavior for this case by rescuing RedirectBackError. + # When using <tt>redirect_to :back</tt>, if there is no referrer, ActionController::RedirectBackError will be raised. You may specify some fallback + # behavior for this case by rescuing ActionController::RedirectBackError. def redirect_to(options = {}, response_status = {}) #:doc: - raise ActionControllerError.new("Cannot redirect to nil!") if options.nil? + raise ActionControllerError.new("Cannot redirect to nil!") unless options raise AbstractController::DoubleRenderError if response_body + logger.debug { "Redirected by #{caller(1).first rescue "unknown"}" } if logger self.status = _extract_redirect_to_status(options, response_status) self.location = _compute_redirect_to_location(options) @@ -80,7 +88,8 @@ module ActionController # The scheme name consist of a letter followed by any combination of # letters, digits, and the plus ("+"), period ("."), or hyphen ("-") # characters; and is terminated by a colon (":"). - when %r{^\w[\w+.-]*:.*} + # The protocol relative scheme starts with a double slash "//" + when %r{^(\w[\w+.-]*:|//).*} options when String request.protocol + request.host_with_port + options @@ -91,7 +100,7 @@ module ActionController _compute_redirect_to_location options.call else url_for(options) - end.gsub(/[\r\n]/, '') + end.delete("\0\r\n") end end end diff --git a/actionpack/lib/action_controller/metal/renderers.rb b/actionpack/lib/action_controller/metal/renderers.rb index 0ad9dbeda9..1927c8bdc7 100644 --- a/actionpack/lib/action_controller/metal/renderers.rb +++ b/actionpack/lib/action_controller/metal/renderers.rb @@ -1,5 +1,6 @@ require 'active_support/core_ext/class/attribute' require 'active_support/core_ext/object/blank' +require 'set' module ActionController # See <tt>Renderers.add</tt> @@ -12,16 +13,13 @@ module ActionController included do class_attribute :_renderers - self._renderers = {}.freeze + self._renderers = Set.new.freeze end module ClassMethods def use_renderers(*args) - new = _renderers.dup - args.each do |key| - new[key] = RENDERERS[key] - end - self._renderers = new.freeze + renderers = _renderers + args + self._renderers = renderers.freeze end alias use_renderer use_renderers end @@ -31,10 +29,10 @@ module ActionController end def _handle_render_options(options) - _renderers.each do |name, value| - if options.key?(name.to_sym) + _renderers.each do |name| + if options.key?(name) _process_options(options) - return send("_render_option_#{name}", options.delete(name.to_sym), options) + return send("_render_option_#{name}", options.delete(name), options) end end nil @@ -42,7 +40,7 @@ module ActionController # Hash of available renderers, mapping a renderer name to its proc. # Default keys are :json, :js, :xml. - RENDERERS = {} + RENDERERS = Set.new # Adds a new renderer to call within controller actions. # A renderer is invoked by passing its name as an option to @@ -51,7 +49,6 @@ module ActionController # is the value paired with its key and the second is the remaining # hash of options passed to +render+. # - # === Example # Create a csv renderer: # # ActionController::Renderers.add :csv do |obj, options| @@ -79,7 +76,7 @@ module ActionController # <tt>ActionController::MimeResponds#respond_with</tt> def self.add(key, &block) define_method("_render_option_#{key}", &block) - RENDERERS[key] = block + RENDERERS << key.to_sym end module All @@ -93,9 +90,14 @@ module ActionController add :json do |json, options| json = json.to_json(options) unless json.kind_of?(String) - json = "#{options[:callback]}(#{json})" unless options[:callback].blank? - self.content_type ||= Mime::JSON - json + + if options[:callback].present? + self.content_type ||= Mime::JS + "#{options[:callback]}(#{json})" + else + self.content_type ||= Mime::JSON + json + end end add :js do |js, options| diff --git a/actionpack/lib/action_controller/metal/rendering.rb b/actionpack/lib/action_controller/metal/rendering.rb index 70fd79bb8b..c5e7d4e357 100644 --- a/actionpack/lib/action_controller/metal/rendering.rb +++ b/actionpack/lib/action_controller/metal/rendering.rb @@ -14,7 +14,7 @@ module ActionController def render(*args) #:nodoc: raise ::AbstractController::DoubleRenderError if response_body super - self.content_type ||= Mime[formats.first].to_s + self.content_type ||= Mime[lookup_context.rendered_format].to_s response_body end @@ -29,6 +29,10 @@ module ActionController self.response_body = nil end + def render_to_body(*) + super || " " + end + private # Normalize arguments by catching blocks and setting them on :update. @@ -44,6 +48,10 @@ module ActionController options[:text] = options[:text].to_text end + if options.delete(:nothing) || (options.key?(:text) && options[:text].nil?) + options[:text] = " " + end + if options[:status] options[:status] = Rack::Utils.status_code(options[:status]) end diff --git a/actionpack/lib/action_controller/metal/request_forgery_protection.rb b/actionpack/lib/action_controller/metal/request_forgery_protection.rb index 13044a7450..95b0e99ed5 100644 --- a/actionpack/lib/action_controller/metal/request_forgery_protection.rb +++ b/actionpack/lib/action_controller/metal/request_forgery_protection.rb @@ -1,4 +1,5 @@ require 'active_support/core_ext/class/attribute' +require 'action_controller/metal/exceptions' module ActionController #:nodoc: class InvalidAuthenticityToken < ActionControllerError #:nodoc: @@ -7,17 +8,15 @@ module ActionController #:nodoc: # Controller actions are protected from Cross-Site Request Forgery (CSRF) attacks # by including a token in the rendered html for your application. This token is # stored as a random string in the session, to which an attacker does not have - # access. When a request reaches your application, \Rails then verifies the received - # token with the token in the session. Only HTML and javascript requests are checked, + # access. When a request reaches your application, \Rails verifies the received + # token with the token in the session. Only HTML and JavaScript requests are checked, # so this will not protect your XML API (presumably you'll have a different # authentication scheme there anyway). Also, GET requests are not protected as these # should be idempotent. # # CSRF protection is turned on with the <tt>protect_from_forgery</tt> method, - # which will check the token and raise an ActionController::InvalidAuthenticityToken - # if it doesn't match what was expected. A call to this method is generated for new - # \Rails applications by default. You can customize the error message by editing - # public/422.html. + # which checks the token and resets the session if it doesn't match what was expected. + # A call to this method is generated for new \Rails applications by default. # # The token parameter is named <tt>authenticity_token</tt> by default. The name and # value of this token must be added to every layout that renders forms by including @@ -37,6 +36,10 @@ module ActionController #:nodoc: config_accessor :request_forgery_protection_token self.request_forgery_protection_token ||= :authenticity_token + # Controls how unverified request will be handled + config_accessor :request_forgery_protection_method + self.request_forgery_protection_method ||= :reset_session + # Controls whether request forgery protection is turned on or not. Turned off by default only in test mode. config_accessor :allow_forgery_protection self.allow_forgery_protection = true if allow_forgery_protection.nil? @@ -48,8 +51,6 @@ module ActionController #:nodoc: module ClassMethods # Turn on request forgery protection. Bear in mind that only non-GET, HTML/JavaScript requests are checked. # - # Example: - # # class FooController < ApplicationController # protect_from_forgery :except => :index # @@ -63,27 +64,41 @@ module ActionController #:nodoc: # # Valid Options: # - # * <tt>:only/:except</tt> - Passed to the <tt>before_filter</tt> call. Set which actions are verified. + # * <tt>:only/:except</tt> - Passed to the <tt>before_filter</tt> call. Set which actions are verified. + # * <tt>:with</tt> - Set the method to handle unverified request. Valid values: <tt>:exception</tt> and <tt>:reset_session</tt> (default). def protect_from_forgery(options = {}) self.request_forgery_protection_token ||= :authenticity_token + self.request_forgery_protection_method = options.delete(:with) if options.key?(:with) prepend_before_filter :verify_authenticity_token, options end end protected - # The actual before_filter that is used. Modify this to change how you handle unverified requests. + # The actual before_filter that is used. Modify this to change how you handle unverified requests. def verify_authenticity_token unless verified_request? - logger.debug "WARNING: Can't verify CSRF token authenticity" if logger + logger.warn "Can't verify CSRF token authenticity" if logger handle_unverified_request end end + # This is the method that defines the application behavior when a request is found to be unverified. + # By default, \Rails uses <tt>request_forgery_protection_method</tt> when it finds an unverified request: + # + # * <tt>:reset_session</tt> - Resets the session. + # * <tt>:exception</tt>: - Raises ActionController::InvalidAuthenticityToken exception. def handle_unverified_request - reset_session + case request_forgery_protection_method + when :exception + raise ActionController::InvalidAuthenticityToken + when :reset_session + reset_session + else + raise ArgumentError, 'Invalid request forgery protection method, use :exception or :reset_session' + end end - # Returns true or false if a request is verified. Checks: + # Returns true or false if a request is verified. Checks: # # * is it a GET request? Gets should be safe and idempotent # * Does the form_authenticity_token match the given token value from the params? @@ -96,7 +111,7 @@ module ActionController #:nodoc: # Sets the token value for the current session. def form_authenticity_token - session[:_csrf_token] ||= ActiveSupport::SecureRandom.base64(32) + session[:_csrf_token] ||= SecureRandom.base64(32) end # The form's authenticity parameter. Override to provide your own. diff --git a/actionpack/lib/action_controller/metal/rescue.rb b/actionpack/lib/action_controller/metal/rescue.rb index eb037aa1b0..68cc9a9c9b 100644 --- a/actionpack/lib/action_controller/metal/rescue.rb +++ b/actionpack/lib/action_controller/metal/rescue.rb @@ -1,4 +1,7 @@ module ActionController #:nodoc: + # This module is responsible to provide `rescue_from` helpers + # to controllers and configure when detailed exceptions must be + # shown. module Rescue extend ActiveSupport::Concern include ActiveSupport::Rescuable @@ -12,10 +15,20 @@ module ActionController #:nodoc: super(exception) end + # Override this method if you want to customize when detailed + # exceptions must be shown. This method is only called when + # consider_all_requests_local is false. By default, it returns + # false, but someone may set it to `request.local?` so local + # requests in production still shows the detailed exception pages. + def show_detailed_exceptions? + false + end + private def process_action(*args) super rescue Exception => exception + request.env['action_dispatch.show_detailed_exceptions'] ||= show_detailed_exceptions? rescue_with_handler(exception) || raise(exception) end end diff --git a/actionpack/lib/action_controller/metal/responder.rb b/actionpack/lib/action_controller/metal/responder.rb index ebadb29ea7..83407846dc 100644 --- a/actionpack/lib/action_controller/metal/responder.rb +++ b/actionpack/lib/action_controller/metal/responder.rb @@ -9,7 +9,7 @@ module ActionController #:nodoc: # respond_to :html, :xml, :json # # def index - # @people = Person.find(:all) + # @people = Person.all # respond_with(@people) # end # end @@ -53,7 +53,7 @@ module ActionController #:nodoc: # end # end # - # The same happens for PUT and DELETE requests. + # The same happens for PATCH/PUT and DELETE requests. # # === Nested resources # @@ -63,7 +63,7 @@ module ActionController #:nodoc: # # def create # @project = Project.find(params[:project_id]) - # @task = @project.comments.build(params[:task]) + # @task = @project.tasks.build(params[:task]) # flash[:notice] = 'Task was successfully created.' if @task.save # respond_with(@project, @task) # end @@ -84,8 +84,8 @@ module ActionController #:nodoc: # # === Custom options # - # <code>respond_with</code> also allow you to pass options that are forwarded - # to the underlying render call. Those options are only applied success + # <code>respond_with</code> also allows you to pass options that are forwarded + # to the underlying render call. Those options are only applied for success # scenarios. For instance, you can do the following in the create method above: # # def create @@ -95,7 +95,7 @@ module ActionController #:nodoc: # respond_with(@project, @task, :status => 201) # end # - # This will return status 201 if the task was saved with success. If not, + # This will return status 201 if the task was saved successfully. If not, # it will simply ignore the given options and return status 422 and the # resource errors. To customize the failure scenario, you can pass a # a block to <code>respond_with</code>: @@ -116,8 +116,9 @@ module ActionController #:nodoc: class Responder attr_reader :controller, :request, :format, :resource, :resources, :options - ACTIONS_FOR_VERBS = { + DEFAULT_ACTIONS_FOR_VERBS = { :post => :new, + :patch => :edit, :put => :edit } @@ -133,7 +134,7 @@ module ActionController #:nodoc: end delegate :head, :render, :redirect_to, :to => :controller - delegate :get?, :post?, :put?, :delete?, :to => :request + delegate :get?, :post?, :patch?, :put?, :delete?, :to => :request # Undefine :to_json and :to_yaml since it's defined on Object undef_method(:to_json) if method_defined?(:to_json) @@ -162,12 +163,17 @@ module ActionController #:nodoc: navigation_behavior(e) end + # to_js simply tries to render a template. If no template is found, raises the error. + def to_js + default_render + end + # All other formats follow the procedure below. First we try to render a # template, if the template is not available, we verify if the resource # responds to :to_format and display it. # def to_format - if get? || !has_errors? + if get? || !has_errors? || response_overridden? default_render else display_errors @@ -197,10 +203,8 @@ module ActionController #:nodoc: display resource elsif post? display resource, :status => :created, :location => api_location - elsif has_empty_resource_definition? - display empty_resource, :status => :ok else - head :ok + head :no_content end end @@ -219,11 +223,15 @@ module ActionController #:nodoc: alias :navigation_location :resource_location alias :api_location :resource_location - # If a given response block was given, use it, otherwise call render on + # If a response block was given, use it, otherwise call render on # controller. # def default_render - @default_response.call(options) + if @default_response + @default_response.call(options) + else + controller.default_render(options) + end end # Display is just a shortcut to render a resource with the current format. @@ -248,7 +256,7 @@ module ActionController #:nodoc: end def display_errors - controller.render format => resource.errors, :status => :unprocessable_entity + controller.render format => resource_errors, :status => :unprocessable_entity end # Check whether the resource has errors. @@ -257,29 +265,23 @@ module ActionController #:nodoc: resource.respond_to?(:errors) && !resource.errors.empty? end - # By default, render the <code>:edit</code> action for HTML requests with failure, unless - # the verb is POST. + # By default, render the <code>:edit</code> action for HTML requests with errors, unless + # the verb was POST. # def default_action - @action ||= ACTIONS_FOR_VERBS[request.request_method_symbol] + @action ||= DEFAULT_ACTIONS_FOR_VERBS[request.request_method_symbol] end - # Check whether resource needs a specific definition of empty resource to be valid - # - def has_empty_resource_definition? - respond_to?("empty_#{format}_resource") + def resource_errors + respond_to?("#{format}_resource_errors", true) ? send("#{format}_resource_errors") : resource.errors end - # Delegate to proper empty resource method - # - def empty_resource - send("empty_#{format}_resource") + def json_resource_errors + {:errors => resource.errors} end - # Return a valid empty JSON resource - # - def empty_json_resource - "{}" + def response_overridden? + @default_response.present? end end end diff --git a/actionpack/lib/action_controller/metal/session_management.rb b/actionpack/lib/action_controller/metal/session_management.rb deleted file mode 100644 index 91d89ff9a4..0000000000 --- a/actionpack/lib/action_controller/metal/session_management.rb +++ /dev/null @@ -1,9 +0,0 @@ -module ActionController #:nodoc: - module SessionManagement #:nodoc: - extend ActiveSupport::Concern - - module ClassMethods - - end - end -end diff --git a/actionpack/lib/action_controller/metal/streaming.rb b/actionpack/lib/action_controller/metal/streaming.rb index 3892a12407..eeb37db2e7 100644 --- a/actionpack/lib/action_controller/metal/streaming.rb +++ b/actionpack/lib/action_controller/metal/streaming.rb @@ -1,4 +1,3 @@ -require 'active_support/core_ext/file/path' require 'rack/chunked' module ActionController #:nodoc: @@ -24,20 +23,8 @@ module ActionController #:nodoc: # # == 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: + # Streaming can be added to a given template easily, all you need to do is + # to pass the :stream option. # # class PostsController # def index @@ -72,6 +59,9 @@ module ActionController #:nodoc: # render :stream => true # end # + # Notice that :stream only works with templates. Rendering :json + # or :xml with :stream won't work. + # # == Communication between layout and template # # When streaming, rendering happens top-down instead of inside-out. @@ -149,17 +139,17 @@ module ActionController #:nodoc: # 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+ + # If you try to modify cookies, session or flash, an <tt>ActionDispatch::ClosedError</tt> # 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 + # or production. For instance, <tt>Rack::Bug</tt> 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 + # Also <tt>Rack::Cache</tt> won't work with streaming as it does not support # streaming bodies yet. Whenever streaming Cache-Control is automatically # set to "no-cache". # @@ -172,7 +162,7 @@ module ActionController #:nodoc: # 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> + # "><script>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 @@ -204,38 +194,14 @@ module ActionController #:nodoc: # ==== Passenger # # To be described. - # + # module Streaming extend ActiveSupport::Concern include AbstractController::Rendering - attr_internal :stream - - module ClassMethods - # Render streaming templates. It accepts :only, :except, :if and :unless as options - # to specify when to stream, as in ActionController filters. - def stream(options={}) - if defined?(Fiber) - before_filter :_stream_filter, options - else - raise "You cannot use streaming if Fiber is not available." - end - end - end protected - # Mark following render calls as streaming. - def _stream_filter #:nodoc: - self.stream = true - end - - # Consider the stream option when normalazing options. - def _normalize_options(options) #:nodoc: - super - options[:stream] = self.stream unless options.key?(:stream) - end - # Set proper cache control and transfer encoding when streaming def _process_options(options) #:nodoc: super @@ -250,7 +216,7 @@ module ActionController #:nodoc: end end - # Call render_to_body if we are streaming instead of usual +render+. + # Call render_body if we are streaming instead of usual +render+. def _render_template(options) #:nodoc: if options.delete(:stream) Rack::Chunked::Body.new view_renderer.render_body(view_context, options) @@ -260,4 +226,3 @@ module ActionController #:nodoc: end end end -
\ No newline at end of file diff --git a/actionpack/lib/action_controller/metal/testing.rb b/actionpack/lib/action_controller/metal/testing.rb index f4efeb33ba..d1813ee745 100644 --- a/actionpack/lib/action_controller/metal/testing.rb +++ b/actionpack/lib/action_controller/metal/testing.rb @@ -4,6 +4,11 @@ module ActionController include RackDelegation + def recycle! + @_url_options = nil + end + + # TODO: Clean this up def process_with_new_base_test(request, response) @_request = request diff --git a/actionpack/lib/action_controller/metal/url_for.rb b/actionpack/lib/action_controller/metal/url_for.rb index 08132b1900..e28c05cc2d 100644 --- a/actionpack/lib/action_controller/metal/url_for.rb +++ b/actionpack/lib/action_controller/metal/url_for.rb @@ -1,25 +1,22 @@ -# Includes +url_for+ into the host class. The class has to provide a +RouteSet+ by implementing -# the <tt>_routes</tt> method. Otherwise, an exception will be raised. -# -# In addition to <tt>AbstractController::UrlFor</tt>, this module accesses the HTTP layer to define -# url options like the +host+. In order to do so, this module requires the host class -# to implement +env+ and +request+, which need to be a Rack-compatible. -# -# Example: -# -# class RootUrl -# include ActionController::UrlFor -# include Rails.application.routes.url_helpers -# -# delegate :env, :request, :to => :controller -# -# def initialize(controller) -# @controller = controller -# @url = root_path # named route from the application. -# end -# end -# => module ActionController + # Includes +url_for+ into the host class. The class has to provide a +RouteSet+ by implementing + # the <tt>_routes</tt> method. Otherwise, an exception will be raised. + # + # In addition to <tt>AbstractController::UrlFor</tt>, this module accesses the HTTP layer to define + # url options like the +host+. In order to do so, this module requires the host class + # to implement +env+ and +request+, which need to be a Rack-compatible. + # + # class RootUrl + # include ActionController::UrlFor + # include Rails.application.routes.url_helpers + # + # delegate :env, :request, :to => :controller + # + # def initialize(controller) + # @controller = controller + # @url = root_path # named route from the application. + # end + # end module UrlFor extend ActiveSupport::Concern @@ -42,6 +39,5 @@ module ActionController @_url_options end end - end end |