diff options
Diffstat (limited to 'actionpack/lib/action_controller')
33 files changed, 941 insertions, 594 deletions
diff --git a/actionpack/lib/action_controller/base.rb b/actionpack/lib/action_controller/base.rb index 9b3bf99fc3..3b0d094f4f 100644 --- a/actionpack/lib/action_controller/base.rb +++ b/actionpack/lib/action_controller/base.rb @@ -1,8 +1,23 @@ require "action_controller/log_subscriber" +require "action_controller/metal/params_wrapper" module ActionController + # The <tt>metal</tt> anonymous class was introduced to solve issue with including modules in <tt>ActionController::Base</tt>. + # Modules needs to be included in particluar order. First we need to have <tt>AbstractController::Rendering</tt> included, + # next we should include actuall implementation which would be for example <tt>ActionView::Rendering</tt> and after that + # <tt>ActionController::Rendering</tt>. This order must be preserved and as we want to have middle module included dynamicaly + # <tt>metal</tt> class was introduced. It has <tt>AbstractController::Rendering</tt> included and is parent class of + # <tt>ActionController::Base</tt> which includes <tt>ActionController::Rendering</tt>. If we include <tt>ActionView::Rendering</tt> + # beetween them to perserve the required order, we can simply do this by: + # + # ActionController::Base.superclass.send(:include, ActionView::Rendering) + # + metal = Class.new(Metal) do + include AbstractController::Rendering + end + # Action Controllers are the core of a web request in \Rails. They are made up of one or more actions that are executed - # on request and then either render a template or redirect to another action. An action is defined as a public method + # on request and then either it renders a template or redirects to another action. An action is defined as a public method # on the controller, which will automatically be made accessible to the web-server through \Rails Routes. # # By default, only the ApplicationController in a \Rails application inherits from <tt>ActionController::Base</tt>. All other @@ -58,7 +73,7 @@ module ActionController # <input type="text" name="post[address]" value="hyacintvej"> # # A request stemming from a form holding these inputs will include <tt>{ "post" => { "name" => "david", "address" => "hyacintvej" } }</tt>. - # If the address input had been named "post[address][street]", the params would have included + # If the address input had been named <tt>post[address][street]</tt>, the params would have included # <tt>{ "post" => { "address" => { "street" => "hyacintvej" } } }</tt>. There's no limit to the depth of the nesting. # # == Sessions @@ -159,7 +174,7 @@ module ActionController # render action: "overthere" # won't be called if monkeys is nil # end # - class Base < Metal + class Base < metal abstract! # We document the request and response methods here because albeit they are @@ -199,7 +214,6 @@ module ActionController end MODULES = [ - AbstractController::Layouts, AbstractController::Translation, AbstractController::AssetPaths, @@ -222,7 +236,6 @@ module ActionController ForceSSL, Streaming, DataStreaming, - RecordIdentifier, HttpAuthentication::Basic::ControllerMethods, HttpAuthentication::Digest::ControllerMethods, HttpAuthentication::Token::ControllerMethods, diff --git a/actionpack/lib/action_controller/caching.rb b/actionpack/lib/action_controller/caching.rb index 462f147371..12d798d0c1 100644 --- a/actionpack/lib/action_controller/caching.rb +++ b/actionpack/lib/action_controller/caching.rb @@ -6,11 +6,10 @@ module ActionController # \Caching is a cheap way of speeding up slow applications by keeping the result of # calculations, renderings, and database calls around for subsequent requests. # - # You can read more about each approach and the sweeping assistance by clicking the - # modules below. + # You can read more about each approach by clicking the modules below. # - # Note: To turn off all caching and sweeping, set - # config.action_controller.perform_caching = false. + # Note: To turn off all caching, set + # config.action_controller.perform_caching = false # # == \Caching stores # @@ -30,8 +29,6 @@ module ActionController eager_autoload do autoload :Fragments - autoload :Sweeper, 'action_controller/caching/sweeping' - autoload :Sweeping, 'action_controller/caching/sweeping' end module ConfigMethods @@ -54,7 +51,6 @@ module ActionController include ConfigMethods include Fragments - include Sweeping if defined?(ActiveRecord) included do extend ConfigMethods @@ -62,22 +58,22 @@ module ActionController config_accessor :default_static_extension self.default_static_extension ||= '.html' - def self.page_cache_extension=(extension) - ActiveSupport::Deprecation.deprecation_warning(:page_cache_extension, :default_static_extension) - self.default_static_extension = extension - end - - def self.page_cache_extension - ActiveSupport::Deprecation.deprecation_warning(:page_cache_extension, :default_static_extension) - default_static_extension - end - config_accessor :perform_caching self.perform_caching = true if perform_caching.nil? + + class_attribute :_view_cache_dependencies + self._view_cache_dependencies = [] + helper_method :view_cache_dependencies if respond_to?(:helper_method) + end + + module ClassMethods + def view_cache_dependency(&dependency) + self._view_cache_dependencies += [dependency] + end end - def caching_allowed? - request.get? && response.status == 200 + def view_cache_dependencies + self.class._view_cache_dependencies.map { |dep| instance_exec(&dep) }.compact end protected diff --git a/actionpack/lib/action_controller/caching/sweeping.rb b/actionpack/lib/action_controller/caching/sweeping.rb deleted file mode 100644 index 317ac74b40..0000000000 --- a/actionpack/lib/action_controller/caching/sweeping.rb +++ /dev/null @@ -1,116 +0,0 @@ -module ActionController - module Caching - # Sweepers are the terminators of the caching world and responsible for expiring - # caches when Active Record objects change. They do this by being half-observers, - # half-filters and implementing callbacks for both roles. - # - # class ListSweeper < ActionController::Caching::Sweeper - # observe List, Item - # - # def after_save(record) - # list = record.is_a?(List) ? record : record.list - # expire_page(controller: 'lists', action: %w( show public feed ), id: list.id) - # expire_action(controller: 'lists', action: 'all') - # list.shares.each { |share| expire_page(controller: 'lists', action: 'show', id: share.url_key) } - # end - # end - # - # The sweeper is assigned in the controllers that wish to have its job performed using - # the +cache_sweeper+ class method: - # - # class ListsController < ApplicationController - # caches_action :index, :show, :public, :feed - # cache_sweeper :list_sweeper, only: [ :edit, :destroy, :share ] - # end - # - # In the example above, four actions are cached and three actions are responsible for expiring those caches. - # - # You can also name an explicit class in the declaration of a sweeper, which is needed - # if the sweeper is in a module: - # - # class ListsController < ApplicationController - # caches_action :index, :show, :public, :feed - # cache_sweeper OpenBar::Sweeper, only: [ :edit, :destroy, :share ] - # end - module Sweeping - extend ActiveSupport::Concern - - module ClassMethods # :nodoc: - def cache_sweeper(*sweepers) - configuration = sweepers.extract_options! - - sweepers.each do |sweeper| - ActiveRecord::Base.observers << sweeper if defined?(ActiveRecord) and defined?(ActiveRecord::Base) - sweeper_instance = (sweeper.is_a?(Symbol) ? Object.const_get(sweeper.to_s.classify) : sweeper).instance - - if sweeper_instance.is_a?(Sweeper) - around_filter(sweeper_instance, :only => configuration[:only]) - else - after_filter(sweeper_instance, :only => configuration[:only]) - end - end - end - end - end - - if defined?(ActiveRecord) and defined?(ActiveRecord::Observer) - class Sweeper < ActiveRecord::Observer # :nodoc: - attr_accessor :controller - - def initialize(*args) - super - @controller = nil - end - - def before(controller) - self.controller = controller - callback(:before) if controller.perform_caching - true # before method from sweeper should always return true - end - - def after(controller) - self.controller = controller - callback(:after) if controller.perform_caching - end - - def around(controller) - before(controller) - yield - after(controller) - ensure - clean_up - end - - protected - # gets the action cache path for the given options. - def action_path_for(options) - Actions::ActionCachePath.new(controller, options).path - end - - # Retrieve instance variables set in the controller. - def assigns(key) - controller.instance_variable_get("@#{key}") - end - - private - def clean_up - # Clean up, so that the controller can be collected after this request - self.controller = nil - end - - def callback(timing) - controller_callback_method_name = "#{timing}_#{controller.controller_name.underscore}" - action_callback_method_name = "#{controller_callback_method_name}_#{controller.action_name}" - - __send__(controller_callback_method_name) if respond_to?(controller_callback_method_name, true) - __send__(action_callback_method_name) if respond_to?(action_callback_method_name, true) - end - - def method_missing(method, *arguments, &block) - return super unless @controller - @controller.__send__(method, *arguments, &block) - end - end - end - end -end diff --git a/actionpack/lib/action_controller/deprecated.rb b/actionpack/lib/action_controller/deprecated.rb deleted file mode 100644 index 2405bebb97..0000000000 --- a/actionpack/lib/action_controller/deprecated.rb +++ /dev/null @@ -1,7 +0,0 @@ -ActionController::AbstractRequest = ActionController::Request = ActionDispatch::Request -ActionController::AbstractResponse = ActionController::Response = ActionDispatch::Response -ActionController::Routing = ActionDispatch::Routing - -ActiveSupport::Deprecation.warn 'ActionController::AbstractRequest and ActionController::Request are deprecated and will be removed, use ActionDispatch::Request instead.' -ActiveSupport::Deprecation.warn 'ActionController::AbstractResponse and ActionController::Response are deprecated and will be removed, use ActionDispatch::Response instead.' -ActiveSupport::Deprecation.warn 'ActionController::Routing is deprecated and will be removed, use ActionDispatch::Routing instead.'
\ No newline at end of file diff --git a/actionpack/lib/action_controller/deprecated/integration_test.rb b/actionpack/lib/action_controller/deprecated/integration_test.rb deleted file mode 100644 index 54eae48f47..0000000000 --- a/actionpack/lib/action_controller/deprecated/integration_test.rb +++ /dev/null @@ -1,5 +0,0 @@ -ActionController::Integration = ActionDispatch::Integration -ActionController::IntegrationTest = ActionDispatch::IntegrationTest - -ActiveSupport::Deprecation.warn 'ActionController::Integration is deprecated and will be removed, use ActionDispatch::Integration instead.' -ActiveSupport::Deprecation.warn 'ActionController::IntegrationTest is deprecated and will be removed, use ActionDispatch::IntegrationTest instead.' diff --git a/actionpack/lib/action_controller/deprecated/performance_test.rb b/actionpack/lib/action_controller/deprecated/performance_test.rb deleted file mode 100644 index c7ba5a2fe7..0000000000 --- a/actionpack/lib/action_controller/deprecated/performance_test.rb +++ /dev/null @@ -1,3 +0,0 @@ -ActionController::PerformanceTest = ActionDispatch::PerformanceTest - -ActiveSupport::Deprecation.warn 'ActionController::PerformanceTest is deprecated and will be removed, use ActionDispatch::PerformanceTest instead.' diff --git a/actionpack/lib/action_controller/log_subscriber.rb b/actionpack/lib/action_controller/log_subscriber.rb index 3d274e7dd7..9279d8bcea 100644 --- a/actionpack/lib/action_controller/log_subscriber.rb +++ b/actionpack/lib/action_controller/log_subscriber.rb @@ -33,7 +33,7 @@ module ActionController end def halted_callback(event) - info("Filter chain halted as #{event.payload[:filter]} rendered or redirected") + info("Filter chain halted as #{event.payload[:filter].inspect} rendered or redirected") end def send_file(event) @@ -48,6 +48,11 @@ module ActionController info("Sent data #{event.payload[:filename]} (#{event.duration.round(1)}ms)") end + def unpermitted_parameters(event) + unpermitted_keys = event.payload[:keys] + debug("Unpermitted parameters: #{unpermitted_keys.join(", ")}") + end + %w(write_fragment read_fragment exist_fragment? expire_fragment expire_page write_page).each do |method| class_eval <<-METHOD, __FILE__, __LINE__ + 1 diff --git a/actionpack/lib/action_controller/metal.rb b/actionpack/lib/action_controller/metal.rb index f5ab1e2350..b84c9e78c3 100644 --- a/actionpack/lib/action_controller/metal.rb +++ b/actionpack/lib/action_controller/metal.rb @@ -1,3 +1,4 @@ +require 'active_support/core_ext/array/extract_options' require 'action_dispatch/middleware/stack' module ActionController @@ -5,7 +6,7 @@ module ActionController # allowing the following syntax in controllers: # # class PostsController < ApplicationController - # use AuthenticationMiddleware, :except => [:index, :show] + # use AuthenticationMiddleware, except: [:index, :show] # end # class MiddlewareStack < ActionDispatch::MiddlewareStack #:nodoc: @@ -35,8 +36,7 @@ module ActionController raise "MiddlewareStack#build requires an app" unless app middlewares.reverse.inject(app) do |a, middleware| - middleware.valid?(action) ? - middleware.build(a) : a + middleware.valid?(action) ? middleware.build(a) : a end end end @@ -56,7 +56,7 @@ module ActionController # And then to route requests to your metal controller, you would add # something like this to <tt>config/routes.rb</tt>: # - # match 'hello', :to => HelloController.action(:index) + # get 'hello', to: HelloController.action(:index) # # The +action+ method returns a valid Rack application for the \Rails # router to dispatch to. diff --git a/actionpack/lib/action_controller/metal/conditional_get.rb b/actionpack/lib/action_controller/metal/conditional_get.rb index 3f37a6a618..6e0cd51d8b 100644 --- a/actionpack/lib/action_controller/metal/conditional_get.rb +++ b/actionpack/lib/action_controller/metal/conditional_get.rb @@ -1,4 +1,4 @@ -require 'active_support/core_ext/class/attribute' +require 'active_support/core_ext/hash/keys' module ActionController module ConditionalGet @@ -42,7 +42,7 @@ module ActionController # * <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). # - # === Example: + # === Example: # # def show # @article = Article.find(params[:id]) @@ -64,7 +64,7 @@ module ActionController # # def show # @article = Article.find(params[:id]) - # fresh_when(@article, :public => true) + # fresh_when(@article, public: true) # end def fresh_when(record_or_options, additional_options = {}) if record_or_options.is_a? Hash diff --git a/actionpack/lib/action_controller/metal/data_streaming.rb b/actionpack/lib/action_controller/metal/data_streaming.rb index 5422cb93c4..75c4d3ef99 100644 --- a/actionpack/lib/action_controller/metal/data_streaming.rb +++ b/actionpack/lib/action_controller/metal/data_streaming.rb @@ -47,11 +47,11 @@ module ActionController #:nodoc: # # Show a JPEG in the browser: # - # send_file '/path/to.jpeg', :type => 'image/jpeg', :disposition => 'inline' + # send_file '/path/to.jpeg', type: 'image/jpeg', disposition: 'inline' # # Show a 404 page in the browser: # - # send_file '/path/to/404.html', :type => 'text/html; charset=utf-8', :status => 404 + # send_file '/path/to/404.html', type: 'text/html; charset=utf-8', status: 404 # # Read about the other Content-* HTTP headers if you'd like to # provide the user with more information (such as Content-Description) in @@ -96,7 +96,7 @@ module ActionController #:nodoc: end # Sends the given binary data to the browser. This method is similar to - # <tt>render :text => data</tt>, but also allows you to specify whether + # <tt>render text: data</tt>, but also allows you to specify whether # the browser should display the response as a file attachment (i.e. in a # download dialog) or as inline data. You may also set the content type, # the apparent file name, and other things. @@ -117,11 +117,11 @@ module ActionController #:nodoc: # # Download a dynamically-generated tarball: # - # send_data generate_tgz('dir'), :filename => 'dir.tgz' + # send_data generate_tgz('dir'), filename: 'dir.tgz' # # Display an image Active Record in the browser: # - # send_data image.data, :type => image.content_type, :disposition => 'inline' + # send_data image.data, type: image.content_type, disposition: 'inline' # # See +send_file+ for more information on HTTP Content-* headers and caching. def send_data(data, options = {}) #:doc: @@ -150,6 +150,7 @@ module ActionController #:nodoc: disposition = options.fetch(:disposition, DEFAULT_SEND_FILE_DISPOSITION) unless disposition.nil? + disposition = disposition.to_s disposition += %(; filename="#{options[:filename]}") if options[:filename] headers['Content-Disposition'] = disposition end diff --git a/actionpack/lib/action_controller/metal/exceptions.rb b/actionpack/lib/action_controller/metal/exceptions.rb index 3c9d0c86a7..3844dbf2a6 100644 --- a/actionpack/lib/action_controller/metal/exceptions.rb +++ b/actionpack/lib/action_controller/metal/exceptions.rb @@ -3,6 +3,15 @@ module ActionController end 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}") + @original_exception = e + set_backtrace e.backtrace + end end class RenderError < ActionControllerError #:nodoc: diff --git a/actionpack/lib/action_controller/metal/flash.rb b/actionpack/lib/action_controller/metal/flash.rb index b078beb675..65351284b9 100644 --- a/actionpack/lib/action_controller/metal/flash.rb +++ b/actionpack/lib/action_controller/metal/flash.rb @@ -11,6 +11,23 @@ module ActionController #:nodoc: end module ClassMethods + # Creates new flash types. You can pass as many types as you want to create + # flash types other than the default <tt>alert</tt> and <tt>notice</tt> in + # your controllers and views. For instance: + # + # # in application_controller.rb + # class ApplicationController < ActionController::Base + # add_flash_types :warning + # end + # + # # in your controller + # redirect_to user_path(@user), warning: "Incomplete profile" + # + # # in your view + # <%= warning %> + # + # This method will automatically define a new method for each of the given + # names, and it will be available in your views. def add_flash_types(*types) types.each do |type| next if _flash_types.include?(type) @@ -20,7 +37,7 @@ module ActionController #:nodoc: end helper_method type - _flash_types << type + self._flash_types += [type] end end end diff --git a/actionpack/lib/action_controller/metal/force_ssl.rb b/actionpack/lib/action_controller/metal/force_ssl.rb index e905a3cf1d..a2cb6d1e66 100644 --- a/actionpack/lib/action_controller/metal/force_ssl.rb +++ b/actionpack/lib/action_controller/metal/force_ssl.rb @@ -1,3 +1,6 @@ +require 'active_support/core_ext/hash/except' +require 'active_support/core_ext/hash/slice' + module ActionController # This module provides a method which will redirect browser to use HTTPS # protocol. This will ensure that user's sensitive information will be @@ -14,6 +17,10 @@ module ActionController extend ActiveSupport::Concern include AbstractController::Callbacks + ACTION_OPTIONS = [:only, :except, :if, :unless] + URL_OPTIONS = [:protocol, :host, :domain, :subdomain, :port, :path] + REDIRECT_OPTIONS = [:status, :flash, :alert, :notice] + module ClassMethods # Force the request to this particular controller or specified actions to be # under HTTPS protocol. @@ -22,25 +29,41 @@ module ActionController # an +:if+ or +:unless+ condition. # # class AccountsController < ApplicationController - # force_ssl :if => :ssl_configured? + # force_ssl if: :ssl_configured? # # def ssl_configured? # !Rails.env.development? # end # end # - # ==== Options - # * <tt>host</tt> - Redirect to a different host name - # * <tt>only</tt> - The callback should be run only for this action - # * <tt>except</tt> - The callback should be run for all actions except this action - # * <tt>if</tt> - A symbol naming an instance method or a proc; the callback - # will be called only when it returns a true value. - # * <tt>unless</tt> - A symbol naming an instance method or a proc; the callback - # will be called only when it returns a false value. + # ==== URL Options + # You can pass any of the following options to affect the redirect url + # * <tt>host</tt> - Redirect to a different host name + # * <tt>subdomain</tt> - Redirect to a different subdomain + # * <tt>domain</tt> - Redirect to a different domain + # * <tt>port</tt> - Redirect to a non-standard port + # * <tt>path</tt> - Redirect to a different path + # + # ==== Redirect Options + # You can pass any of the following options to affect the redirect status and response + # * <tt>status</tt> - Redirect with a custom status (default is 301 Moved Permanently) + # * <tt>flash</tt> - Set a flash message when redirecting + # * <tt>alert</tt> - Set an alert message when redirecting + # * <tt>notice</tt> - Set a notice message when redirecting + # + # ==== Action Options + # 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. def force_ssl(options = {}) - host = options.delete(:host) - before_filter(options) do - force_ssl_redirect(host) + action_options = options.slice(*ACTION_OPTIONS) + redirect_options = options.except(*ACTION_OPTIONS) + before_action(action_options) do + force_ssl_redirect(redirect_options) end end end @@ -48,14 +71,26 @@ module ActionController # Redirect the existing request to use the HTTPS protocol. # # ==== Parameters - # * <tt>host</tt> - Redirect to a different host name - def force_ssl_redirect(host = nil) + # * <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? - redirect_options = {:protocol => 'https://', :status => :moved_permanently} - redirect_options.merge!(:host => host) if host - redirect_options.merge!(:params => request.query_parameters) + options = { + :protocol => 'https://', + :host => request.host, + :path => request.fullpath, + :status => :moved_permanently + } + + if host_or_options.is_a?(Hash) + options.merge!(host_or_options) + elsif host_or_options + options.merge!(:host => host_or_options) + end + + secure_url = ActionDispatch::Http::URL.url_for(options.slice(*URL_OPTIONS)) flash.keep if respond_to?(:flash) - redirect_to redirect_options + redirect_to secure_url, options.slice(*REDIRECT_OPTIONS) end end end diff --git a/actionpack/lib/action_controller/metal/head.rb b/actionpack/lib/action_controller/metal/head.rb index 747e1273be..424473801d 100644 --- a/actionpack/lib/action_controller/metal/head.rb +++ b/actionpack/lib/action_controller/metal/head.rb @@ -1,15 +1,13 @@ module ActionController module Head - extend ActiveSupport::Concern - # Return a response that has no content (merely headers). The options # argument is interpreted to be a hash of header names and values. # This allows you to easily return a response that consists only of # significant headers: # - # head :created, :location => person_path(@person) + # head :created, location: person_path(@person) # - # head :created, :location => @person + # head :created, location: @person # # It can also be used to return exceptional conditions: # @@ -31,6 +29,7 @@ module ActionController if include_content?(self.status) 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') diff --git a/actionpack/lib/action_controller/metal/helpers.rb b/actionpack/lib/action_controller/metal/helpers.rb index d2cbbd3330..a9c3e438fb 100644 --- a/actionpack/lib/action_controller/metal/helpers.rb +++ b/actionpack/lib/action_controller/metal/helpers.rb @@ -1,4 +1,3 @@ - module ActionController # The \Rails framework provides a large number of helpers for working with assets, dates, forms, # numbers and model objects, to name a few. These helpers are available to all templates @@ -6,7 +5,7 @@ module ActionController # # In addition to using the standard template helpers provided, creating custom helpers to # extract complicated logic or reusable functionality is strongly encouraged. By default, each controller - # will include all helpers. + # 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 @@ -74,7 +73,11 @@ module ActionController # Provides a proxy to access helpers methods from outside the view. def helpers - @helper_proxy ||= ActionView::Base.new.extend(_helpers) + @helper_proxy ||= begin + proxy = ActionView::Base.new + proxy.config = config.inheritable_copy + proxy.extend(_helpers) + end end # Overwrite modules_for_helpers to accept :all as argument, which loads @@ -91,11 +94,10 @@ module ActionController end def all_helpers_from_path(path) - helpers = [] - Array(path).each do |_path| - extract = /^#{Regexp.quote(_path.to_s)}\/?(.*)_helper.rb$/ + 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') } - helpers += names.sort + names.sort! end helpers.uniq! helpers diff --git a/actionpack/lib/action_controller/metal/hide_actions.rb b/actionpack/lib/action_controller/metal/hide_actions.rb index 420b22cf56..af36ffa240 100644 --- a/actionpack/lib/action_controller/metal/hide_actions.rb +++ b/actionpack/lib/action_controller/metal/hide_actions.rb @@ -26,20 +26,14 @@ module ActionController self.hidden_actions = hidden_actions.dup.merge(args.map(&:to_s)).freeze end - def inherited(klass) - klass.class_eval { @visible_actions = {} } - super - end - def visible_action?(action_name) - return @visible_actions[action_name] if @visible_actions.key?(action_name) - @visible_actions[action_name] = !hidden_actions.include?(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) }) + @action_methods ||= Set.new(super.reject { |name| hidden_actions.include?(name) }).freeze end end end diff --git a/actionpack/lib/action_controller/metal/http_authentication.rb b/actionpack/lib/action_controller/metal/http_authentication.rb index 03b8d8db1a..158d552ec7 100644 --- a/actionpack/lib/action_controller/metal/http_authentication.rb +++ b/actionpack/lib/action_controller/metal/http_authentication.rb @@ -8,14 +8,14 @@ module ActionController # === Simple \Basic example # # class PostsController < ApplicationController - # http_basic_authenticate_with :name => "dhh", :password => "secret", :except => :index + # http_basic_authenticate_with name: "dhh", password: "secret", except: :index # # def index - # render :text => "Everyone can see me!" + # render text: "Everyone can see me!" # end # # def edit - # render :text => "I'm only accessible if you know the password" + # render text: "I'm only accessible if you know the password" # end # end # @@ -25,11 +25,11 @@ module ActionController # the regular HTML interface is protected by a session approach: # # class ApplicationController < ActionController::Base - # before_filter :set_account, :authenticate + # before_action :set_account, :authenticate # # protected # def set_account - # @account = Account.find_by_url_name(request.subdomains.first) + # @account = Account.find_by(url_name: request.subdomains.first) # end # # def authenticate @@ -68,7 +68,7 @@ module ActionController module ClassMethods def http_basic_authenticate_with(options = {}) - before_filter(options.except(:name, :password, :realm)) do + before_action(options.except(:name, :password, :realm)) do authenticate_or_request_with_http_basic(options[:realm] || "Application") do |name, password| name == options[:name] && password == options[:password] end @@ -124,14 +124,14 @@ module ActionController # USERS = {"dhh" => "secret", #plain text password # "dap" => Digest::MD5.hexdigest(["dap",REALM,"secret"].join(":"))} #ha1 digest password # - # before_filter :authenticate, :except => [:index] + # before_action :authenticate, except: [:index] # # def index - # render :text => "Everyone can see me!" + # render text: "Everyone can see me!" # end # # def edit - # render :text => "I'm only accessible if you know the password" + # render text: "I'm only accessible if you know the password" # end # # private @@ -228,7 +228,7 @@ module ActionController end def decode_credentials(header) - HashWithIndifferentAccess[header.to_s.gsub(/^Digest\s+/,'').split(',').map do |pair| + ActiveSupport::HashWithIndifferentAccess[header.to_s.gsub(/^Digest\s+/, '').split(',').map do |pair| key, value = pair.split('=', 2) [key.strip, value.to_s.gsub(/^"|"$/,'').delete('\'')] end] @@ -249,9 +249,9 @@ module ActionController end def secret_token(request) - secret = request.env["action_dispatch.secret_token"] - raise "You must set config.secret_token in your app's config" if secret.blank? - secret + key_generator = request.env["action_dispatch.key_generator"] + http_auth_salt = request.env["action_dispatch.http_auth_salt"] + key_generator.generate_key(http_auth_salt) end # Uses an MD5 digest based on time to generate a value to be used only once. @@ -299,6 +299,7 @@ module ActionController # allow a user to use new nonce without prompting user again for their # username and password. def validate_nonce(secret_key, request, value, seconds_to_timeout=5*60) + return false if value.nil? t = ::Base64.decode64(value).split(":").first.to_i nonce(secret_key, t) == value && (t - Time.now.to_i).abs <= seconds_to_timeout end @@ -317,14 +318,14 @@ module ActionController # class PostsController < ApplicationController # TOKEN = "secret" # - # before_filter :authenticate, :except => [ :index ] + # before_action :authenticate, except: [ :index ] # # def index - # render :text => "Everyone can see me!" + # render text: "Everyone can see me!" # end # # def edit - # render :text => "I'm only accessible if you know the password" + # render text: "I'm only accessible if you know the password" # end # # private @@ -340,11 +341,11 @@ module ActionController # the regular HTML interface is protected by a session approach: # # class ApplicationController < ActionController::Base - # before_filter :set_account, :authenticate + # before_action :set_account, :authenticate # # protected # def set_account - # @account = Account.find_by_url_name(request.subdomains.first) + # @account = Account.find_by(url_name: request.subdomains.first) # end # # def authenticate @@ -384,6 +385,8 @@ module ActionController # # RewriteRule ^(.*)$ dispatch.fcgi [E=X-HTTP_AUTHORIZATION:%{HTTP:Authorization},QSA,L] module Token + TOKEN_REGEX = /^Token / + AUTHN_PAIR_DELIMITERS = /(?:,|;|\t+)/ extend self module ControllerMethods @@ -424,27 +427,41 @@ module ActionController # Parses the token and options out of the token authorization header. If # the header looks like this: # Authorization: Token token="abc", nonce="def" - # Then the returned token is "abc", and the options is {:nonce => "def"} + # Then the returned token is "abc", and the options is {nonce: "def"} # # 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. def token_and_options(request) - if request.authorization.to_s[/^Token (.*)/] - values = Hash[$1.split(',').map do |value| - value.strip! # remove any spaces between commas and values - key, value = value.split(/\=\"?/) # split key=value pairs - if value - value.chomp!('"') # chomp trailing " in value - value.gsub!(/\\\"/, '"') # unescape remaining quotes - [key, value] - end - end.compact] - [values.delete("token"), values.with_indifferent_access] + authorization_request = request.authorization.to_s + if authorization_request[TOKEN_REGEX] + params = token_params_from authorization_request + [params.shift.last, Hash[params].with_indifferent_access] end end + def token_params_from(auth) + rewrite_param_values params_array_from raw_params auth + end + + # Takes raw_params and turns it into an array of parameters + def params_array_from(raw_params) + raw_params.map { |param| param.split %r/=(.+)?/ } + end + + # This removes the `"` characters wrapping the value. + def rewrite_param_values(array_params) + array_params.each { |param| param.last.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`. + def raw_params(auth) + auth.sub(TOKEN_REGEX, '').split(/"\s*#{AUTHN_PAIR_DELIMITERS}\s*/) + end + # Encodes the given token and options into an Authorization header value. # # token - String token. diff --git a/actionpack/lib/action_controller/metal/instrumentation.rb b/actionpack/lib/action_controller/metal/instrumentation.rb index ca4ae532ca..d3aa8f90c5 100644 --- a/actionpack/lib/action_controller/metal/instrumentation.rb +++ b/actionpack/lib/action_controller/metal/instrumentation.rb @@ -60,7 +60,7 @@ module ActionController ActiveSupport::Notifications.instrument("redirect_to.action_controller") do |payload| result = super payload[:status] = response.status - payload[:location] = response.location + payload[:location] = response.filtered_location result end end diff --git a/actionpack/lib/action_controller/metal/live.rb b/actionpack/lib/action_controller/metal/live.rb index 32e5afa335..0dd788645b 100644 --- a/actionpack/lib/action_controller/metal/live.rb +++ b/actionpack/lib/action_controller/metal/live.rb @@ -1,5 +1,6 @@ require 'action_dispatch/http/response' require 'delegate' +require 'active_support/json' module ActionController # Mix this module in to your controller, and all actions in that controller @@ -14,6 +15,7 @@ module ActionController # response.stream.write "hello world\n" # sleep 1 # } + # ensure # response.stream.close # end # end @@ -31,8 +33,82 @@ 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 + # 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. + # + # Writing an object will convert it into standard SSE format with whatever + # options you have configured. You may choose to set the following options: + # + # 1) Event. If specified, an event with this name will be dispatched on + # the browser. + # 2) Retry. The reconnection time in milliseconds used when attempting + # to send the event. + # 3) Id. If the connection dies while sending an SSE to the browser, then + # the server will receive a +Last-Event-ID+ header with value equal to +id+. + # + # After setting an option in the constructor of the SSE object, all future + # SSEs sent accross the stream will use those options unless overridden. + # + # Example Usage: + # + # class MyController < ActionController::Base + # include ActionController::Live + # + # def index + # response.headers['Content-Type'] = 'text/event-stream' + # sse = SSE.new(response.stream, retry: 300, event: "event-name") + # sse.write({ name: 'John'}) + # sse.write({ name: 'John'}, id: 10) + # sse.write({ name: 'John'}, id: 10, event: "other-event") + # sse.write({ name: 'John'}, id: 10, event: "other-event", retry: 500) + # ensure + # sse.close + # end + # end + # + # Note: SSEs are not currently supported by IE. However, they are supported + # by Chrome, Firefox, Opera, and Safari. + class SSE + + WHITELISTED_OPTIONS = %w( retry event id ) + + def initialize(stream, options = {}) + @stream = stream + @options = options + end + + def close + @stream.close + end + + def write(object, options = {}) + case object + when String + perform_write(object, options) + else + perform_write(ActiveSupport::JSON.encode(object), options) + end + end + + private + + def perform_write(json, options) + current_options = @options.merge(options).stringify_keys + + WHITELISTED_OPTIONS.each do |option_name| + if (option_value = current_options[option_name]) + @stream.write "#{option_name}: #{option_value}\n" + end + end + + @stream.write "data: #{json}\n\n" + end + end + class Buffer < ActionDispatch::Response::Buffer #:nodoc: def initialize(response) + @error_callback = nil super(response, SizedQueue.new(10)) end @@ -55,6 +131,14 @@ module ActionController super @buf.push nil end + + def on_error(&block) + @error_callback = block + end + + def call_on_error + @error_callback.call + end end class Response < ActionDispatch::Response #:nodoc: all @@ -97,6 +181,10 @@ module ActionController def merge_default_headers(original, default) Header.new self, super end + + def handle_conditional_get! + super unless committed? + end end def process(name) @@ -116,6 +204,16 @@ module ActionController begin super(name) + rescue => e + begin + @_response.stream.write(ActionView::Base.streaming_completion_on_exception) if request.format == :html + @_response.stream.call_on_error + rescue => exception + log_error(exception) + ensure + log_error(e) + @_response.stream.close + end ensure @_response.commit! end @@ -124,6 +222,16 @@ module ActionController @_response.await_commit end + def log_error(exception) + 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") + end + def response_body=(body) super response.stream.close if response diff --git a/actionpack/lib/action_controller/metal/mime_responds.rb b/actionpack/lib/action_controller/metal/mime_responds.rb index 18ae2c3bfc..a072fce1a1 100644 --- a/actionpack/lib/action_controller/metal/mime_responds.rb +++ b/actionpack/lib/action_controller/metal/mime_responds.rb @@ -1,3 +1,4 @@ +require 'active_support/core_ext/array/extract_options' require 'abstract_controller/collector' module ActionController #:nodoc: @@ -23,13 +24,13 @@ module ActionController #:nodoc: # <tt>:except</tt> with an array of actions or a single action: # # respond_to :html - # respond_to :xml, :json, :except => [ :edit ] + # 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 + # respond_to :json, only: :create # # This specifies that the <tt>:create</tt> action and no other responds # to <tt>:json</tt>. @@ -70,7 +71,7 @@ module ActionController #:nodoc: # # respond_to do |format| # format.html - # format.xml { render :xml => @people } + # format.xml { render xml: @people } # end # end # @@ -82,7 +83,7 @@ module ActionController #:nodoc: # (by name) if it does not already exist, without web-services, it might look like this: # # def create - # @company = Company.find_or_create_by_name(params[:company][:name]) + # @company = Company.find_or_create_by(name: params[:company][:name]) # @person = @company.people.create(params[:person]) # # redirect_to(person_list_url) @@ -92,13 +93,13 @@ module ActionController #:nodoc: # # def create # company = params[:person].delete(:company) - # @company = Company.find_or_create_by_name(company[:name]) + # @company = Company.find_or_create_by(name: company[:name]) # @person = @company.people.create(params[:person]) # # respond_to do |format| # format.html { redirect_to(person_list_url) } # format.js - # format.xml { render :xml => @person.to_xml(:include => @company) } + # format.xml { render xml: @person.to_xml(include: @company) } # end # end # @@ -120,7 +121,7 @@ module ActionController #:nodoc: # Note, however, the extra bit at the top of that action: # # company = params[:person].delete(:company) - # @company = Company.find_or_create_by_name(company[:name]) + # @company = Company.find_or_create_by(name: company[:name]) # # This is because the incoming XML document (if a web-service request is in process) can only contain a # single root-node. So, we have to rearrange things so that the request looks like this (url-encoded): @@ -162,11 +163,11 @@ module ActionController #:nodoc: # # In the example above, if the format is xml, it will render: # - # render :xml => @people + # render xml: @people # # Or if the format is json: # - # render :json => @people + # 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: @@ -227,7 +228,7 @@ module ActionController #:nodoc: # i.e. its +show+ action. # 2. If there are validation errors, the response # renders a default action, which is <tt>:new</tt> for a - # +post+ request or <tt>:edit</tt> for +put+. + # +post+ request or <tt>:edit</tt> for +patch+ or +put+. # Thus an example like this - # # respond_to :html, :xml @@ -246,10 +247,10 @@ module ActionController #:nodoc: # if @user.save # flash[:notice] = 'User was successfully created.' # format.html { redirect_to(@user) } - # format.xml { render :xml => @user } + # format.xml { render xml: @user } # else - # format.html { render :action => "new" } - # format.xml { render :xml => @user } + # format.html { render action: "new" } + # format.xml { render xml: @user } # end # end # end @@ -260,7 +261,7 @@ module ActionController #:nodoc: # 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>. + # <code>render xml: resource</code>. # # === Nested resources # @@ -309,7 +310,7 @@ module ActionController #:nodoc: # 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 + # 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. @@ -320,11 +321,12 @@ module ActionController #:nodoc: # 2. <tt>:action</tt> - overwrites the default render action used after an # unsuccessful html +post+ request. def respond_with(*resources, &block) - raise "In order to use respond_with, first you need to declare the formats your " << + raise "In order to use respond_with, first you need to declare the formats your " \ "controller responds to in the class level" if self.class.mimes_for_respond_to.empty? if 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 @@ -363,9 +365,7 @@ module ActionController #:nodoc: format = collector.negotiate_format(request) if format - self.content_type ||= format.to_s - lookup_context.formats = [format.to_sym] - lookup_context.rendered_format = lookup_context.formats.first + _process_format(format) collector else raise ActionController::UnknownFormat @@ -381,7 +381,7 @@ module ActionController #:nodoc: # # respond_to do |format| # format.html - # format.xml { render :xml => @people } + # format.xml { render xml: @people } # end # # In this usage, the argument passed to the block (+format+ above) is an @@ -419,7 +419,7 @@ module ActionController #:nodoc: end def response - @responses[format] || @responses[Mime::ALL] + @responses.fetch(format, @responses[Mime::ALL]) end def negotiate_format(request) diff --git a/actionpack/lib/action_controller/metal/params_wrapper.rb b/actionpack/lib/action_controller/metal/params_wrapper.rb index 88b9e78da7..c9f1d8dcb4 100644 --- a/actionpack/lib/action_controller/metal/params_wrapper.rb +++ b/actionpack/lib/action_controller/metal/params_wrapper.rb @@ -1,7 +1,8 @@ require 'active_support/core_ext/hash/slice' require 'active_support/core_ext/hash/except' require 'active_support/core_ext/module/anonymous' -require 'action_dispatch/http/mime_types' +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 @@ -72,17 +73,104 @@ module ActionController EXCLUDE_PARAMETERS = %w(authenticity_token _method utf8) + require 'mutex_m' + + class Options < Struct.new(:name, :format, :include, :exclude, :klass, :model) # :nodoc: + include Mutex_m + + def self.from_hash(hash) + name = hash[:name] + format = Array(hash[:format]) + include = hash[:include] && Array(hash[:include]).collect(&:to_s) + exclude = hash[:exclude] && Array(hash[:exclude]).collect(&:to_s) + new name, format, include, exclude, nil, nil + end + + def initialize(name, format, include, exclude, klass, model) # nodoc + super + @include_set = include + @name_set = name + end + + def model + super || synchronize { super || self.model = _default_wrap_model } + end + + def include + return super if @include_set + + m = model + synchronize do + return super if @include_set + + @include_set = true + + unless super || exclude + if m.respond_to?(:attribute_names) && m.attribute_names.any? + self.include = m.attribute_names + end + end + end + end + + def name + return super if @name_set + + m = model + synchronize do + return super if @name_set + + @name_set = true + + unless super || klass.anonymous? + self.name = m ? m.to_s.demodulize.underscore : + klass.controller_name.singularize + end + end + end + + 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+ + # will try to find if the +User+ model exists. + # + # This method also does namespace lookup. Foo::Bar::UsersController will + # try to find Foo::Bar::User, Foo::User and finally User. + def _default_wrap_model #:nodoc: + return nil if klass.anonymous? + model_name = klass.name.sub(/Controller$/, '').classify + + begin + if model_klass = model_name.safe_constantize + model_klass + else + namespaces = model_name.split("::") + namespaces.delete_at(-2) + break if namespaces.last == model_name + model_name = namespaces.join("::") + end + end until model_klass + + model_klass + end + end + included do class_attribute :_wrapper_options - self._wrapper_options = { :format => [] } + self._wrapper_options = Options.from_hash(format: []) end module ClassMethods + def _set_wrapper_options(options) + self._wrapper_options = Options.from_hash(options) + end + # Sets the name of the wrapper key, or the model which +ParamsWrapper+ # would use to determine the attribute names from. # # ==== Examples - # wrap_parameters :format => :xml + # wrap_parameters format: :xml # # enables the parameter wrapper for XML format # # wrap_parameters :person @@ -92,7 +180,7 @@ module ActionController # # wraps parameters by determining the wrapper key from Person class # (+person+, in this case) and the list of attribute names # - # wrap_parameters :include => [:username, :title] + # wrap_parameters include: [:username, :title] # # wraps only +:username+ and +:title+ attributes from parameters. # # wrap_parameters false @@ -119,68 +207,24 @@ module ActionController model = name_or_model_or_options end - _set_wrapper_defaults(_wrapper_options.slice(:format).merge(options), model) + opts = Options.from_hash _wrapper_options.to_h.slice(:format).merge(options) + opts.model = model + opts.klass = self + + self._wrapper_options = opts end # Sets the default wrapper key or model which will be used to determine # wrapper key and attribute names. Will be called automatically when the # module is inherited. def inherited(klass) - if klass._wrapper_options[:format].present? - klass._set_wrapper_defaults(klass._wrapper_options.slice(:format)) + if klass._wrapper_options.format.any? + params = klass._wrapper_options.dup + params.klass = klass + klass._wrapper_options = params end super end - - protected - - # Determine the wrapper model from the controller's name. By convention, - # this could be done by trying to find the defined model that has the - # same singularize name as the controller. For example, +UsersController+ - # will try to find if the +User+ model exists. - # - # This method also does namespace lookup. Foo::Bar::UsersController will - # try to find Foo::Bar::User, Foo::User and finally User. - def _default_wrap_model #:nodoc: - return nil if self.anonymous? - model_name = self.name.sub(/Controller$/, '').classify - - begin - if model_klass = model_name.safe_constantize - model_klass - else - namespaces = model_name.split("::") - namespaces.delete_at(-2) - break if namespaces.last == model_name - model_name = namespaces.join("::") - end - end until model_klass - - model_klass - end - - def _set_wrapper_defaults(options, model=nil) - options = options.dup - - unless options[:include] || options[:exclude] - model ||= _default_wrap_model - if model.respond_to?(:attribute_names) && model.attribute_names.present? - options[:include] = model.attribute_names - end - end - - unless options[:name] || self.anonymous? - model ||= _default_wrap_model - options[:name] = model ? model.to_s.demodulize.underscore : - controller_name.singularize - end - - options[:include] = Array(options[:include]).collect(&:to_s) if options[:include] - options[:exclude] = Array(options[:exclude]).collect(&:to_s) if options[:exclude] - options[:format] = Array(options[:format]) - - self._wrapper_options = options - end end # Performs parameters wrapping upon the request. Will be called automatically @@ -205,20 +249,20 @@ module ActionController # Returns the wrapper key which will use to stored wrapped parameters. def _wrapper_key - _wrapper_options[:name] + _wrapper_options.name end # Returns the list of enabled formats. def _wrapper_formats - _wrapper_options[:format] + _wrapper_options.format end # Returns the list of parameters which will be selected for wrapped. def _wrap_parameters(parameters) - value = if include_only = _wrapper_options[:include] + value = if include_only = _wrapper_options.include parameters.slice(*include_only) else - exclude = _wrapper_options[:exclude] || [] + exclude = _wrapper_options.exclude || [] parameters.except(*(exclude + EXCLUDE_PARAMETERS)) end diff --git a/actionpack/lib/action_controller/metal/redirecting.rb b/actionpack/lib/action_controller/metal/redirecting.rb index ee0e69d87c..ab14a61b97 100644 --- a/actionpack/lib/action_controller/metal/redirecting.rb +++ b/actionpack/lib/action_controller/metal/redirecting.rb @@ -24,7 +24,7 @@ module ActionController # * <tt>:back</tt> - Back to the page that issued the request. Useful for forms that are triggered from multiple places. # Short-hand for <tt>redirect_to(request.env["HTTP_REFERER"])</tt> # - # redirect_to :action => "show", :id => 5 + # redirect_to action: "show", id: 5 # redirect_to post # redirect_to "http://www.rubyonrails.org" # redirect_to "/images/screenshot.jpg" @@ -32,12 +32,12 @@ module ActionController # redirect_to :back # redirect_to proc { edit_post_url(@post) } # - # The redirection happens as a "302 Moved" header unless otherwise specified. + # The redirection happens as a "302 Found" header unless otherwise specified. # - # redirect_to post_url(@post), :status => :found - # redirect_to :action=>'atom', :status => :moved_permanently - # redirect_to post_url(@post), :status => 301 - # redirect_to :action=>'atom', :status => 302 + # redirect_to post_url(@post), status: :found + # redirect_to action: 'atom', status: :moved_permanently + # redirect_to post_url(@post), status: 301 + # redirect_to action: 'atom', status: 302 # # The status code can either be a standard {HTTP Status code}[http://www.iana.org/assignments/http-status-codes] as an # integer, or a symbol representing the downcased, underscored and symbolized description. @@ -49,32 +49,51 @@ module ActionController # around this you can return a <tt>303 See Other</tt> status code which will be # followed using a GET request. # - # redirect_to posts_url, :status => :see_other - # redirect_to :action => 'index', :status => 303 + # redirect_to posts_url, status: :see_other + # redirect_to action: 'index', status: 303 # # It is also possible to assign a flash message as part of the redirection. There are two special accessors for the commonly used flash names # +alert+ and +notice+ as well as a general purpose +flash+ bucket. # - # redirect_to post_url(@post), :alert => "Watch it, mister!" - # redirect_to post_url(@post), :status=> :found, :notice => "Pay attention to the road" - # redirect_to post_url(@post), :status => 301, :flash => { :updated_post_id => @post.id } - # redirect_to { :action=>'atom' }, :alert => "Something serious happened" + # redirect_to post_url(@post), alert: "Watch it, mister!" + # redirect_to post_url(@post), status: :found, notice: "Pay attention to the road" + # redirect_to post_url(@post), status: 301, flash: { updated_post_id: @post.id } + # redirect_to { action: 'atom' }, alert: "Something serious happened" # # When using <tt>redirect_to :back</tt>, if there is no referrer, ActionController::RedirectBackError will be raised. You may specify some fallback # behavior for this case by rescuing ActionController::RedirectBackError. def redirect_to(options = {}, response_status = {}) #:doc: raise ActionControllerError.new("Cannot redirect to nil!") unless options raise AbstractController::DoubleRenderError if response_body - logger.debug { "Redirected by #{caller(1).first rescue "unknown"}" } if logger self.status = _extract_redirect_to_status(options, response_status) self.location = _compute_redirect_to_location(options) self.response_body = "<html><body>You are being <a href=\"#{ERB::Util.h(location)}\">redirected</a>.</body></html>" end + def _compute_redirect_to_location(options) #:nodoc: + case options + # The scheme name consist of a letter followed by any combination of + # letters, digits, and the plus ("+"), period ("."), or hyphen ("-") + # characters; and is terminated by a colon (":"). + # See http://tools.ietf.org/html/rfc3986#section-3.1 + # The protocol relative scheme starts with a double slash "//". + when /\A([a-z][a-z\d\-+\.]*:|\/\/).*/i + options + when String + request.protocol + request.host_with_port + options + when :back + request.headers["Referer"] or raise RedirectBackError + when Proc + _compute_redirect_to_location options.call + else + url_for(options) + end.delete("\0\r\n") + end + private def _extract_redirect_to_status(options, response_status) - status = if options.is_a?(Hash) && options.key?(:status) + if options.is_a?(Hash) && options.key?(:status) Rack::Utils.status_code(options.delete(:status)) elsif response_status.key?(:status) Rack::Utils.status_code(response_status[:status]) @@ -82,25 +101,5 @@ module ActionController 302 end end - - def _compute_redirect_to_location(options) - case options - # The scheme name consist of a letter followed by any combination of - # letters, digits, and the plus ("+"), period ("."), or hyphen ("-") - # characters; and is terminated by a colon (":"). - # The protocol relative scheme starts with a double slash "//" - when %r{^(\w[\w+.-]*:|//).*} - options - when String - request.protocol + request.host_with_port + options - when :back - raise RedirectBackError unless refer = request.headers["Referer"] - refer - when Proc - _compute_redirect_to_location options.call - else - url_for(options) - end.delete("\0\r\n") - end end end diff --git a/actionpack/lib/action_controller/metal/renderers.rb b/actionpack/lib/action_controller/metal/renderers.rb index 78aeeef2bf..62a3844b04 100644 --- a/actionpack/lib/action_controller/metal/renderers.rb +++ b/actionpack/lib/action_controller/metal/renderers.rb @@ -6,6 +6,12 @@ module ActionController Renderers.add(key, &block) end + class MissingRenderer < LoadError + def initialize(format) + super "No renderer defined for format: #{format}" + end + end + module Renderers extend ActiveSupport::Concern @@ -52,8 +58,8 @@ 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, - # :disposition => "attachment; filename=#{filename}.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. @@ -66,7 +72,7 @@ module ActionController # @csvable = Csvable.find(params[:id]) # respond_to do |format| # format.html - # format.csv { render :csv => @csvable, :filename => @csvable.name } + # format.csv { render csv: @csvable, filename: @csvable.name } # } # end # To use renderers and their mime types in more concise ways, see diff --git a/actionpack/lib/action_controller/metal/rendering.rb b/actionpack/lib/action_controller/metal/rendering.rb index c5e7d4e357..90f0ef0b1c 100644 --- a/actionpack/lib/action_controller/metal/rendering.rb +++ b/actionpack/lib/action_controller/metal/rendering.rb @@ -2,39 +2,45 @@ module ActionController module Rendering extend ActiveSupport::Concern - include AbstractController::Rendering - # Before processing, set the request formats in current controller formats. def process_action(*) #:nodoc: - self.formats = request.formats.map { |x| x.ref } + self.formats = request.formats.map(&:ref).compact super end # Check for double render errors and set the content_type after rendering. def render(*args) #:nodoc: - raise ::AbstractController::DoubleRenderError if response_body + raise ::AbstractController::DoubleRenderError if self.response_body super - self.content_type ||= Mime[lookup_context.rendered_format].to_s - response_body end # Overwrite render_to_string because body can now be set to a rack body. def render_to_string(*) - if self.response_body = super + result = super + if result.respond_to?(:each) string = "" - response_body.each { |r| string << r } + result.each { |r| string << r } string + else + result end - ensure - self.response_body = nil end - def render_to_body(*) - super || " " + def render_to_body(options = {}) + super || if options[:text].present? + options[:text] + else + " " + end end private + def _process_format(format) + super + self.content_type ||= format.to_s + end + # Normalize arguments by catching blocks and setting them on :update. def _normalize_args(action=nil, options={}, &blk) #:nodoc: options = super diff --git a/actionpack/lib/action_controller/metal/request_forgery_protection.rb b/actionpack/lib/action_controller/metal/request_forgery_protection.rb index 17d4a793ac..bd64b1f812 100644 --- a/actionpack/lib/action_controller/metal/request_forgery_protection.rb +++ b/actionpack/lib/action_controller/metal/request_forgery_protection.rb @@ -19,7 +19,7 @@ module ActionController #:nodoc: # # class ApplicationController < ActionController::Base # protect_from_forgery - # skip_before_filter :verify_authenticity_token, :if => :json_request? + # skip_before_action :verify_authenticity_token, if: :json_request? # # protected # @@ -50,6 +50,10 @@ module ActionController #:nodoc: config_accessor :request_forgery_protection_token self.request_forgery_protection_token ||= :authenticity_token + # Holds the class which implements the request forgery protection. + config_accessor :forgery_protection_strategy + self.forgery_protection_strategy = nil + # 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? @@ -62,19 +66,19 @@ module ActionController #:nodoc: # Turn on request forgery protection. Bear in mind that only non-GET, HTML/JavaScript requests are checked. # # class FooController < ApplicationController - # protect_from_forgery :except => :index + # protect_from_forgery except: :index # # You can disable csrf protection on controller-by-controller basis: # - # skip_before_filter :verify_authenticity_token + # skip_before_action :verify_authenticity_token # # It can also be disabled for specific controller actions: # - # skip_before_filter :verify_authenticity_token, :except => [:create] + # skip_before_action :verify_authenticity_token, except: [:create] # # Valid Options: # - # * <tt>:only/:except</tt> - Passed to the <tt>before_filter</tt> call. Set which actions are verified. + # * <tt>:only/:except</tt> - Passed to the <tt>before_action</tt> call. Set which actions are verified. # * <tt>:with</tt> - Set the method to handle unverified request. # # Valid unverified request handling methods are: @@ -82,14 +86,14 @@ 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 = {}) - include protection_method_module(options[:with] || :null_session) + self.forgery_protection_strategy = protection_method_class(options[:with] || :null_session) self.request_forgery_protection_token ||= :authenticity_token - prepend_before_filter :verify_authenticity_token, options + prepend_before_action :verify_authenticity_token, options end private - def protection_method_module(name) + def protection_method_class(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' @@ -97,23 +101,32 @@ module ActionController #:nodoc: end module ProtectionMethods - module NullSession - protected + class NullSession + def initialize(controller) + @controller = controller + end # 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 = @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) end + protected + class NullSessionHash < Rack::Session::Abstract::SessionHash #:nodoc: - def initialize - super(nil, nil) + def initialize(env) + super(nil, env) + @data = {} @loaded = true end + # no-op + def destroy; end + def exists? true end @@ -121,11 +134,11 @@ module ActionController #:nodoc: class NullCookieJar < ActionDispatch::Cookies::CookieJar #:nodoc: def self.build(request) - secret = request.env[ActionDispatch::Cookies::TOKEN_KEY] - host = request.host - secure = request.ssl? + key_generator = request.env[ActionDispatch::Cookies::GENERATOR_KEY] + host = request.host + secure = request.ssl? - new(secret, host, secure) + new(key_generator, host, secure, options_for_env({})) end def write(*) @@ -134,16 +147,20 @@ module ActionController #:nodoc: end end - module ResetSession - protected + class ResetSession + def initialize(controller) + @controller = controller + end def handle_unverified_request - reset_session + @controller.reset_session end end - module Exception - protected + class Exception + def initialize(controller) + @controller = controller + end def handle_unverified_request raise ActionController::InvalidAuthenticityToken @@ -152,7 +169,11 @@ module ActionController #:nodoc: end protected - # The actual before_filter that is used. Modify this to change how you handle unverified requests. + def handle_unverified_request + forgery_protection_strategy.new(self).handle_unverified_request + end + + # The actual before_action that is used. Modify this to change how you handle unverified requests. def verify_authenticity_token unless verified_request? logger.warn "Can't verify CSRF token authenticity" if logger @@ -162,11 +183,11 @@ module ActionController #:nodoc: # Returns true or false if a request is verified. Checks: # - # * is it a GET 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? || + !protect_against_forgery? || request.get? || request.head? || form_authenticity_token == params[request_forgery_protection_token] || form_authenticity_token == request.headers['X-CSRF-Token'] end @@ -181,6 +202,7 @@ module ActionController #:nodoc: params[request_forgery_protection_token] end + # Checks if the controller allows forgery protection. def protect_against_forgery? allow_forgery_protection end diff --git a/actionpack/lib/action_controller/metal/responder.rb b/actionpack/lib/action_controller/metal/responder.rb index 42a0959a58..b4ba169e8f 100644 --- a/actionpack/lib/action_controller/metal/responder.rb +++ b/actionpack/lib/action_controller/metal/responder.rb @@ -45,10 +45,10 @@ module ActionController #:nodoc: # if @user.save # flash[:notice] = 'User was successfully created.' # format.html { redirect_to(@user) } - # format.xml { render :xml => @user, :status => :created, :location => @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 } + # format.html { render action: "new" } + # format.xml { render xml: @user.errors, status: :unprocessable_entity } # end # end # end @@ -92,18 +92,22 @@ module ActionController #:nodoc: # @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) + # 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. To customize the failure scenario, you can pass a - # a block to <code>respond_with</code>: + # 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| + # respond_with(@project, @task, status: 201) do |format| # if @task.save # flash[:notice] = 'Task was successfully created.' # else @@ -140,7 +144,7 @@ module ActionController #:nodoc: undef_method(:to_json) if method_defined?(:to_json) undef_method(:to_yaml) if method_defined?(:to_yaml) - # Initializes a new responder an invoke the proper format. If the format is + # Initializes a new responder and invokes the proper format. If the format is # not defined, call to_format. # def self.call(*args) @@ -198,6 +202,7 @@ module ActionController #:nodoc: # 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 @@ -236,20 +241,20 @@ module ActionController #:nodoc: # Display is just a shortcut to render a resource with the current format. # - # display @user, :status => :ok + # display @user, status: :ok # # For XML requests it's equivalent to: # - # render :xml => @user, :status => :ok + # render xml: @user, status: :ok # # Options sent by the user are also used: # - # respond_with(@user, :status => :created) - # display(@user, :status => :ok) + # respond_with(@user, status: :created) + # display(@user, status: :ok) # # Results in: # - # render :xml => @user, :status => :created + # render xml: @user, status: :created # def display(resource, given_options={}) controller.render given_options.merge!(options).merge!(format => resource) @@ -265,6 +270,11 @@ module ActionController #:nodoc: resource.respond_to?(:errors) && !resource.errors.empty? end + # Check whether the neceessary 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. # diff --git a/actionpack/lib/action_controller/metal/streaming.rb b/actionpack/lib/action_controller/metal/streaming.rb index 9f3c997024..62d5931b45 100644 --- a/actionpack/lib/action_controller/metal/streaming.rb +++ b/actionpack/lib/action_controller/metal/streaming.rb @@ -21,15 +21,13 @@ module ActionController #:nodoc: # supports fibers (fibers are supported since version 1.9.2 of the main # Ruby implementation). # - # == Examples - # # Streaming can be added to a given template easily, all you need to do is # to pass the :stream option. # # class PostsController # def index - # @posts = Post.scoped - # render :stream => true + # @posts = Post.all + # render stream: true # end # end # @@ -53,10 +51,10 @@ module ActionController #:nodoc: # # def dashboard # # Allow lazy execution of the queries - # @posts = Post.scoped - # @pages = Page.scoped - # @articles = Article.scoped - # render :stream => true + # @posts = Post.all + # @pages = Page.all + # @articles = Article.all + # render stream: true # end # # Notice that :stream only works with templates. Rendering :json @@ -176,7 +174,7 @@ module ActionController #:nodoc: # need to create a config file as follow: # # # unicorn.config.rb - # listen 3000, :tcp_nopush => false + # listen 3000, tcp_nopush: false # # And use it on initialization: # @@ -195,31 +193,29 @@ module ActionController #:nodoc: module Streaming extend ActiveSupport::Concern - include AbstractController::Rendering - protected - # Set proper cache control and transfer encoding when streaming - def _process_options(options) #:nodoc: - super - if options[:stream] - if env["HTTP_VERSION"] == "HTTP/1.0" - options.delete(:stream) - else - headers["Cache-Control"] ||= "no-cache" - headers["Transfer-Encoding"] = "chunked" - headers.delete("Content-Length") + # Set proper cache control and transfer encoding when streaming + def _process_options(options) #:nodoc: + super + if options[:stream] + if env["HTTP_VERSION"] == "HTTP/1.0" + options.delete(:stream) + else + headers["Cache-Control"] ||= "no-cache" + headers["Transfer-Encoding"] = "chunked" + headers.delete("Content-Length") + end end end - end - # Call render_body if we are streaming instead of usual +render+. - def _render_template(options) #:nodoc: - if options.delete(:stream) - Rack::Chunked::Body.new view_renderer.render_body(view_context, options) - else - super + # Call render_body if we are streaming instead of usual +render+. + def _render_template(options) #:nodoc: + if options.delete(:stream) + Rack::Chunked::Body.new view_renderer.render_body(view_context, options) + else + super + end end - end end end diff --git a/actionpack/lib/action_controller/metal/strong_parameters.rb b/actionpack/lib/action_controller/metal/strong_parameters.rb index 6f46954266..8ae7e474a3 100644 --- a/actionpack/lib/action_controller/metal/strong_parameters.rb +++ b/actionpack/lib/action_controller/metal/strong_parameters.rb @@ -1,6 +1,8 @@ -require 'active_support/concern' require 'active_support/core_ext/hash/indifferent_access' +require 'active_support/core_ext/array/wrap' require 'active_support/rescuable' +require 'action_dispatch/http/upload' +require 'stringio' module ActionController # Raised when a required parameter is missing. @@ -8,8 +10,6 @@ module ActionController # params = ActionController::Parameters.new(a: {}) # params.fetch(:b) # # => ActionController::ParameterMissing: param not found: b - # params.require(:a) - # # => ActionController::ParameterMissing: param not found: a class ParameterMissing < KeyError attr_reader :param # :nodoc: @@ -19,13 +19,41 @@ module ActionController end end - # == Action Controller Parameters + # Raised when a required parameter value is empty. + # + # params = ActionController::Parameters.new(a: {}) + # params.require(:a) + # # => ActionController::EmptyParameter: value is empty for required key: a + class EmptyParameter < IndexError + attr_reader :param + + def initialize(param) + @param = param + super("value is empty for required key: #{param}") + end + end + + # Raised when a supplied parameter is not expected. + # + # params = ActionController::Parameters.new(a: "123", b: "456") + # params.permit(:c) + # # => ActionController::UnpermittedParameters: found unexpected keys: a, b + class UnpermittedParameters < IndexError + attr_reader :params # :nodoc: + + def initialize(params) # :nodoc: + @params = params + super("found unpermitted parameters: #{params.join(", ")}") + 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. + # as permitted and limit which attributes should be allowed for mass updating. # # params = ActionController::Parameters.new({ # person: { @@ -40,13 +68,20 @@ module ActionController # permitted.class # => ActionController::Parameters # permitted.permitted? # => true # - # Person.first.update_attributes!(permitted) + # Person.first.update!(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+. + # It provides two options that controls the top-level behavior of new instances: + # + # * +permit_all_parameters+ - If it's +true+, all the parameters will be + # permitted by default. The default is +false+. + # * +action_on_unpermitted_parameters+ - Allow to control the behavior when parameters + # that are not explicitly permitted are found. The values can be <tt>:log</tt> to + # write a message on the logger or <tt>:raise</tt> to raise + # ActionController::UnpermittedParameters exception. The default value is <tt>:log</tt> + # in test and development environments, +false+ otherwise. + # + # Examples: # # params = ActionController::Parameters.new # params.permitted? # => false @@ -56,6 +91,16 @@ module ActionController # params = ActionController::Parameters.new # params.permitted? # => true # + # params = ActionController::Parameters.new(a: "123", b: "456") + # params.permit(:c) + # # => {} + # + # ActionController::Parameters.action_on_unpermitted_parameters = :raise + # + # params = ActionController::Parameters.new(a: "123", b: "456") + # 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>. @@ -65,24 +110,27 @@ module ActionController # params["key"] # => "value" class Parameters < ActiveSupport::HashWithIndifferentAccess cattr_accessor :permit_all_parameters, instance_accessor: false - attr_accessor :permitted # :nodoc: + cattr_accessor :action_on_unpermitted_parameters, instance_accessor: false + + # Never raise an UnpermittedParameters exception because of these params + # are present. They are added by Rails and it's of no concern. + NEVER_UNPERMITTED_PARAMS = %w( controller action ) # 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 + # class Person < ActiveRecord::Base # end # # params = ActionController::Parameters.new(name: 'Francesco') # params.permitted? # => false - # Person.new(params) # => ActiveModel::ForbiddenAttributesError + # Person.new(params) # => ActiveModel::ForbiddenAttributesError # # ActionController::Parameters.permit_all_parameters = true # # params = ActionController::Parameters.new(name: 'Francesco') - # params.permitted? # => true + # params.permitted? # => true # Person.new(params) # => #<Person id: nil, name: "Francesco"> def initialize(attributes = nil) super(attributes) @@ -106,7 +154,7 @@ module ActionController # end # # params = ActionController::Parameters.new(name: 'Francesco') - # params.permitted? # => false + # params.permitted? # => false # Person.new(params) # => ActiveModel::ForbiddenAttributesError # params.permit! # params.permitted? # => true @@ -121,41 +169,59 @@ 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. + # Ensures that a parameter is present. If it's present and not empty, + # returns the parameter at the given +key+, if it's empty raises + # an <tt>ActionController::EmptyParameter</tt> error, 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: param not found: person + # ActionController::Parameters.new(person: { name: 'Francesco' }).require(:person) + # # => {"name"=>"Francesco"} # # ActionController::Parameters.new(person: {}).require(:person) + # # => ActionController::EmptyParameter: value is empty for required key: person + # + # ActionController::Parameters.new(name: {}).require(:person) # # => ActionController::ParameterMissing: param not found: person def require(key) - self[key].presence || raise(ParameterMissing.new(key)) + raise(ActionController::ParameterMissing.new(key)) unless self.key?(key) + self[key].presence || raise(ActionController::EmptyParameter.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 + # includes only the given +filters+ and sets the +permitted+ attribute + # 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.permitted? # => true # permitted.has_key?(:name) # => true # permitted.has_key?(:age) # => true # permitted.has_key?(:role) # => false # + # Only permitted scalars pass the filter. For example, given + # + # params.permit(:name) + # + # +:name+ passes 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+. + # Otherwise, the key +:name+ is filtered out. + # + # You may declare that the parameter should be an array of permitted scalars + # by mapping it to an empty array: + # + # params = ActionController::Parameters.new(tags: ['rails', 'parameters']) + # params.permit(tags: []) + # # You can also use +permit+ on nested parameters, like: # # params = ActionController::Parameters.new({ - # person: { + # person: { # name: 'Francesco', # age: 22, # pets: [{ @@ -165,10 +231,10 @@ module ActionController # } # }) # - # permitted = params.permit(person: [ :name, { pets: :name } ]) + # permitted = params.permit(person: [ :name, { pets: :name } ]) # permitted.permitted? # => true # permitted[:person][:name] # => "Francesco" - # permitted[:person][:age] # => nil + # permitted[:person][:age] # => nil # permitted[:person][:pets][0][:name] # => "Purplish" # permitted[:person][:pets][0][:category] # => nil # @@ -179,7 +245,7 @@ module ActionController # params = ActionController::Parameters.new({ # person: { # contact: { - # email: 'none@test.com' + # email: 'none@test.com', # phone: '555-1234' # } # } @@ -189,47 +255,32 @@ module ActionController # # => {} # # params.require(:person).permit(contact: :phone) - # # => {"contact"=>{"phone"=>"555-1234"}} + # # => {"contact"=>{"phone"=>"555-1234"}} # # params.require(:person).permit(contact: [ :email, :phone ]) # # => {"contact"=>{"email"=>"none@test.com", "phone"=>"555-1234"}} def permit(*filters) params = self.class.new - filters.each do |filter| + filters.flatten.each do |filter| case filter - when Symbol, String then - if has_key?(filter) - _value = self[filter] - params[filter] = _value unless Hash === _value - end - keys.grep(/\A#{Regexp.escape(filter)}\(\di\)\z/) { |key| params[key] = self[key] } + when Symbol, String + permitted_scalar_filter(params, 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 + hash_filter(params, filter) end end + unpermitted_parameters!(params) if self.class.action_on_unpermitted_parameters + 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 = ActionController::Parameters.new(person: { name: 'Francesco' }) + # params[:person] # => {"name"=>"Francesco"} # params[:none] # => nil def [](key) convert_hashes_to_parameters(key, super) @@ -242,12 +293,19 @@ module ActionController # 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: param not found: none + # params.fetch(:person) # => {"name"=>"Francesco"} + # params.fetch(:none) # => ActionController::ParameterMissing: param not found: none # params.fetch(:none, 'Francesco') # => "Francesco" - # params.fetch(:none) { 'Francesco' } # => "Francesco" + # params.fetch(:none) { 'Francesco' } # => "Francesco" def fetch(key, *args) - convert_hashes_to_parameters(key, super) + value = super + # Don't rely on +convert_hashes_to_parameters+ + # so as to not mutate via a +fetch+ + if value.is_a?(Hash) + value = self.class.new(value) + value.permit! if permitted? + end + value rescue KeyError raise ActionController::ParameterMissing.new(key) end @@ -260,7 +318,9 @@ module ActionController # params.slice(:a, :b) # => {"a"=>1, "b"=>2} # params.slice(:d) # => {} def slice(*keys) - self.class.new(super) + self.class.new(super).tap do |new_instance| + new_instance.permitted = @permitted + end end # Returns an exact copy of the <tt>ActionController::Parameters</tt> @@ -273,10 +333,15 @@ module ActionController # copy_params.permitted? # => true def dup super.tap do |duplicate| - duplicate.instance_variable_set :@permitted, @permitted + duplicate.permitted = @permitted end end + protected + def permitted=(new_permitted) + @permitted = new_permitted + end + private def convert_hashes_to_parameters(key, value) if value.is_a?(Parameters) || !value.is_a?(Hash) @@ -290,7 +355,7 @@ module ActionController 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/ } + elsif fields_for_style?(object) hash = object.class.new object.each { |k,v| hash[k] = yield v } hash @@ -298,12 +363,111 @@ module ActionController yield object 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? + case self.class.action_on_unpermitted_parameters + when :log + name = "unpermitted_parameters.action_controller" + ActiveSupport::Notifications.instrument(name, keys: unpermitted_keys) + when :raise + raise ActionController::UnpermittedParameters.new(unpermitted_keys) + end + end + end + + def unpermitted_keys(params) + self.keys - params.keys - NEVER_UNPERMITTED_PARAMS + end + + # + # --- Filtering ---------------------------------------------------------- + # + + # This is a white list of permitted scalar types that includes the ones + # supported in XML and JSON requests. + # + # This list is in particular used to filter ordinary requests, String goes + # as first element to quickly short-circuit the common case. + # + # If you modify this collection please update the API of +permit+ above. + PERMITTED_SCALAR_TYPES = [ + String, + Symbol, + NilClass, + Numeric, + TrueClass, + FalseClass, + Date, + Time, + # DateTimes are Dates, we document the type but avoid the redundant check. + StringIO, + IO, + ActionDispatch::Http::UploadedFile, + Rack::Test::UploadedFile, + ] + + def permitted_scalar?(value) + PERMITTED_SCALAR_TYPES.any? {|type| value.is_a?(type)} + end + + def permitted_scalar_filter(params, key) + if has_key?(key) && permitted_scalar?(self[key]) + params[key] = self[key] + end + + keys.grep(/\A#{Regexp.escape(key)}\(\d+[if]?\)\z/) do |k| + if permitted_scalar?(self[k]) + params[k] = self[k] + end + end + 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] + end + end + + EMPTY_ARRAY = [] + def hash_filter(params, filter) + filter = filter.with_indifferent_access + + # Slicing filters out non-declared keys. + slice(*filter.keys).each do |key, value| + next unless value + + if filter[key] == EMPTY_ARRAY + # Declaration { comment_ids: [] }. + array_of_permitted_scalars_filter(params, key) + 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 + end + end + end + end end # == Strong \Parameters # # It provides an interface for protecting attributes from end-user - # assignment. This makes Action Controller parameters forbidden + # assignment. This makes Action Controller parameters forbidden # to be used in Active Model mass assignment until they have been # whitelisted. # @@ -326,13 +490,13 @@ module ActionController # # into a 400 Bad Request reply. # def update # redirect_to current_account.people.find(params[:id]).tap { |person| - # person.update_attributes!(person_params) + # person.update!(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 + # # 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 @@ -340,18 +504,37 @@ module ActionController # end # end # + # In order to use <tt>accepts_nested_attribute_for</tt> with Strong \Parameters, you + # will need to specify which nested attributes should be whitelisted. + # + # class Person + # has_many :pets + # accepts_nested_attributes_for :pets + # end + # + # class PeopleController < ActionController::Base + # def create + # Person.create(person_params) + # end + # + # ... + # + # private + # + # def person_params + # # It's mandatory to specify the nested attributes that should be whitelisted. + # # If you use `permit` with just the key that points to the nested attributes hash, + # # it will return an empty hash. + # params.require(:person).permit(:name, :age, pets_attributes: [ :name, :category ]) + # 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 diff --git a/actionpack/lib/action_controller/metal/url_for.rb b/actionpack/lib/action_controller/metal/url_for.rb index 0cdd17bc2e..754249cbc8 100644 --- a/actionpack/lib/action_controller/metal/url_for.rb +++ b/actionpack/lib/action_controller/metal/url_for.rb @@ -10,7 +10,7 @@ module ActionController # include ActionController::UrlFor # include Rails.application.routes.url_helpers # - # delegate :env, :request, :to => :controller + # delegate :env, :request, to: :controller # # def initialize(controller) # @controller = controller @@ -32,7 +32,8 @@ module ActionController if (same_origin = _routes.equal?(env["action_dispatch.routes"])) || (script_name = env["ROUTES_#{_routes.object_id}_SCRIPT_NAME"]) || - (original_script_name = env['SCRIPT_NAME']) + (original_script_name = env['ORIGINAL_SCRIPT_NAME']) + @_url_options.dup.tap do |options| if original_script_name options[:original_script_name] = original_script_name diff --git a/actionpack/lib/action_controller/railtie.rb b/actionpack/lib/action_controller/railtie.rb index ee0f053bad..0833e65d23 100644 --- a/actionpack/lib/action_controller/railtie.rb +++ b/actionpack/lib/action_controller/railtie.rb @@ -1,7 +1,6 @@ require "rails" require "action_controller" require "action_dispatch/railtie" -require "action_view/railtie" require "abstract_controller/railties/routes_helpers" require "action_controller/railties/helpers" @@ -20,23 +19,27 @@ module ActionController end initializer "action_controller.parameters_config" do |app| - ActionController::Parameters.permit_all_parameters = app.config.action_controller.delete(:permit_all_parameters) { false } + options = app.config.action_controller + + ActionController::Parameters.permit_all_parameters = options.delete(:permit_all_parameters) { false } + ActionController::Parameters.action_on_unpermitted_parameters = options.delete(:action_on_unpermitted_parameters) do + (Rails.env.test? || Rails.env.development?) ? :log : false + end end initializer "action_controller.set_configs" do |app| paths = app.config.paths options = app.config.action_controller - options.logger ||= Rails.logger - options.cache_store ||= Rails.cache + options.logger ||= Rails.logger + options.cache_store ||= Rails.cache - options.javascripts_dir ||= paths["public/javascripts"].first - options.stylesheets_dir ||= paths["public/stylesheets"].first + options.javascripts_dir ||= paths["public/javascripts"].first + options.stylesheets_dir ||= paths["public/stylesheets"].first # Ensure readers methods get compiled - options.asset_path ||= app.config.asset_path - options.asset_host ||= app.config.asset_host - options.relative_url_root ||= app.config.relative_url_root + options.asset_host ||= app.config.asset_host + options.relative_url_root ||= app.config.relative_url_root ActiveSupport.on_load(:action_controller) do include app.routes.mounted_helpers diff --git a/actionpack/lib/action_controller/record_identifier.rb b/actionpack/lib/action_controller/record_identifier.rb deleted file mode 100644 index bffd2a02d0..0000000000 --- a/actionpack/lib/action_controller/record_identifier.rb +++ /dev/null @@ -1,20 +0,0 @@ -require 'active_support/deprecation' -require 'action_view/record_identifier' - -module ActionController - module RecordIdentifier - MESSAGE = 'method will no longer be included by default in controllers since Rails 4.1. ' + - 'If you would like to use it in controllers, please include ' + - 'ActionView::RecodIdentifier module.' - - def dom_id(record, prefix = nil) - ActiveSupport::Deprecation.warn 'dom_id ' + MESSAGE - ActionView::RecordIdentifier.dom_id(record, prefix) - end - - def dom_class(record, prefix = nil) - ActiveSupport::Deprecation.warn 'dom_class ' + MESSAGE - ActionView::RecordIdentifier.dom_class(record, prefix) - end - end -end diff --git a/actionpack/lib/action_controller/test_case.rb b/actionpack/lib/action_controller/test_case.rb index d911d47a1d..5ed3d2ebc1 100644 --- a/actionpack/lib/action_controller/test_case.rb +++ b/actionpack/lib/action_controller/test_case.rb @@ -1,6 +1,7 @@ require 'rack/session/abstract/id' require 'active_support/core_ext/object/to_query' require 'active_support/core_ext/module/anonymous' +require 'active_support/core_ext/hash/keys' module ActionController module TemplateAssertions @@ -15,8 +16,9 @@ module ActionController @_partials = Hash.new(0) @_templates = Hash.new(0) @_layouts = Hash.new(0) + @_files = Hash.new(0) - ActiveSupport::Notifications.subscribe("render_template.action_view") do |name, start, finish, id, payload| + ActiveSupport::Notifications.subscribe("render_template.action_view") do |_name, _start, _finish, _id, payload| path = payload[:layout] if path @_layouts[path] += 1 @@ -26,7 +28,7 @@ module ActionController end end - ActiveSupport::Notifications.subscribe("!render_template.action_view") do |name, start, finish, id, payload| + ActiveSupport::Notifications.subscribe("!render_template.action_view") do |_name, _start, _finish, _id, payload| path = payload[:virtual_path] next unless path partial = path =~ /^.*\/_[^\/]*$/ @@ -38,6 +40,16 @@ module ActionController @_templates[path] += 1 end + + ActiveSupport::Notifications.subscribe("!render_template.action_view") do |_name, _start, _finish, _id, payload| + next if payload[:virtual_path] # files don't have virtual path + + path = payload[:identifier] + if path + @_files[path] += 1 + @_files[path.split("/").last] += 1 + end + end end def teardown_subscriptions @@ -61,28 +73,27 @@ module ActionController # assert_template %r{\Aadmin/posts/new\Z} # # # assert that the layout 'admin' was rendered - # assert_template :layout => 'admin' - # assert_template :layout => 'layouts/admin' - # assert_template :layout => :admin + # assert_template layout: 'admin' + # assert_template layout: 'layouts/admin' + # assert_template layout: :admin # # # assert that no layout was rendered - # assert_template :layout => nil - # assert_template :layout => false + # assert_template layout: nil + # assert_template layout: false # # # assert that the "_customer" partial was rendered twice - # assert_template :partial => '_customer', :count => 2 + # assert_template partial: '_customer', count: 2 # # # assert that no partials were rendered - # assert_template :partial => false + # assert_template partial: false # # In a view test case, you can also assert that specific locals are passed # to partials: # # # assert that the "_customer" partial was rendered with a specific object - # assert_template :partial => '_customer', :locals => { :customer => @customer } + # assert_template partial: '_customer', locals: { customer: @customer } def assert_template(options = {}, message = nil) - # Force body to be read in case the - # template is being streamed + # Force body to be read in case the template is being streamed. response.body case options @@ -94,7 +105,7 @@ module ActionController matches_template = case options when String - rendered.any? do |t, num| + !options.empty? && rendered.any? do |t, num| options_splited = options.split(File::SEPARATOR) t_splited = t.split(File::SEPARATOR) t_splited.last(options_splited.size) == options_splited @@ -106,6 +117,8 @@ module ActionController end assert matches_template, msg when Hash + options.assert_valid_keys(:layout, :partial, :locals, :count, :file) + if options.key?(:layout) expected_layout = options[:layout] msg = message || sprintf("expecting layout <%s> but action rendered <%s>", @@ -121,10 +134,18 @@ module ActionController end end + if options[:file] + assert_includes @_files.keys, options[:file] + end + if expected_partial = options[:partial] if expected_locals = options[:locals] if defined?(@_rendered_views) - view = expected_partial.to_s.sub(/^_/,'') + view = expected_partial.to_s.sub(/^_/, '').sub(/\/_(?=[^\/]+\z)/, '/') + + partial_was_not_rendered_msg = "expected %s to be rendered but it was not." % view + assert_includes @_rendered_views.rendered_views, view, partial_was_not_rendered_msg + msg = 'expecting %s to be rendered with %s but was with %s' % [expected_partial, expected_locals, @_rendered_views.locals_for(view)] @@ -234,18 +255,39 @@ module ActionController end end + # Methods #destroy and #load! are overridden to avoid calling methods on the + # @store object, which does not exist for the TestSession class. class TestSession < Rack::Session::Abstract::SessionHash #:nodoc: DEFAULT_OPTIONS = Rack::Session::Abstract::ID::DEFAULT_OPTIONS def initialize(session = {}) super(nil, nil) - replace(session.stringify_keys) + @id = SecureRandom.hex(16) + @data = stringify_keys(session) @loaded = true end def exists? true end + + def keys + @data.keys + end + + def values + @data.values + end + + def destroy + clear + end + + private + + def load! + @id + end end # Superclass for ActionController functional tests. Functional tests allow you to @@ -267,21 +309,21 @@ module ActionController # class BooksControllerTest < ActionController::TestCase # def test_create # # Simulate a POST response with the given HTTP parameters. - # post(:create, :book => { :title => "Love Hina" }) + # post(:create, book: { title: "Love Hina" }) # # # Assert that the controller tried to redirect us to # # the created book's URI. # assert_response :found # # # Assert that the controller really put the book in the database. - # assert_not_nil Book.find_by_title("Love Hina") + # assert_not_nil Book.find_by(title: "Love Hina") # end # end # # You can also send a real document in the simulated HTTP request. # # def test_create - # json = {:book => { :title => "Love Hina" }}.to_json + # json = {book: { title: "Love Hina" }}.to_json # post :create, json # end # @@ -356,15 +398,8 @@ module ActionController # # If you're using named routes, they can be easily tested using the original named routes' methods straight in the test case. # - # assert_redirected_to page_url(:title => 'foo') + # assert_redirected_to page_url(title: 'foo') class TestCase < ActiveSupport::TestCase - - # Use AC::TestCase for the base class when describing a controller - register_spec_type(self) do |desc| - Class === desc && desc < ActionController::Metal - end - register_spec_type(/Controller( ?Test)?\z/i, self) - module Behavior extend ActiveSupport::Concern include ActionDispatch::TestProcess @@ -416,41 +451,54 @@ module ActionController end - # Executes a request simulating GET HTTP method and set/volley the response + # Simulate a GET request with the given parameters. + # + # - +action+: The controller action to call. + # - +parameters+: The HTTP parameters that you want to pass. This may + # be +nil+, a hash, or a string that is appropriately encoded + # (<tt>application/x-www-form-urlencoded</tt> or <tt>multipart/form-data</tt>). + # - +session+: A hash of parameters to store in the session. This may be +nil+. + # - +flash+: A hash of parameters to store in the flash. This may be +nil+. + # + # You can also simulate POST, PATCH, PUT, DELETE, HEAD, and OPTIONS requests with + # +post+, +patch+, +put+, +delete+, +head+, and +options+. + # + # Note that the request method is not verified. The different methods are + # available to make the tests more expressive. def get(action, *args) process(action, "GET", *args) end - # Executes a request simulating POST HTTP method and set/volley the response + # Simulate a POST request with the given parameters and set/volley the response. + # See +get+ for more details. def post(action, *args) process(action, "POST", *args) end - # Executes a request simulating PATCH HTTP method and set/volley the response + # Simulate a PATCH request with the given parameters and set/volley the response. + # See +get+ for more details. def patch(action, *args) process(action, "PATCH", *args) end - # Executes a request simulating PUT HTTP method and set/volley the response + # Simulate a PUT request with the given parameters and set/volley the response. + # See +get+ for more details. def put(action, *args) process(action, "PUT", *args) end - # Executes a request simulating DELETE HTTP method and set/volley the response + # Simulate a DELETE request with the given parameters and set/volley the response. + # See +get+ for more details. def delete(action, *args) process(action, "DELETE", *args) end - # Executes a request simulating HEAD HTTP method and set/volley the response + # Simulate a HEAD request with the given parameters and set/volley the response. + # See +get+ for more details. def head(action, *args) process(action, "HEAD", *args) end - # Executes a request simulating OPTIONS HTTP method and set/volley the response - def options(action, *args) - process(action, "OPTIONS", *args) - end - def xml_http_request(request_method, action, parameters = nil, session = nil, flash = nil) @request.env['HTTP_X_REQUESTED_WITH'] = 'XMLHttpRequest' @request.env['HTTP_ACCEPT'] ||= [Mime::JS, Mime::HTML, Mime::XML, 'text/xml', Mime::ALL].join(', ') @@ -476,7 +524,6 @@ module ActionController def process(action, http_method = 'GET', *args) check_required_ivars - http_method, args = handle_old_process_api(http_method, args) if args.first.is_a?(String) && http_method != 'HEAD' @request.env['RAW_POST_DATA'] = args.shift @@ -504,12 +551,12 @@ module ActionController parameters ||= {} controller_class_name = @controller.class.anonymous? ? "anonymous" : - @controller.class.name.underscore.sub(/_controller$/, '') + @controller.class.controller_path @request.assign_parameters(@routes, controller_class_name, action.to_s, parameters) @request.session.update(session) if session - @request.session["flash"] = @request.flash.update(flash || {}) + @request.flash.update(flash || {}) @controller.request = @request @controller.response = @response @@ -526,6 +573,7 @@ module ActionController @response.prepare! @assigns = @controller.respond_to?(:view_assigns) ? @controller.view_assigns : {} + @request.session['flash'] = @request.flash.to_session_value @request.session.delete('flash') if @request.session['flash'].blank? @response end @@ -579,17 +627,6 @@ module ActionController end end - def handle_old_process_api(http_method, args) - # 4.0: Remove this method. - if http_method.is_a?(Hash) - ActiveSupport::Deprecation.warn("TestCase#process now expects the HTTP method as second argument: process(action, http_method, params, session, flash)") - args.unshift(http_method) - http_method = args.last.is_a?(String) ? args.last : "GET" - end - - [http_method, args] - end - def build_request_uri(action, parameters) unless @request.env["PATH_INFO"] options = @controller.respond_to?(:url_options) ? @controller.__send__(:url_options).merge(parameters) : parameters diff --git a/actionpack/lib/action_controller/vendor/html-scanner.rb b/actionpack/lib/action_controller/vendor/html-scanner.rb deleted file mode 100644 index 896208bc05..0000000000 --- a/actionpack/lib/action_controller/vendor/html-scanner.rb +++ /dev/null @@ -1,5 +0,0 @@ -require 'action_view/vendor/html-scanner' -require 'active_support/deprecation' - -ActiveSupport::Deprecation.warn 'Vendored html-scanner was moved to action_view, please require "action_view/vendor/html-scanner" instead. ' + - 'This file will be removed in Rails 4.1' |