diff options
Diffstat (limited to 'actionpack/lib/action_controller/metal')
26 files changed, 825 insertions, 981 deletions
diff --git a/actionpack/lib/action_controller/metal/basic_implicit_render.rb b/actionpack/lib/action_controller/metal/basic_implicit_render.rb new file mode 100644 index 0000000000..6c6f8381ff --- /dev/null +++ b/actionpack/lib/action_controller/metal/basic_implicit_render.rb @@ -0,0 +1,11 @@ +module ActionController + module BasicImplicitRender + def send_action(method, *args) + super.tap { default_render unless performed? } + end + + def default_render(*args) + head :no_content + end + end +end diff --git a/actionpack/lib/action_controller/metal/conditional_get.rb b/actionpack/lib/action_controller/metal/conditional_get.rb index 6e0cd51d8b..89d589c486 100644 --- a/actionpack/lib/action_controller/metal/conditional_get.rb +++ b/actionpack/lib/action_controller/metal/conditional_get.rb @@ -4,7 +4,6 @@ module ActionController module ConditionalGet extend ActiveSupport::Concern - include RackDelegation include Head included do @@ -13,9 +12,9 @@ module ActionController end module ClassMethods - # Allows you to consider additional controller-wide information when generating an etag. + # Allows you to consider additional controller-wide information when generating an ETag. # For example, if you serve pages tailored depending on who's logged in at the moment, you - # may want to add the current user id to be part of the etag to prevent authorized displaying + # may want to add the current user id to be part of the ETag to prevent unauthorized displaying # of cached pages. # # class InvoicesController < ApplicationController @@ -32,7 +31,7 @@ module ActionController end end - # Sets the etag, +last_modified+, or both on the response and renders a + # Sets the +etag+, +last_modified+, or both on the response and renders a # <tt>304 Not Modified</tt> response if the request is already fresh. # # === Parameters: @@ -40,44 +39,63 @@ module ActionController # * <tt>:etag</tt>. # * <tt>:last_modified</tt>. # * <tt>:public</tt> By default the Cache-Control header is private, set this to - # +true+ if you want your application to be cachable by other devices (proxy caches). + # +true+ if you want your application to be cacheable by other devices (proxy caches). + # * <tt>:template</tt> By default, the template digest for the current + # controller/action is included in ETags. If the action renders a + # different template, you can include its digest instead. If the action + # doesn't render a template at all, you can pass <tt>template: false</tt> + # to skip any attempt to check for a template digest. # # === Example: # # def show # @article = Article.find(params[:id]) - # fresh_when(etag: @article, last_modified: @article.created_at, public: true) + # fresh_when(etag: @article, last_modified: @article.updated_at, public: true) # end # - # This will render the show template if the request isn't sending a matching etag or + # 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. # - # 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. + # You can also just pass a record. In this case +last_modified+ will be set + # by calling +updated_at+ and +etag+ by passing the object itself. # # def show # @article = Article.find(params[:id]) # fresh_when(@article) # end # - # When passing a record, you can still set whether the public header: + # You can also pass an object that responds to +maximum+, such as a + # collection of active records. In this case +last_modified+ will be set by + # calling +maximum(:updated_at)+ on the collection (the timestamp of the + # most recently updated record) and the +etag+ by passing the object itself. + # + # def index + # @articles = Article.all + # fresh_when(@articles) + # end + # + # When passing a record or a collection, you can still set 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) + # + # When rendering a different template than the default controller/action + # style, you can indicate which digest to include in the ETag: + # + # before_action { fresh_when @article, template: 'widgets/show' } + # + def fresh_when(object = nil, etag: object, last_modified: nil, public: false, template: nil) + last_modified ||= object.try(:updated_at) || object.try(:maximum, :updated_at) + + if etag || template + response.etag = combine_etags(etag: etag, last_modified: last_modified, + public: public, template: template) end - response.etag = combine_etags(options[:etag]) if options[:etag] - response.last_modified = options[:last_modified] if options[:last_modified] - response.cache_control[:public] = true if options[:public] + response.last_modified = last_modified if last_modified + response.cache_control[:public] = true if public head :not_modified if request.fresh?(response) end @@ -92,14 +110,19 @@ module ActionController # * <tt>:etag</tt>. # * <tt>:last_modified</tt>. # * <tt>:public</tt> By default the Cache-Control header is private, set this to - # +true+ if you want your application to be cachable by other devices (proxy caches). + # +true+ if you want your application to be cacheable by other devices (proxy caches). + # * <tt>:template</tt> By default, the template digest for the current + # controller/action is included in ETags. If the action renders a + # different template, you can include its digest instead. If the action + # doesn't render a template at all, you can pass <tt>template: false</tt> + # to skip any attempt to check for a template digest. # # === Example: # # def show # @article = Article.find(params[:id]) # - # if stale?(etag: @article, last_modified: @article.created_at) + # if stale?(etag: @article, last_modified: @article.updated_at) # @statistics = @article.really_expensive_call # respond_to do |format| # # all the supported formats @@ -107,8 +130,8 @@ module ActionController # end # end # - # 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. + # You can also just pass a record. In this case +last_modified+ will be set + # by calling +updated_at+ and +etag+ by passing the object itself. # # def show # @article = Article.find(params[:id]) @@ -121,7 +144,23 @@ module ActionController # end # end # - # When passing a record, you can still set whether the public header: + # You can also pass an object that responds to +maximum+, such as a + # collection of active records. In this case +last_modified+ will be set by + # calling +maximum(:updated_at)+ on the collection (the timestamp of the + # most recently updated record) and the +etag+ by passing the object itself. + # + # def index + # @articles = Article.all + # + # if stale?(@articles) + # @statistics = @articles.really_expensive_call + # respond_to do |format| + # # all the supported formats + # end + # end + # end + # + # When passing a record or a collection, you can still set the public header: # # def show # @article = Article.find(params[:id]) @@ -133,8 +172,16 @@ module ActionController # end # end # end - def stale?(record_or_options, additional_options = {}) - fresh_when(record_or_options, additional_options) + # + # When rendering a different template than the default controller/action + # style, you can indicate which digest to include in the ETag: + # + # def show + # super if stale? @article, template: 'widgets/show' + # end + # + def stale?(object = nil, etag: object, last_modified: nil, public: nil, template: nil) + fresh_when(object, etag: etag, last_modified: last_modified, public: public, template: template) !request.fresh?(response) end @@ -167,9 +214,28 @@ module ActionController response.cache_control.replace(:no_cache => true) end + # Cache or yield the block. The cache is supposed to never expire. + # + # You can use this method when you have a HTTP response that never changes, + # and the browser and proxies should cache it indefinitely. + # + # * +public+: By default, HTTP responses are private, cached only on the + # user's web browser. To allow proxies to cache the response, set +true+ to + # indicate that they can serve the cached response to all users. + # + # * +version+: the version passed as a key for the cache. + def http_cache_forever(public: false, version: 'v1') + expires_in 100.years, public: public + + yield if stale?(etag: "#{version}-#{request.fullpath}", + last_modified: Time.parse('2011-01-01').utc, + public: public) + end + private - def combine_etags(etag) - [ etag, *etaggers.map { |etagger| instance_exec(&etagger) }.compact ] + def combine_etags(options) + etags = etaggers.map { |etagger| instance_exec(options, &etagger) }.compact + etags.unshift options[:etag] end end end diff --git a/actionpack/lib/action_controller/metal/cookies.rb b/actionpack/lib/action_controller/metal/cookies.rb index d787f014cd..f8efb2b076 100644 --- a/actionpack/lib/action_controller/metal/cookies.rb +++ b/actionpack/lib/action_controller/metal/cookies.rb @@ -2,8 +2,6 @@ module ActionController #:nodoc: module Cookies extend ActiveSupport::Concern - include RackDelegation - included do helper_method :cookies end diff --git a/actionpack/lib/action_controller/metal/data_streaming.rb b/actionpack/lib/action_controller/metal/data_streaming.rb index 1abd8d3a33..957e7a3019 100644 --- a/actionpack/lib/action_controller/metal/data_streaming.rb +++ b/actionpack/lib/action_controller/metal/data_streaming.rb @@ -72,27 +72,7 @@ module ActionController #:nodoc: self.status = options[:status] || 200 self.content_type = options[:content_type] if options.key?(:content_type) - self.response_body = FileBody.new(path) - end - - # Avoid having to pass an open file handle as the response body. - # Rack::Sendfile will usually intercept the response and uses - # the path directly, so there is 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 + response.send_file path end # Sends the given binary data to the browser. This method is similar to @@ -126,7 +106,7 @@ 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 - render options.slice(:status, :content_type).merge(:text => data) + render options.slice(:status, :content_type).merge(body: data) end private diff --git a/actionpack/lib/action_controller/metal/etag_with_template_digest.rb b/actionpack/lib/action_controller/metal/etag_with_template_digest.rb new file mode 100644 index 0000000000..669cf55bca --- /dev/null +++ b/actionpack/lib/action_controller/metal/etag_with_template_digest.rb @@ -0,0 +1,50 @@ +module ActionController + # When our views change, they should bubble up into HTTP cache freshness + # and bust browser caches. So the template digest for the current action + # is automatically included in the ETag. + # + # Enabled by default for apps that use Action View. Disable by setting + # + # config.action_controller.etag_with_template_digest = false + # + # Override the template to digest by passing +:template+ to +fresh_when+ + # and +stale?+ calls. For example: + # + # # We're going to render widgets/show, not posts/show + # fresh_when @post, template: 'widgets/show' + # + # # We're not going to render a template, so omit it from the ETag. + # fresh_when @post, template: false + # + module EtagWithTemplateDigest + extend ActiveSupport::Concern + + include ActionController::ConditionalGet + + included do + class_attribute :etag_with_template_digest + self.etag_with_template_digest = true + + ActiveSupport.on_load :action_view, yield: true do + etag do |options| + determine_template_etag(options) if etag_with_template_digest + end + end + end + + private + def determine_template_etag(options) + if template = pick_template_for_etag(options) + lookup_and_digest_template(template) + end + end + + def pick_template_for_etag(options) + options.fetch(:template) { "#{controller_name}/#{action_name}" } + end + + def lookup_and_digest_template(template) + ActionView::Digestor.digest name: template, finder: lookup_context + end + end +end diff --git a/actionpack/lib/action_controller/metal/exceptions.rb b/actionpack/lib/action_controller/metal/exceptions.rb index 3844dbf2a6..5260dc0336 100644 --- a/actionpack/lib/action_controller/metal/exceptions.rb +++ b/actionpack/lib/action_controller/metal/exceptions.rb @@ -5,12 +5,10 @@ module ActionController class BadRequest < ActionControllerError #:nodoc: attr_reader :original_exception - def initialize(type = nil, e = nil) - return super() unless type && e - - super("Invalid #{type} parameters: #{e.message}") + def initialize(msg = nil, e = nil) + super(msg) @original_exception = e - set_backtrace e.backtrace + set_backtrace e.backtrace if e end end @@ -25,7 +23,7 @@ module ActionController end end - class ActionController::UrlGenerationError < RoutingError #:nodoc: + class ActionController::UrlGenerationError < ActionControllerError #:nodoc: end class MethodNotAllowed < ActionControllerError #:nodoc: diff --git a/actionpack/lib/action_controller/metal/force_ssl.rb b/actionpack/lib/action_controller/metal/force_ssl.rb index a2cb6d1e66..e31d65aac2 100644 --- a/actionpack/lib/action_controller/metal/force_ssl.rb +++ b/actionpack/lib/action_controller/metal/force_ssl.rb @@ -55,10 +55,10 @@ module ActionController # You can pass any of the following options to affect the before_action callback # * <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. + # * <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 = {}) action_options = options.slice(*ACTION_OPTIONS) redirect_options = options.except(*ACTION_OPTIONS) @@ -71,8 +71,8 @@ module ActionController # Redirect the existing request to use the HTTPS protocol. # # ==== Parameters - # * <tt>host_or_options</tt> - Either a host name or any of the url & redirect options - # available to the <tt>force_ssl</tt> method. + # * <tt>host_or_options</tt> - Either a host name or any of the url & + # redirect options available to the <tt>force_ssl</tt> method. def force_ssl_redirect(host_or_options = nil) unless request.ssl? options = { @@ -85,7 +85,7 @@ module ActionController if host_or_options.is_a?(Hash) options.merge!(host_or_options) elsif host_or_options - options.merge!(:host => host_or_options) + options[:host] = host_or_options end secure_url = ActionDispatch::Http::URL.url_for(options.slice(*URL_OPTIONS)) diff --git a/actionpack/lib/action_controller/metal/head.rb b/actionpack/lib/action_controller/metal/head.rb index 3d2badf9c2..b2110bf946 100644 --- a/actionpack/lib/action_controller/metal/head.rb +++ b/actionpack/lib/action_controller/metal/head.rb @@ -17,8 +17,18 @@ module ActionController # # See Rack::Utils::SYMBOL_TO_STATUS_CODE for a full list of valid +status+ symbols. def head(status, options = {}) - options, status = status, nil if status.is_a?(Hash) - status ||= options.delete(:status) || :ok + if status.is_a?(Hash) + msg = status[:status] ? 'The :status option' : 'The implicit :ok status' + options, status = status, status.delete(:status) + + ActiveSupport::Deprecation.warn(<<-MSG.squish) + #{msg} on `head` has been deprecated and will be removed in Rails 5.1. + Please pass the status as a separate parameter before the options, instead. + MSG + end + + status ||= :ok + location = options.delete(:location) content_type = options.delete(:content_type) @@ -29,15 +39,14 @@ module ActionController self.status = status self.location = url_for(location) if location - if include_content?(self._status_code) + self.response_body = "" + + if include_content?(self.response_code) self.content_type = content_type || (Mime[formats.first] if formats) - self.response.charset = false if self.response - self.response_body = " " - else - headers.delete('Content-Type') - headers.delete('Content-Length') - self.response_body = "" + self.response.charset = false end + + true end private diff --git a/actionpack/lib/action_controller/metal/helpers.rb b/actionpack/lib/action_controller/metal/helpers.rb index a9c3e438fb..d3853e2e83 100644 --- a/actionpack/lib/action_controller/metal/helpers.rb +++ b/actionpack/lib/action_controller/metal/helpers.rb @@ -7,8 +7,8 @@ module ActionController # extract complicated logic or reusable functionality is strongly encouraged. By default, each controller # will include all helpers. These helpers are only accessible on the controller through <tt>.helpers</tt> # - # 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 + # In previous versions of \Rails the controller will include a helper which + # matches the name 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 @@ -44,7 +44,7 @@ module ActionController # the output might look like this: # # 23 Aug 11:30 | Carolina Railhawks Soccer Match - # N/A | Carolina Railhaws Training Workshop + # N/A | Carolina Railhawks Training Workshop # module Helpers extend ActiveSupport::Concern @@ -73,7 +73,7 @@ module ActionController # Provides a proxy to access helpers methods from outside the view. def helpers - @helper_proxy ||= begin + @helper_proxy ||= begin proxy = ActionView::Base.new proxy.config = config.inheritable_copy proxy.extend(_helpers) @@ -93,10 +93,14 @@ module ActionController super(args) end + # Returns a list of helper names in a given path. + # + # ActionController::Base.all_helpers_from_path 'app/helpers' + # # => ["application", "chart", "rubygems"] def all_helpers_from_path(path) helpers = Array(path).flat_map do |_path| extract = /^#{Regexp.quote(_path.to_s)}\/?(.*)_helper.rb$/ - names = Dir["#{_path}/**/*_helper.rb"].map { |file| file.sub(extract, '\1') } + names = Dir["#{_path}/**/*_helper.rb"].map { |file| file.sub(extract, '\1'.freeze) } names.sort! end helpers.uniq! diff --git a/actionpack/lib/action_controller/metal/hide_actions.rb b/actionpack/lib/action_controller/metal/hide_actions.rb deleted file mode 100644 index af36ffa240..0000000000 --- a/actionpack/lib/action_controller/metal/hide_actions.rb +++ /dev/null @@ -1,40 +0,0 @@ - -module ActionController - # Adds the ability to prevent public methods on a controller to be called as actions. - module HideActions - extend ActiveSupport::Concern - - included do - class_attribute :hidden_actions - self.hidden_actions = Set.new.freeze - end - - private - - # Overrides AbstractController::Base#action_method? to return false if the - # action name is in the list of hidden actions. - def method_for_action(action_name) - self.class.visible_action?(action_name) && super - end - - module ClassMethods - # Sets all of the actions passed in as hidden actions. - # - # ==== Parameters - # * <tt>args</tt> - A list of actions - def hide_action(*args) - self.hidden_actions = hidden_actions.dup.merge(args.map(&:to_s)).freeze - end - - def visible_action?(action_name) - not hidden_actions.include?(action_name) - end - - # Overrides AbstractController::Base#action_methods to remove any methods - # that are listed as hidden methods. - def action_methods - @action_methods ||= Set.new(super.reject { |name| hidden_actions.include?(name) }).freeze - end - end - end -end diff --git a/actionpack/lib/action_controller/metal/http_authentication.rb b/actionpack/lib/action_controller/metal/http_authentication.rb index 25c123edf7..0a36fecd27 100644 --- a/actionpack/lib/action_controller/metal/http_authentication.rb +++ b/actionpack/lib/action_controller/metal/http_authentication.rb @@ -34,7 +34,7 @@ module ActionController # # def authenticate # case request.format - # when Mime::XML, Mime::ATOM + # when Mime[:xml], Mime[:atom] # if user = authenticate_with_http_basic { |u, p| @account.users.authenticate(u, p) } # @current_user = user # else @@ -53,10 +53,8 @@ module ActionController # In your integration tests, you can do something like this: # # def test_access_granted_from_xml - # get( - # "/notes/1.xml", nil, - # 'HTTP_AUTHORIZATION' => ActionController::HttpAuthentication::Basic.encode_credentials(users(:dhh).name, users(:dhh).password) - # ) + # @request.env['HTTP_AUTHORIZATION'] = ActionController::HttpAuthentication::Basic.encode_credentials(users(:dhh).name, users(:dhh).password) + # get "/notes/1.xml" # # assert_equal 200, status # end @@ -76,16 +74,16 @@ module ActionController end end - def authenticate_or_request_with_http_basic(realm = "Application", &login_procedure) - authenticate_with_http_basic(&login_procedure) || request_http_basic_authentication(realm) + def authenticate_or_request_with_http_basic(realm = "Application", message = nil, &login_procedure) + authenticate_with_http_basic(&login_procedure) || request_http_basic_authentication(realm, message) end def authenticate_with_http_basic(&login_procedure) HttpAuthentication::Basic.authenticate(request, &login_procedure) end - def request_http_basic_authentication(realm = "Application") - HttpAuthentication::Basic.authentication_request(self, realm) + def request_http_basic_authentication(realm = "Application", message = nil) + HttpAuthentication::Basic.authentication_request(self, realm, message) end end @@ -96,7 +94,7 @@ module ActionController end def has_basic_credentials?(request) - request.authorization.present? && (auth_scheme(request) == 'Basic') + request.authorization.present? && (auth_scheme(request).downcase == 'basic') end def user_name_and_password(request) @@ -108,21 +106,22 @@ module ActionController end def auth_scheme(request) - request.authorization.split(' ', 2).first + request.authorization.to_s.split(' ', 2).first end def auth_param(request) - request.authorization.split(' ', 2).second + request.authorization.to_s.split(' ', 2).second end def encode_credentials(user_name, password) "Basic #{::Base64.strict_encode64("#{user_name}:#{password}")}" end - def authentication_request(controller, realm) - controller.headers["WWW-Authenticate"] = %(Basic realm="#{realm.gsub(/"/, "")}") + def authentication_request(controller, realm, message) + message ||= "HTTP Basic: Access denied.\n" + controller.headers["WWW-Authenticate"] = %(Basic realm="#{realm.tr('"'.freeze, "".freeze)}") controller.status = 401 - controller.response_body = "HTTP Basic: Access denied.\n" + controller.response_body = message end end @@ -172,8 +171,8 @@ module ActionController extend self module ControllerMethods - def authenticate_or_request_with_http_digest(realm = "Application", &password_procedure) - authenticate_with_http_digest(realm, &password_procedure) || request_http_digest_authentication(realm) + def authenticate_or_request_with_http_digest(realm = "Application", message = nil, &password_procedure) + authenticate_with_http_digest(realm, &password_procedure) || request_http_digest_authentication(realm, message) end # Authenticate with HTTP Digest, returns true or false @@ -204,7 +203,7 @@ module ActionController password = password_procedure.call(credentials[:username]) return false unless password - method = request.env['rack.methodoverride.original_method'] || request.env['REQUEST_METHOD'] + method = request.get_header('rack.methodoverride.original_method') || request.get_header('REQUEST_METHOD') uri = credentials[:uri] [true, false].any? do |trailing_question_mark| @@ -261,8 +260,8 @@ module ActionController end def secret_token(request) - key_generator = request.env["action_dispatch.key_generator"] - http_auth_salt = request.env["action_dispatch.http_auth_salt"] + key_generator = request.key_generator + http_auth_salt = request.http_auth_salt key_generator.generate_key(http_auth_salt) end @@ -316,7 +315,7 @@ module ActionController nonce(secret_key, t) == value && (t - Time.now.to_i).abs <= seconds_to_timeout end - # Opaque based on random generation - but changing each request? + # Opaque based on digest of secret key def opaque(secret_key) ::Digest::MD5.hexdigest(secret_key) end @@ -362,7 +361,7 @@ module ActionController # # def authenticate # case request.format - # when Mime::XML, Mime::ATOM + # when Mime[:xml], Mime[:atom] # if user = authenticate_with_http_token { |t, o| @account.users.authenticate(t, o) } # @current_user = user # else @@ -397,21 +396,22 @@ module ActionController # # RewriteRule ^(.*)$ dispatch.fcgi [E=X-HTTP_AUTHORIZATION:%{HTTP:Authorization},QSA,L] module Token - TOKEN_REGEX = /^Token / + TOKEN_KEY = 'token=' + TOKEN_REGEX = /^(Token|Bearer) / AUTHN_PAIR_DELIMITERS = /(?:,|;|\t+)/ extend self module ControllerMethods - def authenticate_or_request_with_http_token(realm = "Application", &login_procedure) - authenticate_with_http_token(&login_procedure) || request_http_token_authentication(realm) + def authenticate_or_request_with_http_token(realm = "Application", message = nil, &login_procedure) + authenticate_with_http_token(&login_procedure) || request_http_token_authentication(realm, message) end def authenticate_with_http_token(&login_procedure) Token.authenticate(self, &login_procedure) end - def request_http_token_authentication(realm = "Application") - Token.authentication_request(self, realm) + def request_http_token_authentication(realm = "Application", message = nil) + Token.authentication_request(self, realm, message) end end @@ -436,15 +436,17 @@ module ActionController end end - # Parses the token and options out of the token authorization header. If - # the header looks like this: + # Parses the token and options out of the token authorization header. + # The value for the Authorization header is expected to have the prefix + # <tt>"Token"</tt> or <tt>"Bearer"</tt>. If the header looks like this: # Authorization: Token token="abc", nonce="def" - # Then the returned token is "abc", and the options is {nonce: "def"} + # Then the returned token is <tt>"abc"</tt>, and the options are + # <tt>{nonce: "def"}</tt> # # request - ActionDispatch::Request instance with the current headers. # - # Returns an Array of [String, Hash] if a token is present. - # Returns nil if no token is found. + # Returns an +Array+ of <tt>[String, Hash]</tt> if a token is present. + # Returns +nil+ if no token is found. def token_and_options(request) authorization_request = request.authorization.to_s if authorization_request[TOKEN_REGEX] @@ -462,16 +464,22 @@ module ActionController raw_params.map { |param| param.split %r/=(.+)?/ } end - # This removes the `"` characters wrapping the value. + # This removes the <tt>"</tt> characters wrapping the value. def rewrite_param_values(array_params) array_params.each { |param| (param[1] || "").gsub! %r/^"|"$/, '' } end # This method takes an authorization body and splits up the key-value - # pairs by the standardized `:`, `;`, or `\t` delimiters defined in - # `AUTHN_PAIR_DELIMITERS`. + # pairs by the standardized <tt>:</tt>, <tt>;</tt>, or <tt>\t</tt> + # delimiters defined in +AUTHN_PAIR_DELIMITERS+. def raw_params(auth) - auth.sub(TOKEN_REGEX, '').split(/\s*#{AUTHN_PAIR_DELIMITERS}\s*/) + _raw_params = auth.sub(TOKEN_REGEX, '').split(/\s*#{AUTHN_PAIR_DELIMITERS}\s*/) + + if !(_raw_params.first =~ %r{\A#{TOKEN_KEY}}) + _raw_params[0] = "#{TOKEN_KEY}#{_raw_params.first}" + end + + _raw_params end # Encodes the given token and options into an Authorization header value. @@ -481,21 +489,22 @@ module ActionController # # Returns String. def encode_credentials(token, options = {}) - values = ["token=#{token.to_s.inspect}"] + options.map do |key, value| + values = ["#{TOKEN_KEY}#{token.to_s.inspect}"] + options.map do |key, value| "#{key}=#{value.to_s.inspect}" end "Token #{values * ", "}" end - # Sets a WWW-Authenticate to let the client know a token is desired. + # Sets a WWW-Authenticate header to let the client know a token is desired. # # controller - ActionController::Base instance for the outgoing response. # realm - String realm to use in the header. # # Returns nothing. - def authentication_request(controller, realm) - controller.headers["WWW-Authenticate"] = %(Token realm="#{realm.gsub(/"/, "")}") - controller.__send__ :render, :text => "HTTP Token: Access denied.\n", :status => :unauthorized + def authentication_request(controller, realm, message = nil) + message ||= "HTTP Token: Access denied.\n" + controller.headers["WWW-Authenticate"] = %(Token realm="#{realm.tr('"'.freeze, "".freeze)}") + controller.__send__ :render, plain: message, status: :unauthorized end end end diff --git a/actionpack/lib/action_controller/metal/implicit_render.rb b/actionpack/lib/action_controller/metal/implicit_render.rb index ae04b53825..17fcc2fa02 100644 --- a/actionpack/lib/action_controller/metal/implicit_render.rb +++ b/actionpack/lib/action_controller/metal/implicit_render.rb @@ -1,13 +1,30 @@ module ActionController module ImplicitRender - def send_action(method, *args) - ret = super - default_render unless performed? - ret - end + include BasicImplicitRender + + # Renders the template corresponding to the controller action, if it exists. + # The action name, format, and variant are all taken into account. + # For example, the "new" action with an HTML format and variant "phone" + # would try to render the <tt>new.html+phone.erb</tt> template. + # + # If no template is found <tt>ActionController::BasicImplicitRender</tt>'s implementation is called, unless + # a block is passed. In that case, it will override the super implementation. + # + # default_render do + # head 404 # No template was found + # end def default_render(*args) - render(*args) + if template_exists?(action_name.to_s, _prefixes, variants: request.variant) + render(*args) + else + if block_given? + yield(*args) + else + logger.info "No template found for #{self.class.name}\##{action_name}, rendering head :no_content" if logger + super + end + end end def method_for_action(action_name) diff --git a/actionpack/lib/action_controller/metal/instrumentation.rb b/actionpack/lib/action_controller/metal/instrumentation.rb index b0e164bc57..3dbf34eb2a 100644 --- a/actionpack/lib/action_controller/metal/instrumentation.rb +++ b/actionpack/lib/action_controller/metal/instrumentation.rb @@ -11,7 +11,6 @@ module ActionController extend ActiveSupport::Concern include AbstractController::Logger - include ActionController::RackDelegation attr_internal :view_runtime @@ -21,17 +20,20 @@ module ActionController :action => self.action_name, :params => request.filtered_parameters, :format => request.format.try(:ref), - :method => request.method, + :method => request.request_method, :path => (request.fullpath rescue "unknown") } ActiveSupport::Notifications.instrument("start_processing.action_controller", raw_payload.dup) ActiveSupport::Notifications.instrument("process_action.action_controller", raw_payload) do |payload| - result = super - payload[:status] = response.status - append_info_to_payload(payload) - result + begin + result = super + payload[:status] = response.status + result + ensure + append_info_to_payload(payload) + end end end diff --git a/actionpack/lib/action_controller/metal/live.rb b/actionpack/lib/action_controller/metal/live.rb index 706ce04062..7db8d13e24 100644 --- a/actionpack/lib/action_controller/metal/live.rb +++ b/actionpack/lib/action_controller/metal/live.rb @@ -33,6 +33,20 @@ module ActionController # the main thread. Make sure your actions are thread safe, and this shouldn't # be a problem (don't share state across threads, etc). module Live + extend ActiveSupport::Concern + + module ClassMethods + def make_response!(request) + if request.env["HTTP_VERSION"] == "HTTP/1.0" + super + else + Live::Response.new.tap do |res| + res.request = request + end + end + end + end + # This class provides the ability to write an SSE (Server Sent Event) # to an IO stream. The class is initialized with a stream and can be used # to either write a JSON string or an object which can be converted to JSON. @@ -102,7 +116,7 @@ module ActionController end end - message = json.gsub("\n", "\ndata: ") + message = json.gsub("\n".freeze, "\ndata: ".freeze) @stream.write "data: #{message}\n\n" end end @@ -131,8 +145,8 @@ module ActionController def write(string) unless @response.committed? - @response.headers["Cache-Control"] = "no-cache" - @response.headers.delete "Content-Length" + @response.set_header "Cache-Control", "no-cache" + @response.delete_header "Content-Length" end super @@ -189,12 +203,6 @@ module ActionController !@aborted end - def await_close - synchronize do - @cv.wait_until { @closed } - end - end - def on_error(&block) @error_callback = block end @@ -205,29 +213,6 @@ module ActionController end class Response < ActionDispatch::Response #:nodoc: all - class Header < DelegateClass(Hash) # :nodoc: - def initialize(response, header) - @response = response - super(header) - end - - def []=(k,v) - if @response.committed? - raise ActionDispatch::IllegalStateError, 'header already sent' - end - - super - end - - def merge(other) - self.class.new @response, __getobj__.merge(other) - end - - def to_hash - __getobj__.dup - end - end - private def before_committed @@ -248,14 +233,6 @@ module ActionController body.each { |part| buf.write part } buf end - - def merge_default_headers(original, default) - Header.new self, super - end - - def handle_conditional_get! - super unless committed? - end end def process(name) @@ -303,10 +280,12 @@ module ActionController logger = ActionController::Base.logger return unless logger - message = "\n#{exception.class} (#{exception.message}):\n" - message << exception.annoted_source_code.to_s if exception.respond_to?(:annoted_source_code) - message << " " << exception.backtrace.join("\n ") - logger.fatal("#{message}\n\n") + logger.fatal do + message = "\n#{exception.class} (#{exception.message}):\n" + message << exception.annoted_source_code.to_s if exception.respond_to?(:annoted_source_code) + message << " " << exception.backtrace.join("\n ") + "#{message}\n\n" + end end def response_body=(body) @@ -315,12 +294,7 @@ module ActionController end def set_response!(request) - if request.env["HTTP_VERSION"] == "HTTP/1.0" - super - else - @_response = Live::Response.new - @_response.request = request - end + @_response = self.class.make_response! request end end end diff --git a/actionpack/lib/action_controller/metal/mime_responds.rb b/actionpack/lib/action_controller/metal/mime_responds.rb index 00e7e980f8..6e346fadfe 100644 --- a/actionpack/lib/action_controller/metal/mime_responds.rb +++ b/actionpack/lib/action_controller/metal/mime_responds.rb @@ -1,62 +1,7 @@ -require 'active_support/core_ext/array/extract_options' require 'abstract_controller/collector' module ActionController #:nodoc: module MimeResponds - extend ActiveSupport::Concern - - included do - class_attribute :responder, :mimes_for_respond_to - self.responder = ActionController::Responder - clear_respond_to - end - - module ClassMethods - # Defines mime types that are rendered by default when invoking - # <tt>respond_with</tt>. - # - # respond_to :html, :xml, :json - # - # Specifies that all actions in the controller respond to requests - # for <tt>:html</tt>, <tt>:xml</tt> and <tt>:json</tt>. - # - # To specify on per-action basis, use <tt>:only</tt> and - # <tt>:except</tt> with an array of actions or a single action: - # - # respond_to :html - # respond_to :xml, :json, except: [ :edit ] - # - # This specifies that all actions respond to <tt>:html</tt> - # and all actions except <tt>:edit</tt> respond to <tt>:xml</tt> and - # <tt>:json</tt>. - # - # respond_to :json, only: :create - # - # This specifies that the <tt>:create</tt> action and no other responds - # to <tt>:json</tt>. - def respond_to(*mimes) - options = mimes.extract_options! - - 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| - mime = mime.to_sym - new[mime] = {} - new[mime][:only] = only_actions unless only_actions.empty? - new[mime][:except] = except_actions unless except_actions.empty? - end - self.mimes_for_respond_to = new.freeze - end - - # Clear all mime types in <tt>respond_to</tt>. - # - def clear_respond_to - self.mimes_for_respond_to = Hash.new.freeze - end - end - # Without web-service support, an action which collects the data for displaying a list of people # might look something like this: # @@ -146,11 +91,11 @@ module ActionController #:nodoc: # and accept Rails' defaults, life will be much easier. # # If you need to use a MIME type which isn't supported by default, you can register your own handlers in - # config/initializers/mime_types.rb as follows. + # +config/initializers/mime_types.rb+ as follows. # # Mime::Type.register "image/jpg", :jpg # - # Respond to also allows you to specify a common block for different formats by using any: + # Respond to also allows you to specify a common block for different formats by using +any+: # # def index # @people = Person.all @@ -169,18 +114,6 @@ module ActionController #:nodoc: # # render json: @people # - # Since this is a common pattern, you can use the class method respond_to - # with the respond_with method to have the same results: - # - # class PeopleController < ApplicationController - # respond_to :html, :xml, :json - # - # def index - # @people = Person.all - # respond_with(@people) - # end - # end - # # Formats can have different variants. # # The request variant is a specialization of the request format, like <tt>:tablet</tt>, @@ -217,22 +150,22 @@ module ActionController #:nodoc: # format.html.phone { redirect_to progress_path } # format.html.none { render "trash" } # end - # - # Variants also support common `any`/`all` block that formats have. + # + # Variants also support common +any+/+all+ block that formats have. # # It works for both inline: # # respond_to do |format| - # format.html.any { render text: "any" } - # format.html.phone { render text: "phone" } + # format.html.any { render html: "any" } + # format.html.phone { render html: "phone" } # end # # and block syntax: # # respond_to do |format| # format.html do |variant| - # variant.any(:tablet, :phablet){ render text: "any" } - # variant.phone { render text: "phone" } + # variant.any(:tablet, :phablet){ render html: "any" } + # variant.phone { render html: "phone" } # end # end # @@ -241,201 +174,26 @@ module ActionController #:nodoc: # request.variant = [:tablet, :phone] # # which will work similarly to formats and MIME types negotiation. If there will be no - # :tablet variant declared, :phone variant will be picked: + # +:tablet+ variant declared, +:phone+ variant will be picked: # # respond_to do |format| # format.html.none # format.html.phone # this gets rendered # end # - # Be sure to check the documentation of +respond_with+ and - # <tt>ActionController::MimeResponds.respond_to</tt> for more examples. - def respond_to(*mimes, &block) + # Be sure to check the documentation of <tt>ActionController::MimeResponds.respond_to</tt> + # for more examples. + def respond_to(*mimes) raise ArgumentError, "respond_to takes either types or a block, never both" if mimes.any? && block_given? - if collector = retrieve_collector_from_mimes(mimes, &block) - response = collector.response - response ? response.call : render({}) - end - end - - # For a given controller action, respond_with generates an appropriate - # response based on the mime-type requested by the client. - # - # If the method is called with just a resource, as in this example - - # - # 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 +patch+ or +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 - # - # 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]) - # flash[:notice] = "User was successfully created." if @user.save - # - # respond_with(@user) do |format| - # format.html { render } - # end - # end - # - # 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) - if self.class.mimes_for_respond_to.empty? - raise "In order to use respond_with, first you need to declare the " \ - "formats your controller responds to in the class level." - end - - if collector = retrieve_collector_from_mimes(&block) - options = resources.size == 1 ? {} : resources.extract_options! - options = options.clone - options[:default_response] = collector.response - (options.delete(:responder) || self.class.responder).call(self, resources, options) - end - end - - protected - - # 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_s - - self.class.mimes_for_respond_to.keys.select do |mime| - config = self.class.mimes_for_respond_to[mime] - - if config[:except] - !config[:except].include?(action) - elsif config[:only] - config[:only].include?(action) - else - true - end - end - end - - # 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+. - # - # 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, request.variant) - block.call(collector) if block_given? - format = collector.negotiate_format(request) + yield collector if block_given? - if format + if format = collector.negotiate_format(request) _process_format(format) - collector + _set_rendered_content_type format + response = collector.response + response ? response.call : render({}) else raise ActionController::UnknownFormat end @@ -444,8 +202,8 @@ module ActionController #: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. + # The public controller methods +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| @@ -471,7 +229,7 @@ module ActionController #:nodoc: @responses = {} @variant = variant - mimes.each { |mime| @responses["Mime::#{mime.upcase}".constantize] = nil } + mimes.each { |mime| @responses[Mime[mime]] = nil } end def any(*args, &block) @@ -531,16 +289,17 @@ module ActionController #:nodoc: end def variant - if @variant.nil? + if @variant.empty? @variants[:none] || @variants[:any] - elsif (@variants.keys & @variant).any? - @variant.each do |v| - return @variants[v] if @variants.key?(v) - end else - @variants[:any] + @variants[variant_key] end end + + private + def variant_key + @variant.find { |variant| @variants.key?(variant) } || :any + end end end end diff --git a/actionpack/lib/action_controller/metal/params_wrapper.rb b/actionpack/lib/action_controller/metal/params_wrapper.rb index 2ca8955741..c38fc40b81 100644 --- a/actionpack/lib/action_controller/metal/params_wrapper.rb +++ b/actionpack/lib/action_controller/metal/params_wrapper.rb @@ -1,22 +1,20 @@ require 'active_support/core_ext/hash/slice' require 'active_support/core_ext/hash/except' require 'active_support/core_ext/module/anonymous' -require 'active_support/core_ext/struct' require 'action_dispatch/http/mime_type' module ActionController - # Wraps the parameters hash into a nested hash. This will allow clients to submit - # POST requests without having to specify any root elements. + # Wraps the parameters hash into a nested hash. This will allow clients to + # submit requests without having to specify any root elements. # # 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. + # and can be customized. # # You could also turn it on per controller by setting the format array to # a non-empty array: # # class UsersController < ApplicationController - # wrap_parameters format: [:json, :xml] + # wrap_parameters format: [:json, :xml, :url_encoded_form, :multipart_form] # end # # If you enable +ParamsWrapper+ for +:json+ format, instead of having to @@ -42,7 +40,7 @@ module ActionController # wrap_parameters :person, include: [:username, :password] # end # - # On ActiveRecord models with no +:include+ or +:exclude+ option set, + # On Active Record models with no +:include+ or +:exclude+ option set, # it will only wrap the parameters returned by the class method # <tt>attribute_names</tt>. # @@ -86,7 +84,7 @@ module ActionController new name, format, include, exclude, nil, nil end - def initialize(name, format, include, exclude, klass, model) # nodoc + def initialize(name, format, include, exclude, klass, model) # :nodoc: super @include_set = include @name_set = name @@ -132,7 +130,7 @@ module ActionController private # 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+ + # same singular name as the controller. For example, +UsersController+ # will try to find if the +User+ model exists. # # This method also does namespace lookup. Foo::Bar::UsersController will @@ -244,7 +242,7 @@ module ActionController request.parameters.merge! wrapped_hash request.request_parameters.merge! wrapped_hash - # This will make the wrapped hash displayed in the log file + # This will display the wrapped hash in the log file request.filtered_parameters.merge! wrapped_filtered_hash end super @@ -252,7 +250,7 @@ module ActionController private - # Returns the wrapper key which will use to stored wrapped parameters. + # Returns the wrapper key which will be used to store wrapped parameters. def _wrapper_key _wrapper_options.name end @@ -278,7 +276,9 @@ module ActionController # Checks if we should perform parameters wrapping. def _wrapper_enabled? - ref = request.content_mime_type.try(:ref) + return false unless request.has_content_type? + + ref = request.content_mime_type.ref _wrapper_formats.include?(ref) && _wrapper_key && !request.request_parameters[_wrapper_key] end end diff --git a/actionpack/lib/action_controller/metal/rack_delegation.rb b/actionpack/lib/action_controller/metal/rack_delegation.rb deleted file mode 100644 index 6921834044..0000000000 --- a/actionpack/lib/action_controller/metal/rack_delegation.rb +++ /dev/null @@ -1,32 +0,0 @@ -require 'action_dispatch/http/request' -require 'action_dispatch/http/response' - -module ActionController - module RackDelegation - extend ActiveSupport::Concern - - delegate :headers, :status=, :location=, :content_type=, - :status, :location, :content_type, :_status_code, :to => "@_response" - - def dispatch(action, request) - set_response!(request) - super(action, request) - end - - def response_body=(body) - response.body = body if response - super - end - - def reset_session - @_request.reset_session - end - - private - - def set_response!(request) - @_response = ActionDispatch::Response.new - @_response.request = request - end - end -end diff --git a/actionpack/lib/action_controller/metal/redirecting.rb b/actionpack/lib/action_controller/metal/redirecting.rb index ca8c0278d0..0febc905f1 100644 --- a/actionpack/lib/action_controller/metal/redirecting.rb +++ b/actionpack/lib/action_controller/metal/redirecting.rb @@ -11,7 +11,6 @@ module ActionController extend ActiveSupport::Concern include AbstractController::Logger - include ActionController::RackDelegation include ActionController::UrlFor # Redirects the browser to the target specified in +options+. This parameter can be any one of: @@ -72,11 +71,11 @@ module ActionController raise AbstractController::DoubleRenderError if response_body self.status = _extract_redirect_to_status(options, response_status) - self.location = _compute_redirect_to_location(options) + self.location = _compute_redirect_to_location(request, options) self.response_body = "<html><body>You are being <a href=\"#{ERB::Util.unwrapped_html_escape(location)}\">redirected</a>.</body></html>" end - def _compute_redirect_to_location(options) #:nodoc: + def _compute_redirect_to_location(request, options) #:nodoc: case options # The scheme name consist of a letter followed by any combination of # letters, digits, and the plus ("+"), period ("."), or hyphen ("-") @@ -90,11 +89,13 @@ module ActionController when :back request.headers["Referer"] or raise RedirectBackError when Proc - _compute_redirect_to_location options.call + _compute_redirect_to_location request, options.call else url_for(options) end.delete("\0\r\n") end + module_function :_compute_redirect_to_location + public :_compute_redirect_to_location private def _extract_redirect_to_status(options, response_status) diff --git a/actionpack/lib/action_controller/metal/renderers.rb b/actionpack/lib/action_controller/metal/renderers.rb index 02c4e563f5..22e0bb5955 100644 --- a/actionpack/lib/action_controller/metal/renderers.rb +++ b/actionpack/lib/action_controller/metal/renderers.rb @@ -34,14 +34,15 @@ module ActionController end def render_to_body(options) - _handle_render_options(options) || super + _render_to_body_with_renderer(options) || super end - def _handle_render_options(options) + def _render_to_body_with_renderer(options) _renderers.each do |name| if options.key?(name) _process_options(options) - return send("_render_option_#{name}", options.delete(name), options) + method_name = Renderers._render_with_renderer_method_name(name) + return send(method_name, options.delete(name), options) end end nil @@ -51,6 +52,10 @@ module ActionController # Default values are <tt>:json</tt>, <tt>:js</tt>, <tt>:xml</tt>. RENDERERS = Set.new + def self._render_with_renderer_method_name(key) + "_render_with_renderer_#{key}" + end + # Adds a new renderer to call within controller actions. # A renderer is invoked by passing its name as an option to # <tt>AbstractController::Rendering#render</tt>. To create a renderer @@ -63,11 +68,11 @@ module ActionController # ActionController::Renderers.add :csv do |obj, options| # filename = options[:filename] || 'data' # str = obj.respond_to?(:to_csv) ? obj.to_csv : obj.to_s - # send_data str, type: Mime::CSV, + # send_data str, type: Mime[:csv], # disposition: "attachment; filename=#{filename}.csv" # end # - # Note that we used Mime::CSV for the csv mime type as it comes with Rails. + # Note that we used Mime[:csv] for the csv mime type as it comes with Rails. # For a custom renderer, you'll need to register a mime type with # <tt>Mime::Type.register</tt>. # @@ -81,22 +86,21 @@ module ActionController # end # end # To use renderers and their mime types in more concise ways, see - # <tt>ActionController::MimeResponds::ClassMethods.respond_to</tt> and - # <tt>ActionController::MimeResponds#respond_with</tt> + # <tt>ActionController::MimeResponds::ClassMethods.respond_to</tt> def self.add(key, &block) - define_method("_render_option_#{key}", &block) + define_method(_render_with_renderer_method_name(key), &block) RENDERERS << key.to_sym end # This method is the opposite of add method. # - # Usage: + # To remove a csv renderer: # # ActionController::Renderers.remove(:csv) def self.remove(key) RENDERERS.delete(key.to_sym) - method = "_render_option_#{key}" - remove_method(method) if method_defined?(method) + method_name = _render_with_renderer_method_name(key) + remove_method(method_name) if method_defined?(method_name) end module All @@ -112,24 +116,24 @@ module ActionController json = json.to_json(options) unless json.kind_of?(String) if options[:callback].present? - if self.content_type.nil? || self.content_type == Mime::JSON - self.content_type = Mime::JS + if content_type.nil? || content_type == Mime[:json] + self.content_type = Mime[:js] end "/**/#{options[:callback]}(#{json})" else - self.content_type ||= Mime::JSON + self.content_type ||= Mime[:json] json end end add :js do |js, options| - self.content_type ||= Mime::JS + self.content_type ||= Mime[:js] js.respond_to?(:to_js) ? js.to_js(options) : js end add :xml do |xml, options| - self.content_type ||= Mime::XML + self.content_type ||= Mime[:xml] xml.respond_to?(:to_xml) ? xml.to_xml(options) : xml end end diff --git a/actionpack/lib/action_controller/metal/rendering.rb b/actionpack/lib/action_controller/metal/rendering.rb index 7bbff0450a..cce6fe7787 100644 --- a/actionpack/lib/action_controller/metal/rendering.rb +++ b/actionpack/lib/action_controller/metal/rendering.rb @@ -1,9 +1,29 @@ +require 'active_support/core_ext/string/filters' + module ActionController module Rendering extend ActiveSupport::Concern RENDER_FORMATS_IN_PRIORITY = [:body, :text, :plain, :html] + module ClassMethods + # Documentation at ActionController::Renderer#render + delegate :render, to: :renderer + + # Returns a renderer instance (inherited from ActionController::Renderer) + # for the controller. + attr_reader :renderer + + def setup_renderer! # :nodoc: + @renderer = Renderer.for(self) + end + + def inherited(klass) + klass.setup_renderer! + super + end + end + # Before processing, set the request formats in current controller formats. def process_action(*) #:nodoc: self.formats = request.formats.map(&:ref).compact @@ -42,13 +62,13 @@ module ActionController nil end - def _process_format(format, options = {}) - super + def _set_html_content_type + self.content_type = Mime[:html].to_s + end - if options[:plain] - self.content_type = Mime::TEXT - else - self.content_type ||= format.to_s + def _set_rendered_content_type(format) + unless response.content_type + self.content_type = format.to_s end end @@ -63,11 +83,23 @@ module ActionController def _normalize_options(options) #:nodoc: _normalize_text(options) + if options[:text] + ActiveSupport::Deprecation.warn <<-WARNING.squish + `render :text` is deprecated because it does not actually render a + `text/plain` response. Switch to `render plain: 'plain text'` to + render as `text/plain`, `render html: '<strong>HTML</strong>'` to + render as `text/html`, or `render body: 'raw'` to match the deprecated + behavior and render with the default Content-Type, which is + `text/plain`. + WARNING + end + if options[:html] options[:html] = ERB::Util.html_escape(options[:html]) end if options.delete(:nothing) + ActiveSupport::Deprecation.warn("`:nothing` option is deprecated and will be removed in Rails 5.1. Use `head` method to respond with empty response body.") options[:body] = nil end diff --git a/actionpack/lib/action_controller/metal/request_forgery_protection.rb b/actionpack/lib/action_controller/metal/request_forgery_protection.rb index 0efa0fb259..64f6f7cf51 100644 --- a/actionpack/lib/action_controller/metal/request_forgery_protection.rb +++ b/actionpack/lib/action_controller/metal/request_forgery_protection.rb @@ -1,5 +1,6 @@ require 'rack/session/abstract/id' require 'action_controller/metal/exceptions' +require 'active_support/security_utils' module ActionController #:nodoc: class InvalidAuthenticityToken < ActionControllerError #:nodoc: @@ -9,12 +10,17 @@ module ActionController #:nodoc: end # 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 + # 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 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). + # token with the token in the session. All requests are checked except GET requests + # as these should be idempotent. Keep in mind that all session-oriented requests + # should be CSRF protected, including JavaScript and HTML requests. + # + # Since HTML and JavaScript requests are typically made from the browser, we + # need to ensure to verify request authenticity for the web browser. We can + # use session-oriented authentication for these types of requests, by using + # the `protect_from_forgery` method in our controllers. # # GET requests are not protected since they don't have side effects like writing # to the database and don't leak sensitive information. JavaScript requests are @@ -25,26 +31,25 @@ module ActionController #:nodoc: # Ajax) requests are allowed to make GET requests for JavaScript responses. # # It's important to remember that XML or JSON requests are also affected and if - # you're building an API you'll need something like: + # you're building an API you should change forgery protection method in + # <tt>ApplicationController</tt> (by default: <tt>:exception</tt>): # # class ApplicationController < ActionController::Base - # protect_from_forgery - # skip_before_action :verify_authenticity_token, if: :json_request? - # - # protected - # - # def json_request? - # request.format.json? - # end + # protect_from_forgery unless: -> { request.format.json? } # end # - # CSRF protection is turned on with the <tt>protect_from_forgery</tt> method, - # 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. + # CSRF protection is turned on with the <tt>protect_from_forgery</tt> method. + # By default <tt>protect_from_forgery</tt> protects your session with + # <tt>:null_session</tt> method, which provides an empty session + # during request. + # + # We may want to disable CSRF protection for APIs since they are typically + # designed to be state-less. That is, the request API client will handle + # the session for you instead of Rails. # # 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 - # <tt>csrf_meta_tags</tt> in the html +head+. + # <tt>csrf_meta_tags</tt> in the HTML +head+. # # Learn more about CSRF attacks and securing your application in the # {Ruby on Rails Security Guide}[http://guides.rubyonrails.org/security.html]. @@ -85,13 +90,21 @@ module ActionController #:nodoc: # # class FooController < ApplicationController # protect_from_forgery except: :index + # end + # + # You can disable forgery protection on controller by skipping the verification before_action: # - # You can disable CSRF protection on controller by skipping the verification before_action: # skip_before_action :verify_authenticity_token # # Valid Options: # - # * <tt>:only/:except</tt> - Passed to the <tt>before_action</tt> call. Set which actions are verified. + # * <tt>:only/:except</tt> - Only apply forgery protection to a subset of actions. Like <tt>only: [ :create, :create_all ]</tt>. + # * <tt>:if/:unless</tt> - Turn off the forgery protection entirely depending on the passed Proc or method reference. + # * <tt>:prepend</tt> - By default, the verification of the authentication token is added to the front of the + # callback chain. If you need to make the verification depend on other callbacks, like authentication methods + # (say cookies vs OAuth), this might not work for you. Pass <tt>prepend: false</tt> to just add the + # verification callback in the position of the protect_from_forgery call. This means any callbacks added + # before are run first. # * <tt>:with</tt> - Set the method to handle unverified request. # # Valid unverified request handling methods are: @@ -99,9 +112,11 @@ module ActionController #:nodoc: # * <tt>:reset_session</tt> - Resets the session. # * <tt>:null_session</tt> - Provides an empty session during request but doesn't reset it completely. Used as default if <tt>:with</tt> option is not specified. def protect_from_forgery(options = {}) + options = options.reverse_merge(prepend: true) + self.forgery_protection_strategy = protection_method_class(options[:with] || :null_session) self.request_forgery_protection_token ||= :authenticity_token - prepend_before_action :verify_authenticity_token, options + before_action :verify_authenticity_token, options append_after_action :verify_same_origin_request end @@ -123,17 +138,17 @@ module ActionController #:nodoc: # This is the method that defines the application behavior when a request is found to be unverified. def handle_unverified_request request = @controller.request - request.session = NullSessionHash.new(request.env) - request.env['action_dispatch.request.flash_hash'] = nil - request.env['rack.session.options'] = { skip: true } - request.env['action_dispatch.cookies'] = NullCookieJar.build(request) + request.session = NullSessionHash.new(request) + request.flash = nil + request.session_options = { skip: true } + request.cookie_jar = NullCookieJar.build(request, {}) end protected class NullSessionHash < Rack::Session::Abstract::SessionHash #:nodoc: - def initialize(env) - super(nil, env) + def initialize(req) + super(nil, req) @data = {} @loaded = true end @@ -147,14 +162,6 @@ module ActionController #:nodoc: end class NullCookieJar < ActionDispatch::Cookies::CookieJar #:nodoc: - def self.build(request) - key_generator = request.env[ActionDispatch::Cookies::GENERATOR_KEY] - host = request.host - secure = request.ssl? - - new(key_generator, host, secure, options_for_env({})) - end - def write(*) # nothing end @@ -208,6 +215,7 @@ module ActionController #:nodoc: forgery_protection_strategy.new(self).handle_unverified_request end + #:nodoc: CROSS_ORIGIN_JAVASCRIPT_WARNING = "Security warning: an embedded " \ "<script> tag on another site requested protected JavaScript. " \ "If you know what you're doing, go ahead and disable forgery " \ @@ -240,20 +248,83 @@ module ActionController #:nodoc: content_type =~ %r(\Atext/javascript) && !request.xhr? end + AUTHENTICITY_TOKEN_LENGTH = 32 + # Returns true or false if a request is verified. Checks: # - # * is it a GET or HEAD request? Gets should be safe and idempotent + # * Is it a GET or HEAD request? Gets should be safe and idempotent # * Does the form_authenticity_token match the given token value from the params? # * Does the X-CSRF-Token header match the form_authenticity_token def verified_request? !protect_against_forgery? || request.get? || request.head? || - form_authenticity_token == form_authenticity_param || - form_authenticity_token == request.headers['X-CSRF-Token'] + valid_authenticity_token?(session, form_authenticity_param) || + valid_authenticity_token?(session, request.headers['X-CSRF-Token']) end # Sets the token value for the current session. def form_authenticity_token - session[:_csrf_token] ||= SecureRandom.base64(32) + masked_authenticity_token(session) + end + + # Creates a masked version of the authenticity token that varies + # on each request. The masking is used to mitigate SSL attacks + # like BREACH. + def masked_authenticity_token(session) + one_time_pad = SecureRandom.random_bytes(AUTHENTICITY_TOKEN_LENGTH) + encrypted_csrf_token = xor_byte_strings(one_time_pad, real_csrf_token(session)) + masked_token = one_time_pad + encrypted_csrf_token + Base64.strict_encode64(masked_token) + end + + # Checks the client's masked token to see if it matches the + # session token. Essentially the inverse of + # +masked_authenticity_token+. + def valid_authenticity_token?(session, encoded_masked_token) + if encoded_masked_token.nil? || encoded_masked_token.empty? || !encoded_masked_token.is_a?(String) + return false + end + + begin + masked_token = Base64.strict_decode64(encoded_masked_token) + rescue ArgumentError # encoded_masked_token is invalid Base64 + return false + end + + # See if it's actually a masked token or not. In order to + # deploy this code, we should be able to handle any unmasked + # tokens that we've issued without error. + + if masked_token.length == AUTHENTICITY_TOKEN_LENGTH + # This is actually an unmasked token. This is expected if + # you have just upgraded to masked tokens, but should stop + # happening shortly after installing this gem + compare_with_real_token masked_token, session + + elsif masked_token.length == AUTHENTICITY_TOKEN_LENGTH * 2 + # Split the token into the one-time pad and the encrypted + # value and decrypt it + one_time_pad = masked_token[0...AUTHENTICITY_TOKEN_LENGTH] + encrypted_csrf_token = masked_token[AUTHENTICITY_TOKEN_LENGTH..-1] + csrf_token = xor_byte_strings(one_time_pad, encrypted_csrf_token) + + compare_with_real_token csrf_token, session + + else + false # Token is malformed + end + end + + def compare_with_real_token(token, session) + ActiveSupport::SecurityUtils.secure_compare(token, real_csrf_token(session)) + end + + def real_csrf_token(session) + session[:_csrf_token] ||= SecureRandom.base64(AUTHENTICITY_TOKEN_LENGTH) + Base64.strict_decode64(session[:_csrf_token]) + end + + def xor_byte_strings(s1, s2) + s1.bytes.zip(s2.bytes).map { |(c1,c2)| c1 ^ c2 }.pack('c*') end # The form's authenticity parameter. Override to provide your own. diff --git a/actionpack/lib/action_controller/metal/responder.rb b/actionpack/lib/action_controller/metal/responder.rb deleted file mode 100644 index 5096558c67..0000000000 --- a/actionpack/lib/action_controller/metal/responder.rb +++ /dev/null @@ -1,297 +0,0 @@ -require 'active_support/json' - -module ActionController #:nodoc: - # Responsible for exposing a resource to different mime requests, - # usually depending on the HTTP verb. The responder is triggered when - # <code>respond_with</code> is called. The simplest case to study is a GET request: - # - # class PeopleController < ApplicationController - # respond_to :html, :xml, :json - # - # def index - # @people = Person.all - # respond_with(@people) - # end - # end - # - # When a request comes in, for example for an XML response, three steps happen: - # - # 1) the responder searches for a template at people/index.xml; - # - # 2) if the template is not available, it will invoke <code>#to_xml</code> on the given resource; - # - # 3) if the responder does not <code>respond_to :to_xml</code>, call <code>#to_format</code> on it. - # - # === Built-in HTTP verb semantics - # - # The default \Rails responder holds semantics for each HTTP verb. Depending on the - # content type, verb and the resource status, it will behave differently. - # - # Using \Rails default responder, a POST request for creating an object could - # be written as: - # - # def create - # @user = User.new(params[:user]) - # flash[:notice] = 'User was successfully created.' if @user.save - # respond_with(@user) - # end - # - # Which is exactly the same as: - # - # 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, status: :created, location: @user } - # else - # format.html { render action: "new" } - # format.xml { render xml: @user.errors, status: :unprocessable_entity } - # end - # end - # end - # - # The same happens for PATCH/PUT and DELETE requests. - # - # === Nested resources - # - # You can supply nested resources as you do in <code>form_for</code> and <code>polymorphic_url</code>. - # Consider the project has many tasks example. The create action for - # TasksController would be like: - # - # def create - # @project = Project.find(params[:project_id]) - # @task = @project.tasks.build(params[:task]) - # flash[:notice] = 'Task was successfully created.' if @task.save - # respond_with(@project, @task) - # end - # - # 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 - # polymorphic urls. If a project has one manager which has many tasks, it - # should be invoked as: - # - # 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 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 - # @project = Project.find(params[:project_id]) - # @task = @project.tasks.build(params[:task]) - # flash[:notice] = 'Task was successfully created.' if @task.save - # respond_with(@project, @task, status: 201) - # end - # - # 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. You can also override the location to redirect to: - # - # respond_with(@project, location: root_path) - # - # To customize the failure scenario, you can pass a block to - # <code>respond_with</code>: - # - # def create - # @project = Project.find(params[:project_id]) - # @task = @project.tasks.build(params[:task]) - # respond_with(@project, @task, status: 201) do |format| - # if @task.save - # flash[:notice] = 'Task was successfully created.' - # else - # format.html { render "some_special_template" } - # end - # end - # end - # - # Using <code>respond_with</code> with a block follows the same syntax as <code>respond_to</code>. - class Responder - attr_reader :controller, :request, :format, :resource, :resources, :options - - DEFAULT_ACTIONS_FOR_VERBS = { - :post => :new, - :patch => :edit, - :put => :edit - } - - def initialize(controller, resources, options={}) - @controller = controller - @request = @controller.request - @format = @controller.formats.first - @resource = resources.last - @resources = resources - @options = options - @action = options.delete(:action) - @default_response = options.delete(:default_response) - end - - delegate :head, :render, :redirect_to, :to => :controller - 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) - undef_method(:to_yaml) if method_defined?(:to_yaml) - - # Initializes a new responder and invokes the proper format. If the format is - # not defined, call to_format. - # - def self.call(*args) - new(*args).respond - end - - # Main entry point for responder responsible to dispatch to the proper format. - # - def respond - method = "to_#{format}" - respond_to?(method) ? send(method) : to_format - end - - # HTML format does not render the resource, it always attempt to render a - # template. - # - def to_html - default_render - rescue ActionView::MissingTemplate => e - 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? || response_overridden? - default_render - else - display_errors - end - rescue ActionView::MissingTemplate => e - api_behavior(e) - end - - protected - - # This is the common behavior for formats associated with browsing, like :html, :iphone and so forth. - def navigation_behavior(error) - if get? - raise error - elsif has_errors? && default_action - render :action => default_action - else - redirect_to navigation_location - end - end - - # This is the common behavior for formats associated with APIs, such as :xml and :json. - def api_behavior(error) - raise error unless resourceful? - raise MissingRenderer.new(format) unless has_renderer? - - if get? - display resource - elsif post? - display resource, :status => :created, :location => api_location - else - head :no_content - end - end - - # Checks whether the resource responds to the current format or not. - # - def resourceful? - resource.respond_to?("to_#{format}") - end - - # Returns the resource location by retrieving it from the options or - # returning the resources array. - # - def resource_location - options[:location] || resources - end - alias :navigation_location :resource_location - alias :api_location :resource_location - - # If a response block was given, use it, otherwise call render on - # controller. - # - def default_render - 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. - # - # display @user, status: :ok - # - # For XML requests it's equivalent to: - # - # render xml: @user, status: :ok - # - # Options sent by the user are also used: - # - # respond_with(@user, status: :created) - # display(@user, status: :ok) - # - # Results in: - # - # render xml: @user, status: :created - # - def display(resource, given_options={}) - controller.render given_options.merge!(options).merge!(format => resource) - end - - def display_errors - controller.render format => resource_errors, :status => :unprocessable_entity - end - - # Check whether the resource has errors. - # - def has_errors? - resource.respond_to?(:errors) && !resource.errors.empty? - end - - # Check whether the necessary Renderer is available - def has_renderer? - Renderers::RENDERERS.include?(format) - end - - # By default, render the <code>:edit</code> action for HTML requests with errors, unless - # the verb was POST. - # - def default_action - @action ||= DEFAULT_ACTIONS_FOR_VERBS[request.request_method_symbol] - end - - def resource_errors - respond_to?("#{format}_resource_errors", true) ? send("#{format}_resource_errors") : resource.errors - end - - def json_resource_errors - {:errors => resource.errors} - end - - def response_overridden? - @default_response.present? - end - end -end diff --git a/actionpack/lib/action_controller/metal/streaming.rb b/actionpack/lib/action_controller/metal/streaming.rb index 04401cad7b..a6115674aa 100644 --- a/actionpack/lib/action_controller/metal/streaming.rb +++ b/actionpack/lib/action_controller/metal/streaming.rb @@ -110,9 +110,9 @@ module ActionController #:nodoc: # 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. + # assets, which kills the purpose of streaming. For this purpose, you can use + # a 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: # @@ -199,7 +199,7 @@ module ActionController #:nodoc: def _process_options(options) #:nodoc: super if options[:stream] - if env["HTTP_VERSION"] == "HTTP/1.0" + if request.version == "HTTP/1.0" options.delete(:stream) else headers["Cache-Control"] ||= "no-cache" diff --git a/actionpack/lib/action_controller/metal/strong_parameters.rb b/actionpack/lib/action_controller/metal/strong_parameters.rb index bc27ecaa20..130ba61786 100644 --- a/actionpack/lib/action_controller/metal/strong_parameters.rb +++ b/actionpack/lib/action_controller/metal/strong_parameters.rb @@ -1,6 +1,6 @@ require 'active_support/core_ext/hash/indifferent_access' require 'active_support/core_ext/array/wrap' -require 'active_support/deprecation' +require 'active_support/core_ext/string/filters' require 'active_support/rescuable' require 'action_dispatch/http/upload' require 'stringio' @@ -11,9 +11,9 @@ module ActionController # # params = ActionController::Parameters.new(a: {}) # params.fetch(:b) - # # => ActionController::ParameterMissing: param not found: b + # # => ActionController::ParameterMissing: param is missing or the value is empty: b # params.require(:a) - # # => ActionController::ParameterMissing: param not found: a + # # => ActionController::ParameterMissing: param is missing or the value is empty: a class ParameterMissing < KeyError attr_reader :param # :nodoc: @@ -23,11 +23,13 @@ module ActionController end end - # Raised when a supplied parameter is not expected. + # Raised when a supplied parameter is not expected and + # ActionController::Parameters.action_on_unpermitted_parameters + # is set to <tt>:raise</tt>. # # params = ActionController::Parameters.new(a: "123", b: "456") # params.permit(:c) - # # => ActionController::UnpermittedParameters: found unexpected keys: a, b + # # => ActionController::UnpermittedParameters: found unpermitted parameters: a, b class UnpermittedParameters < IndexError attr_reader :params # :nodoc: @@ -91,17 +93,22 @@ module ActionController # params.permit(:c) # # => ActionController::UnpermittedParameters: found unpermitted keys: a, b # - # <tt>ActionController::Parameters</tt> is inherited from - # <tt>ActiveSupport::HashWithIndifferentAccess</tt>, this means - # that you can fetch values using either <tt>:key</tt> or <tt>"key"</tt>. + # Please note that these options *are not thread-safe*. In a multi-threaded + # environment they should only be set once at boot-time and never mutated at + # runtime. + # + # You can fetch values of <tt>ActionController::Parameters</tt> using either + # <tt>:key</tt> or <tt>"key"</tt>. # # params = ActionController::Parameters.new(key: 'value') # params[:key] # => "value" # params["key"] # => "value" - class Parameters < ActiveSupport::HashWithIndifferentAccess + class Parameters cattr_accessor :permit_all_parameters, instance_accessor: false cattr_accessor :action_on_unpermitted_parameters, instance_accessor: false + delegate :keys, :key?, :has_key?, :empty?, :inspect, to: :@parameters + # By default, never raise an UnpermittedParameters exception if these # params are present. The default includes both 'controller' and 'action' # because they are added by Rails and should be of no concern. One way @@ -113,11 +120,13 @@ module ActionController self.always_permitted_parameters = %w( controller action ) def self.const_missing(const_name) - super unless const_name == :NEVER_UNPERMITTED_PARAMS - ActiveSupport::Deprecation.warn "`ActionController::Parameters::NEVER_UNPERMITTED_PARAMS`"\ - " has been deprecated. Use "\ - "`ActionController::Parameters.always_permitted_parameters` instead." - self.always_permitted_parameters + return super unless const_name == :NEVER_UNPERMITTED_PARAMS + ActiveSupport::Deprecation.warn(<<-MSG.squish) + `ActionController::Parameters::NEVER_UNPERMITTED_PARAMS` has been deprecated. + Use `ActionController::Parameters.always_permitted_parameters` instead. + MSG + + always_permitted_parameters end # Returns a new instance of <tt>ActionController::Parameters</tt>. @@ -136,11 +145,56 @@ module ActionController # params = ActionController::Parameters.new(name: 'Francesco') # params.permitted? # => true # Person.new(params) # => #<Person id: nil, name: "Francesco"> - def initialize(attributes = nil) - super(attributes) + def initialize(parameters = {}) + @parameters = parameters.with_indifferent_access @permitted = self.class.permit_all_parameters end + # Returns true if another +Parameters+ object contains the same content and + # permitted flag, or other Hash-like object contains the same content. This + # override is in place so you can perform a comparison with `Hash`. + def ==(other_hash) + if other_hash.respond_to?(:permitted?) + super + else + @parameters == other_hash + end + end + + # Returns a safe +Hash+ representation of this parameter with all + # unpermitted keys removed. + # + # params = ActionController::Parameters.new({ + # name: 'Senjougahara Hitagi', + # oddity: 'Heavy stone crab' + # }) + # params.to_h # => {} + # + # safe_params = params.permit(:name) + # safe_params.to_h # => {"name"=>"Senjougahara Hitagi"} + def to_h + if permitted? + @parameters.to_h + else + slice(*self.class.always_permitted_parameters).permit!.to_h + end + end + + # Returns an unsafe, unfiltered +Hash+ representation of this parameter. + def to_unsafe_h + @parameters.to_h + end + alias_method :to_unsafe_hash, :to_unsafe_h + + # Convert all hashes in values into parameters, then yield each pair like + # the same way as <tt>Hash#each_pair</tt> + def each_pair(&block) + @parameters.each_pair do |key, value| + yield key, convert_hashes_to_parameters(key, value) + end + end + alias_method :each, :each_pair + # Attribute that keeps track of converted arrays, if any, to avoid double # looping in the common use case permit + mass-assignment. Defined in a # method to instantiate it only if needed. @@ -176,7 +230,6 @@ module ActionController # Person.new(params) # => #<Person id: nil, name: "Francesco"> def permit! each_pair do |key, value| - value = convert_hashes_to_parameters(key, value) Array.wrap(value).each do |v| v.permit! if v.respond_to? :permit! end @@ -186,19 +239,58 @@ module ActionController self end - # Ensures that a parameter is present. If it's present, returns - # the parameter at the given +key+, otherwise raises an - # <tt>ActionController::ParameterMissing</tt> error. + # This method accepts both a single key and an array of keys. + # + # When passed a single key, if it exists and its associated value is + # either present or the singleton +false+, returns said value: # # ActionController::Parameters.new(person: { name: 'Francesco' }).require(:person) # # => {"name"=>"Francesco"} # + # Otherwise raises <tt>ActionController::ParameterMissing</tt>: + # + # ActionController::Parameters.new.require(:person) + # # ActionController::ParameterMissing: param is missing or the value is empty: person + # # ActionController::Parameters.new(person: nil).require(:person) - # # => ActionController::ParameterMissing: param not found: person + # # ActionController::ParameterMissing: param is missing or the value is empty: person + # + # ActionController::Parameters.new(person: "\t").require(:person) + # # ActionController::ParameterMissing: param is missing or the value is empty: person # # ActionController::Parameters.new(person: {}).require(:person) - # # => ActionController::ParameterMissing: param not found: person + # # ActionController::ParameterMissing: param is missing or the value is empty: person + # + # When given an array of keys, the method tries to require each one of them + # in order. If it succeeds, an array with the respective return values is + # returned: + # + # params = ActionController::Parameters.new(user: { ... }, profile: { ... }) + # user_params, profile_params = params.require(:user, :profile) + # + # Otherwise, the method reraises the first exception found: + # + # params = ActionController::Parameters.new(user: {}, profile: {}) + # user_params, profile_params = params.require(:user, :profile) + # # ActionController::ParameterMissing: param is missing or the value is empty: user + # + # Technically this method can be used to fetch terminal values: + # + # # CAREFUL + # params = ActionController::Parameters.new(person: { name: 'Finn' }) + # name = params.require(:person).require(:name) # CAREFUL + # + # but take into account that at some point those ones have to be permitted: + # + # def person_params + # params.require(:person).permit(:name).tap do |person_params| + # person_params.require(:name) # SAFER + # end + # end + # + # for example. def require(key) + return key.map { |k| require(k) } if key.is_a?(Array) value = self[key] if value.present? || value == false value @@ -226,7 +318,7 @@ module ActionController # # params.permit(:name) # - # +:name+ passes it is a key of +params+ whose associated value is of type + # +:name+ passes if it is a key of +params+ whose associated value is of type # +String+, +Symbol+, +NilClass+, +Numeric+, +TrueClass+, +FalseClass+, # +Date+, +Time+, +DateTime+, +StringIO+, +IO+, # +ActionDispatch::Http::UploadedFile+ or +Rack::Test::UploadedFile+. @@ -303,7 +395,13 @@ module ActionController # params[:person] # => {"name"=>"Francesco"} # params[:none] # => nil def [](key) - convert_hashes_to_parameters(key, super) + convert_hashes_to_parameters(key, @parameters[key]) + end + + # Assigns a value to a given +key+. The given key may still get filtered out + # when +permit+ is called. + def []=(key, value) + @parameters[key] = value end # Returns a parameter for the given +key+. If the +key+ @@ -314,13 +412,19 @@ module ActionController # # params = ActionController::Parameters.new(person: { name: 'Francesco' }) # params.fetch(:person) # => {"name"=>"Francesco"} - # params.fetch(:none) # => ActionController::ParameterMissing: param not found: none + # params.fetch(:none) # => ActionController::ParameterMissing: param is missing or the value is empty: none # params.fetch(:none, 'Francesco') # => "Francesco" # params.fetch(:none) { 'Francesco' } # => "Francesco" - def fetch(key, *args) - convert_hashes_to_parameters(key, super, false) - rescue KeyError - raise ActionController::ParameterMissing.new(key) + def fetch(key, *args, &block) + convert_value_to_parameters( + @parameters.fetch(key) { + if block_given? + yield + else + args.fetch(0) { raise ActionController::ParameterMissing.new(key) } + end + } + ) end # Returns a new <tt>ActionController::Parameters</tt> instance that @@ -331,11 +435,117 @@ module ActionController # params.slice(:a, :b) # => {"a"=>1, "b"=>2} # params.slice(:d) # => {} def slice(*keys) - self.class.new(super).tap do |new_instance| - new_instance.permitted = @permitted + new_instance_with_inherited_permitted_status(@parameters.slice(*keys)) + end + + # Returns current <tt>ActionController::Parameters</tt> instance which + # contains only the given +keys+. + def slice!(*keys) + @parameters.slice!(*keys) + self + end + + # Returns a new <tt>ActionController::Parameters</tt> instance that + # filters out the given +keys+. + # + # params = ActionController::Parameters.new(a: 1, b: 2, c: 3) + # params.except(:a, :b) # => {"c"=>3} + # params.except(:d) # => {"a"=>1,"b"=>2,"c"=>3} + def except(*keys) + new_instance_with_inherited_permitted_status(@parameters.except(*keys)) + end + + # Removes and returns the key/value pairs matching the given keys. + # + # params = ActionController::Parameters.new(a: 1, b: 2, c: 3) + # params.extract!(:a, :b) # => {"a"=>1, "b"=>2} + # params # => {"c"=>3} + def extract!(*keys) + new_instance_with_inherited_permitted_status(@parameters.extract!(*keys)) + end + + # Returns a new <tt>ActionController::Parameters</tt> with the results of + # running +block+ once for every value. The keys are unchanged. + # + # params = ActionController::Parameters.new(a: 1, b: 2, c: 3) + # params.transform_values { |x| x * 2 } + # # => {"a"=>2, "b"=>4, "c"=>6} + def transform_values(&block) + if block + new_instance_with_inherited_permitted_status( + @parameters.transform_values(&block) + ) + else + @parameters.transform_values end end + # Performs values transformation and returns the altered + # <tt>ActionController::Parameters</tt> instance. + def transform_values!(&block) + @parameters.transform_values!(&block) + self + end + + # Returns a new <tt>ActionController::Parameters</tt> instance with the + # results of running +block+ once for every key. The values are unchanged. + def transform_keys(&block) + if block + new_instance_with_inherited_permitted_status( + @parameters.transform_keys(&block) + ) + else + @parameters.transform_keys + end + end + + # Performs keys transformation and returns the altered + # <tt>ActionController::Parameters</tt> instance. + def transform_keys!(&block) + @parameters.transform_keys!(&block) + self + end + + # Deletes and returns a key-value pair from +Parameters+ whose key is equal + # to key. If the key is not found, returns the default value. If the + # optional code block is given and the key is not found, pass in the key + # and return the result of block. + def delete(key, &block) + convert_value_to_parameters(@parameters.delete(key)) + end + + # Returns a new instance of <tt>ActionController::Parameters</tt> with only + # items that the block evaluates to true. + def select(&block) + new_instance_with_inherited_permitted_status(@parameters.select(&block)) + end + + # Equivalent to Hash#keep_if, but returns nil if no changes were made. + def select!(&block) + @parameters.select!(&block) + self + end + alias_method :keep_if, :select! + + # Returns a new instance of <tt>ActionController::Parameters</tt> with items + # that the block evaluates to true removed. + def reject(&block) + new_instance_with_inherited_permitted_status(@parameters.reject(&block)) + end + + # Removes items that the block evaluates to true and returns self. + def reject!(&block) + @parameters.reject!(&block) + self + end + alias_method :delete_if, :reject! + + # Returns values that were assigned to the given +keys+. Note that all the + # +Hash+ objects will be converted to <tt>ActionController::Parameters</tt>. + def values_at(*keys) + convert_value_to_parameters(@parameters.values_at(*keys)) + end + # Returns an exact copy of the <tt>ActionController::Parameters</tt> # instance. +permitted+ state is kept on the duped object. # @@ -350,46 +560,72 @@ module ActionController end end + # Returns a new <tt>ActionController::Parameters</tt> with all keys from + # +other_hash+ merges into current hash. + def merge(other_hash) + new_instance_with_inherited_permitted_status( + @parameters.merge(other_hash) + ) + end + + # This is required by ActiveModel attribute assignment, so that user can + # pass +Parameters+ to a mass assignment methods in a model. It should not + # matter as we are using +HashWithIndifferentAccess+ internally. + def stringify_keys # :nodoc: + dup + end + protected def permitted=(new_permitted) @permitted = new_permitted end + def fields_for_style? + @parameters.all? { |k, v| k =~ /\A-?\d+\z/ && v.is_a?(Hash) } + end + private - def convert_hashes_to_parameters(key, value, assign_if_converted=true) + def new_instance_with_inherited_permitted_status(hash) + self.class.new(hash).tap do |new_instance| + new_instance.permitted = @permitted + end + end + + def convert_hashes_to_parameters(key, value) converted = convert_value_to_parameters(value) - self[key] = converted if assign_if_converted && !converted.equal?(value) + @parameters[key] = converted unless converted.equal?(value) converted end def convert_value_to_parameters(value) - if value.is_a?(Array) && !converted_arrays.member?(value) + case value + when Array + return value if converted_arrays.member?(value) converted = value.map { |_| convert_value_to_parameters(_) } converted_arrays << converted converted - elsif value.is_a?(Parameters) || !value.is_a?(Hash) - value - else + when Hash self.class.new(value) + else + value end end def each_element(object) - if object.is_a?(Array) - object.map { |el| yield el }.compact - elsif fields_for_style?(object) - hash = object.class.new - object.each { |k,v| hash[k] = yield v } - hash - else - yield object + case object + when Array + object.grep(Parameters).map { |el| yield el }.compact + when Parameters + if object.fields_for_style? + hash = object.class.new + object.each { |k,v| hash[k] = yield v } + hash + else + yield object + end end end - def fields_for_style?(object) - object.is_a?(Hash) && object.all? { |k, v| k =~ /\A-?\d+\z/ && v.is_a?(Hash) } - end - def unpermitted_parameters!(params) unpermitted_keys = unpermitted_keys(params) if unpermitted_keys.any? @@ -451,14 +687,8 @@ module ActionController end def array_of_permitted_scalars?(value) - if value.is_a?(Array) - value.all? {|element| permitted_scalar?(element)} - end - end - - def array_of_permitted_scalars_filter(params, key) - if has_key?(key) && array_of_permitted_scalars?(self[key]) - params[key] = self[key] + if value.is_a?(Array) && value.all? {|element| permitted_scalar?(element)} + yield value end end @@ -469,17 +699,17 @@ module ActionController # Slicing filters out non-declared keys. slice(*filter.keys).each do |key, value| next unless value + next unless has_key? key if filter[key] == EMPTY_ARRAY # Declaration { comment_ids: [] }. - array_of_permitted_scalars_filter(params, key) + array_of_permitted_scalars?(self[key]) do |val| + params[key] = val + end else # Declaration { user: :name } or { user: [:name, :age, { address: ... }] }. params[key] = each_element(value) do |element| - if element.is_a?(Hash) - element = self.class.new(element) unless element.respond_to?(:permit) - element.permit(*Array.wrap(filter[key])) - end + element.permit(*Array.wrap(filter[key])) end end end diff --git a/actionpack/lib/action_controller/metal/testing.rb b/actionpack/lib/action_controller/metal/testing.rb index dd8da4b5dc..b2b3b4283f 100644 --- a/actionpack/lib/action_controller/metal/testing.rb +++ b/actionpack/lib/action_controller/metal/testing.rb @@ -2,14 +2,6 @@ module ActionController module Testing extend ActiveSupport::Concern - include RackDelegation - - # TODO : Rewrite tests using controller.headers= to use Rack env - def headers=(new_headers) - @_response ||= ActionDispatch::Response.new - @_response.headers.replace(new_headers) - end - # Behavior specific to functional tests module Functional # :nodoc: def set_response!(request) @@ -24,7 +16,7 @@ module ActionController module ClassMethods def before_filters - _process_action_callbacks.find_all{|x| x.kind == :before}.map{|x| x.name} + _process_action_callbacks.find_all{|x| x.kind == :before}.map(&:name) end end end diff --git a/actionpack/lib/action_controller/metal/url_for.rb b/actionpack/lib/action_controller/metal/url_for.rb index 07265be3fe..dbf7241a14 100644 --- a/actionpack/lib/action_controller/metal/url_for.rb +++ b/actionpack/lib/action_controller/metal/url_for.rb @@ -4,7 +4,10 @@ module ActionController # # 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. + # to implement +env+ which needs to be Rack-compatible and +request+ + # which is either an instance of +ActionDispatch::Request+ or an object + # that responds to the +host+, +optional_port+, +protocol+ and + # +symbolized_path_parameter+ methods. # # class RootUrl # include ActionController::UrlFor @@ -28,20 +31,23 @@ module ActionController :port => request.optional_port, :protocol => request.protocol, :_recall => request.path_parameters - }.merge(super).freeze + }.merge!(super).freeze - if (same_origin = _routes.equal?(env["action_dispatch.routes".freeze])) || - (script_name = env["ROUTES_#{_routes.object_id}_SCRIPT_NAME"]) || - (original_script_name = env['ORIGINAL_SCRIPT_NAME'.freeze]) + if (same_origin = _routes.equal?(request.routes)) || + (script_name = request.engine_script_name(_routes)) || + (original_script_name = request.original_script_name) - @_url_options.dup.tap do |options| - if original_script_name - options[:original_script_name] = original_script_name + options = @_url_options.dup + if original_script_name + options[:original_script_name] = original_script_name + else + if same_origin + options[:script_name] = request.script_name.empty? ? "".freeze : request.script_name.dup else - options[:script_name] = same_origin ? request.script_name.dup : script_name + options[:script_name] = script_name end - options.freeze end + options.freeze else @_url_options end |