diff options
Diffstat (limited to 'actionpack')
28 files changed, 867 insertions, 115 deletions
diff --git a/actionpack/CHANGELOG.md b/actionpack/CHANGELOG.md index 36e8479441..9f0f214137 100644 --- a/actionpack/CHANGELOG.md +++ b/actionpack/CHANGELOG.md @@ -1,6 +1,67 @@ ## Rails 4.0.0 (unreleased) ## -* Add .rb template handler, this handler simply allows arbitrary Ruby code as a template. *Guillermo Iguaran* +* Add `ActionController::StrongParameters`, this module converts `params` hash into + an instance of ActionController::Parameters that allows whitelisting of permitted + parameters. Non-permitted parameters are forbidden to be used in Active Model by default + For more details check the documentation of the module or the + [strong_parameters gem](https://github.com/rails/strong_parameters) + + *DHH + Guillermo Iguaran* + +* Remove Integration between `attr_accessible`/`attr_protected` and + `ActionController::ParamsWrapper`. ParamWrapper now wraps all the parameters returned + by the class method attribute_names + + *Guillermo Iguaran* + +* Fix #7646, the log now displays the correct status code when an exception is raised. + + *Yves Senn* + +* Allow pass couple extensions to `ActionView::Template.register_template_handler` call. *Tima Maslyuchenko* + +* Fixed a bug with shorthand routes scoped with the `:module` option not + adding the module to the controller as described in issue #6497. + This should now work properly: + + scope :module => "engine" do + get "api/version" # routes to engine/api#version + end + + *Luiz Felipe Garcia Pereira* + +* Sprockets integration has been extracted from Action Pack and the `sprockets-rails` + gem should be added to Gemfile (under the assets group) in order to use Rails asset + pipeline in future versions of Rails. + + *Guillermo Iguaran* + +* `ActionDispatch::Session::MemCacheStore` now uses `dalli` instead of the deprecated + `memcache-client` gem. As side effect the autoloading of unloaded classes objects + saved as values in session isn't supported anymore when mem_cache session store is + used, this can have an impact in apps only when config.cache_classes is false. + + *Arun Agrawal + Guillermo Iguaran* + +* Support multiple etags in If-None-Match header. *Travis Warlick* + +* Allow to configure how unverified request will be handled using `:with` + option in `protect_from_forgery` method. + + Valid unverified request handling methods are: + + - `:exception` - Raises ActionController::InvalidAuthenticityToken exception. + - `:reset_session` - Resets the session. + - `:null_session` - Provides an empty session during request but doesn't + reset it completely. Used as default if `:with` option is not specified. + + New applications are generated with: + + protect_from_forgery :with => :exception + + *Sergey Nartimov* + +* Add .ruby template handler, this handler simply allows arbitrary Ruby code as a template. *Guillermo Iguaran* * Add `separator` option for `ActionView::Helpers::TextHelper#excerpt`: diff --git a/actionpack/lib/action_controller.rb b/actionpack/lib/action_controller.rb index 8c1f548e47..1a13d7af29 100644 --- a/actionpack/lib/action_controller.rb +++ b/actionpack/lib/action_controller.rb @@ -2,6 +2,7 @@ require 'active_support/rails' require 'abstract_controller' require 'action_dispatch' require 'action_controller/metal/live' +require 'action_controller/metal/strong_parameters' module ActionController extend ActiveSupport::Autoload @@ -34,6 +35,7 @@ module ActionController autoload :Rescue autoload :Responder autoload :Streaming + autoload :StrongParameters autoload :Testing autoload :UrlFor end diff --git a/actionpack/lib/action_controller/base.rb b/actionpack/lib/action_controller/base.rb index f829f5e8a2..6b8d9384d4 100644 --- a/actionpack/lib/action_controller/base.rb +++ b/actionpack/lib/action_controller/base.rb @@ -162,7 +162,24 @@ module ActionController class Base < Metal abstract! - # Shortcut helper that returns all the ActionController::Base modules except the ones passed in the argument: + # We document the request and response methods here because albeit they are + # implemented in ActionController::Metal, the type of the returned objects + # is unknown at that level. + + ## + # :method: request + # + # Returns an ActionDispatch::Request instance that represents the + # current request. + + ## + # :method: response + # + # Returns an ActionDispatch::Response that represents the current + # response. + + # Shortcut helper that returns all the modules included in + # ActionController::Base except the ones passed as arguments: # # class MetalController # ActionController::Base.without_modules(:ParamsWrapper, :Streaming).each do |left| @@ -170,8 +187,9 @@ module ActionController # end # end # - # This gives better control over what you want to exclude and makes it easier - # to create a bare controller class, instead of listing the modules required manually. + # This gives better control over what you want to exclude and makes it + # easier to create a bare controller class, instead of listing the modules + # required manually. def self.without_modules(*modules) modules = modules.map do |m| m.is_a?(Symbol) ? ActionController.const_get(m) : m @@ -196,6 +214,7 @@ module ActionController Caching, MimeResponds, ImplicitRender, + StrongParameters, Cookies, Flash, diff --git a/actionpack/lib/action_controller/log_subscriber.rb b/actionpack/lib/action_controller/log_subscriber.rb index a7c0e971e7..f41d1bb4b9 100644 --- a/actionpack/lib/action_controller/log_subscriber.rb +++ b/actionpack/lib/action_controller/log_subscriber.rb @@ -19,7 +19,8 @@ module ActionController status = payload[:status] if status.nil? && payload[:exception].present? - status = ActionDispatch::ExceptionWrapper.new({}, payload[:exception]).status_code + exception_class_name = payload[:exception].first + status = ActionDispatch::ExceptionWrapper.status_code_for_exception(exception_class_name) end message = "Completed #{status} #{Rack::Utils::HTTP_STATUS_CODES[status]} in %.0fms" % event.duration message << " (#{additions.join(" | ")})" unless additions.blank? diff --git a/actionpack/lib/action_controller/metal.rb b/actionpack/lib/action_controller/metal.rb index b38f990efa..f5ab1e2350 100644 --- a/actionpack/lib/action_controller/metal.rb +++ b/actionpack/lib/action_controller/metal.rb @@ -112,7 +112,7 @@ module ActionController # ==== Returns # * <tt>string</tt> def self.controller_name - @controller_name ||= self.name.demodulize.sub(/Controller$/, '').underscore + @controller_name ||= name.demodulize.sub(/Controller$/, '').underscore end # Delegates to the class' <tt>controller_name</tt> @@ -203,36 +203,29 @@ module ActionController class_attribute :middleware_stack self.middleware_stack = ActionController::MiddlewareStack.new - def self.inherited(base) #nodoc: - base.middleware_stack = self.middleware_stack.dup + def self.inherited(base) # :nodoc: + base.middleware_stack = middleware_stack.dup super end - # Adds given middleware class and its args to bottom of middleware_stack + # Pushes the given Rack middleware and its arguments to the bottom of the + # middleware stack. def self.use(*args, &block) middleware_stack.use(*args, &block) end - # Alias for middleware_stack + # Alias for +middleware_stack+. def self.middleware middleware_stack end - # Makes the controller a rack endpoint that points to the action in - # the given env's action_dispatch.request.path_parameters key. + # Makes the controller a Rack endpoint that runs the action in the given + # +env+'s +action_dispatch.request.path_parameters+ key. def self.call(env) action(env['action_dispatch.request.path_parameters'][:action]).call(env) end - # Return a rack endpoint for the given action. Memoize the endpoint, so - # multiple calls into MyController.action will return the same object - # for the same action. - # - # ==== Parameters - # * <tt>action</tt> - An action name - # - # ==== Returns - # * <tt>proc</tt> - A rack application + # Returns a Rack endpoint for the given action name. def self.action(name, klass = ActionDispatch::Request) middleware_stack.build(name.to_s) do |env| new.dispatch(name, klass.new(env)) diff --git a/actionpack/lib/action_controller/metal/params_wrapper.rb b/actionpack/lib/action_controller/metal/params_wrapper.rb index 2736948ce0..88b9e78da7 100644 --- a/actionpack/lib/action_controller/metal/params_wrapper.rb +++ b/actionpack/lib/action_controller/metal/params_wrapper.rb @@ -15,7 +15,7 @@ module ActionController # a non-empty array: # # class UsersController < ApplicationController - # wrap_parameters :format => [:json, :xml] + # wrap_parameters format: [:json, :xml] # end # # If you enable +ParamsWrapper+ for +:json+ format, instead of having to @@ -38,13 +38,12 @@ module ActionController # +:exclude+ options like this: # # class UsersController < ApplicationController - # wrap_parameters :person, :include => [:username, :password] + # wrap_parameters :person, include: [:username, :password] # end # # On ActiveRecord models with no +:include+ or +:exclude+ option set, - # if attr_accessible is set on that model, it will only wrap the accessible - # parameters, else it will only wrap the parameters returned by the class - # method attribute_names. + # it will only wrap the parameters returned by the class method + # <tt>attribute_names</tt>. # # If you're going to pass the parameters to an +ActiveModel+ object (such as # <tt>User.new(params[:user])</tt>), you might consider passing the model class to @@ -165,10 +164,7 @@ module ActionController unless options[:include] || options[:exclude] model ||= _default_wrap_model - role = options.fetch(:as, :default) - if model.respond_to?(:accessible_attributes) && model.accessible_attributes(role).present? - options[:include] = model.accessible_attributes(role).to_a - elsif model.respond_to?(:attribute_names) && model.attribute_names.present? + if model.respond_to?(:attribute_names) && model.attribute_names.present? options[:include] = model.attribute_names end end diff --git a/actionpack/lib/action_controller/metal/request_forgery_protection.rb b/actionpack/lib/action_controller/metal/request_forgery_protection.rb index d5f1cbc1a8..17d4a793ac 100644 --- a/actionpack/lib/action_controller/metal/request_forgery_protection.rb +++ b/actionpack/lib/action_controller/metal/request_forgery_protection.rb @@ -1,3 +1,4 @@ +require 'rack/session/abstract/id' require 'action_controller/metal/exceptions' module ActionController #:nodoc: @@ -49,10 +50,6 @@ module ActionController #:nodoc: config_accessor :request_forgery_protection_token self.request_forgery_protection_token ||= :authenticity_token - # Controls how unverified request will be handled - config_accessor :request_forgery_protection_method - self.request_forgery_protection_method ||= :reset_session - # Controls whether request forgery protection is turned on or not. Turned off by default only in test mode. config_accessor :allow_forgery_protection self.allow_forgery_protection = true if allow_forgery_protection.nil? @@ -78,12 +75,80 @@ module ActionController #:nodoc: # Valid Options: # # * <tt>:only/:except</tt> - Passed to the <tt>before_filter</tt> call. Set which actions are verified. - # * <tt>:with</tt> - Set the method to handle unverified request. Valid values: <tt>:exception</tt> and <tt>:reset_session</tt> (default). + # * <tt>:with</tt> - Set the method to handle unverified request. + # + # Valid unverified request handling methods are: + # * <tt>:exception</tt> - Raises ActionController::InvalidAuthenticityToken exception. + # * <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 = {}) + include protection_method_module(options[:with] || :null_session) self.request_forgery_protection_token ||= :authenticity_token - self.request_forgery_protection_method = options.delete(:with) if options.key?(:with) prepend_before_filter :verify_authenticity_token, options end + + private + + def protection_method_module(name) + ActionController::RequestForgeryProtection::ProtectionMethods.const_get(name.to_s.classify) + rescue NameError + raise ArgumentError, 'Invalid request forgery protection method, use :null_session, :exception, or :reset_session' + end + end + + module ProtectionMethods + module NullSession + protected + + # This is the method that defines the application behavior when a request is found to be unverified. + def handle_unverified_request + request.session = NullSessionHash.new + request.env['action_dispatch.request.flash_hash'] = nil + request.env['rack.session.options'] = { skip: true } + request.env['action_dispatch.cookies'] = NullCookieJar.build(request) + end + + class NullSessionHash < Rack::Session::Abstract::SessionHash #:nodoc: + def initialize + super(nil, nil) + @loaded = true + end + + def exists? + true + end + end + + class NullCookieJar < ActionDispatch::Cookies::CookieJar #:nodoc: + def self.build(request) + secret = request.env[ActionDispatch::Cookies::TOKEN_KEY] + host = request.host + secure = request.ssl? + + new(secret, host, secure) + end + + def write(*) + # nothing + end + end + end + + module ResetSession + protected + + def handle_unverified_request + reset_session + end + end + + module Exception + protected + + def handle_unverified_request + raise ActionController::InvalidAuthenticityToken + end + end end protected @@ -95,22 +160,6 @@ module ActionController #:nodoc: end end - # This is the method that defines the application behavior when a request is found to be unverified. - # By default, \Rails uses <tt>request_forgery_protection_method</tt> when it finds an unverified request: - # - # * <tt>:reset_session</tt> - Resets the session. - # * <tt>:exception</tt>: - Raises ActionController::InvalidAuthenticityToken exception. - def handle_unverified_request - case request_forgery_protection_method - when :exception - raise ActionController::InvalidAuthenticityToken - when :reset_session - reset_session - else - raise ArgumentError, 'Invalid request forgery protection method, use :exception or :reset_session' - end - end - # Returns true or false if a request is verified. Checks: # # * is it a GET request? Gets should be safe and idempotent diff --git a/actionpack/lib/action_controller/metal/strong_parameters.rb b/actionpack/lib/action_controller/metal/strong_parameters.rb new file mode 100644 index 0000000000..24768b23a8 --- /dev/null +++ b/actionpack/lib/action_controller/metal/strong_parameters.rb @@ -0,0 +1,335 @@ +require 'active_support/concern' +require 'active_support/core_ext/hash/indifferent_access' +require 'active_support/rescuable' + +module ActionController + # Raised when a required parameter is missing. + # + # params = ActionController::Parameters.new(a: {}) + # params.fetch(:b) + # # => ActionController::ParameterMissing: key not found: b + # params.require(:a) + # # => ActionController::ParameterMissing: key not found: a + class ParameterMissing < KeyError + attr_reader :param # :nodoc: + + def initialize(param) # :nodoc: + @param = param + super("key not found: #{param}") + end + end + + # == Action Controller Parameters + # + # Allows to choose which attributes should be whitelisted for mass updating + # and thus prevent accidentally exposing that which shouldn’t be exposed. + # Provides two methods for this purpose: #require and #permit. The former is + # used to mark parameters as required. The latter is used to set the parameter + # as permitted and limit which attributes should be allowed for mass updating. + # + # params = ActionController::Parameters.new({ + # person: { + # name: 'Francesco', + # age: 22, + # role: 'admin' + # } + # }) + # + # permitted = params.require(:person).permit(:name, :age) + # permitted # => {"name"=>"Francesco", "age"=>22} + # permitted.class # => ActionController::Parameters + # permitted.permitted? # => true + # + # Person.first.update_attributes!(permitted) + # # => #<Person id: 1, name: "Francesco", age: 22, role: "user"> + # + # It provides a +permit_all_parameters+ option that controls the top-level + # behaviour of new instances. If it's +true+, all the parameters will be + # permitted by default. The default value for +permit_all_parameters+ + # option is +false+. + # + # params = ActionController::Parameters.new + # params.permitted? # => false + # + # ActionController::Parameters.permit_all_parameters = true + # + # params = ActionController::Parameters.new + # params.permitted? # => true + # + # <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>. + # + # params = ActionController::Parameters.new(key: 'value') + # params[:key] # => "value" + # params["key"] # => "value" + class Parameters < ActiveSupport::HashWithIndifferentAccess + cattr_accessor :permit_all_parameters, instance_accessor: false + attr_accessor :permitted # :nodoc: + + # Returns a new instance of <tt>ActionController::Parameters</tt>. + # Also, sets the +permitted+ attribute to the default value of + # <tt>ActionController::Parameters.permit_all_parameters</tt>. + # + # class Person + # include ActiveRecord::Base + # end + # + # params = ActionController::Parameters.new(name: 'Francesco') + # params.permitted? # => false + # Person.new(params) # => ActiveModel::ForbiddenAttributesError + # + # ActionController::Parameters.permit_all_parameters = true + # + # params = ActionController::Parameters.new(name: 'Francesco') + # params.permitted? # => true + # Person.new(params) # => #<Person id: nil, name: "Francesco"> + def initialize(attributes = nil) + super(attributes) + @permitted = self.class.permit_all_parameters + end + + # Returns +true+ if the parameter is permitted, +false+ otherwise. + # + # params = ActionController::Parameters.new + # params.permitted? # => false + # params.permit! + # params.permitted? # => true + def permitted? + @permitted + end + + # Sets the +permitted+ attribute to +true+. This can be used to pass + # mass assignment. Returns +self+. + # + # class Person < ActiveRecord::Base + # end + # + # params = ActionController::Parameters.new(name: 'Francesco') + # params.permitted? # => false + # Person.new(params) # => ActiveModel::ForbiddenAttributesError + # params.permit! + # params.permitted? # => true + # Person.new(params) # => #<Person id: nil, name: "Francesco"> + def permit! + @permitted = true + 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. + # + # ActionController::Parameters.new(person: { name: 'Francesco' }).require(:person) + # # => {"name"=>"Francesco"} + # + # ActionController::Parameters.new(person: nil).require(:person) + # # => ActionController::ParameterMissing: key not found: person + # + # ActionController::Parameters.new(person: {}).require(:person) + # # => ActionController::ParameterMissing: key not found: person + def require(key) + self[key].presence || raise(ParameterMissing.new(key)) + end + + # Alias of #require. + alias :required :require + + # Returns a new <tt>ActionController::Parameters</tt> instance that + # includes only the given +filters+ and sets the +permitted+ for the + # object to +true+. This is useful for limiting which attributes + # should be allowed for mass updating. + # + # params = ActionController::Parameters.new(user: { name: 'Francesco', age: 22, role: 'admin' }) + # permitted = params.require(:user).permit(:name, :age) + # permitted.permitted? # => true + # permitted.has_key?(:name) # => true + # permitted.has_key?(:age) # => true + # permitted.has_key?(:role) # => false + # + # You can also use +permit+ on nested parameters, like: + # + # params = ActionController::Parameters.new({ + # person: { + # name: 'Francesco', + # age: 22, + # pets: [{ + # name: 'Purplish', + # category: 'dogs' + # }] + # } + # }) + # + # permitted = params.permit(person: [ :name, { pets: [ :name ] } ]) + # permitted.permitted? # => true + # permitted[:person][:name] # => "Francesco" + # permitted[:person][:age] # => nil + # permitted[:person][:pets][0][:name] # => "Purplish" + # permitted[:person][:pets][0][:category] # => nil + def permit(*filters) + params = self.class.new + + filters.each do |filter| + case filter + when Symbol, String then + params[filter] = self[filter] if has_key?(filter) + when Hash then + self.slice(*filter.keys).each do |key, values| + return unless values + + key = key.to_sym + + params[key] = each_element(values) do |value| + # filters are a Hash, so we expect value to be a Hash too + next if filter.is_a?(Hash) && !value.is_a?(Hash) + + value = self.class.new(value) if !value.respond_to?(:permit) + + value.permit(*Array.wrap(filter[key])) + end + end + end + end + + params.permit! + end + + # Returns a parameter for the given +key+. If not found, + # returns +nil+. + # + # params = ActionController::Parameters.new(person: { name: 'Francesco' }) + # params[:person] # => {"name"=>"Francesco"} + # params[:none] # => nil + def [](key) + convert_hashes_to_parameters(key, super) + end + + # Returns a parameter for the given +key+. If the +key+ + # can't be found, there are several options: With no other arguments, + # it will raise an <tt>ActionController::ParameterMissing</tt> error; + # if more arguments are given, then that will be returned; if a block + # is given, then that will be run and its result returned. + # + # params = ActionController::Parameters.new(person: { name: 'Francesco' }) + # params.fetch(:person) # => {"name"=>"Francesco"} + # params.fetch(:none) # => ActionController::ParameterMissing: key not found: none + # params.fetch(:none, 'Francesco') # => "Francesco" + # params.fetch(:none) { 'Francesco' } # => "Francesco" + def fetch(key, *args) + convert_hashes_to_parameters(key, super) + rescue KeyError + raise ActionController::ParameterMissing.new(key) + end + + # Returns a new <tt>ActionController::Parameters</tt> instance that + # includes only the given +keys+. If the given +keys+ + # don't exist, returns an empty hash. + # + # params = ActionController::Parameters.new(a: 1, b: 2, c: 3) + # params.slice(:a, :b) # => {"a"=>1, "b"=>2} + # params.slice(:d) # => {} + def slice(*keys) + self.class.new(super) + end + + # Returns an exact copy of the <tt>ActionController::Parameters</tt> + # instance. +permitted+ state is kept on the duped object. + # + # params = ActionController::Parameters.new(a: 1) + # params.permit! + # params.permitted? # => true + # copy_params = params.dup # => {"a"=>1} + # copy_params.permitted? # => true + def dup + super.tap do |duplicate| + duplicate.instance_variable_set :@permitted, @permitted + end + end + + private + def convert_hashes_to_parameters(key, value) + if value.is_a?(Parameters) || !value.is_a?(Hash) + value + else + # Convert to Parameters on first access + self[key] = self.class.new(value) + end + end + + def each_element(object) + if object.is_a?(Array) + object.map { |el| yield el }.compact + elsif object.is_a?(Hash) && object.keys.all? { |k| k =~ /\A-?\d+\z/ } + hash = object.class.new + object.each { |k,v| hash[k] = yield v } + hash + else + yield object + end + end + end + + # == Strong Parameters + # + # It provides an interface for proctecting attributes from end-user + # assignment. This makes Action Controller parameters are forbidden + # to be used in Active Model mass assignmets until they have been + # whitelisted. + # + # In addition, parameters can be marked as required and flow through a + # predefined raise/rescue flow to end up as a 400 Bad Request with no + # effort. + # + # class PeopleController < ActionController::Base + # # This will raise an ActiveModel::ForbiddenAttributes exception because + # # it's using mass assignment without an explicit permit step. + # def create + # Person.create(params[:person]) + # end + # + # # This will pass with flying colors as long as there's a person key in the + # # parameters, otherwise it'll raise a ActionController::MissingParameter + # # exception, which will get caught by ActionController::Base and turned + # # into that 400 Bad Request reply. + # def update + # redirect_to current_account.people.find(params[:id]).tap { |person| + # person.update_attributes!(person_params) + # } + # end + # + # private + # # Using a private method to encapsulate the permissible parameters is + # # just a good pattern since you'll be able to reuse the same permit + # # list between create and update. Also, you can specialize this method + # # with per-user checking of permissible attributes. + # def person_params + # params.require(:person).permit(:name, :age) + # end + # end + # + # See ActionController::Parameters.require and ActionController::Parameters.permit + # for more information. + module StrongParameters + extend ActiveSupport::Concern + include ActiveSupport::Rescuable + + included do + rescue_from(ActionController::ParameterMissing) do |parameter_missing_exception| + render text: "Required parameter missing: #{parameter_missing_exception.param}", status: :bad_request + end + end + + # Returns a new ActionController::Parameters object that + # has been instantiated with the <tt>request.parameters</tt>. + def params + @_params ||= Parameters.new(request.parameters) + end + + # Assigns the given +value+ to the +params+ hash. If +value+ + # is a Hash, this will create an ActionController::Parameters + # object that has been instantiated with the given +value+ hash. + def params=(value) + @_params = value.is_a?(Hash) ? Parameters.new(value) : value + end + end +end diff --git a/actionpack/lib/action_controller/railtie.rb b/actionpack/lib/action_controller/railtie.rb index 3ecc105e22..f2c68432c1 100644 --- a/actionpack/lib/action_controller/railtie.rb +++ b/actionpack/lib/action_controller/railtie.rb @@ -19,6 +19,10 @@ module ActionController ActionController::Helpers.helpers_path = app.helpers_paths end + initializer "action_controller.parameters_config" do |app| + ActionController::Parameters.permit_all_parameters = app.config.action_controller.delete(:permit_all_parameters) { false } + end + initializer "action_controller.set_configs" do |app| paths = app.config.paths options = app.config.action_controller diff --git a/actionpack/lib/action_dispatch/http/cache.rb b/actionpack/lib/action_dispatch/http/cache.rb index a7f93b780e..0d6015d993 100644 --- a/actionpack/lib/action_dispatch/http/cache.rb +++ b/actionpack/lib/action_dispatch/http/cache.rb @@ -17,12 +17,21 @@ module ActionDispatch env[HTTP_IF_NONE_MATCH] end + def if_none_match_etags + (if_none_match ? if_none_match.split(/\s*,\s*/) : []).collect do |etag| + etag.gsub(/^\"|\"$/, "") + end + end + def not_modified?(modified_at) if_modified_since && modified_at && if_modified_since >= modified_at end def etag_matches?(etag) - if_none_match && if_none_match == etag + if etag + etag = etag.gsub(/^\"|\"$/, "") + if_none_match_etags.include?(etag) + end end # Check response freshness (Last-Modified and ETag) against request diff --git a/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb b/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb index 7349b578d2..ae38c56a67 100644 --- a/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb +++ b/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb @@ -37,7 +37,7 @@ module ActionDispatch end def status_code - Rack::Utils.status_code(@@rescue_responses[@exception.class.name]) + self.class.status_code_for_exception(@exception.class.name) end def application_trace @@ -52,6 +52,10 @@ module ActionDispatch clean_backtrace(:all) end + def self.status_code_for_exception(class_name) + Rack::Utils.status_code(@@rescue_responses[class_name]) + end + private def original_exception(exception) diff --git a/actionpack/lib/action_dispatch/routing/mapper.rb b/actionpack/lib/action_dispatch/routing/mapper.rb index ddb34a2394..49afa01d25 100644 --- a/actionpack/lib/action_dispatch/routing/mapper.rb +++ b/actionpack/lib/action_dispatch/routing/mapper.rb @@ -182,7 +182,7 @@ module ActionDispatch controller ||= default_controller action ||= default_action - unless controller.is_a?(Regexp) || to_shorthand + unless controller.is_a?(Regexp) controller = [@scope[:module], controller].compact.join("/").presence end diff --git a/actionpack/lib/action_view/helpers/form_helper.rb b/actionpack/lib/action_view/helpers/form_helper.rb index 17fe7dc1b4..0bb08cd7ff 100644 --- a/actionpack/lib/action_view/helpers/form_helper.rb +++ b/actionpack/lib/action_view/helpers/form_helper.rb @@ -785,7 +785,7 @@ module ActionView # Returns an input tag of the "password" type tailored for accessing a specified attribute (identified by +method+) on an object # assigned to the template (identified by +object+). Additional options on the input tag can be passed as a # hash with +options+. These options will be tagged onto the HTML as an HTML element attribute as in the example - # shown. + # shown. For security reasons this field is blank by default; pass in a value via +options+ if this is not desired. # # ==== Examples # password_field(:login, :pass, :size => 20) diff --git a/actionpack/lib/action_view/lookup_context.rb b/actionpack/lib/action_view/lookup_context.rb index af367ac28d..76f4dea7b8 100644 --- a/actionpack/lib/action_view/lookup_context.rb +++ b/actionpack/lib/action_view/lookup_context.rb @@ -149,7 +149,7 @@ module ActionView # as well as incorrectly putting part of the path in the template # name instead of the prefix. def normalize_name(name, prefixes) #:nodoc: - prefixes = nil if prefixes.blank? + prefixes = prefixes.presence parts = name.to_s.split('/') parts.shift if parts.first.empty? name = parts.pop diff --git a/actionpack/lib/action_view/template/handlers.rb b/actionpack/lib/action_view/template/handlers.rb index 52e032bbd8..d9cddc0040 100644 --- a/actionpack/lib/action_view/template/handlers.rb +++ b/actionpack/lib/action_view/template/handlers.rb @@ -10,7 +10,7 @@ module ActionView #:nodoc: base.register_default_template_handler :erb, ERB.new base.register_template_handler :builder, Builder.new base.register_template_handler :raw, Raw.new - base.register_template_handler :rb, :source.to_proc + base.register_template_handler :ruby, :source.to_proc end @@template_handlers = {} @@ -21,11 +21,14 @@ module ActionView #:nodoc: end # Register an object that knows how to handle template files with the given - # extension. This can be used to implement new template types. + # extensions. This can be used to implement new template types. # The handler must respond to `:call`, which will be passed the template # and should return the rendered template as a String. - def register_template_handler(extension, handler) - @@template_handlers[extension.to_sym] = handler + def register_template_handler(*extensions, handler) + raise(ArgumentError, "Extension is required") if extensions.empty? + extensions.each do |extension| + @@template_handlers[extension.to_sym] = handler + end @@template_extensions = nil end diff --git a/actionpack/test/controller/log_subscriber_test.rb b/actionpack/test/controller/log_subscriber_test.rb index 700fd788fa..a72b6dde1a 100644 --- a/actionpack/test/controller/log_subscriber_test.rb +++ b/actionpack/test/controller/log_subscriber_test.rb @@ -54,6 +54,10 @@ module Another def with_rescued_exception raise SpecialException end + + def with_action_not_found + raise AbstractController::ActionNotFound + end end end @@ -225,6 +229,17 @@ class ACLogSubscriberTest < ActionController::TestCase assert_match(/Completed 406/, logs.last) end + def test_process_action_with_with_action_not_found_logs_404 + begin + get :with_action_not_found + wait + rescue AbstractController::ActionNotFound + end + + assert_equal 2, logs.size + assert_match(/Completed 404/, logs.last) + end + def logs @logs ||= @logger.logged(:info) end diff --git a/actionpack/test/controller/parameters/nested_parameters_test.rb b/actionpack/test/controller/parameters/nested_parameters_test.rb new file mode 100644 index 0000000000..41f5b6e127 --- /dev/null +++ b/actionpack/test/controller/parameters/nested_parameters_test.rb @@ -0,0 +1,113 @@ +require 'abstract_unit' +require 'action_controller/metal/strong_parameters' + +class NestedParametersTest < ActiveSupport::TestCase + test "permitted nested parameters" do + params = ActionController::Parameters.new({ + book: { + title: "Romeo and Juliet", + authors: [{ + name: "William Shakespeare", + born: "1564-04-26" + }, { + name: "Christopher Marlowe" + }], + details: { + pages: 200, + genre: "Tragedy" + } + }, + magazine: "Mjallo!" + }) + + permitted = params.permit book: [ :title, { authors: [ :name ] }, { details: :pages } ] + + assert permitted.permitted? + assert_equal "Romeo and Juliet", permitted[:book][:title] + assert_equal "William Shakespeare", permitted[:book][:authors][0][:name] + assert_equal "Christopher Marlowe", permitted[:book][:authors][1][:name] + assert_equal 200, permitted[:book][:details][:pages] + assert_nil permitted[:book][:details][:genre] + assert_nil permitted[:book][:authors][0][:born] + assert_nil permitted[:magazine] + end + + test "nested arrays with strings" do + params = ActionController::Parameters.new({ + :book => { + :genres => ["Tragedy"] + } + }) + + permitted = params.permit :book => :genres + assert_equal ["Tragedy"], permitted[:book][:genres] + end + + test "permit may specify symbols or strings" do + params = ActionController::Parameters.new({ + :book => { + :title => "Romeo and Juliet", + :author => "William Shakespeare" + }, + :magazine => "Shakespeare Today" + }) + + permitted = params.permit({:book => ["title", :author]}, "magazine") + assert_equal "Romeo and Juliet", permitted[:book][:title] + assert_equal "William Shakespeare", permitted[:book][:author] + assert_equal "Shakespeare Today", permitted[:magazine] + end + + test "nested array with strings that should be hashes" do + params = ActionController::Parameters.new({ + book: { + genres: ["Tragedy"] + } + }) + + permitted = params.permit book: { genres: :type } + assert_empty permitted[:book][:genres] + end + + test "nested array with strings that should be hashes and additional values" do + params = ActionController::Parameters.new({ + book: { + title: "Romeo and Juliet", + genres: ["Tragedy"] + } + }) + + permitted = params.permit book: [ :title, { genres: :type } ] + assert_equal "Romeo and Juliet", permitted[:book][:title] + assert_empty permitted[:book][:genres] + end + + test "nested string that should be a hash" do + params = ActionController::Parameters.new({ + book: { + genre: "Tragedy" + } + }) + + permitted = params.permit book: { genre: :type } + assert_nil permitted[:book][:genre] + end + + test "fields_for-style nested params" do + params = ActionController::Parameters.new({ + book: { + authors_attributes: { + :'0' => { name: 'William Shakespeare', age_of_death: '52' }, + :'-1' => { name: 'Unattributed Assistant' } + } + } + }) + permitted = params.permit book: { authors_attributes: [ :name ] } + + assert_not_nil permitted[:book][:authors_attributes]['0'] + assert_not_nil permitted[:book][:authors_attributes]['-1'] + assert_nil permitted[:book][:authors_attributes]['0'][:age_of_death] + assert_equal 'William Shakespeare', permitted[:book][:authors_attributes]['0'][:name] + assert_equal 'Unattributed Assistant', permitted[:book][:authors_attributes]['-1'][:name] + end +end diff --git a/actionpack/test/controller/parameters/parameters_permit_test.rb b/actionpack/test/controller/parameters/parameters_permit_test.rb new file mode 100644 index 0000000000..7fe8e6051b --- /dev/null +++ b/actionpack/test/controller/parameters/parameters_permit_test.rb @@ -0,0 +1,73 @@ +require 'abstract_unit' +require 'action_controller/metal/strong_parameters' + +class ParametersPermitTest < ActiveSupport::TestCase + setup do + @params = ActionController::Parameters.new({ person: { + age: "32", name: { first: "David", last: "Heinemeier Hansson" } + }}) + end + + test "fetch raises ParameterMissing exception" do + e = assert_raises(ActionController::ParameterMissing) do + @params.fetch :foo + end + assert_equal :foo, e.param + end + + test "fetch doesnt raise ParameterMissing exception if there is a default" do + assert_equal "monkey", @params.fetch(:foo, "monkey") + assert_equal "monkey", @params.fetch(:foo) { "monkey" } + end + + test "permitted is sticky on accessors" do + assert !@params.slice(:person).permitted? + assert !@params[:person][:name].permitted? + + @params.each { |key, value| assert(value.permitted?) if key == :person } + + assert !@params.fetch(:person).permitted? + + assert !@params.values_at(:person).first.permitted? + end + + test "permitted is sticky on mutators" do + assert !@params.delete_if { |k| k == :person }.permitted? + assert !@params.keep_if { |k,v| k == :person }.permitted? + end + + test "permitted is sticky beyond merges" do + assert !@params.merge(a: "b").permitted? + end + + test "modifying the parameters" do + @params[:person][:hometown] = "Chicago" + @params[:person][:family] = { brother: "Jonas" } + + assert_equal "Chicago", @params[:person][:hometown] + assert_equal "Jonas", @params[:person][:family][:brother] + end + + test "permitting parameters that are not there should not include the keys" do + assert !@params.permit(:person, :funky).has_key?(:funky) + end + + test "permit state is kept on a dup" do + @params.permit! + assert_equal @params.permitted?, @params.dup.permitted? + end + + test "permitted takes a default value when Parameters.permit_all_parameters is set" do + begin + ActionController::Parameters.permit_all_parameters = true + params = ActionController::Parameters.new({ person: { + age: "32", name: { first: "David", last: "Heinemeier Hansson" } + }}) + + assert params.slice(:person).permitted? + assert params[:person][:name].permitted? + ensure + ActionController::Parameters.permit_all_parameters = false + end + end +end diff --git a/actionpack/test/controller/parameters/parameters_require_test.rb b/actionpack/test/controller/parameters/parameters_require_test.rb new file mode 100644 index 0000000000..bdaba8d2d8 --- /dev/null +++ b/actionpack/test/controller/parameters/parameters_require_test.rb @@ -0,0 +1,10 @@ +require 'abstract_unit' +require 'action_controller/metal/strong_parameters' + +class ParametersRequireTest < ActiveSupport::TestCase + test "required parameters must be present not merely not nil" do + assert_raises(ActionController::ParameterMissing) do + ActionController::Parameters.new(person: {}).require(:person) + end + end +end diff --git a/actionpack/test/controller/params_wrapper_test.rb b/actionpack/test/controller/params_wrapper_test.rb index 5b05f77045..209f021cf7 100644 --- a/actionpack/test/controller/params_wrapper_test.rb +++ b/actionpack/test/controller/params_wrapper_test.rb @@ -155,7 +155,6 @@ class ParamsWrapperTest < ActionController::TestCase end def test_derived_wrapped_keys_from_matching_model - User.expects(:respond_to?).with(:accessible_attributes).returns(false) User.expects(:respond_to?).with(:attribute_names).returns(true) User.expects(:attribute_names).twice.returns(["username"]) @@ -168,7 +167,6 @@ class ParamsWrapperTest < ActionController::TestCase def test_derived_wrapped_keys_from_specified_model with_default_wrapper_options do - Person.expects(:respond_to?).with(:accessible_attributes).returns(false) Person.expects(:respond_to?).with(:attribute_names).returns(true) Person.expects(:attribute_names).twice.returns(["username"]) @@ -179,46 +177,8 @@ class ParamsWrapperTest < ActionController::TestCase assert_parameters({ 'username' => 'sikachu', 'title' => 'Developer', 'person' => { 'username' => 'sikachu' }}) end end - - def test_accessible_wrapped_keys_from_matching_model - User.expects(:respond_to?).with(:accessible_attributes).returns(true) - User.expects(:accessible_attributes).with(:default).twice.returns(["username"]) - - with_default_wrapper_options do - @request.env['CONTENT_TYPE'] = 'application/json' - post :parse, { 'username' => 'sikachu', 'title' => 'Developer' } - assert_parameters({ 'username' => 'sikachu', 'title' => 'Developer', 'user' => { 'username' => 'sikachu' }}) - end - end - - def test_accessible_wrapped_keys_from_specified_model - with_default_wrapper_options do - Person.expects(:respond_to?).with(:accessible_attributes).returns(true) - Person.expects(:accessible_attributes).with(:default).twice.returns(["username"]) - - UsersController.wrap_parameters Person - - @request.env['CONTENT_TYPE'] = 'application/json' - post :parse, { 'username' => 'sikachu', 'title' => 'Developer' } - assert_parameters({ 'username' => 'sikachu', 'title' => 'Developer', 'person' => { 'username' => 'sikachu' }}) - end - end - - def test_accessible_wrapped_keys_with_role_from_specified_model - with_default_wrapper_options do - Person.expects(:respond_to?).with(:accessible_attributes).returns(true) - Person.expects(:accessible_attributes).with(:admin).twice.returns(["username"]) - - UsersController.wrap_parameters Person, :as => :admin - - @request.env['CONTENT_TYPE'] = 'application/json' - post :parse, { 'username' => 'sikachu', 'title' => 'Developer' } - assert_parameters({ 'username' => 'sikachu', 'title' => 'Developer', 'person' => { 'username' => 'sikachu' }}) - end - end def test_not_wrapping_abstract_model - User.expects(:respond_to?).with(:accessible_attributes).returns(false) User.expects(:respond_to?).with(:attribute_names).returns(true) User.expects(:attribute_names).returns([]) diff --git a/actionpack/test/controller/permitted_params_test.rb b/actionpack/test/controller/permitted_params_test.rb new file mode 100644 index 0000000000..f46249d712 --- /dev/null +++ b/actionpack/test/controller/permitted_params_test.rb @@ -0,0 +1,25 @@ +require 'abstract_unit' + +class PeopleController < ActionController::Base + def create + render text: params[:person].permitted? ? "permitted" : "forbidden" + end + + def create_with_permit + render text: params[:person].permit(:name).permitted? ? "permitted" : "forbidden" + end +end + +class ActionControllerPermittedParamsTest < ActionController::TestCase + tests PeopleController + + test "parameters are forbidden" do + post :create, { person: { name: "Mjallo!" } } + assert_equal "forbidden", response.body + end + + test "parameters can be permitted and are then not forbidden" do + post :create_with_permit, { person: { name: "Mjallo!" } } + assert_equal "permitted", response.body + end +end diff --git a/actionpack/test/controller/request_forgery_protection_test.rb b/actionpack/test/controller/request_forgery_protection_test.rb index 0289f4070b..1f637eb791 100644 --- a/actionpack/test/controller/request_forgery_protection_test.rb +++ b/actionpack/test/controller/request_forgery_protection_test.rb @@ -56,22 +56,18 @@ module RequestForgeryProtectionActions end # sample controllers -class RequestForgeryProtectionController < ActionController::Base +class RequestForgeryProtectionControllerUsingResetSession < ActionController::Base include RequestForgeryProtectionActions - protect_from_forgery :only => %w(index meta) + protect_from_forgery :only => %w(index meta), :with => :reset_session end class RequestForgeryProtectionControllerUsingException < ActionController::Base include RequestForgeryProtectionActions - protect_from_forgery :only => %w(index meta) - - def handle_unverified_request - raise(ActionController::InvalidAuthenticityToken) - end + protect_from_forgery :only => %w(index meta), :with => :exception end -class FreeCookieController < RequestForgeryProtectionController +class FreeCookieController < RequestForgeryProtectionControllerUsingResetSession self.allow_forgery_protection = false def index @@ -83,7 +79,7 @@ class FreeCookieController < RequestForgeryProtectionController end end -class CustomAuthenticityParamController < RequestForgeryProtectionController +class CustomAuthenticityParamController < RequestForgeryProtectionControllerUsingResetSession def form_authenticity_param 'foobar' end @@ -268,7 +264,7 @@ end # OK let's get our test on -class RequestForgeryProtectionControllerTest < ActionController::TestCase +class RequestForgeryProtectionControllerUsingResetSessionTest < ActionController::TestCase include RequestForgeryProtectionTests setup do diff --git a/actionpack/test/controller/required_params_test.rb b/actionpack/test/controller/required_params_test.rb new file mode 100644 index 0000000000..661bcb3945 --- /dev/null +++ b/actionpack/test/controller/required_params_test.rb @@ -0,0 +1,30 @@ +require 'abstract_unit' + +class BooksController < ActionController::Base + def create + params.require(:book).require(:name) + head :ok + end +end + +class ActionControllerRequiredParamsTest < ActionController::TestCase + tests BooksController + + test "missing required parameters will raise exception" do + post :create, { magazine: { name: "Mjallo!" } } + assert_response :bad_request + + post :create, { book: { title: "Mjallo!" } } + assert_response :bad_request + end + + test "required parameters that are present will not raise" do + post :create, { book: { name: "Mjallo!" } } + assert_response :ok + end + + test "missing parameters will be mentioned in the return" do + post :create, { magazine: { name: "Mjallo!" } } + assert_equal "Required parameter missing: book", response.body + end +end diff --git a/actionpack/test/dispatch/request_test.rb b/actionpack/test/dispatch/request_test.rb index a434e49dbd..a2b9571660 100644 --- a/actionpack/test/dispatch/request_test.rb +++ b/actionpack/test/dispatch/request_test.rb @@ -746,6 +746,45 @@ class RequestTest < ActiveSupport::TestCase assert_equal "/foo?bar", path end + test "if_none_match_etags none" do + request = stub_request + + assert_equal nil, request.if_none_match + assert_equal [], request.if_none_match_etags + assert !request.etag_matches?("foo") + assert !request.etag_matches?(nil) + end + + test "if_none_match_etags single" do + header = 'the-etag' + request = stub_request('HTTP_IF_NONE_MATCH' => header) + + assert_equal header, request.if_none_match + assert_equal [header], request.if_none_match_etags + assert request.etag_matches?("the-etag") + end + + test "if_none_match_etags quoted single" do + header = '"the-etag"' + request = stub_request('HTTP_IF_NONE_MATCH' => header) + + assert_equal header, request.if_none_match + assert_equal ['the-etag'], request.if_none_match_etags + assert request.etag_matches?("the-etag") + end + + test "if_none_match_etags multiple" do + header = 'etag1, etag2, "third etag", "etag4"' + expected = ['etag1', 'etag2', 'third etag', 'etag4'] + request = stub_request('HTTP_IF_NONE_MATCH' => header) + + assert_equal header, request.if_none_match + assert_equal expected, request.if_none_match_etags + expected.each do |etag| + assert request.etag_matches?(etag), etag + end + end + protected def stub_request(env = {}) diff --git a/actionpack/test/dispatch/routing_test.rb b/actionpack/test/dispatch/routing_test.rb index 856248e2ac..4e83ad16d7 100644 --- a/actionpack/test/dispatch/routing_test.rb +++ b/actionpack/test/dispatch/routing_test.rb @@ -363,6 +363,7 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest resources :errors, :shallow => true do resources :notices end + get 'api/version' end scope :path => 'api' do @@ -1280,6 +1281,12 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest assert_equal 'account#shorthand', @response.body end + def test_match_shorthand_with_module + assert_equal '/api/version', api_version_path + get '/api/version' + assert_equal 'api/api#version', @response.body + end + def test_dynamically_generated_helpers_on_collection_do_not_clobber_resources_url_helper assert_equal '/replies', replies_path end diff --git a/actionpack/test/fixtures/company.rb b/actionpack/test/fixtures/company.rb index e29978801e..f3ac3642fa 100644 --- a/actionpack/test/fixtures/company.rb +++ b/actionpack/test/fixtures/company.rb @@ -1,6 +1,5 @@ class Company < ActiveRecord::Base has_one :mascot - attr_protected :rating self.sequence_name = :companies_nonstd_seq validates_presence_of :name diff --git a/actionpack/test/fixtures/ruby_template.rb b/actionpack/test/fixtures/ruby_template.ruby index 5097bce47c..5097bce47c 100644 --- a/actionpack/test/fixtures/ruby_template.rb +++ b/actionpack/test/fixtures/ruby_template.ruby diff --git a/actionpack/test/template/render_test.rb b/actionpack/test/template/render_test.rb index 6279abaae5..ddf5c6a1b3 100644 --- a/actionpack/test/template/render_test.rb +++ b/actionpack/test/template/render_test.rb @@ -97,12 +97,12 @@ module RenderTestCases assert_equal %q;Here are some characters: !@#$%^&*()-="'}{`; + "\n", @view.render(:template => "plain_text_with_characters") end - def test_render_rb_template_with_handlers + def test_render_ruby_template_with_handlers assert_equal "Hello from Ruby code", @view.render(:template => "ruby_template") end - def test_render_rb_template_inline - assert_equal '4', @view.render(:inline => "(2**2).to_s", :type => :rb) + def test_render_ruby_template_inline + assert_equal '4', @view.render(:inline => "(2**2).to_s", :type => :ruby) end def test_render_file_with_localization_on_context_level @@ -451,6 +451,15 @@ module RenderTestCases assert_equal %(<title>David</title>), @view.render(:file => "test/layout_render_object") end + + def test_render_with_passing_couple_extensions_to_one_register_template_handler_function_call + ActionView::Template.register_template_handler :foo1, :foo2, CustomHandler + assert_equal @view.render(:inline => "Hello, World!", :type => :foo1), @view.render(:inline => "Hello, World!", :type => :foo2) + end + + def test_render_throws_exception_when_no_extensions_passed_to_register_template_handler_function_call + assert_raises(ArgumentError) { ActionView::Template.register_template_handler CustomHandler } + end end class CachedViewRenderTest < ActiveSupport::TestCase |