diff options
Diffstat (limited to 'actionpack')
118 files changed, 3084 insertions, 739 deletions
diff --git a/actionpack/CHANGELOG.md b/actionpack/CHANGELOG.md index 4f95a9bab9..6b73b29ace 100644 --- a/actionpack/CHANGELOG.md +++ b/actionpack/CHANGELOG.md @@ -1,3 +1,213 @@ +* Update default rendering policies when the controller action did + not explicitly indicate a response. + + For API controllers, the implicit render always renders "204 No Content" + and does not account for any templates. + + For other controllers, the following conditions are checked: + + First, if a template exists for the controller action, it is rendered. + This template lookup takes into account the action name, locales, format, + variant, template handlers, etc. (see +render+ for details). + + Second, if other templates exist for the controller action but is not in + the right format (or variant, etc.), an <tt>ActionController::UnknownFormat</tt> + is raised. The list of available templates is assumed to be a complete + enumeration of all the possible formats (or variants, etc.); that is, + having only HTML and JSON templates indicate that the controller action is + not meant to handle XML requests. + + Third, if the current request is an "interactive" browser request (the user + navigated here by entering the URL in the address bar, submiting a form, + clicking on a link, etc. as opposed to an XHR or non-browser API request), + <tt>ActionView::UnknownFormat</tt> is raised to display a helpful error + message. + + Finally, it falls back to the same "204 No Content" behavior as API controllers. + + *Godfrey Chan*, *Jon Moss*, *Kasper Timm Hansen*, *Mike Clark*, *Matthew Draper* + +## Rails 5.0.0.beta3 (February 24, 2016) ## + +* Add application/gzip as a default mime type. + + *Mehmet Emin İNAÇ* + +* Add request encoding and response parsing to integration tests. + + What previously was: + + ```ruby + require 'test_helper' + + class ApiTest < ActionDispatch::IntegrationTest + test 'creates articles' do + assert_difference -> { Article.count } do + post articles_path(format: :json), + params: { article: { title: 'Ahoy!' } }.to_json, + headers: { 'Content-Type' => 'application/json' } + end + + assert_equal({ 'id' => Article.last.id, 'title' => 'Ahoy!' }, JSON.parse(response.body)) + end + end + ``` + + Can now be written as: + + ```ruby + require 'test_helper' + + class ApiTest < ActionDispatch::IntegrationTest + test 'creates articles' do + assert_difference -> { Article.count } do + post articles_path, params: { article: { title: 'Ahoy!' } }, as: :json + end + + assert_equal({ 'id' => Article.last.id, 'title' => 'Ahoy!' }, response.parsed_body) + end + end + ``` + + Passing `as: :json` to integration test request helpers will set the format, + content type and encode the parameters as JSON. + + Then on the response side, `parsed_body` will parse the body according to the + content type the response has. + + Currently JSON is the only supported MIME type. Add your own with + `ActionDispatch::IntegrationTest.register_encoder`. + + *Kasper Timm Hansen* + +* Add image/svg+xml as a default mime type. + + *DHH* + +## Rails 5.0.0.beta2 (February 01, 2016) ## + +* Add `-g` and `-c` options to `bin/rails routes`. These options return the url `name`, `verb` and + `path` field that match the pattern or match a specific controller. + + Deprecate `CONTROLLER` env variable in `bin/rails routes`. + + See #18902. + + *Anton Davydov* & *Vipul A M* + +* Response etags to always be weak: Prefixes 'W/' to value returned by + `ActionDispatch::Http::Cache::Response#etag=`, such that etags set in + `fresh_when` and `stale?` are weak. + + Fixes #17556. + + *Abhishek Yadav* + +* Provide the name of HTTP Status code in assertions. + + *Sean Collins* + +* More explicit error message when running `rake routes`. `CONTROLLER` argument + can now be supplied in different ways: + `Rails::WelcomeController`, `Rails::Welcome`, `rails/welcome`. + + Fixes #22918. + + *Edouard Chin* + +* Allow `ActionController::Parameters` instances as an argument to URL + helper methods. An `ArgumentError` will be raised if the passed parameters + are not secure. + + Fixes #22832. + + *Prathamesh Sonpatki* + +* Add option for per-form CSRF tokens. + + *Greg Ose & Ben Toews* + +* Add tests and documentation for `ActionController::Renderers::use_renderers`. + + *Benjamin Fleischer* + +* Fix `ActionController::Parameters#convert_parameters_to_hashes` to return filtered + or unfiltered values based on from where it is called, `to_h` or `to_unsafe_h` + respectively. + + Fixes #22841. + + *Prathamesh Sonpatki* + +* Add `ActionController::Parameters#include?` + + *Justin Coyne* + +## Rails 5.0.0.beta1 (December 18, 2015) ## + +* Deprecate `redirect_to :back` in favor of `redirect_back`, which accepts a + required `fallback_location` argument, thus eliminating the possibility of a + `RedirectBackError`. + + *Derek Prior* + +* Add `redirect_back` method to `ActionController::Redirecting` to provide a + way to safely redirect to the `HTTP_REFERER` if it is present, falling back + to a provided redirect otherwise. + + *Derek Prior* + +* `ActionController::TestCase` will be moved to it's own gem in Rails 5.1 + + With the speed improvements made to `ActionDispatch::IntegrationTest` we no + longer need to keep two separate code bases for testing controllers. In + Rails 5.1 `ActionController::TestCase` will be deprecated and moved into a + gem outside of Rails source. + + This is a documentation deprecation so that going forward so new tests will use + `ActionDispatch::IntegrationTest` instead of `ActionController::TestCase`. + + *Eileen M. Uchitelle* + +* Add a `response_format` option to `ActionDispatch::DebugExceptions` + to configure the format of the response when errors occur in + development mode. + + If `response_format` is `:default` the debug info will be rendered + in an HTML page. In the other hand, if the provided value is `:api` + the debug info will be rendered in the original response format. + + *Jorge Bejar* + +* Change the `protect_from_forgery` prepend default to `false`. + + Per this comment + https://github.com/rails/rails/pull/18334#issuecomment-69234050 we want + `protect_from_forgery` to default to `prepend: false`. + + `protect_from_forgery` will now be inserted into the callback chain at the + point it is called in your application. This is useful for cases where you + want to `protect_from_forgery` after you perform required authentication + callbacks or other callbacks that are required to run after forgery protection. + + If you want `protect_from_forgery` callbacks to always run first, regardless of + position they are called in your application then you can add `prepend: true` + to your `protect_from_forgery` call. + + Example: + + ```ruby + protect_from_forgery prepend: true + ``` + + *Eileen M. Uchitelle* + +* In url_for, never append a question mark to the URL when the query string + is empty anyway. (It used to do that when called like `url_for(controller: + 'x', action: 'y', q: {})`.) + + *Paul Grayson* + * Catch invalid UTF-8 querystring values and respond with BadRequest Check querystring params for invalid UTF-8 characters, and raise an @@ -17,31 +227,29 @@ *Agis Anastasopoulos* -* Deprecate `config.static_cache_control` in favor of - `config.public_file_server.headers` - - *Yuki Nishijima* - -* Add the ability of returning arbitrary headers to ActionDispatch::Static +* Add the ability of returning arbitrary headers to `ActionDispatch::Static`. Now ActionDispatch::Static can accept HTTP headers so that developers will have control of returning arbitrary headers like 'Access-Control-Allow-Origin' when a response is delivered. They can be configured with `#config`: - config.public_file_server.headers = { - "Cache-Control" => "public, max-age=60", - "Access-Control-Allow-Origin" => "http://rubyonrails.org" - } + Example: + + config.public_file_server.headers = { + "Cache-Control" => "public, max-age=60", + "Access-Control-Allow-Origin" => "http://rubyonrails.org" + } *Yuki Nishijima* * Allow multiple `root` routes in same scope level. Example: - ```ruby - root 'blog#show', constraints: ->(req) { Hostname.blog_site?(req.host) } - root 'landing#show' - ``` + Example: + + root 'blog#show', constraints: ->(req) { Hostname.blog_site?(req.host) } + root 'landing#show' + *Rafael Sales* * Fix regression in mounted engine named routes generation for app deployed to @@ -52,19 +260,19 @@ *Matthew Erhard* -* ActionDispatch::Response#new no longer applies default headers. If you want +* `ActionDispatch::Response#new` no longer applies default headers. If you want default headers applied to the response object, then call - `ActionDispatch::Response.create`. This change only impacts people who are + `ActionDispatch::Response.create`. This change only impacts people who are directly constructing an `ActionDispatch::Response` object. -* Accessing mime types via constants like `Mime::HTML` is deprecated. Please +* Accessing mime types via constants like `Mime::HTML` is deprecated. Please change code like this: - Mime::HTML + Mime::HTML To this: - Mime[:html] + Mime[:html] This change is so that Rails will not manage a list of constants, and fixes an issue where if a type isn't registered you could possibly get the wrong @@ -110,7 +318,7 @@ *Jeremy Friesen* -* Using strings or symbols for middleware class names is deprecated. Convert +* Using strings or symbols for middleware class names is deprecated. Convert things like this: middleware.use "Foo::Bar" @@ -119,10 +327,10 @@ middleware.use Foo::Bar -* ActionController::TestSession now accepts a default value as well as +* `ActionController::TestSession` now accepts a default value as well as a block for generating a default value based off the key provided. - This fixes calls to session#fetch in ApplicationController instances that + This fixes calls to `session#fetch` in `ApplicationController` instances that take more two arguments or a block from raising `ArgumentError: wrong number of arguments (2 for 1)` when performing controller tests. @@ -173,10 +381,10 @@ *Grey Baker* * Add support for API only apps. - ActionController::API is added as a replacement of - ActionController::Base for this kind of applications. + `ActionController::API` is added as a replacement of + `ActionController::Base` for this kind of applications. - *Santiago Pastorino & Jorge Bejar* + *Santiago Pastorino*, *Jorge Bejar* * Remove `assigns` and `assert_template`. Both methods have been extracted into a gem at https://github.com/rails/rails-controller-testing. @@ -251,7 +459,7 @@ * Allow `Bearer` as token-keyword in `Authorization-Header`. - Aditionally to `Token`, the keyword `Bearer` is acceptable as a keyword + Additionally to `Token`, the keyword `Bearer` is acceptable as a keyword for the auth-token. The `Bearer` keyword is described in the original OAuth RFC and used in libraries like Angular-JWT. @@ -259,7 +467,7 @@ *Peter Schröder* -* Drop request class from RouteSet constructor. +* Drop request class from `RouteSet` constructor. If you would like to use a custom request class, please subclass and implement the `request_class` method. @@ -288,7 +496,7 @@ *Jeremy Kemper*, *Yves Senn* -* Deprecate AbstractController#skip_action_callback in favor of individual skip_callback methods +* Deprecate `AbstractController#skip_action_callback` in favor of individual skip_callback methods (which can be made to raise an error if no callback was removed). *Iain Beeston* @@ -494,9 +702,7 @@ Fixes an issue where when an exception is raised in the request the additional payload data is not available. - See: - * #14903 - * https://github.com/roidrage/lograge/issues/37 + See #14903. *Dieter Komendera*, *Margus Pärt* diff --git a/actionpack/MIT-LICENSE b/actionpack/MIT-LICENSE index 3ec7a617cf..8573eb1225 100644 --- a/actionpack/MIT-LICENSE +++ b/actionpack/MIT-LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2004-2015 David Heinemeier Hansson +Copyright (c) 2004-2016 David Heinemeier Hansson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/actionpack/README.rdoc b/actionpack/README.rdoc index 44c980b070..0720c66cb9 100644 --- a/actionpack/README.rdoc +++ b/actionpack/README.rdoc @@ -28,7 +28,7 @@ can be used outside of Rails. The latest version of Action Pack can be installed with RubyGems: - % gem install actionpack + $ gem install actionpack Source code can be downloaded as part of the Rails project on GitHub @@ -55,4 +55,3 @@ Bug reports can be filed for the Ruby on Rails project here: Feature requests should be discussed on the rails-core mailing list here: * https://groups.google.com/forum/?fromgroups#!forum/rubyonrails-core - diff --git a/actionpack/Rakefile b/actionpack/Rakefile index 601263bfac..37a269cffd 100644 --- a/actionpack/Rakefile +++ b/actionpack/Rakefile @@ -5,6 +5,9 @@ test_files = Dir.glob('test/**/*_test.rb') desc "Default Task" task :default => :test +task :package +task "package:clean" + # Run the unit tests Rake::TestTask.new do |t| t.libs << 'test' diff --git a/actionpack/lib/abstract_controller.rb b/actionpack/lib/abstract_controller.rb index 56c4033387..1e57cbaac4 100644 --- a/actionpack/lib/abstract_controller.rb +++ b/actionpack/lib/abstract_controller.rb @@ -6,6 +6,7 @@ module AbstractController extend ActiveSupport::Autoload autoload :Base + autoload :Caching autoload :Callbacks autoload :Collector autoload :DoubleRenderError, "abstract_controller/rendering" @@ -15,4 +16,9 @@ module AbstractController autoload :Translation autoload :AssetPaths autoload :UrlFor + + def self.eager_load! + super + AbstractController::Caching.eager_load! + end end diff --git a/actionpack/lib/abstract_controller/base.rb b/actionpack/lib/abstract_controller/base.rb index 7f349f2741..8edea0f52b 100644 --- a/actionpack/lib/abstract_controller/base.rb +++ b/actionpack/lib/abstract_controller/base.rb @@ -49,7 +49,7 @@ module AbstractController # instance methods on that abstract class. Public instance methods of # a controller would normally be considered action methods, so methods # declared on abstract classes are being removed. - # (ActionController::Metal and ActionController::Base are defined as abstract) + # (<tt>ActionController::Metal</tt> and ActionController::Base are defined as abstract) def internal_methods controller = self @@ -80,7 +80,7 @@ module AbstractController # action_methods are cached and there is sometimes need to refresh # them. ::clear_action_methods! allows you to do that, so next time - # you run action_methods, they will be recalculated + # you run action_methods, they will be recalculated. def clear_action_methods! @action_methods = nil end diff --git a/actionpack/lib/abstract_controller/caching.rb b/actionpack/lib/abstract_controller/caching.rb new file mode 100644 index 0000000000..0dea50889a --- /dev/null +++ b/actionpack/lib/abstract_controller/caching.rb @@ -0,0 +1,62 @@ +module AbstractController + module Caching + extend ActiveSupport::Concern + extend ActiveSupport::Autoload + + eager_autoload do + autoload :Fragments + end + + module ConfigMethods + def cache_store + config.cache_store + end + + def cache_store=(store) + config.cache_store = ActiveSupport::Cache.lookup_store(store) + end + + private + def cache_configured? + perform_caching && cache_store + end + end + + include ConfigMethods + include AbstractController::Caching::Fragments + + included do + extend ConfigMethods + + config_accessor :default_static_extension + self.default_static_extension ||= '.html' + + 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 view_cache_dependencies + self.class._view_cache_dependencies.map { |dep| instance_exec(&dep) }.compact + end + + protected + # Convenience accessor. + def cache(key, options = {}, &block) + if cache_configured? + cache_store.fetch(ActiveSupport::Cache.expand_cache_key(key, :controller), options, &block) + else + yield + end + end + end +end diff --git a/actionpack/lib/action_controller/caching/fragments.rb b/actionpack/lib/abstract_controller/caching/fragments.rb index 2694d4c12f..3257a731ed 100644 --- a/actionpack/lib/action_controller/caching/fragments.rb +++ b/actionpack/lib/abstract_controller/caching/fragments.rb @@ -1,4 +1,4 @@ -module ActionController +module AbstractController module Caching # Fragment caching is used for caching various blocks within # views without caching the entire action as a whole. This is @@ -14,12 +14,57 @@ module ActionController # # expire_fragment('name_of_cache') module Fragments + extend ActiveSupport::Concern + + included do + if respond_to?(:class_attribute) + class_attribute :fragment_cache_keys + else + mattr_writer :fragment_cache_keys + end + + self.fragment_cache_keys = [] + + helper_method :fragment_cache_key if respond_to?(:helper_method) + end + + module ClassMethods + # Allows you to specify controller-wide key prefixes for + # cache fragments. Pass either a constant +value+, or a block + # which computes a value each time a cache key is generated. + # + # For example, you may want to prefix all fragment cache keys + # with a global version identifier, so you can easily + # invalidate all caches. + # + # class ApplicationController + # fragment_cache_key "v1" + # end + # + # When it's time to invalidate all fragments, simply change + # the string constant. Or, progressively roll out the cache + # invalidation using a computed value: + # + # class ApplicationController + # fragment_cache_key do + # @account.id.odd? ? "v1" : "v2" + # end + # end + def fragment_cache_key(value = nil, &key) + self.fragment_cache_keys += [key || ->{ value }] + end + end + # Given a key (as described in +expire_fragment+), returns # a key suitable for use in reading, writing, or expiring a - # cached fragment. All keys are prefixed with <tt>views/</tt> and uses - # ActiveSupport::Cache.expand_cache_key for the expansion. + # cached fragment. All keys begin with <tt>views/</tt>, + # followed by any controller-wide key prefix values, ending + # with the specified +key+ value. The key is expanded using + # ActiveSupport::Cache.expand_cache_key. def fragment_cache_key(key) - ActiveSupport::Cache.expand_cache_key(key.is_a?(Hash) ? url_for(key).split("://").last : key, :views) + head = self.class.fragment_cache_keys.map { |k| instance_exec(&k) } + tail = key.is_a?(Hash) ? url_for(key).split("://").last : key + ActiveSupport::Cache.expand_cache_key([*head, *tail], :views) end # Writes +content+ to the location signified by @@ -90,13 +135,8 @@ module ActionController end def instrument_fragment_cache(name, key) # :nodoc: - payload = { - controller: controller_name, - action: action_name, - key: key - } - - ActiveSupport::Notifications.instrument("#{name}.action_controller", payload) { yield } + payload = instrument_payload(key) + ActiveSupport::Notifications.instrument("#{name}.#{instrument_name}", payload) { yield } end end end diff --git a/actionpack/lib/abstract_controller/callbacks.rb b/actionpack/lib/abstract_controller/callbacks.rb index 287550db42..d63ce9c1c3 100644 --- a/actionpack/lib/abstract_controller/callbacks.rb +++ b/actionpack/lib/abstract_controller/callbacks.rb @@ -39,8 +39,8 @@ module AbstractController # except: :index, if: -> { true } # the :except option will be ignored. # # ==== Options - # * <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>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. def _normalize_callback_options(options) _normalize_callback_option(options, :only, :if) _normalize_callback_option(options, :except, :unless) @@ -48,7 +48,8 @@ module AbstractController def _normalize_callback_option(options, from, to) # :nodoc: if from = options[from] - from = Array(from).map {|o| "action_name == '#{o}'"}.join(" || ") + _from = Array(from).map(&:to_s).to_set + from = proc {|c| _from.include? c.action_name } options[to] = Array(options[to]).unshift(from) end end @@ -59,7 +60,7 @@ module AbstractController # * <tt>names</tt> - A list of valid names that could be used for # callbacks. Note that skipping uses Ruby equality, so it's # impossible to skip a callback defined using an anonymous proc - # using #skip_action_callback + # using #skip_action_callback. def skip_action_callback(*names) ActiveSupport::Deprecation.warn('`skip_action_callback` is deprecated and will be removed in Rails 5.1. Please use skip_before_action, skip_after_action or skip_around_action instead.') skip_before_action(*names, raise: false) @@ -82,8 +83,8 @@ module AbstractController # * <tt>block</tt> - A proc that should be added to the callbacks. # # ==== Block Parameters - # * <tt>name</tt> - The callback to be added - # * <tt>options</tt> - A hash of options to be used when adding the callback + # * <tt>name</tt> - The callback to be added. + # * <tt>options</tt> - A hash of options to be used when adding the callback. def _insert_callbacks(callbacks, block = nil) options = callbacks.extract_options! _normalize_callback_options(options) diff --git a/actionpack/lib/abstract_controller/rendering.rb b/actionpack/lib/abstract_controller/rendering.rb index a73f188623..e765d73ce4 100644 --- a/actionpack/lib/abstract_controller/rendering.rb +++ b/actionpack/lib/abstract_controller/rendering.rb @@ -82,7 +82,13 @@ module AbstractController # <tt>render :file => "foo/bar"</tt>. # :api: plugin def _normalize_args(action=nil, options={}) - if action.is_a? Hash + if action.respond_to?(:permitted?) + if action.permitted? + action + else + raise ArgumentError, "render parameters are not permitted" + end + elsif action.is_a?(Hash) action else options diff --git a/actionpack/lib/action_controller.rb b/actionpack/lib/action_controller.rb index 3d3af555c9..62f5905205 100644 --- a/actionpack/lib/action_controller.rb +++ b/actionpack/lib/action_controller.rb @@ -9,12 +9,15 @@ module ActionController autoload :API autoload :Base - autoload :Caching autoload :Metal autoload :Middleware autoload :Renderer autoload :FormBuilder + eager_autoload do + autoload :Caching + end + autoload_under "metal" do autoload :ConditionalGet autoload :Cookies @@ -41,13 +44,12 @@ module ActionController autoload :UrlFor end + autoload_under "api" do + autoload :ApiRendering + end + autoload :TestCase, 'action_controller/test_case' autoload :TemplateAssertions, 'action_controller/test_case' - - def self.eager_load! - super - ActionController::Caching.eager_load! - end end # Common Active Support usage in Action Controller diff --git a/actionpack/lib/action_controller/api.rb b/actionpack/lib/action_controller/api.rb index 1a46d49a49..ff12705abe 100644 --- a/actionpack/lib/action_controller/api.rb +++ b/actionpack/lib/action_controller/api.rb @@ -112,7 +112,7 @@ module ActionController UrlFor, Redirecting, - Rendering, + ApiRendering, Renderers::All, ConditionalGet, BasicImplicitRender, diff --git a/actionpack/lib/action_controller/api/api_rendering.rb b/actionpack/lib/action_controller/api/api_rendering.rb new file mode 100644 index 0000000000..3a08d28c39 --- /dev/null +++ b/actionpack/lib/action_controller/api/api_rendering.rb @@ -0,0 +1,14 @@ +module ActionController + module ApiRendering + extend ActiveSupport::Concern + + included do + include Rendering + end + + def render_to_body(options = {}) + _process_options(options) + super + end + end +end diff --git a/actionpack/lib/action_controller/caching.rb b/actionpack/lib/action_controller/caching.rb index 0b8fa2ea09..a9a8508abc 100644 --- a/actionpack/lib/action_controller/caching.rb +++ b/actionpack/lib/action_controller/caching.rb @@ -1,6 +1,3 @@ -require 'fileutils' -require 'uri' - 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. @@ -23,65 +20,25 @@ module ActionController # config.action_controller.cache_store = :mem_cache_store, Memcached::Rails.new('localhost:11211') # config.action_controller.cache_store = MyOwnStore.new('parameter') module Caching - extend ActiveSupport::Concern extend ActiveSupport::Autoload - - eager_autoload do - autoload :Fragments - end - - module ConfigMethods - def cache_store - config.cache_store - end - - def cache_store=(store) - config.cache_store = ActiveSupport::Cache.lookup_store(store) - end - - private - def cache_configured? - perform_caching && cache_store - end - end - - include AbstractController::Callbacks - - include ConfigMethods - include Fragments + extend ActiveSupport::Concern included do - extend ConfigMethods - - config_accessor :default_static_extension - self.default_static_extension ||= '.html' - - 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) + include AbstractController::Caching end - module ClassMethods - def view_cache_dependency(&dependency) - self._view_cache_dependencies += [dependency] - end - end + private - def view_cache_dependencies - self.class._view_cache_dependencies.map { |dep| instance_exec(&dep) }.compact - end + def instrument_payload(key) + { + controller: controller_name, + action: action_name, + key: key + } + end - protected - # Convenience accessor. - def cache(key, options = {}, &block) - if cache_configured? - cache_store.fetch(ActiveSupport::Cache.expand_cache_key(key, :controller), options, &block) - else - yield - end + def instrument_name + "action_controller" end end end diff --git a/actionpack/lib/action_controller/log_subscriber.rb b/actionpack/lib/action_controller/log_subscriber.rb index 4c9f14e409..a0917b4fdb 100644 --- a/actionpack/lib/action_controller/log_subscriber.rb +++ b/actionpack/lib/action_controller/log_subscriber.rb @@ -25,7 +25,9 @@ module ActionController status = ActionDispatch::ExceptionWrapper.status_code_for_exception(exception_class_name) end message = "Completed #{status} #{Rack::Utils::HTTP_STATUS_CODES[status]} in #{event.duration.round}ms" - message << " (#{additions.join(" | ".freeze)})" unless additions.blank? + message << " (#{additions.join(" | ".freeze)})" unless additions.empty? + message << "\n\n" if defined?(Rails.env) && Rails.env.development? + message end end diff --git a/actionpack/lib/action_controller/metal.rb b/actionpack/lib/action_controller/metal.rb index 8e040bb465..f6e67b02d7 100644 --- a/actionpack/lib/action_controller/metal.rb +++ b/actionpack/lib/action_controller/metal.rb @@ -166,7 +166,7 @@ module ActionController alias :response_code :status # :nodoc: - # Basic url_for that can be overridden for more robust functionality + # Basic url_for that can be overridden for more robust functionality. def url_for(string) string end @@ -174,10 +174,8 @@ module ActionController def response_body=(body) body = [body] unless body.nil? || body.respond_to?(:each) response.reset_body! - body.each { |part| - next if part.empty? - response.write part - } + return unless body + response.body = body super end diff --git a/actionpack/lib/action_controller/metal/basic_implicit_render.rb b/actionpack/lib/action_controller/metal/basic_implicit_render.rb index 6c6f8381ff..cef65a362c 100644 --- a/actionpack/lib/action_controller/metal/basic_implicit_render.rb +++ b/actionpack/lib/action_controller/metal/basic_implicit_render.rb @@ -1,5 +1,5 @@ module ActionController - module BasicImplicitRender + module BasicImplicitRender # :nodoc: def send_action(method, *args) super.tap { default_render unless performed? } end diff --git a/actionpack/lib/action_controller/metal/conditional_get.rb b/actionpack/lib/action_controller/metal/conditional_get.rb index 89d589c486..f8e0d9cf6c 100644 --- a/actionpack/lib/action_controller/metal/conditional_get.rb +++ b/actionpack/lib/action_controller/metal/conditional_get.rb @@ -66,7 +66,7 @@ module ActionController # # You can also pass an object that responds to +maximum+, such as a # collection of active records. In this case +last_modified+ will be set by - # calling +maximum(:updated_at)+ on the collection (the timestamp of the + # calling <tt>maximum(:updated_at)</tt> on the collection (the timestamp of the # most recently updated record) and the +etag+ by passing the object itself. # # def index @@ -228,7 +228,7 @@ module ActionController expires_in 100.years, public: public yield if stale?(etag: "#{version}-#{request.fullpath}", - last_modified: Time.parse('2011-01-01').utc, + last_modified: Time.new(2011, 1, 1).utc, public: public) end diff --git a/actionpack/lib/action_controller/metal/head.rb b/actionpack/lib/action_controller/metal/head.rb index b2110bf946..5e9832fd4e 100644 --- a/actionpack/lib/action_controller/metal/head.rb +++ b/actionpack/lib/action_controller/metal/head.rb @@ -50,7 +50,6 @@ module ActionController end private - # :nodoc: def include_content?(status) case status when 100..199 diff --git a/actionpack/lib/action_controller/metal/http_authentication.rb b/actionpack/lib/action_controller/metal/http_authentication.rb index 0a36fecd27..35be6d9300 100644 --- a/actionpack/lib/action_controller/metal/http_authentication.rb +++ b/actionpack/lib/action_controller/metal/http_authentication.rb @@ -1,4 +1,5 @@ require 'base64' +require 'active_support/security_utils' module ActionController # Makes it dead easy to do HTTP Basic, Digest and Token authentication. @@ -68,7 +69,11 @@ module ActionController def http_basic_authenticate_with(options = {}) 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] + # This comparison uses & so that it doesn't short circuit and + # uses `variable_size_secure_compare` so that length information + # isn't leaked. + ActiveSupport::SecurityUtils.variable_size_secure_compare(name, options[:name]) & + ActiveSupport::SecurityUtils.variable_size_secure_compare(password, options[:password]) end end end @@ -397,7 +402,7 @@ module ActionController # RewriteRule ^(.*)$ dispatch.fcgi [E=X-HTTP_AUTHORIZATION:%{HTTP:Authorization},QSA,L] module Token TOKEN_KEY = 'token=' - TOKEN_REGEX = /^(Token|Bearer) / + TOKEN_REGEX = /^(Token|Bearer)\s+/ AUTHN_PAIR_DELIMITERS = /(?:,|;|\t+)/ extend self diff --git a/actionpack/lib/action_controller/metal/implicit_render.rb b/actionpack/lib/action_controller/metal/implicit_render.rb index 17fcc2fa02..6b540d42c7 100644 --- a/actionpack/lib/action_controller/metal/implicit_render.rb +++ b/actionpack/lib/action_controller/metal/implicit_render.rb @@ -1,29 +1,80 @@ +require 'active_support/core_ext/string/strip' + module ActionController + # Handles implicit rendering for a controller action when it did not + # explicitly indicate an appropiate response via methods such as +render+, + # +respond_to+, +redirect+ or +head+. + # + # For API controllers, the implicit render always renders "204 No Content" + # and does not account for any templates. + # + # For other controllers, the following conditions are checked: + # + # First, if a template exists for the controller action, it is rendered. + # This template lookup takes into account the action name, locales, format, + # variant, template handlers, etc. (see +render+ for details). + # + # Second, if other templates exist for the controller action but is not in + # the right format (or variant, etc.), an <tt>ActionController::UnknownFormat</tt> + # is raised. The list of available templates is assumed to be a complete + # enumeration of all the possible formats (or variants, etc.); that is, + # having only HTML and JSON templates indicate that the controller action is + # not meant to handle XML requests. + # + # Third, if the current request is an "interactive" browser request (the user + # navigated here by entering the URL in the address bar, submiting a form, + # clicking on a link, etc. as opposed to an XHR or non-browser API request), + # <tt>ActionView::UnknownFormat</tt> is raised to display a helpful error + # message. + # + # Finally, it falls back to the same "204 No Content" behavior as API controllers. module ImplicitRender + # :stopdoc: include BasicImplicitRender - # Renders the template corresponding to the controller action, if it exists. - # The action name, format, and variant are all taken into account. - # For example, the "new" action with an HTML format and variant "phone" - # would try to render the <tt>new.html+phone.erb</tt> template. - # - # If no template is found <tt>ActionController::BasicImplicitRender</tt>'s implementation is called, unless - # a block is passed. In that case, it will override the super implementation. - # - # default_render do - # head 404 # No template was found - # end def default_render(*args) if template_exists?(action_name.to_s, _prefixes, variants: request.variant) render(*args) - else - if block_given? - yield(*args) - else - logger.info "No template found for #{self.class.name}\##{action_name}, rendering head :no_content" if logger - super + elsif any_templates?(action_name.to_s, _prefixes) + message = "#{self.class.name}\##{action_name} does not know how to respond " \ + "to this request. There are other templates available for this controller " \ + "action but none of them were suitable for this request.\n\n" \ + "This usually happens when the client requested an unsupported format " \ + "(e.g. requesting HTML content from a JSON endpoint or vice versa), but " \ + "it might also be failing due to other constraints, such as locales or" \ + "variants.\n" + + if request.formats.any? + message << "\nRequested format(s): #{request.formats.join(", ")}" end + + if request.variant.any? + message << "\nRequested variant(s): #{request.variant.join(", ")}" + end + + raise ActionController::UnknownFormat, message + elsif interactive_browser_request? + message = "You did not define any templates for #{self.class.name}\##{action_name}. " \ + "This is not necessarily a problem (e.g. you might be building an API endpoint " \ + "that does not require any templates), and the controller would usually respond " \ + "with `head :no_content` for your convenience.\n\n" \ + "However, you appear to have navigated here from an interactive browser request – " \ + "such as by navigating to this URL directly, clicking on a link or submitting a form. " \ + "Rendering a `head :no_content` in this case could have resulted in unexpected UI " \ + "behavior in the browser.\n\n" \ + "If you expected the `head :no_content` response, you do not need to take any " \ + "actions – requests coming from an XHR (AJAX) request or other non-browser clients " \ + "will receive the \"204 No Content\" response as expected.\n\n" \ + "If you did not expect this behavior, you can resolve this error by adding a " \ + "template for this controller action (usually `#{action_name}.html.erb`) or " \ + "otherwise indicate the appropriate response in the action using `render`, " \ + "`redirect_to`, `head`, etc.\n" + + raise ActionController::UnknownFormat, message + else + logger.info "No template found for #{self.class.name}\##{action_name}, rendering head :no_content" if logger + super end end @@ -32,5 +83,11 @@ module ActionController "default_render" end end + + private + + def interactive_browser_request? + request.format == Mime[:html] && !request.xhr? + end end end diff --git a/actionpack/lib/action_controller/metal/instrumentation.rb b/actionpack/lib/action_controller/metal/instrumentation.rb index 3dbf34eb2a..bf74b39ac4 100644 --- a/actionpack/lib/action_controller/metal/instrumentation.rb +++ b/actionpack/lib/action_controller/metal/instrumentation.rb @@ -19,9 +19,9 @@ module ActionController :controller => self.class.name, :action => self.action_name, :params => request.filtered_parameters, - :format => request.format.try(:ref), + :format => request.format.ref, :method => request.request_method, - :path => (request.fullpath rescue "unknown") + :path => request.fullpath } ActiveSupport::Notifications.instrument("start_processing.action_controller", raw_payload.dup) diff --git a/actionpack/lib/action_controller/metal/live.rb b/actionpack/lib/action_controller/metal/live.rb index 27b3eb4e58..fc20e7a421 100644 --- a/actionpack/lib/action_controller/metal/live.rb +++ b/actionpack/lib/action_controller/metal/live.rb @@ -36,8 +36,7 @@ module ActionController extend ActiveSupport::Concern module ClassMethods - def make_response!(response) - request = response.request + def make_response!(request) if request.get_header("HTTP_VERSION") == "HTTP/1.0" super else @@ -223,12 +222,6 @@ module ActionController jar.write self unless committed? end - def before_sending - super - request.cookie_jar.commit! - headers.freeze - end - def build_buffer(response, body) buf = Live::Buffer.new response body.each { |part| buf.write part } @@ -244,39 +237,55 @@ module ActionController # This processes the action in a child thread. It lets us return the # response code and headers back up the rack stack, and still process # the body in parallel with sending data to the client - Thread.new { - t2 = Thread.current - t2.abort_on_exception = true - - # Since we're processing the view in a different thread, copy the - # thread locals from the main thread to the child thread. :'( - locals.each { |k,v| t2[k] = v } - - begin - super(name) - rescue => e - if @_response.committed? - 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 + new_controller_thread { + ActiveSupport::Dependencies.interlock.running do + t2 = Thread.current + + # Since we're processing the view in a different thread, copy the + # thread locals from the main thread to the child thread. :'( + locals.each { |k,v| t2[k] = v } + + begin + super(name) + rescue => e + if @_response.committed? + 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 + else + error = e end - else - error = e + ensure + @_response.commit! end - ensure - @_response.commit! end } - @_response.await_commit + ActiveSupport::Dependencies.interlock.permit_concurrent_loads do + @_response.await_commit + end + raise error if error end + # Spawn a new thread to serve up the controller in. This is to get + # around the fact that Rack isn't based around IOs and we need to use + # a thread to stream data from the response bodies. Nobody should call + # this method except in Rails internals. Seriously! + def new_controller_thread # :nodoc: + Thread.new { + t2 = Thread.current + t2.abort_on_exception = true + yield + } + end + def log_error(exception) logger = ActionController::Base.logger return unless logger @@ -293,9 +302,5 @@ module ActionController super response.close if response end - - def set_response!(response) - @_response = self.class.make_response! response - end end end diff --git a/actionpack/lib/action_controller/metal/mime_responds.rb b/actionpack/lib/action_controller/metal/mime_responds.rb index 6e346fadfe..173a14a1d2 100644 --- a/actionpack/lib/action_controller/metal/mime_responds.rb +++ b/actionpack/lib/action_controller/metal/mime_responds.rb @@ -9,6 +9,13 @@ module ActionController #:nodoc: # @people = Person.all # end # + # That action implicitly responds to all formats, but formats can also be whitelisted: + # + # def index + # @people = Person.all + # respond_to :html, :js + # end + # # Here's the same action, with web-service support baked in: # # def index @@ -16,11 +23,12 @@ module ActionController #:nodoc: # # respond_to do |format| # format.html + # format.js # format.xml { render xml: @people } # end # end # - # What that says is, "if the client wants HTML in response to this action, just respond as we + # What that says is, "if the client wants HTML or JS in response to this action, just respond as we # would have before, but if the client wants XML, return them the list of people in XML format." # (Rails determines the desired response format from the HTTP Accept header submitted by the client.) # @@ -180,9 +188,6 @@ module ActionController #:nodoc: # format.html.none # format.html.phone # this gets rendered # end - # - # Be sure to check the documentation of <tt>ActionController::MimeResponds.respond_to</tt> - # for more examples. def respond_to(*mimes) raise ArgumentError, "respond_to takes either types or a block, never both" if mimes.any? && block_given? diff --git a/actionpack/lib/action_controller/metal/redirecting.rb b/actionpack/lib/action_controller/metal/redirecting.rb index 0febc905f1..b13ba06962 100644 --- a/actionpack/lib/action_controller/metal/redirecting.rb +++ b/actionpack/lib/action_controller/metal/redirecting.rb @@ -20,8 +20,6 @@ module ActionController # * <tt>String</tt> starting with <tt>protocol://</tt> (like <tt>http://</tt>) or a protocol relative reference (like <tt>//</tt>) - Is passed straight through as the target for redirection. # * <tt>String</tt> not containing a protocol - The current protocol and host is prepended to the string. # * <tt>Proc</tt> - A block that will be executed in the controller's context. Should return any option accepted by +redirect_to+. - # * <tt>:back</tt> - Back to the page that issued the request. Useful for forms that are triggered from multiple places. - # Short-hand for <tt>redirect_to(request.env["HTTP_REFERER"])</tt> # # === Examples: # @@ -30,7 +28,6 @@ module ActionController # redirect_to "http://www.rubyonrails.org" # redirect_to "/images/screenshot.jpg" # redirect_to articles_url - # redirect_to :back # redirect_to proc { edit_post_url(@post) } # # The redirection happens as a "302 Found" header unless otherwise specified using the <tt>:status</tt> option: @@ -61,13 +58,8 @@ module ActionController # 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, - # <tt>ActionController::RedirectBackError</tt> will be raised. You - # may specify some fallback behavior for this case by rescuing - # <tt>ActionController::RedirectBackError</tt>. def redirect_to(options = {}, response_status = {}) #:doc: raise ActionControllerError.new("Cannot redirect to nil!") unless options - raise ActionControllerError.new("Cannot redirect to a parameter hash!") if options.is_a?(ActionController::Parameters) raise AbstractController::DoubleRenderError if response_body self.status = _extract_redirect_to_status(options, response_status) @@ -75,6 +67,32 @@ module ActionController self.response_body = "<html><body>You are being <a href=\"#{ERB::Util.unwrapped_html_escape(location)}\">redirected</a>.</body></html>" end + # Redirects the browser to the page that issued the request (the referrer) + # if possible, otherwise redirects to the provided default fallback + # location. + # + # The referrer information is pulled from the HTTP `Referer` (sic) header on + # the request. This is an optional header and its presence on the request is + # subject to browser security settings and user preferences. If the request + # is missing this header, the <tt>fallback_location</tt> will be used. + # + # redirect_back fallback_location: { action: "show", id: 5 } + # redirect_back fallback_location: post + # redirect_back fallback_location: "http://www.rubyonrails.org" + # redirect_back fallback_location: "/images/screenshot.jpg" + # redirect_back fallback_location: articles_url + # redirect_back fallback_location: proc { edit_post_url(@post) } + # + # All options that can be passed to <tt>redirect_to</tt> are accepted as + # options and the behavior is indetical. + def redirect_back(fallback_location:, **args) + if referer = request.headers["Referer"] + redirect_to referer, **args + else + redirect_to fallback_location, **args + end + end + def _compute_redirect_to_location(request, options) #:nodoc: case options # The scheme name consist of a letter followed by any combination of @@ -87,6 +105,12 @@ module ActionController when String request.protocol + request.host_with_port + options when :back + ActiveSupport::Deprecation.warn(<<-MESSAGE.squish) + `redirect_to :back` is deprecated and will be removed from Rails 5.1. + Please use `redirect_back(fallback_location: fallback_location)` where + `fallback_location` represents the location to use if the request has + no HTTP referer information. + MESSAGE request.headers["Referer"] or raise RedirectBackError when Proc _compute_redirect_to_location request, options.call diff --git a/actionpack/lib/action_controller/metal/renderers.rb b/actionpack/lib/action_controller/metal/renderers.rb index 22e0bb5955..90fb34e386 100644 --- a/actionpack/lib/action_controller/metal/renderers.rb +++ b/actionpack/lib/action_controller/metal/renderers.rb @@ -11,6 +11,7 @@ module ActionController Renderers.remove(key) end + # See <tt>Responder#api_behavior</tt> class MissingRenderer < LoadError def initialize(format) super "No renderer defined for format: #{format}" @@ -20,40 +21,25 @@ module ActionController module Renderers extend ActiveSupport::Concern + # A Set containing renderer names that correspond to available renderer procs. + # Default values are <tt>:json</tt>, <tt>:js</tt>, <tt>:xml</tt>. + RENDERERS = Set.new + included do class_attribute :_renderers self._renderers = Set.new.freeze end - module ClassMethods - def use_renderers(*args) - renderers = _renderers + args - self._renderers = renderers.freeze - end - alias use_renderer use_renderers - end - - def render_to_body(options) - _render_to_body_with_renderer(options) || super - end + # Used in <tt>ActionController::Base</tt> + # and <tt>ActionController::API</tt> to include all + # renderers by default. + module All + extend ActiveSupport::Concern + include Renderers - def _render_to_body_with_renderer(options) - _renderers.each do |name| - if options.key?(name) - _process_options(options) - method_name = Renderers._render_with_renderer_method_name(name) - return send(method_name, options.delete(name), options) - end + included do + self._renderers = RENDERERS end - nil - end - - # A Set containing renderer names that correspond to available renderer procs. - # Default values are <tt>:json</tt>, <tt>:js</tt>, <tt>:xml</tt>. - RENDERERS = Set.new - - def self._render_with_renderer_method_name(key) - "_render_with_renderer_#{key}" end # Adds a new renderer to call within controller actions. @@ -103,13 +89,70 @@ module ActionController remove_method(method_name) if method_defined?(method_name) end - module All - extend ActiveSupport::Concern - include Renderers + def self._render_with_renderer_method_name(key) + "_render_with_renderer_#{key}" + end - included do - self._renderers = RENDERERS + module ClassMethods + + # Adds, by name, a renderer or renderers to the +_renderers+ available + # to call within controller actions. + # + # It is useful when rendering from an <tt>ActionController::Metal</tt> controller or + # otherwise to add an available renderer proc to a specific controller. + # + # Both <tt>ActionController::Base</tt> and <tt>ActionController::API</tt> + # include <tt>ActionController::Renderers::All</tt>, making all renderers + # avaialable in the controller. See <tt>Renderers::RENDERERS</tt> and <tt>Renderers.add</tt>. + # + # Since <tt>ActionController::Metal</tt> controllers cannot render, the controller + # must include <tt>AbstractController::Rendering</tt>, <tt>ActionController::Rendering</tt>, + # and <tt>ActionController::Renderers</tt>, and have at lest one renderer. + # + # Rather than including <tt>ActionController::Renderers::All</tt> and including all renderers, + # you may specify which renderers to include by passing the renderer name or names to + # +use_renderers+. For example, a controller that includes only the <tt>:json</tt> renderer + # (+_render_with_renderer_json+) might look like: + # + # class MetalRenderingController < ActionController::Metal + # include AbstractController::Rendering + # include ActionController::Rendering + # include ActionController::Renderers + # + # use_renderers :json + # + # def show + # render json: record + # end + # end + # + # You must specify a +use_renderer+, else the +controller.renderer+ and + # +controller._renderers+ will be <tt>nil</tt>, and the action will fail. + def use_renderers(*args) + renderers = _renderers + args + self._renderers = renderers.freeze end + alias use_renderer use_renderers + end + + # Called by +render+ in <tt>AbstractController::Rendering</tt> + # which sets the return value as the +response_body+. + # + # If no renderer is found, +super+ returns control to + # <tt>ActionView::Rendering.render_to_body</tt>, if present. + def render_to_body(options) + _render_to_body_with_renderer(options) || super + end + + def _render_to_body_with_renderer(options) + _renderers.each do |name| + if options.key?(name) + _process_options(options) + method_name = Renderers._render_with_renderer_method_name(name) + return send(method_name, options.delete(name), options) + end + end + nil end add :json do |json, options| diff --git a/actionpack/lib/action_controller/metal/request_forgery_protection.rb b/actionpack/lib/action_controller/metal/request_forgery_protection.rb index 64f6f7cf51..b2f0b382b9 100644 --- a/actionpack/lib/action_controller/metal/request_forgery_protection.rb +++ b/actionpack/lib/action_controller/metal/request_forgery_protection.rb @@ -77,6 +77,14 @@ module ActionController #:nodoc: config_accessor :log_warning_on_csrf_failure self.log_warning_on_csrf_failure = true + # Controls whether the Origin header is checked in addition to the CSRF token. + config_accessor :forgery_protection_origin_check + self.forgery_protection_origin_check = false + + # Controls whether form-action/method specific CSRF tokens are used. + config_accessor :per_form_csrf_tokens + self.per_form_csrf_tokens = false + helper_method :form_authenticity_token helper_method :protect_against_forgery? end @@ -98,13 +106,13 @@ module ActionController #:nodoc: # # Valid Options: # - # * <tt>:only/:except</tt> - Only apply forgery protection to a subset of actions. Like <tt>only: [ :create, :create_all ]</tt>. + # * <tt>:only/:except</tt> - Only apply forgery protection to a subset of actions. For example <tt>only: [ :create, :create_all ]</tt>. # * <tt>:if/:unless</tt> - Turn off the forgery protection entirely depending on the passed Proc or method reference. - # * <tt>:prepend</tt> - By default, the verification of the authentication token is added to the front of the - # callback chain. If you need to make the verification depend on other callbacks, like authentication methods - # (say cookies vs OAuth), this might not work for you. Pass <tt>prepend: false</tt> to just add the - # verification callback in the position of the protect_from_forgery call. This means any callbacks added - # before are run first. + # * <tt>:prepend</tt> - By default, the verification of the authentication token will be added at the position of the + # protect_from_forgery call in your application. This means any callbacks added before are run first. This is useful + # when you want your forgery protection to depend on other callbacks, like authentication methods (Oauth vs Cookie auth). + # + # If you need to add verification to the beginning of the callback chain, use <tt>prepend: true</tt>. # * <tt>:with</tt> - Set the method to handle unverified request. # # Valid unverified request handling methods are: @@ -112,7 +120,7 @@ module ActionController #:nodoc: # * <tt>:reset_session</tt> - Resets the session. # * <tt>:null_session</tt> - Provides an empty session during request but doesn't reset it completely. Used as default if <tt>:with</tt> option is not specified. def protect_from_forgery(options = {}) - options = options.reverse_merge(prepend: true) + options = options.reverse_merge(prepend: false) self.forgery_protection_strategy = protection_method_class(options[:with] || :null_session) self.request_forgery_protection_token ||= :authenticity_token @@ -257,21 +265,41 @@ module ActionController #:nodoc: # * Does the X-CSRF-Token header match the form_authenticity_token def verified_request? !protect_against_forgery? || request.get? || request.head? || - valid_authenticity_token?(session, form_authenticity_param) || - valid_authenticity_token?(session, request.headers['X-CSRF-Token']) + (valid_request_origin? && any_authenticity_token_valid?) + end + + # Checks if any of the authenticity tokens from the request are valid. + def any_authenticity_token_valid? + request_authenticity_tokens.any? do |token| + valid_authenticity_token?(session, token) + end + end + + # Possible authenticity tokens sent in the request. + def request_authenticity_tokens + [form_authenticity_param, request.x_csrf_token] end # Sets the token value for the current session. - def form_authenticity_token - masked_authenticity_token(session) + def form_authenticity_token(form_options: {}) + masked_authenticity_token(session, form_options: form_options) end # Creates a masked version of the authenticity token that varies # on each request. The masking is used to mitigate SSL attacks # like BREACH. - def masked_authenticity_token(session) + def masked_authenticity_token(session, form_options: {}) + action, method = form_options.values_at(:action, :method) + + raw_token = if per_form_csrf_tokens && action && method + action_path = normalize_action_path(action) + per_form_csrf_token(session, action_path, method) + else + real_csrf_token(session) + end + one_time_pad = SecureRandom.random_bytes(AUTHENTICITY_TOKEN_LENGTH) - encrypted_csrf_token = xor_byte_strings(one_time_pad, real_csrf_token(session)) + encrypted_csrf_token = xor_byte_strings(one_time_pad, raw_token) masked_token = one_time_pad + encrypted_csrf_token Base64.strict_encode64(masked_token) end @@ -301,30 +329,58 @@ module ActionController #:nodoc: compare_with_real_token masked_token, session elsif masked_token.length == AUTHENTICITY_TOKEN_LENGTH * 2 - # Split the token into the one-time pad and the encrypted - # value and decrypt it - one_time_pad = masked_token[0...AUTHENTICITY_TOKEN_LENGTH] - encrypted_csrf_token = masked_token[AUTHENTICITY_TOKEN_LENGTH..-1] - csrf_token = xor_byte_strings(one_time_pad, encrypted_csrf_token) - - compare_with_real_token csrf_token, session + csrf_token = unmask_token(masked_token) + compare_with_real_token(csrf_token, session) || + valid_per_form_csrf_token?(csrf_token, session) else false # Token is malformed end end + def unmask_token(masked_token) + # Split the token into the one-time pad and the encrypted + # value and decrypt it + one_time_pad = masked_token[0...AUTHENTICITY_TOKEN_LENGTH] + encrypted_csrf_token = masked_token[AUTHENTICITY_TOKEN_LENGTH..-1] + xor_byte_strings(one_time_pad, encrypted_csrf_token) + end + def compare_with_real_token(token, session) ActiveSupport::SecurityUtils.secure_compare(token, real_csrf_token(session)) end + def valid_per_form_csrf_token?(token, session) + if per_form_csrf_tokens + correct_token = per_form_csrf_token( + session, + normalize_action_path(request.fullpath), + request.request_method + ) + + ActiveSupport::SecurityUtils.secure_compare(token, correct_token) + else + false + end + end + def real_csrf_token(session) session[:_csrf_token] ||= SecureRandom.base64(AUTHENTICITY_TOKEN_LENGTH) Base64.strict_decode64(session[:_csrf_token]) end + def per_form_csrf_token(session, action_path, method) + OpenSSL::HMAC.digest( + OpenSSL::Digest::SHA256.new, + real_csrf_token(session), + [action_path, method.downcase].join("#") + ) + end + def xor_byte_strings(s1, s2) - s1.bytes.zip(s2.bytes).map { |(c1,c2)| c1 ^ c2 }.pack('c*') + s2_bytes = s2.bytes + s1.each_byte.with_index { |c1, i| s2_bytes[i] ^= c1 } + s2_bytes.pack('C*') end # The form's authenticity parameter. Override to provide your own. @@ -336,5 +392,20 @@ module ActionController #:nodoc: def protect_against_forgery? allow_forgery_protection end + + # Checks if the request originated from the same origin by looking at the + # Origin header. + def valid_request_origin? + if forgery_protection_origin_check + # We accept blank origin headers because some user agents don't send it. + request.origin.nil? || request.origin == request.base_url + else + true + end + end + + def normalize_action_path(action_path) + action_path.split('?').first.to_s.chomp('/') + end end end diff --git a/actionpack/lib/action_controller/metal/strong_parameters.rb b/actionpack/lib/action_controller/metal/strong_parameters.rb index 130ba61786..a01110d474 100644 --- a/actionpack/lib/action_controller/metal/strong_parameters.rb +++ b/actionpack/lib/action_controller/metal/strong_parameters.rb @@ -1,8 +1,10 @@ require 'active_support/core_ext/hash/indifferent_access' +require 'active_support/core_ext/hash/transform_values' require 'active_support/core_ext/array/wrap' require 'active_support/core_ext/string/filters' require 'active_support/rescuable' require 'action_dispatch/http/upload' +require 'rack/test' require 'stringio' require 'set' @@ -107,7 +109,8 @@ module ActionController cattr_accessor :permit_all_parameters, instance_accessor: false cattr_accessor :action_on_unpermitted_parameters, instance_accessor: false - delegate :keys, :key?, :has_key?, :empty?, :inspect, to: :@parameters + delegate :keys, :key?, :has_key?, :values, :has_value?, :value?, :empty?, :include?, + :as_json, to: :@parameters # By default, never raise an UnpermittedParameters exception if these # params are present. The default includes both 'controller' and 'action' @@ -119,16 +122,6 @@ module ActionController cattr_accessor :always_permitted_parameters self.always_permitted_parameters = %w( controller action ) - def self.const_missing(const_name) - return super unless const_name == :NEVER_UNPERMITTED_PARAMS - ActiveSupport::Deprecation.warn(<<-MSG.squish) - `ActionController::Parameters::NEVER_UNPERMITTED_PARAMS` has been deprecated. - Use `ActionController::Parameters.always_permitted_parameters` instead. - MSG - - always_permitted_parameters - end - # Returns a new instance of <tt>ActionController::Parameters</tt>. # Also, sets the +permitted+ attribute to the default value of # <tt>ActionController::Parameters.permit_all_parameters</tt>. @@ -151,18 +144,26 @@ module ActionController end # Returns true if another +Parameters+ object contains the same content and - # permitted flag, or other Hash-like object contains the same content. This - # override is in place so you can perform a comparison with `Hash`. - def ==(other_hash) - if other_hash.respond_to?(:permitted?) - super + # permitted flag. + def ==(other) + if other.respond_to?(:permitted?) + self.permitted? == other.permitted? && self.parameters == other.parameters + elsif other.is_a?(Hash) + ActiveSupport::Deprecation.warn <<-WARNING.squish + Comparing equality between `ActionController::Parameters` and a + `Hash` is deprecated and will be removed in Rails 5.1. Please only do + comparisons between instances of `ActionController::Parameters`. If + you need to compare to a hash, first convert it using + `ActionController::Parameters#new`. + WARNING + @parameters == other.with_indifferent_access else - @parameters == other_hash + @parameters == other end end - # Returns a safe +Hash+ representation of this parameter with all - # unpermitted keys removed. + # Returns a safe <tt>ActiveSupport::HashWithIndifferentAccess</tt> + # representation of this parameter with all unpermitted keys removed. # # params = ActionController::Parameters.new({ # name: 'Senjougahara Hitagi', @@ -174,15 +175,17 @@ module ActionController # safe_params.to_h # => {"name"=>"Senjougahara Hitagi"} def to_h if permitted? - @parameters.to_h + convert_parameters_to_hashes(@parameters, :to_h) else slice(*self.class.always_permitted_parameters).permit!.to_h end end - # Returns an unsafe, unfiltered +Hash+ representation of this parameter. + # Returns an unsafe, unfiltered + # <tt>ActiveSupport::HashWithIndifferentAccess</tt> representation of this + # parameter. def to_unsafe_h - @parameters.to_h + convert_parameters_to_hashes(@parameters, :to_unsafe_h) end alias_method :to_unsafe_hash, :to_unsafe_h @@ -415,7 +418,7 @@ module ActionController # params.fetch(:none) # => ActionController::ParameterMissing: param is missing or the value is empty: none # params.fetch(:none, 'Francesco') # => "Francesco" # params.fetch(:none) { 'Francesco' } # => "Francesco" - def fetch(key, *args, &block) + def fetch(key, *args) convert_value_to_parameters( @parameters.fetch(key) { if block_given? @@ -510,7 +513,7 @@ module ActionController # to key. If the key is not found, returns the default value. If the # optional code block is given and the key is not found, pass in the key # and return the result of block. - def delete(key, &block) + def delete(key) convert_value_to_parameters(@parameters.delete(key)) end @@ -575,13 +578,37 @@ module ActionController dup end + def inspect + "<#{self.class} #{@parameters} permitted: #{@permitted}>" + end + + def method_missing(method_sym, *args, &block) + if @parameters.respond_to?(method_sym) + message = <<-DEPRECATE.squish + Method #{method_sym} is deprecated and will be removed in Rails 5.1, + as `ActionController::Parameters` no longer inherits from + hash. Using this deprecated behavior exposes potential security + problems. If you continue to use this method you may be creating + a security vulnerability in your app that can be exploited. Instead, + consider using one of these documented methods which are not + deprecated: http://api.rubyonrails.org/v#{ActionPack.version}/classes/ActionController/Parameters.html + DEPRECATE + ActiveSupport::Deprecation.warn(message) + @parameters.public_send(method_sym, *args, &block) + else + super + end + end + protected + attr_reader :parameters + def permitted=(new_permitted) @permitted = new_permitted end def fields_for_style? - @parameters.all? { |k, v| k =~ /\A-?\d+\z/ && v.is_a?(Hash) } + @parameters.all? { |k, v| k =~ /\A-?\d+\z/ && (v.is_a?(Hash) || v.is_a?(Parameters)) } end private @@ -591,6 +618,21 @@ module ActionController end end + def convert_parameters_to_hashes(value, using) + case value + when Array + value.map { |v| convert_parameters_to_hashes(v, using) } + when Hash + value.transform_values do |v| + convert_parameters_to_hashes(v, using) + end.with_indifferent_access + when Parameters + value.send(using) + else + value + end + end + def convert_hashes_to_parameters(key, value) converted = convert_value_to_parameters(value) @parameters[key] = converted unless converted.equal?(value) diff --git a/actionpack/lib/action_controller/test_case.rb b/actionpack/lib/action_controller/test_case.rb index 442ffd6d7c..700317614f 100644 --- a/actionpack/lib/action_controller/test_case.rb +++ b/actionpack/lib/action_controller/test_case.rb @@ -7,6 +7,24 @@ require 'action_controller/template_assertions' require 'rails-dom-testing' module ActionController + # :stopdoc: + class Metal + include Testing::Functional + end + + module Live + # Disable controller / rendering threads in tests. User tests can access + # the database on the main thread, so they could open a txn, then the + # controller thread will open a new connection and try to access data + # that's only visible to the main thread's txn. This is the problem in #23483 + remove_method :new_controller_thread + def new_controller_thread # :nodoc: + yield + end + end + + # ActionController::TestCase will be deprecated and moved to a gem in Rails 5.1. + # Please use ActionDispatch::IntegrationTest going forward. class TestRequest < ActionDispatch::TestRequest #:nodoc: DEFAULT_ENV = ActionDispatch::TestRequest::DEFAULT_ENV.dup DEFAULT_ENV.delete 'PATH_INFO' @@ -34,7 +52,7 @@ module ActionController self.session = session self.session_options = TestSession::DEFAULT_OPTIONS @custom_param_parsers = { - Mime[:xml] => lambda { |raw_post| Hash.from_xml(raw_post)['hash'] } + xml: lambda { |raw_post| Hash.from_xml(raw_post)['hash'] } } end @@ -87,7 +105,7 @@ module ActionController when :url_encoded_form data = non_path_parameters.to_query else - @custom_param_parsers[content_mime_type] = ->(_) { non_path_parameters } + @custom_param_parsers[content_mime_type.symbol] = ->(_) { non_path_parameters } data = non_path_parameters.to_query end end @@ -452,16 +470,16 @@ module ActionController parameters = nil end - if parameters.present? || session.present? || flash.present? + if parameters || session || flash non_kwarg_request_warning end end - if body.present? + if body @request.set_header 'RAW_POST_DATA', body end - if http_method.present? + if http_method http_method = http_method.to_s.upcase else http_method = "GET" @@ -469,16 +487,12 @@ module ActionController parameters ||= {} - if format.present? + if format parameters[:format] = format end @html_document = nil - unless @controller.respond_to?(:recycle!) - @controller.extend(Testing::Functional) - end - self.cookies.update @request.cookies self.cookies.update_cookies_from_jar @request.set_header 'HTTP_COOKIE', cookies.to_header @@ -658,4 +672,5 @@ module ActionController include Behavior end + # :startdoc: end diff --git a/actionpack/lib/action_dispatch.rb b/actionpack/lib/action_dispatch.rb index f6336c8c7a..1e4df07d6e 100644 --- a/actionpack/lib/action_dispatch.rb +++ b/actionpack/lib/action_dispatch.rb @@ -1,5 +1,5 @@ #-- -# Copyright (c) 2004-2015 David Heinemeier Hansson +# Copyright (c) 2004-2016 David Heinemeier Hansson # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -95,6 +95,7 @@ module ActionDispatch autoload :TestProcess autoload :TestRequest autoload :TestResponse + autoload :AssertionResponse end end diff --git a/actionpack/lib/action_dispatch/http/cache.rb b/actionpack/lib/action_dispatch/http/cache.rb index 30ade14c26..4bd727c14e 100644 --- a/actionpack/lib/action_dispatch/http/cache.rb +++ b/actionpack/lib/action_dispatch/http/cache.rb @@ -80,9 +80,17 @@ module ActionDispatch set_header DATE, utc_time.httpdate end + # This method allows you to set the ETag for cached content, which + # will be returned to the end user. + # + # By default, Action Dispatch sets all ETags to be weak. + # This ensures that if the content changes only semantically, + # the whole page doesn't have to be regenerated from scratch + # by the web server. With strong ETags, pages are compared + # byte by byte, and are regenerated only if they are not exactly equal. def etag=(etag) key = ActiveSupport::Cache.expand_cache_key(etag) - super %("#{Digest::MD5.hexdigest(key)}") + super %(W/"#{Digest::MD5.hexdigest(key)}") end def etag?; etag; end @@ -91,7 +99,7 @@ module ActionDispatch DATE = 'Date'.freeze LAST_MODIFIED = "Last-Modified".freeze - SPECIAL_KEYS = Set.new(%w[extras no-cache max-age public must-revalidate]) + SPECIAL_KEYS = Set.new(%w[extras no-cache max-age public private must-revalidate]) def cache_control_segments if cache_control = _cache_control diff --git a/actionpack/lib/action_dispatch/http/filter_parameters.rb b/actionpack/lib/action_dispatch/http/filter_parameters.rb index 9dcab79c3a..041eca48ca 100644 --- a/actionpack/lib/action_dispatch/http/filter_parameters.rb +++ b/actionpack/lib/action_dispatch/http/filter_parameters.rb @@ -4,9 +4,11 @@ module ActionDispatch module Http # Allows you to specify sensitive parameters which will be replaced from # the request log by looking in the query string of the request and all - # sub-hashes of the params hash to filter. If a block is given, each key and - # value of the params hash and all sub-hashes is passed to it, the value - # or key can be replaced using String#replace or similar method. + # sub-hashes of the params hash to filter. Filtering only certain sub-keys + # from a hash is possible by using the dot notation: 'credit_card.number'. + # If a block is given, each key and value of the params hash and all + # sub-hashes is passed to it, the value or key can be replaced using + # String#replace or similar method. # # env["action_dispatch.parameter_filter"] = [:password] # => replaces the value to all keys matching /password/i with "[FILTERED]" @@ -14,6 +16,10 @@ module ActionDispatch # env["action_dispatch.parameter_filter"] = [:foo, "bar"] # => replaces the value to all keys matching /foo|bar/i with "[FILTERED]" # + # env["action_dispatch.parameter_filter"] = [ "credit_card.code" ] + # => replaces { credit_card: {code: "xxxx"} } with "[FILTERED]", does not + # change { file: { code: "xxxx"} } + # # env["action_dispatch.parameter_filter"] = -> (k, v) do # v.reverse! if k =~ /secret/i # end diff --git a/actionpack/lib/action_dispatch/http/headers.rb b/actionpack/lib/action_dispatch/http/headers.rb index 12f81dc1a5..8e899174c6 100644 --- a/actionpack/lib/action_dispatch/http/headers.rb +++ b/actionpack/lib/action_dispatch/http/headers.rb @@ -2,9 +2,23 @@ module ActionDispatch module Http # Provides access to the request's HTTP headers from the environment. # - # env = { "CONTENT_TYPE" => "text/plain" } + # env = { "CONTENT_TYPE" => "text/plain", "HTTP_USER_AGENT" => "curl/7.43.0" } # headers = ActionDispatch::Http::Headers.new(env) # headers["Content-Type"] # => "text/plain" + # headers["User-Agent"] # => "curl/7/43/0" + # + # Also note that when headers are mapped to CGI-like variables by the Rack + # server, both dashes and underscores are converted to underscores. This + # ambiguity cannot be resolved at this stage anymore. Both underscores and + # dashes have to be interpreted as if they were originally sent as dashes. + # + # # GET / HTTP/1.1 + # # ... + # # User-Agent: curl/7.43.0 + # # X_Custom_Header: token + # + # headers["X_Custom_Header"] # => nil + # headers["X-Custom-Header"] # => "token" class Headers CGI_VARIABLES = Set.new(%W[ AUTH_TYPE diff --git a/actionpack/lib/action_dispatch/http/mime_negotiation.rb b/actionpack/lib/action_dispatch/http/mime_negotiation.rb index 7acf91902d..e9b25339dc 100644 --- a/actionpack/lib/action_dispatch/http/mime_negotiation.rb +++ b/actionpack/lib/action_dispatch/http/mime_negotiation.rb @@ -69,6 +69,8 @@ module ActionDispatch Array(Mime[parameters[:format]]) elsif use_accept_header && valid_accept_header accepts + elsif extension_format = format_from_path_extension + [extension_format] elsif xhr? [Mime[:js]] else @@ -160,6 +162,13 @@ module ActionDispatch def use_accept_header !self.class.ignore_accept_header end + + def format_from_path_extension + path = @env['action_dispatch.original_path'] || @env['PATH_INFO'] + if match = path && path.match(/\.(\w+)\z/) + Mime[match.captures.first] + end + end end end end diff --git a/actionpack/lib/action_dispatch/http/mime_type.rb b/actionpack/lib/action_dispatch/http/mime_type.rb index b64f660ec5..4672ea7199 100644 --- a/actionpack/lib/action_dispatch/http/mime_type.rb +++ b/actionpack/lib/action_dispatch/http/mime_type.rb @@ -1,3 +1,5 @@ +# -*- frozen-string-literal: true -*- + require 'singleton' require 'active_support/core_ext/module/attribute_accessors' require 'active_support/core_ext/string/starts_ends_with' @@ -31,7 +33,7 @@ module Mime SET = Mimes.new EXTENSION_LOOKUP = {} - LOOKUP = Hash.new { |h, k| h[k] = Type.new(k) unless k.blank? } + LOOKUP = {} class << self def [](type) @@ -47,15 +49,10 @@ module Mime def const_missing(sym) ext = sym.downcase if Mime[ext] - ActiveSupport::Deprecation.warn <<-eow -Accessing mime types via constants is deprecated. Please change: - - `Mime::#{sym}` - -to: - - `Mime[:#{ext}]` - eow + ActiveSupport::Deprecation.warn(<<-MSG.squish) + Accessing mime types via constants is deprecated. + Please change `Mime::#{sym}` to `Mime[:#{ext}]`. + MSG Mime[ext] else super @@ -65,15 +62,10 @@ to: def const_defined?(sym, inherit = true) ext = sym.downcase if Mime[ext] - ActiveSupport::Deprecation.warn <<-eow -Accessing mime types via constants is deprecated. Please change: - - `Mime.const_defined?(#{sym})` - -to: - - `Mime[:#{ext}]` - eow + ActiveSupport::Deprecation.warn(<<-MSG.squish) + Accessing mime types via constants is deprecated. + Please change `Mime.const_defined?(#{sym})` to `Mime[:#{ext}]`. + MSG true else super @@ -116,70 +108,58 @@ to: result = @index <=> item.index if result == 0 result end - - def ==(item) - @name == item.to_s - end end - class AcceptList < Array #:nodoc: - def assort! - sort! + class AcceptList #:nodoc: + def self.sort!(list) + list.sort! + + text_xml_idx = find_item_by_name list, 'text/xml' + app_xml_idx = find_item_by_name list, Mime[:xml].to_s # Take care of the broken text/xml entry by renaming or deleting it if text_xml_idx && app_xml_idx + app_xml = list[app_xml_idx] + text_xml = list[text_xml_idx] + app_xml.q = [text_xml.q, app_xml.q].max # set the q value to the max of the two - exchange_xml_items if app_xml_idx > text_xml_idx # make sure app_xml is ahead of text_xml in the list - delete_at(text_xml_idx) # delete text_xml from the list + if app_xml_idx > text_xml_idx # make sure app_xml is ahead of text_xml in the list + list[app_xml_idx], list[text_xml_idx] = text_xml, app_xml + app_xml_idx, text_xml_idx = text_xml_idx, app_xml_idx + end + list.delete_at(text_xml_idx) # delete text_xml from the list elsif text_xml_idx - text_xml.name = Mime[:xml].to_s + list[text_xml_idx].name = Mime[:xml].to_s end # Look for more specific XML-based types and sort them ahead of app/xml if app_xml_idx + app_xml = list[app_xml_idx] idx = app_xml_idx - while idx < length - type = self[idx] + while idx < list.length + type = list[idx] break if type.q < app_xml.q if type.name.ends_with? '+xml' - self[app_xml_idx], self[idx] = self[idx], app_xml - @app_xml_idx = idx + list[app_xml_idx], list[idx] = list[idx], app_xml + app_xml_idx = idx end idx += 1 end end - map! { |i| Mime::Type.lookup(i.name) }.uniq! - to_a + list.map! { |i| Mime::Type.lookup(i.name) }.uniq! + list end - private - def text_xml_idx - @text_xml_idx ||= index('text/xml') - end - - def app_xml_idx - @app_xml_idx ||= index(Mime[:xml].to_s) - end - - def text_xml - self[text_xml_idx] - end - - def app_xml - self[app_xml_idx] - end - - def exchange_xml_items - self[app_xml_idx], self[text_xml_idx] = text_xml, app_xml - @app_xml_idx, @text_xml_idx = text_xml_idx, app_xml_idx - end + def self.find_item_by_name(array, name) + array.index { |item| item.name == name } + end end class << self - TRAILING_STAR_REGEXP = /(text|application)\/\*/ + TRAILING_STAR_REGEXP = /^(text|application)\/\*/ PARAMETER_SEPARATOR_REGEXP = /;\s*\w+="?\w+"?/ def register_callback(&block) @@ -187,7 +167,7 @@ to: end def lookup(string) - LOOKUP[string] + LOOKUP[string] || Type.new(string) end def lookup_by_extension(extension) @@ -219,21 +199,22 @@ to: accept_header = accept_header.split(PARAMETER_SEPARATOR_REGEXP).first parse_trailing_star(accept_header) || [Mime::Type.lookup(accept_header)].compact else - list, index = AcceptList.new, 0 + list, index = [], 0 accept_header.split(',').each do |header| params, q = header.split(PARAMETER_SEPARATOR_REGEXP) - if params.present? - params.strip! - params = parse_trailing_star(params) || [params] + next unless params + params.strip! + next if params.empty? + + params = parse_trailing_star(params) || [params] - params.each do |m| - list << AcceptItem.new(index, m.to_s, q) - index += 1 - end + params.each do |m| + list << AcceptItem.new(index, m.to_s, q) + index += 1 end end - list.assort! + AcceptList.sort! list end end @@ -265,9 +246,12 @@ to: end end + attr_reader :hash + def initialize(string, symbol = nil, synonyms = []) @symbol, @synonyms = symbol, synonyms @string = string + @hash = [@string, @synonyms, @symbol].hash end def to_s @@ -301,6 +285,13 @@ to: end end + def eql?(other) + super || (self.class == other.class && + @string == other.string && + @synonyms == other.synonyms && + @symbol == other.symbol) + end + def =~(mime_type) return false unless mime_type regexp = Regexp.new(Regexp.quote(mime_type.to_s)) @@ -313,6 +304,10 @@ to: def all?; false; end + protected + + attr_reader :string, :synonyms + private def to_ary; end diff --git a/actionpack/lib/action_dispatch/http/mime_types.rb b/actionpack/lib/action_dispatch/http/mime_types.rb index 87715205d9..66cea88256 100644 --- a/actionpack/lib/action_dispatch/http/mime_types.rb +++ b/actionpack/lib/action_dispatch/http/mime_types.rb @@ -14,6 +14,7 @@ Mime::Type.register "image/jpeg", :jpeg, [], %w(jpg jpeg jpe pjpeg) Mime::Type.register "image/gif", :gif, [], %w(gif) Mime::Type.register "image/bmp", :bmp, [], %w(bmp) Mime::Type.register "image/tiff", :tiff, [], %w(tif tiff) +Mime::Type.register "image/svg+xml", :svg Mime::Type.register "video/mpeg", :mpeg, [], %w(mpg mpeg mpe) @@ -27,7 +28,8 @@ Mime::Type.register "application/x-www-form-urlencoded", :url_encoded_form # http://www.ietf.org/rfc/rfc4627.txt # http://www.json.org/JSONRequest.html -Mime::Type.register "application/json", :json, %w( text/x-json application/jsonrequest application/vnd.api+json ) +Mime::Type.register "application/json", :json, %w( text/x-json application/jsonrequest ) Mime::Type.register "application/pdf", :pdf, [], %w(pdf) Mime::Type.register "application/zip", :zip, [], %w(zip) +Mime::Type.register "application/gzip", :gzip, %w(application/x-gzip), %w(gz) diff --git a/actionpack/lib/action_dispatch/http/parameters.rb b/actionpack/lib/action_dispatch/http/parameters.rb index c9df787351..ff5031d7d5 100644 --- a/actionpack/lib/action_dispatch/http/parameters.rb +++ b/actionpack/lib/action_dispatch/http/parameters.rb @@ -1,22 +1,31 @@ module ActionDispatch module Http module Parameters + extend ActiveSupport::Concern + PARAMETERS_KEY = 'action_dispatch.request.path_parameters' DEFAULT_PARSERS = { - Mime[:json] => lambda { |raw_post| + Mime[:json].symbol => -> (raw_post) { data = ActiveSupport::JSON.decode(raw_post) data.is_a?(Hash) ? data : {:_json => data} } } - def self.included(klass) - class << klass - attr_accessor :parameter_parsers + included do + class << self + attr_reader :parameter_parsers end - klass.parameter_parsers = DEFAULT_PARSERS + self.parameter_parsers = DEFAULT_PARSERS end + + module ClassMethods + def parameter_parsers=(parsers) # :nodoc: + @parameter_parsers = parsers.transform_keys { |key| key.respond_to?(:symbol) ? key.symbol : key } + end + end + # Returns both GET and POST \parameters in a single hash. def parameters params = get_header("action_dispatch.request.parameters") @@ -43,7 +52,7 @@ module ActionDispatch # # {'action' => 'my_action', 'controller' => 'my_controller'} def path_parameters - get_header(PARAMETERS_KEY) || {} + get_header(PARAMETERS_KEY) || set_header(PARAMETERS_KEY, {}) end private @@ -51,7 +60,7 @@ module ActionDispatch def parse_formatted_parameters(parsers) return yield if content_length.zero? - strategy = parsers.fetch(content_mime_type) { return yield } + strategy = parsers.fetch(content_mime_type.symbol) { return yield } begin strategy.call(raw_post) diff --git a/actionpack/lib/action_dispatch/http/request.rb b/actionpack/lib/action_dispatch/http/request.rb index ea61ad0c02..316a9f08b7 100644 --- a/actionpack/lib/action_dispatch/http/request.rb +++ b/actionpack/lib/action_dispatch/http/request.rb @@ -36,8 +36,8 @@ module ActionDispatch HTTP_ACCEPT HTTP_ACCEPT_CHARSET HTTP_ACCEPT_ENCODING HTTP_ACCEPT_LANGUAGE HTTP_CACHE_CONTROL HTTP_FROM HTTP_NEGOTIATE HTTP_PRAGMA HTTP_CLIENT_IP - HTTP_X_FORWARDED_FOR HTTP_VERSION - HTTP_X_REQUEST_ID HTTP_X_FORWARDED_HOST + HTTP_X_FORWARDED_FOR HTTP_ORIGIN HTTP_VERSION + HTTP_X_CSRF_TOKEN HTTP_X_REQUEST_ID HTTP_X_FORWARDED_HOST SERVER_ADDR ].freeze @@ -49,6 +49,10 @@ module ActionDispatch METHOD end + def self.empty + new({}) + end + def initialize(env) super @method = nil @@ -59,6 +63,9 @@ module ActionDispatch @ip = nil end + def commit_cookie_jar! # :nodoc: + end + def check_path_parameters! # If any of the path parameters has an invalid encoding then # raise since it's likely to trigger errors further on. @@ -252,7 +259,7 @@ module ActionDispatch end # Returns the IP address of client as a +String+, - # usually set by the RemoteIp middleware. + # usually set by the RemoteIp middleware. def remote_ip @remote_ip ||= (get_header("action_dispatch.remote_ip") || ip).to_s end @@ -306,10 +313,16 @@ module ActionDispatch end end - # Returns true if the request's content MIME type is - # +application/x-www-form-urlencoded+ or +multipart/form-data+. + # Determine whether the request body contains form-data by checking + # the request Content-Type for one of the media-types: + # "application/x-www-form-urlencoded" or "multipart/form-data". The + # list of form-data media types can be modified through the + # +FORM_DATA_MEDIA_TYPES+ array. + # + # A request body is not assumed to contain form-data when no + # Content-Type header is provided and the request_method is POST. def form_data? - FORM_DATA_MEDIA_TYPES.include?(content_mime_type.to_s) + FORM_DATA_MEDIA_TYPES.include?(media_type) end def body_stream #:nodoc: @@ -390,6 +403,10 @@ module ActionDispatch def commit_flash end + def ssl? + super || scheme == 'wss'.freeze + end + private def check_method(name) HTTP_METHOD_LOOKUP[name] || raise(ActionController::UnknownHttpMethod, "#{name}, accepted HTTP methods are #{HTTP_METHODS[0...-1].join(', ')}, and #{HTTP_METHODS[-1]}") diff --git a/actionpack/lib/action_dispatch/http/response.rb b/actionpack/lib/action_dispatch/http/response.rb index f0127aa276..fa4c54701a 100644 --- a/actionpack/lib/action_dispatch/http/response.rb +++ b/actionpack/lib/action_dispatch/http/response.rb @@ -1,5 +1,6 @@ require 'active_support/core_ext/module/attribute_accessors' require 'action_dispatch/http/filter_redirect' +require 'action_dispatch/http/cache' require 'monitor' module ActionDispatch # :nodoc: @@ -232,7 +233,7 @@ module ActionDispatch # :nodoc: end # Sets the HTTP character set. In case of nil parameter - # it sets the charset to utf-8. + # it sets the charset to utf-8. # # response.charset = 'utf-16' # => 'utf-16' # response.charset = nil # => 'utf-8' @@ -412,6 +413,15 @@ module ActionDispatch # :nodoc: end def before_sending + # Normally we've already committed by now, but it's possible + # (e.g., if the controller action tries to read back its own + # response) to get here before that. In that case, we must force + # an "early" commit: we're about to freeze the headers, so this is + # our last chance. + commit! unless committed? + + headers.freeze + request.commit_cookie_jar! unless committed? end def build_buffer(response, body) diff --git a/actionpack/lib/action_dispatch/http/url.rb b/actionpack/lib/action_dispatch/http/url.rb index 92b10b6d3b..37f41ae988 100644 --- a/actionpack/lib/action_dispatch/http/url.rb +++ b/actionpack/lib/action_dispatch/http/url.rb @@ -81,7 +81,8 @@ module ActionDispatch def add_params(path, params) params = { params: params } unless params.is_a?(Hash) params.reject! { |_,v| v.to_param.nil? } - path << "?#{params.to_query}" unless params.empty? + query = params.to_query + path << "?#{query}" unless query.empty? end def add_anchor(path, anchor) diff --git a/actionpack/lib/action_dispatch/journey/backwards.rb b/actionpack/lib/action_dispatch/journey/backwards.rb deleted file mode 100644 index 3bd20fdf81..0000000000 --- a/actionpack/lib/action_dispatch/journey/backwards.rb +++ /dev/null @@ -1,5 +0,0 @@ -module Rack # :nodoc: - Mount = ActionDispatch::Journey::Router - Mount::RouteSet = ActionDispatch::Journey::Router - Mount::RegexpWithNamedGroups = ActionDispatch::Journey::Path::Pattern -end diff --git a/actionpack/lib/action_dispatch/journey/path/pattern.rb b/actionpack/lib/action_dispatch/journey/path/pattern.rb index 5ee8810066..018b89a2b7 100644 --- a/actionpack/lib/action_dispatch/journey/path/pattern.rb +++ b/actionpack/lib/action_dispatch/journey/path/pattern.rb @@ -124,7 +124,7 @@ module ActionDispatch end def captures - (length - 1).times.map { |i| self[i + 1] } + Array.new(length - 1) { |i| self[i + 1] } end def [](x) diff --git a/actionpack/lib/action_dispatch/journey/route.rb b/actionpack/lib/action_dispatch/journey/route.rb index 35c2b1b86e..cfd6681dd1 100644 --- a/actionpack/lib/action_dispatch/journey/route.rb +++ b/actionpack/lib/action_dispatch/journey/route.rb @@ -3,7 +3,7 @@ module ActionDispatch class Route # :nodoc: attr_reader :app, :path, :defaults, :name, :precedence - attr_reader :constraints + attr_reader :constraints, :internal alias :conditions :constraints module VerbMatchers @@ -55,7 +55,7 @@ module ActionDispatch ## # +path+ is a path constraint. # +constraints+ is a hash of constraints to be applied to this route. - def initialize(name, app, path, constraints, required_defaults, defaults, request_method_match, precedence) + def initialize(name, app, path, constraints, required_defaults, defaults, request_method_match, precedence, internal = false) @name = name @app = app @path = path @@ -70,6 +70,7 @@ module ActionDispatch @decorated_ast = nil @precedence = precedence @path_formatter = @path.build_formatter + @internal = internal end def ast @@ -81,7 +82,7 @@ module ActionDispatch end def requirements # :nodoc: - # needed for rails `rake routes` + # needed for rails `rails routes` @defaults.merge(path.requirements).delete_if { |_,v| /.+?/ == v } diff --git a/actionpack/lib/action_dispatch/journey/router.rb b/actionpack/lib/action_dispatch/journey/router.rb index f649588520..06cdce1724 100644 --- a/actionpack/lib/action_dispatch/journey/router.rb +++ b/actionpack/lib/action_dispatch/journey/router.rb @@ -16,9 +16,6 @@ module ActionDispatch class RoutingError < ::StandardError # :nodoc: end - # :nodoc: - VERSION = '2.0.0' - attr_accessor :routes def initialize(routes) diff --git a/actionpack/lib/action_dispatch/middleware/cookies.rb b/actionpack/lib/action_dispatch/middleware/cookies.rb index 2889acaeb8..f2f3150b56 100644 --- a/actionpack/lib/action_dispatch/middleware/cookies.rb +++ b/actionpack/lib/action_dispatch/middleware/cookies.rb @@ -2,6 +2,7 @@ require 'active_support/core_ext/hash/keys' require 'active_support/key_generator' require 'active_support/message_verifier' require 'active_support/json' +require 'rack/utils' module ActionDispatch class Request @@ -12,6 +13,12 @@ module ActionDispatch end # :stopdoc: + prepend Module.new { + def commit_cookie_jar! + cookie_jar.commit! + end + } + def have_cookie_jar? has_header? 'action_dispatch.cookies'.freeze end @@ -77,6 +84,12 @@ module ActionDispatch # # It can be read using the signed method `cookies.signed[:name]` # cookies.signed[:user_id] = current_user.id # + # # Sets an encrypted cookie value before sending it to the client which + # # prevent users from reading and tampering with its value. + # # The cookie is signed by your app's `secrets.secret_key_base` value. + # # It can be read using the encrypted method `cookies.encrypted[:name]` + # cookies.encrypted[:discount] = 45 + # # # Sets a "permanent" cookie (which expires in 20 years from now). # cookies.permanent[:login] = "XJ-122" # @@ -89,6 +102,7 @@ module ActionDispatch # cookies.size # => 2 # JSON.parse(cookies[:lat_lon]) # => [47.68, -122.37] # cookies.signed[:login] # => "XJ-122" + # cookies.encrypted[:discount] # => 45 # # Example for deleting: # @@ -324,7 +338,7 @@ module ActionDispatch end def to_header - @cookies.map { |k,v| "#{k}=#{v}" }.join ';' + @cookies.map { |k,v| "#{escape(k)}=#{escape(v)}" }.join '; ' end def handle_options(options) #:nodoc: @@ -406,6 +420,10 @@ module ActionDispatch private + def escape(string) + ::Rack::Utils.escape(string) + end + def make_set_cookie_header(header) header = @set_cookies.inject(header) { |m, (k, v)| if write_cookie?(v) diff --git a/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb b/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb index 66bb74b9c5..51a471fb23 100644 --- a/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb +++ b/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb @@ -38,9 +38,10 @@ module ActionDispatch end end - def initialize(app, routes_app = nil) - @app = app - @routes_app = routes_app + def initialize(app, routes_app = nil, response_format = :default) + @app = app + @routes_app = routes_app + @response_format = response_format end def call(env) @@ -66,41 +67,79 @@ module ActionDispatch log_error(request, wrapper) if request.get_header('action_dispatch.show_detailed_exceptions') - traces = wrapper.traces - - trace_to_show = 'Application Trace' - if traces[trace_to_show].empty? && wrapper.rescue_template != 'routing_error' - trace_to_show = 'Full Trace' + case @response_format + when :api + render_for_api_application(request, wrapper) + when :default + render_for_default_application(request, wrapper) end + else + raise exception + end + end - if source_to_show = traces[trace_to_show].first - source_to_show_id = source_to_show[:id] - end + def render_for_default_application(request, wrapper) + template = create_template(request, wrapper) + file = "rescues/#{wrapper.rescue_template}" - template = DebugView.new([RESCUES_TEMPLATE_PATH], - request: request, - exception: wrapper.exception, - traces: traces, - show_source_idx: source_to_show_id, - trace_to_show: trace_to_show, - routes_inspector: routes_inspector(exception), - source_extracts: wrapper.source_extracts, - line_number: wrapper.line_number, - file: wrapper.file - ) - file = "rescues/#{wrapper.rescue_template}" - - if request.xhr? - body = template.render(template: file, layout: false, formats: [:text]) - format = "text/plain" - else - body = template.render(template: file, layout: 'rescues/layout') - format = "text/html" - end - render(wrapper.status_code, body, format) + if request.xhr? + body = template.render(template: file, layout: false, formats: [:text]) + format = "text/plain" else - raise exception + body = template.render(template: file, layout: 'rescues/layout') + format = "text/html" end + render(wrapper.status_code, body, format) + end + + def render_for_api_application(request, wrapper) + body = { + status: wrapper.status_code, + error: Rack::Utils::HTTP_STATUS_CODES.fetch( + wrapper.status_code, + Rack::Utils::HTTP_STATUS_CODES[500] + ), + exception: wrapper.exception.inspect, + traces: wrapper.traces + } + + content_type = request.formats.first + to_format = "to_#{content_type.to_sym}" + + if content_type && body.respond_to?(to_format) + formatted_body = body.public_send(to_format) + format = content_type + else + formatted_body = body.to_json + format = Mime[:json] + end + + render(wrapper.status_code, formatted_body, format) + end + + def create_template(request, wrapper) + traces = wrapper.traces + + trace_to_show = 'Application Trace' + if traces[trace_to_show].empty? && wrapper.rescue_template != 'routing_error' + trace_to_show = 'Full Trace' + end + + if source_to_show = traces[trace_to_show].first + source_to_show_id = source_to_show[:id] + end + + DebugView.new([RESCUES_TEMPLATE_PATH], + request: request, + exception: wrapper.exception, + traces: traces, + show_source_idx: source_to_show_id, + trace_to_show: trace_to_show, + routes_inspector: routes_inspector(wrapper.exception), + source_extracts: wrapper.source_extracts, + line_number: wrapper.line_number, + file: wrapper.file + ) end def render(status, body, format) @@ -117,15 +156,20 @@ module ActionDispatch trace = wrapper.framework_trace if trace.empty? ActiveSupport::Deprecation.silence do - message = "\n#{exception.class} (#{exception.message}):\n" - message << exception.annoted_source_code.to_s if exception.respond_to?(:annoted_source_code) - message << " " << trace.join("\n ") - logger.fatal("#{message}\n\n") + logger.fatal " " + logger.fatal "#{exception.class} (#{exception.message}):" + log_array logger, exception.annoted_source_code if exception.respond_to?(:annoted_source_code) + logger.fatal " " + log_array logger, trace end end + def log_array(logger, array) + array.map { |line| logger.fatal line } + end + def logger(request) - request.logger || stderr_logger + request.logger || ActionView::Base.logger || stderr_logger end def stderr_logger diff --git a/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb b/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb index 3b61824cc9..59edc66086 100644 --- a/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb +++ b/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb @@ -1,4 +1,3 @@ -require 'action_controller/metal/exceptions' require 'active_support/core_ext/module/attribute_accessors' require 'rack/utils' diff --git a/actionpack/lib/action_dispatch/middleware/params_parser.rb b/actionpack/lib/action_dispatch/middleware/params_parser.rb index c2a4f46e67..5841c978af 100644 --- a/actionpack/lib/action_dispatch/middleware/params_parser.rb +++ b/actionpack/lib/action_dispatch/middleware/params_parser.rb @@ -37,6 +37,7 @@ module ActionDispatch # The +parsers+ argument can take Hash of parsers where key is identifying # content mime type, and value is a lambda that is going to process data. def self.new(app, parsers = {}) + parsers = parsers.transform_keys { |key| key.respond_to?(:symbol) ? key.symbol : key } ActionDispatch::Request.parameter_parsers = ActionDispatch::Request::DEFAULT_PARSERS.merge(parsers) app end diff --git a/actionpack/lib/action_dispatch/middleware/remote_ip.rb b/actionpack/lib/action_dispatch/middleware/remote_ip.rb index aee2334da9..31b75498b6 100644 --- a/actionpack/lib/action_dispatch/middleware/remote_ip.rb +++ b/actionpack/lib/action_dispatch/middleware/remote_ip.rb @@ -43,7 +43,7 @@ module ActionDispatch # Create a new +RemoteIp+ middleware instance. # - # The +check_ip_spoofing+ option is on by default. When on, an exception + # The +ip_spoofing_check+ option is on by default. When on, an exception # is raised if it looks like the client is trying to lie about its own IP # address. It makes sense to turn off this check on sites aimed at non-IP # clients (like WAP devices), or behind proxies that set headers in an @@ -57,9 +57,9 @@ module ActionDispatch # with your proxy servers after it. If your proxies aren't removed, pass # them in via the +custom_proxies+ parameter. That way, the middleware will # ignore those IP addresses, and return the one that you want. - def initialize(app, check_ip_spoofing = true, custom_proxies = nil) + def initialize(app, ip_spoofing_check = true, custom_proxies = nil) @app = app - @check_ip = check_ip_spoofing + @check_ip = ip_spoofing_check @proxies = if custom_proxies.blank? TRUSTED_PROXIES elsif custom_proxies.respond_to?(:any?) @@ -116,10 +116,18 @@ module ActionDispatch forwarded_ips = ips_from(@req.x_forwarded_for).reverse # +Client-Ip+ and +X-Forwarded-For+ should not, generally, both be set. - # If they are both set, it means that this request passed through two - # proxies with incompatible IP header conventions, and there is no way - # for us to determine which header is the right one after the fact. - # Since we have no idea, we give up and explode. + # If they are both set, it means that either: + # + # 1) This request passed through two proxies with incompatible IP header + # conventions. + # 2) The client passed one of +Client-Ip+ or +X-Forwarded-For+ + # (whichever the proxy servers weren't using) themselves. + # + # Either way, there is no way for us to determine which header is the + # right one after the fact. Since we have no idea, if we are concerned + # about IP spoofing we need to give up and explode. (If you're not + # concerned about IP spoofing you can turn the +ip_spoofing_check+ + # option off.) should_check_ip = @check_ip && client_ips.last && forwarded_ips.last if should_check_ip && !forwarded_ips.include?(client_ips.last) # We don't know which came from the proxy, and which from the user diff --git a/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb b/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb index 0e636b8257..dec9c60ef2 100644 --- a/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb +++ b/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb @@ -23,7 +23,7 @@ module ActionDispatch # goes a step further than signed cookies in that encrypted cookies cannot # be altered or read by users. This is the default starting in Rails 4. # - # If you have both secret_token and secret_key base set, your cookies will + # If you have both secret_token and secret_key_base set, your cookies will # be encrypted, and signed cookies generated by Rails 3 will be # transparently read and encrypted to provide a smooth upgrade path. # @@ -36,7 +36,7 @@ module ActionDispatch # development: # secret_key_base: 'secret key' # - # To generate a secret key for an existing application, run `rake secret`. + # To generate a secret key for an existing application, run `rails secret`. # # If you are upgrading an existing Rails 3 app, you should leave your # existing secret_token in place and simply add the new secret_key_base. diff --git a/actionpack/lib/action_dispatch/middleware/ssl.rb b/actionpack/lib/action_dispatch/middleware/ssl.rb index 47f475559a..cb442af19b 100644 --- a/actionpack/lib/action_dispatch/middleware/ssl.rb +++ b/actionpack/lib/action_dispatch/middleware/ssl.rb @@ -1,35 +1,43 @@ module ActionDispatch - # This middleware is added to the stack when `config.force_ssl = true`. - # It does three jobs to enforce secure HTTP requests: + # This middleware is added to the stack when `config.force_ssl = true`, and is passed + # the options set in `config.ssl_options`. It does three jobs to enforce secure HTTP + # requests: # - # 1. TLS redirect. http:// requests are permanently redirected to https:// - # with the same URL host, path, etc. Pass `:host` and/or `:port` to - # modify the destination URL. This is always enabled. + # 1. TLS redirect: Permanently redirects http:// requests to https:// + # with the same URL host, path, etc. Enabled by default. Set `config.ssl_options` + # to modify the destination URL + # (e.g. `redirect: { host: "secure.widgets.com", port: 8080 }`), or set + # `redirect: false` to disable this feature. # - # 2. Secure cookies. Sets the `secure` flag on cookies to tell browsers they - # mustn't be sent along with http:// requests. This is always enabled. + # 2. Secure cookies: Sets the `secure` flag on cookies to tell browsers they + # mustn't be sent along with http:// requests. Enabled by default. Set + # `config.ssl_options` with `secure_cookies: false` to disable this feature. # - # 3. HTTP Strict Transport Security (HSTS). Tells the browser to remember + # 3. HTTP Strict Transport Security (HSTS): Tells the browser to remember # this site as TLS-only and automatically redirect non-TLS requests. - # Enabled by default. Pass `hsts: false` to disable. + # Enabled by default. Configure `config.ssl_options` with `hsts: false` to disable. # - # Configure HSTS with `hsts: { … }`: + # Set `config.ssl_options` with `hsts: { … }` to configure HSTS: # * `expires`: How long, in seconds, these settings will stick. Defaults to # `180.days` (recommended). The minimum required to qualify for browser # preload lists is `18.weeks`. # * `subdomains`: Set to `true` to tell the browser to apply these settings # to all subdomains. This protects your cookies from interception by a - # vulnerable site on a subdomain. Defaults to `false`. + # vulnerable site on a subdomain. Defaults to `true`. # * `preload`: Advertise that this site may be included in browsers' # preloaded HSTS lists. HSTS protects your site on every visit *except the # first visit* since it hasn't seen your HSTS header yet. To close this # gap, browser vendors include a baked-in list of HSTS-enabled sites. # Go to https://hstspreload.appspot.com to submit your site for inclusion. # - # Disabling HSTS: To turn off HSTS, omitting the header is not enough. - # Browsers will remember the original HSTS directive until it expires. - # Instead, use the header to tell browsers to expire HSTS immediately. - # Setting `hsts: false` is a shortcut for `hsts: { expires: 0 }`. + # To turn off HSTS, omitting the header is not enough. Browsers will remember the + # original HSTS directive until it expires. Instead, use the header to tell browsers to + # expire HSTS immediately. Setting `hsts: false` is a shortcut for + # `hsts: { expires: 0 }`. + # + # Redirection can be constrained to only whitelisted requests with `constrain_to`: + # + # config.ssl_options = { redirect: { constrain_to: -> request { request.path !~ /healthcheck/ } } } class SSL # Default to 180 days, the low end for https://www.ssllabs.com/ssltest/ # and greater than the 18-week requirement for browser preload lists. @@ -39,18 +47,30 @@ module ActionDispatch { expires: HSTS_EXPIRES_IN, subdomains: false, preload: false } end - def initialize(app, redirect: {}, hsts: {}, **options) + def initialize(app, redirect: {}, hsts: {}, secure_cookies: true, **options) @app = app if options[:host] || options[:port] ActiveSupport::Deprecation.warn <<-end_warning.strip_heredoc The `:host` and `:port` options are moving within `:redirect`: - `config.ssl_options = { redirect: { host: …, port: … }}`. + `config.ssl_options = { redirect: { host: …, port: … } }`. end_warning @redirect = options.slice(:host, :port) else @redirect = redirect end + @constrain_to = @redirect && @redirect[:constrain_to] || proc { @redirect } + @secure_cookies = secure_cookies + + if hsts != true && hsts != false && hsts[:subdomains].nil? + hsts[:subdomains] = false + + ActiveSupport::Deprecation.warn <<-end_warning.strip_heredoc + In Rails 5.1, The `:subdomains` option of HSTS config will be treated as true if + unspecified. Set `config.ssl_options = { hsts: { subdomains: false } }` to opt out + of this behavior. + end_warning + end @hsts_header = build_hsts_header(normalize_hsts_options(hsts)) end @@ -61,10 +81,11 @@ module ActionDispatch if request.ssl? @app.call(env).tap do |status, headers, body| set_hsts_header! headers - flag_cookies_as_secure! headers + flag_cookies_as_secure! headers if @secure_cookies end else - redirect_to_https request + return redirect_to_https request if @constrain_to.call(request) + @app.call(env) end end diff --git a/actionpack/lib/action_dispatch/middleware/stack.rb b/actionpack/lib/action_dispatch/middleware/stack.rb index 90e2ae6802..0b4bee5462 100644 --- a/actionpack/lib/action_dispatch/middleware/stack.rb +++ b/actionpack/lib/action_dispatch/middleware/stack.rb @@ -14,8 +14,21 @@ module ActionDispatch def name; klass.name; end + def ==(middleware) + case middleware + when Middleware + klass == middleware.klass + when Class + klass == middleware + end + end + def inspect - klass.to_s + if klass.is_a?(Class) + klass.to_s + else + klass.class.to_s + end end def build(app) diff --git a/actionpack/lib/action_dispatch/middleware/static.rb b/actionpack/lib/action_dispatch/middleware/static.rb index 75f8e05a3f..41c220236a 100644 --- a/actionpack/lib/action_dispatch/middleware/static.rb +++ b/actionpack/lib/action_dispatch/middleware/static.rb @@ -15,7 +15,6 @@ module ActionDispatch class FileHandler def initialize(root, index: 'index', headers: {}) @root = root.chomp('/') - @compiled_root = /^#{Regexp.escape(root)}/ @file_server = ::Rack::File.new(@root, headers) @index = index end @@ -28,7 +27,7 @@ module ActionDispatch # in the server's `public/` directory (see Static#call). def match?(path) path = ::Rack::Utils.unescape_path path - return false unless path.valid_encoding? + return false unless valid_path?(path) path = Rack::Utils.clean_path_info path paths = [path, "#{path}#{ext}", "#{path}/#{@index}#{ext}"] @@ -95,6 +94,10 @@ module ActionDispatch false end end + + def valid_path?(path) + path.valid_encoding? && !path.include?("\0") + end end # This middleware will attempt to return the contents of a file's body from diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/_source.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/_source.html.erb index e7b913bbe4..e7b913bbe4 100644 --- a/actionpack/lib/action_dispatch/middleware/templates/rescues/_source.erb +++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/_source.html.erb diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/_source.text.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/_source.text.erb new file mode 100644 index 0000000000..23a9c7ba3f --- /dev/null +++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/_source.text.erb @@ -0,0 +1,8 @@ +<% @source_extracts.first(3).each do |source_extract| %> +<% if source_extract[:code] %> +Extracted source (around line #<%= source_extract[:line_number] %>): + +<% source_extract[:code].each do |line, source| -%> +<%= line == source_extract[:line_number] ? "*#{line}" : "##{line}" -%> <%= source -%><% end -%> +<% end %> +<% end %> diff --git a/actionpack/lib/action_dispatch/request/session.rb b/actionpack/lib/action_dispatch/request/session.rb index 9e7fcbd849..42890225fa 100644 --- a/actionpack/lib/action_dispatch/request/session.rb +++ b/actionpack/lib/action_dispatch/request/session.rb @@ -109,7 +109,7 @@ module ActionDispatch @delegate.values end - # Writes given value to given key of the session. + # Writes given value to given key of the session. def []=(key, value) load_for_write! @delegate[key.to_s] = value @@ -148,8 +148,8 @@ module ActionDispatch @delegate.delete key.to_s end - # Returns value of given key from the session, or raises +KeyError+ - # if can't find given key in case of not setted dafault value. + # Returns value of the given key from the session, or raises +KeyError+ + # if can't find the given key and no default value is set. # Returns default value if specified. # # session.fetch(:foo) diff --git a/actionpack/lib/action_dispatch/routing.rb b/actionpack/lib/action_dispatch/routing.rb index 59c3f9248f..dcf800b215 100644 --- a/actionpack/lib/action_dispatch/routing.rb +++ b/actionpack/lib/action_dispatch/routing.rb @@ -159,7 +159,7 @@ module ActionDispatch # # controller 'geocode' do # get 'geocode/:postalcode' => :show, constraints: { - # postalcode: /# Postcode format + # postalcode: /# Postalcode format # \d{5} #Prefix # (-\d{4})? #Suffix # /x @@ -237,9 +237,9 @@ module ActionDispatch # # == View a list of all your routes # - # rake routes + # rails routes # - # Target specific controllers by prefixing the command with <tt>CONTROLLER=x</tt>. + # Target specific controllers by prefixing the command with <tt>-c</tt> option. # module Routing extend ActiveSupport::Autoload diff --git a/actionpack/lib/action_dispatch/routing/inspector.rb b/actionpack/lib/action_dispatch/routing/inspector.rb index f3a5268d2e..5d30a545a2 100644 --- a/actionpack/lib/action_dispatch/routing/inspector.rb +++ b/actionpack/lib/action_dispatch/routing/inspector.rb @@ -41,7 +41,7 @@ module ActionDispatch end def internal? - controller.to_s =~ %r{\Arails/(info|mailers|welcome)} + internal end def engine? @@ -51,7 +51,7 @@ module ActionDispatch ## # This class is just used for displaying route information when someone - # executes `rake routes` or looks at the RoutingError page. + # executes `rails routes` or looks at the RoutingError page. # People should not use this class. class RoutesInspector # :nodoc: def initialize(routes) @@ -60,12 +60,10 @@ module ActionDispatch end def format(formatter, filter = nil) - routes_to_display = filter_routes(filter) - + routes_to_display = filter_routes(normalize_filter(filter)) routes = collect_routes(routes_to_display) - if routes.none? - formatter.no_routes + formatter.no_routes(collect_routes(@routes)) return formatter.result end @@ -82,9 +80,20 @@ module ActionDispatch private + def normalize_filter(filter) + if filter.is_a?(Hash) && filter[:controller] + { controller: /#{filter[:controller].downcase.sub(/_?controller\z/, '').sub('::', '/')}/ } + elsif filter + { controller: /#{filter}/, action: /#{filter}/, verb: /#{filter}/, name: /#{filter}/, path: /#{filter}/ } + end + end + def filter_routes(filter) if filter - @routes.select { |route| route.defaults[:controller] == filter } + @routes.select do |route| + route_wrapper = RouteWrapper.new(route) + filter.any? { |default, value| route_wrapper.send(default) =~ value } + end else @routes end @@ -136,14 +145,18 @@ module ActionDispatch @buffer << draw_header(routes) end - def no_routes - @buffer << <<-MESSAGE.strip_heredoc + def no_routes(routes) + @buffer << + if routes.none? + <<-MESSAGE.strip_heredoc You don't have any routes defined! Please add some routes in config/routes.rb. - - For more information about routes, see the Rails guide: http://guides.rubyonrails.org/routing.html. MESSAGE + else + "No routes were found for this controller" + end + @buffer << "For more information about routes, see the Rails guide: http://guides.rubyonrails.org/routing.html." end private @@ -187,7 +200,7 @@ module ActionDispatch def header(routes) end - def no_routes + def no_routes(*) @buffer << <<-MESSAGE.strip_heredoc <p>You don't have any routes defined!</p> <ul> diff --git a/actionpack/lib/action_dispatch/routing/mapper.rb b/actionpack/lib/action_dispatch/routing/mapper.rb index 7c0404ca62..16b430c36e 100644 --- a/actionpack/lib/action_dispatch/routing/mapper.rb +++ b/actionpack/lib/action_dispatch/routing/mapper.rb @@ -11,7 +11,7 @@ module ActionDispatch class Mapper URL_OPTIONS = [:protocol, :subdomain, :domain, :host, :port] - class Constraints < Endpoint #:nodoc: + class Constraints < Routing::Endpoint #:nodoc: attr_reader :app, :constraints SERVE = ->(app, req) { app.serve req } @@ -107,6 +107,7 @@ module ActionDispatch @ast = ast @anchor = anchor @via = via + @internal = options[:internal] path_params = ast.find_all(&:symbol?).map(&:to_sym) @@ -148,7 +149,8 @@ module ActionDispatch required_defaults, defaults, request_method, - precedence) + precedence, + @internal) route end @@ -184,26 +186,32 @@ module ActionDispatch def build_path(ast, requirements, anchor) pattern = Journey::Path::Pattern.new(ast, requirements, JOINED_SEPARATORS, anchor) - # Get all the symbol nodes followed by literals that are not the - # dummy node. - symbols = ast.find_all { |n| - n.cat? && n.left.symbol? && n.right.cat? && n.right.left.literal? - }.map(&:left) - - # Get all the symbol nodes preceded by literals. - symbols.concat ast.find_all { |n| - n.cat? && n.left.literal? && n.right.cat? && n.right.left.symbol? - }.map { |n| n.right.left } + # Find all the symbol nodes that are adjacent to literal nodes and alter + # the regexp so that Journey will partition them into custom routes. + ast.find_all { |node| + next unless node.cat? + + if node.left.literal? && node.right.symbol? + symbol = node.right + elsif node.left.literal? && node.right.cat? && node.right.left.symbol? + symbol = node.right.left + elsif node.left.symbol? && node.right.literal? + symbol = node.left + elsif node.left.symbol? && node.right.cat? && node.right.left.literal? + symbol = node.left + else + next + end - symbols.each { |x| - x.regexp = /(?:#{Regexp.union(x.regexp, '-')})+/ + if symbol + symbol.regexp = /(?:#{Regexp.union(symbol.regexp, '-')})+/ + end } pattern end private :build_path - private def add_wildcard_options(options, formatted, path_ast) # Add a constraint for wildcard route to make it non-greedy and match the @@ -387,24 +395,6 @@ module ActionDispatch end module Base - # You can specify what Rails should route "/" to with the root method: - # - # root to: 'pages#main' - # - # For options, see +match+, as +root+ uses it internally. - # - # You can also pass a string which will expand - # - # root 'pages#main' - # - # You should put the root route at the top of <tt>config/routes.rb</tt>, - # because this means it will be matched first. As this is the most popular route - # of most Rails applications, this is beneficial. - def root(options = {}) - name = has_named_route?(:root) ? nil : :root - match '/', { as: name, via: :get }.merge!(options) - end - # Matches a url pattern to one or more routes. # # You should not use the +match+ method in your router @@ -600,17 +590,20 @@ module ActionDispatch def mount(app, options = nil) if options path = options.delete(:at) - else - unless Hash === app - raise ArgumentError, "must be called with mount point" - end - + elsif Hash === app options = app app, path = options.find { |k, _| k.respond_to?(:call) } options.delete(app) if app end - raise "A rack application must be specified" unless path + raise ArgumentError, "A rack application must be specified" unless app.respond_to?(:call) + raise ArgumentError, <<-MSG.strip_heredoc unless path + Must be called with mount point + + mount SomeRackApp, at: "some_route" + or + mount(SomeRackApp => "some_route") + MSG rails_app = rails_app? app options[:as] ||= app_name(app, rails_app) @@ -1686,7 +1679,20 @@ to this: @set.add_route(mapping, ast, as, anchor) end - def root(path, options={}) + # You can specify what Rails should route "/" to with the root method: + # + # root to: 'pages#main' + # + # For options, see +match+, as +root+ uses it internally. + # + # You can also pass a string which will expand + # + # root 'pages#main' + # + # You should put the root route at the top of <tt>config/routes.rb</tt>, + # because this means it will be matched first. As this is the most popular route + # of most Rails applications, this is beneficial. + def root(path, options = {}) if path.is_a?(String) options[:to] = path elsif path.is_a?(Hash) and options.empty? @@ -1698,11 +1704,11 @@ to this: if @scope.resources? with_scope_level(:root) do path_scope(parent_resource.path) do - super(options) + match_root_route(options) end end else - super(options) + match_root_route(options) end end @@ -1897,6 +1903,11 @@ to this: ensure @scope = @scope.parent end + + def match_root_route(options) + name = has_named_route?(:root) ? nil : :root + match '/', { :as => name, :via => :get }.merge!(options) + end end # Routing Concerns allow you to declare common routes that can be reused diff --git a/actionpack/lib/action_dispatch/routing/route_set.rb b/actionpack/lib/action_dispatch/routing/route_set.rb index 5f54ea130b..310e98f584 100644 --- a/actionpack/lib/action_dispatch/routing/route_set.rb +++ b/actionpack/lib/action_dispatch/routing/route_set.rb @@ -30,9 +30,9 @@ module ActionDispatch controller = controller req res = controller.make_response! req dispatch(controller, params[:action], req, res) - rescue NameError => e + rescue ActionController::RoutingError if @raise_on_name_error - raise ActionController::RoutingError, e.message, e.backtrace + raise else return [404, {'X-Cascade' => 'pass'}, []] end @@ -42,6 +42,8 @@ module ActionDispatch def controller(req) req.controller_class + rescue NameError => e + raise ActionController::RoutingError, e.message, e.backtrace end def dispatch(controller, action, req, res) @@ -279,8 +281,17 @@ module ActionDispatch helper = UrlHelper.create(route, opts, route_key, url_strategy) mod.module_eval do define_method(name) do |*args| - options = nil - options = args.pop if args.last.is_a? Hash + last = args.last + options = case last + when Hash + args.pop + when ActionController::Parameters + if last.permitted? + args.pop.to_h + else + raise ArgumentError, "Generating a URL from non sanitized request parameters is insecure!" + end + end helper.call self, args, options end end @@ -371,10 +382,6 @@ module ActionDispatch end def eval_block(block) - if block.arity == 1 - raise "You are using the old router DSL which has been removed in Rails 3.1. " << - "Please check how to update your routes file at: http://www.engineyard.com/blog/2010/the-lowdown-on-routes-in-rails-3/" - end mapper = Mapper.new(self) if default_scope mapper.with_default_scope(default_scope, &block) diff --git a/actionpack/lib/action_dispatch/routing/url_for.rb b/actionpack/lib/action_dispatch/routing/url_for.rb index b6c031dcf4..28be189f93 100644 --- a/actionpack/lib/action_dispatch/routing/url_for.rb +++ b/actionpack/lib/action_dispatch/routing/url_for.rb @@ -172,8 +172,11 @@ module ActionDispatch _routes.url_for(options.symbolize_keys.reverse_merge!(url_options), route_name) when ActionController::Parameters + unless options.permitted? + raise ArgumentError.new("Generating a URL from non sanitized request parameters is insecure!") + end route_name = options.delete :use_route - _routes.url_for(options.to_unsafe_h.symbolize_keys. + _routes.url_for(options.to_h.symbolize_keys. reverse_merge!(url_options), route_name) when String options diff --git a/actionpack/lib/action_dispatch/testing/assertion_response.rb b/actionpack/lib/action_dispatch/testing/assertion_response.rb new file mode 100644 index 0000000000..3fb81ff083 --- /dev/null +++ b/actionpack/lib/action_dispatch/testing/assertion_response.rb @@ -0,0 +1,49 @@ +module ActionDispatch + # This is a class that abstracts away an asserted response. + # It purposely does not inherit from Response, because it doesn't need it. + # That means it does not have headers or a body. + # + # As an input to the initializer, we take a Fixnum, a String, or a Symbol. + # If it's a Fixnum or String, we figure out what its symbolized name. + # If it's a Symbol, we figure out what its corresponding code is. + # The resulting code will be a Fixnum, for real HTTP codes, and it will + # be a String for the pseudo-HTTP codes, such as: + # :success, :missing, :redirect and :error + class AssertionResponse + attr_reader :code, :name + + GENERIC_RESPONSE_CODES = { # :nodoc: + success: "2XX", + missing: "404", + redirect: "3XX", + error: "5XX" + } + + def initialize(code_or_name) + if code_or_name.is_a?(Symbol) + @name = code_or_name + @code = code_from_name(code_or_name) + else + @name = name_from_code(code_or_name) + @code = code_or_name + end + + raise ArgumentError, "Invalid response name: #{name}" if @code.nil? + raise ArgumentError, "Invalid response code: #{code}" if @name.nil? + end + + def code_and_name + "#{code}: #{name}" + end + + private + + def code_from_name(name) + GENERIC_RESPONSE_CODES[name] || Rack::Utils::SYMBOL_TO_STATUS_CODE[name] + end + + def name_from_code(code) + GENERIC_RESPONSE_CODES.invert[code] || Rack::Utils::HTTP_STATUS_CODES[code] + end + end +end diff --git a/actionpack/lib/action_dispatch/testing/assertions/response.rb b/actionpack/lib/action_dispatch/testing/assertions/response.rb index eab20b075d..cd55b7d975 100644 --- a/actionpack/lib/action_dispatch/testing/assertions/response.rb +++ b/actionpack/lib/action_dispatch/testing/assertions/response.rb @@ -1,4 +1,3 @@ - module ActionDispatch module Assertions # A small suite of assertions that test responses from \Rails applications. @@ -27,18 +26,12 @@ module ActionDispatch # # Asserts that the response code was status code 401 (unauthorized) # assert_response 401 def assert_response(type, message = nil) - if Symbol === type - if [:success, :missing, :redirect, :error].include?(type) - assert_predicate @response, RESPONSE_PREDICATES[type], message - else - code = Rack::Utils::SYMBOL_TO_STATUS_CODE[type] - if code.nil? - raise ArgumentError, "Invalid response type :#{type}" - end - assert_equal code, @response.response_code, message - end + message ||= generate_response_message(type) + + if RESPONSE_PREDICATES.keys.include?(type) + assert @response.send(RESPONSE_PREDICATES[type]), message else - assert_equal type, @response.response_code, message + assert_equal AssertionResponse.new(type).code, @response.response_code, message end end @@ -82,6 +75,26 @@ module ActionDispatch handle._compute_redirect_to_location(@request, fragment) end end + + def generate_response_message(expected, actual = @response.response_code) + "Expected response to be a <#{code_with_name(expected)}>,"\ + " but was a <#{code_with_name(actual)}>" + .concat location_if_redirected + end + + def location_if_redirected + return '' unless @response.redirection? && @response.location.present? + location = normalize_argument_to_redirection(@response.location) + " redirect to <#{location}>" + end + + def code_with_name(code_or_name) + if RESPONSE_PREDICATES.values.include?("#{code_or_name}?".to_sym) + code_or_name = RESPONSE_PREDICATES.invert["#{code_or_name}?".to_sym] + end + + AssertionResponse.new(code_or_name).code_and_name + end end end end diff --git a/actionpack/lib/action_dispatch/testing/assertions/routing.rb b/actionpack/lib/action_dispatch/testing/assertions/routing.rb index 78ef860548..e7af27463c 100644 --- a/actionpack/lib/action_dispatch/testing/assertions/routing.rb +++ b/actionpack/lib/action_dispatch/testing/assertions/routing.rb @@ -86,6 +86,7 @@ module ActionDispatch end # Load routes.rb if it hasn't been loaded. + options = options.clone generated_path, query_string_keys = @routes.generate_extras(options, defaults) found_extras = options.reject { |k, _| ! query_string_keys.include? k } diff --git a/actionpack/lib/action_dispatch/testing/integration.rb b/actionpack/lib/action_dispatch/testing/integration.rb index 790f9ea5d2..f4534b4173 100644 --- a/actionpack/lib/action_dispatch/testing/integration.rb +++ b/actionpack/lib/action_dispatch/testing/integration.rb @@ -71,7 +71,7 @@ module ActionDispatch end # Performs an XMLHttpRequest request with the given parameters, mirroring - # a request from the Prototype library. + # an AJAX request made from JavaScript. # # The request_method is +:get+, +:post+, +:patch+, +:put+, +:delete+ or # +:head+; the parameters are +nil+, a hash, or a url-encoded or multipart @@ -321,7 +321,9 @@ module ActionDispatch end # Performs the actual request. - def process(method, path, params: nil, headers: nil, env: nil, xhr: false) + def process(method, path, params: nil, headers: nil, env: nil, xhr: false, as: nil) + request_encoder = RequestEncoder.encoder(as) + if path =~ %r{://} location = URI.parse(path) https! URI::HTTPS === location if location.scheme @@ -330,14 +332,17 @@ module ActionDispatch url_host += ":#{location.port}" if default != location.port host! url_host end - path = location.query ? "#{location.path}?#{location.query}" : location.path + path = request_encoder.append_format_to location.path + path = location.query ? "#{path}?#{location.query}" : path + else + path = request_encoder.append_format_to path end hostname, port = host.split(':') request_env = { :method => method, - :params => params, + :params => request_encoder.encode_params(params), "SERVER_NAME" => hostname, "SERVER_PORT" => port || (https? ? "443" : "80"), @@ -347,7 +352,7 @@ module ActionDispatch "REQUEST_URI" => path, "HTTP_HOST" => host, "REMOTE_ADDR" => remote_addr, - "CONTENT_TYPE" => "application/x-www-form-urlencoded", + "CONTENT_TYPE" => request_encoder.content_type, "HTTP_ACCEPT" => accept } @@ -375,6 +380,8 @@ module ActionDispatch @request = ActionDispatch::Request.new(session.last_request.env) response = _mock_session.last_response @response = ActionDispatch::TestResponse.from_response(response) + @response.request = @request + @response.response_parser = RequestEncoder.parser(@response.content_type) @html_document = nil @url_options = nil @@ -386,6 +393,56 @@ module ActionDispatch def build_full_uri(path, env) "#{env['rack.url_scheme']}://#{env['SERVER_NAME']}:#{env['SERVER_PORT']}#{path}" end + + class RequestEncoder # :nodoc: + @encoders = {} + + attr_reader :response_parser + + def initialize(mime_name, param_encoder, response_parser, url_encoded_form = false) + @mime = Mime[mime_name] + + unless @mime + raise ArgumentError, "Can't register a request encoder for " \ + "unregistered MIME Type: #{mime_name}. See `Mime::Type.register`." + end + + @url_encoded_form = url_encoded_form + @path_format = ".#{@mime.symbol}" unless @url_encoded_form + @response_parser = response_parser || -> body { body } + @param_encoder = param_encoder || :"to_#{@mime.symbol}".to_proc + end + + def append_format_to(path) + path << @path_format unless @url_encoded_form + path + end + + def content_type + @mime.to_s + end + + def encode_params(params) + @param_encoder.call(params) + end + + def self.parser(content_type) + mime = Mime::Type.lookup(content_type) + encoder(mime ? mime.ref : nil).response_parser + end + + def self.encoder(name) + @encoders[name] || WWWFormEncoder + end + + def self.register_encoder(mime_name, param_encoder: nil, response_parser: nil) + @encoders[mime_name] = new(mime_name, param_encoder, response_parser) + end + + register_encoder :json, response_parser: -> body { JSON.parse(body) } + + WWWFormEncoder = new(:url_encoded_form, -> params { params }, nil, true) + end end module Runner @@ -642,6 +699,39 @@ module ActionDispatch # end # end # + # You can also test your JSON API easily by setting what the request should + # be encoded as: + # + # require 'test_helper' + # + # class ApiTest < ActionDispatch::IntegrationTest + # test 'creates articles' do + # assert_difference -> { Article.count } do + # post articles_path, params: { article: { title: 'Ahoy!' } }, as: :json + # end + # + # assert_response :success + # assert_equal({ id: Arcticle.last.id, title: 'Ahoy!' }, response.parsed_body) + # end + # end + # + # The `as` option sets the format to JSON, sets the content type to + # 'application/json' and encodes the parameters as JSON. + # + # Calling `parsed_body` on the response parses the response body as what + # the last request was encoded as. If the request wasn't encoded `as` something, + # it's the same as calling `body`. + # + # For any custom MIME Types you've registered, you can even add your own encoders with: + # + # ActionDispatch::IntegrationTest.register_encoder :wibble, + # param_encoder: -> params { params.to_wibble }, + # response_parser: -> body { body } + # + # Where `param_encoder` defines how the params should be encoded and + # `response_parser` defines how the response body should be parsed through + # `parsed_body`. + # # Consult the Rails Testing Guide for more. class IntegrationTest < ActiveSupport::TestCase @@ -670,5 +760,9 @@ module ActionDispatch def document_root_element html_document.root end + + def self.register_encoder(*args) + Integration::Session::RequestEncoder.register_encoder(*args) + end end end diff --git a/actionpack/lib/action_dispatch/testing/test_process.rb b/actionpack/lib/action_dispatch/testing/test_process.rb index c28d701b48..1ecd7d14a7 100644 --- a/actionpack/lib/action_dispatch/testing/test_process.rb +++ b/actionpack/lib/action_dispatch/testing/test_process.rb @@ -1,6 +1,5 @@ require 'action_dispatch/middleware/cookies' require 'action_dispatch/middleware/flash' -require 'active_support/core_ext/hash/indifferent_access' module ActionDispatch module TestProcess @@ -26,7 +25,7 @@ module ActionDispatch @response.redirect_url end - # Shortcut for <tt>Rack::Test::UploadedFile.new(File.join(ActionController::TestCase.fixture_path, path), type)</tt>: + # Shortcut for <tt>Rack::Test::UploadedFile.new(File.join(ActionDispatch::IntegrationTest.fixture_path, path), type)</tt>: # # post :change_avatar, avatar: fixture_file_upload('files/spongebob.png', 'image/png') # diff --git a/actionpack/lib/action_dispatch/testing/test_response.rb b/actionpack/lib/action_dispatch/testing/test_response.rb index 4b79a90242..9d4b73a43d 100644 --- a/actionpack/lib/action_dispatch/testing/test_response.rb +++ b/actionpack/lib/action_dispatch/testing/test_response.rb @@ -18,5 +18,11 @@ module ActionDispatch # Was there a server-side error? alias_method :error?, :server_error? + + attr_writer :response_parser # :nodoc: + + def parsed_body + @parsed_body ||= @response_parser.call(body) + end end end diff --git a/actionpack/lib/action_pack.rb b/actionpack/lib/action_pack.rb index f664dab620..941877d10d 100644 --- a/actionpack/lib/action_pack.rb +++ b/actionpack/lib/action_pack.rb @@ -1,5 +1,5 @@ #-- -# Copyright (c) 2004-2015 David Heinemeier Hansson +# Copyright (c) 2004-2016 David Heinemeier Hansson # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/actionpack/lib/action_pack/gem_version.rb b/actionpack/lib/action_pack/gem_version.rb index 255ac9f4ed..157f401f54 100644 --- a/actionpack/lib/action_pack/gem_version.rb +++ b/actionpack/lib/action_pack/gem_version.rb @@ -8,7 +8,7 @@ module ActionPack MAJOR = 5 MINOR = 0 TINY = 0 - PRE = "alpha" + PRE = "beta3" STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".") end diff --git a/actionpack/test/abstract_unit.rb b/actionpack/test/abstract_unit.rb index ef7aab72c6..1ef10ff956 100644 --- a/actionpack/test/abstract_unit.rb +++ b/actionpack/test/abstract_unit.rb @@ -20,7 +20,11 @@ rescue LoadError puts "'drb/unix' is not available" end -PROCESS_COUNT = (ENV['N'] || 4).to_i +if ENV['TRAVIS'] + PROCESS_COUNT = 0 +else + PROCESS_COUNT = (ENV['N'] || 4).to_i +end require 'active_support/testing/autorun' require 'abstract_controller' @@ -371,6 +375,12 @@ module RoutingTestHelpers end end +class MetalRenderingController < ActionController::Metal + include AbstractController::Rendering + include ActionController::Rendering + include ActionController::Renderers +end + class ResourcesController < ActionController::Base def index() head :ok end alias_method :show, :index diff --git a/actionpack/test/assertions/response_assertions_test.rb b/actionpack/test/assertions/response_assertions_test.rb index 82c747680d..579ce0ed29 100644 --- a/actionpack/test/assertions/response_assertions_test.rb +++ b/actionpack/test/assertions/response_assertions_test.rb @@ -6,7 +6,12 @@ module ActionDispatch class ResponseAssertionsTest < ActiveSupport::TestCase include ResponseAssertions - FakeResponse = Struct.new(:response_code) do + FakeResponse = Struct.new(:response_code, :location) do + def initialize(*) + super + self.location ||= "http://test.example.com/posts" + end + [:successful, :not_found, :redirection, :server_error].each do |sym| define_method("#{sym}?") do sym == response_code @@ -14,6 +19,11 @@ module ActionDispatch end end + def setup + @controller = nil + @request = nil + end + def test_assert_response_predicate_methods [:success, :missing, :redirect, :error].each do |sym| @response = FakeResponse.new RESPONSE_PREDICATES[sym].to_s.sub(/\?/, '').to_sym @@ -58,6 +68,50 @@ module ActionDispatch assert_response :succezz } end + + def test_error_message_shows_404_when_404_asserted_for_success + @response = ActionDispatch::Response.new + @response.status = 404 + + error = assert_raises(Minitest::Assertion) { assert_response :success } + expected = "Expected response to be a <2XX: success>,"\ + " but was a <404: Not Found>" + assert_match expected, error.message + end + + def test_error_message_shows_404_when_asserted_for_200 + @response = ActionDispatch::Response.new + @response.status = 404 + + error = assert_raises(Minitest::Assertion) { assert_response 200 } + expected = "Expected response to be a <200: OK>,"\ + " but was a <404: Not Found>" + assert_match expected, error.message + end + + def test_error_message_shows_302_redirect_when_302_asserted_for_success + @response = ActionDispatch::Response.new + @response.status = 302 + @response.location = 'http://test.host/posts/redirect/1' + + error = assert_raises(Minitest::Assertion) { assert_response :success } + expected = "Expected response to be a <2XX: success>,"\ + " but was a <302: Found>" \ + " redirect to <http://test.host/posts/redirect/1>" + assert_match expected, error.message + end + + def test_error_message_shows_302_redirect_when_302_asserted_for_301 + @response = ActionDispatch::Response.new + @response.status = 302 + @response.location = 'http://test.host/posts/redirect/2' + + error = assert_raises(Minitest::Assertion) { assert_response 301 } + expected = "Expected response to be a <301: Moved Permanently>,"\ + " but was a <302: Found>" \ + " redirect to <http://test.host/posts/redirect/2>" + assert_match expected, error.message + end end end end diff --git a/actionpack/test/controller/api/renderers_test.rb b/actionpack/test/controller/api/renderers_test.rb index 9405538833..911a8144b2 100644 --- a/actionpack/test/controller/api/renderers_test.rb +++ b/actionpack/test/controller/api/renderers_test.rb @@ -19,6 +19,14 @@ class RenderersApiController < ActionController::API def two render xml: Model.new end + + def plain + render plain: 'Hi from plain', status: 500 + end + + def text + render text: 'Hi from text', status: 500 + end end class RenderersApiTest < ActionController::TestCase @@ -35,4 +43,18 @@ class RenderersApiTest < ActionController::TestCase assert_response :success assert_equal({ a: 'b' }.to_xml, @response.body) end + + def test_render_plain + get :plain + assert_response :internal_server_error + assert_equal('Hi from plain', @response.body) + end + + def test_render_text + assert_deprecated do + get :text + end + assert_response :internal_server_error + assert_equal('Hi from text', @response.body) + end end diff --git a/actionpack/test/controller/base_test.rb b/actionpack/test/controller/base_test.rb index fb60dbd993..e3f669dbb5 100644 --- a/actionpack/test/controller/base_test.rb +++ b/actionpack/test/controller/base_test.rb @@ -93,7 +93,7 @@ end class ControllerInstanceTests < ActiveSupport::TestCase def setup @empty = EmptyController.new - @empty.set_request!(ActionDispatch::Request.new({})) + @empty.set_request!(ActionDispatch::Request.empty) @empty.set_response!(EmptyController.make_response!(@empty.request)) @contained = Submodule::ContainedEmptyController.new @empty_controllers = [@empty, @contained] diff --git a/actionpack/test/controller/caching_test.rb b/actionpack/test/controller/caching_test.rb index bc0ffd3eaa..754ac144cc 100644 --- a/actionpack/test/controller/caching_test.rb +++ b/actionpack/test/controller/caching_test.rb @@ -172,6 +172,9 @@ class FunctionalCachingController < CachingController def fragment_cached_without_digest end + + def fragment_cached_with_options + end end class FunctionalFragmentCachingTest < ActionController::TestCase @@ -215,6 +218,15 @@ CACHED assert_equal "<p>ERB</p>", @store.read("views/nodigest") end + def test_fragment_caching_with_options + get :fragment_cached_with_options + assert_response :success + expected_body = "<body>\n<p>ERB</p>\n</body>\n" + + assert_equal expected_body, @response.body + assert_equal "<p>ERB</p>", @store.read("views/with_options") + end + def test_render_inline_before_fragment_caching get :inline_fragment_cached assert_response :success @@ -369,14 +381,14 @@ class CollectionCacheController < ActionController::Base render 'index' end - def index_explicit_render + def index_explicit_render_in_controller @customers = [Customer.new('david', 1)] - render partial: 'customers/customer', collection: @customers + render partial: 'customers/customer', collection: @customers, cached: true end def index_with_comment @customers = [Customer.new('david', 1)] - render partial: 'customers/commented_customer', collection: @customers, as: :customer + render partial: 'customers/commented_customer', collection: @customers, as: :customer, cached: true end end @@ -387,12 +399,13 @@ class AutomaticCollectionCacheTest < ActionController::TestCase @controller.perform_caching = true @controller.partial_rendered_times = 0 @controller.cache_store = ActiveSupport::Cache::MemoryStore.new - ActionView::PartialRenderer.collection_cache = @controller.cache_store + ActionView::PartialRenderer.collection_cache = ActiveSupport::Cache::MemoryStore.new end def test_collection_fetches_cached_views get :index assert_equal 1, @controller.partial_rendered_times + assert_customer_cached 'david/1', 'david, 1' get :index assert_equal 1, @controller.partial_rendered_times @@ -400,13 +413,16 @@ class AutomaticCollectionCacheTest < ActionController::TestCase def test_preserves_order_when_reading_from_cache_plus_rendering get :index, params: { id: 2 } - get :index_ordered + assert_equal 1, @controller.partial_rendered_times + assert_select ':root', 'david, 2' + get :index_ordered + assert_equal 3, @controller.partial_rendered_times assert_select ':root', "david, 1\n david, 2\n david, 3" end def test_explicit_render_call_with_options - get :index_explicit_render + get :index_explicit_render_in_controller assert_select ':root', "david, 1" end @@ -418,4 +434,35 @@ class AutomaticCollectionCacheTest < ActionController::TestCase get :index_with_comment assert_equal 1, @controller.partial_rendered_times end + + private + def assert_customer_cached(key, content) + assert_match content, + ActionView::PartialRenderer.collection_cache.read("views/#{key}/7c228ab609f0baf0b1f2367469210937") + end +end + +class FragmentCacheKeyTestController < CachingController + attr_accessor :account_id + + fragment_cache_key "v1" + fragment_cache_key { account_id } +end + +class FragmentCacheKeyTest < ActionController::TestCase + def setup + super + @store = ActiveSupport::Cache::MemoryStore.new + @controller = FragmentCacheKeyTestController.new + @controller.perform_caching = true + @controller.cache_store = @store + end + + def test_fragment_cache_key + @controller.account_id = "123" + assert_equal 'views/v1/123/what a key', @controller.fragment_cache_key('what a key') + + @controller.account_id = nil + assert_equal 'views/v1//what a key', @controller.fragment_cache_key('what a key') + end end diff --git a/actionpack/test/controller/force_ssl_test.rb b/actionpack/test/controller/force_ssl_test.rb index 22f1cc7c22..03a9c9ae78 100644 --- a/actionpack/test/controller/force_ssl_test.rb +++ b/actionpack/test/controller/force_ssl_test.rb @@ -322,3 +322,12 @@ class RedirectToSSLTest < ActionController::TestCase assert_equal 'ihaz', response.body end end + +class ForceSSLControllerLevelTest < ActionController::TestCase + def test_no_redirect_websocket_ssl_request + request.env['rack.url_scheme'] = 'wss' + request.env['Upgrade'] = 'websocket' + get :cheeseburger + assert_response 200 + end +end diff --git a/actionpack/test/controller/helper_test.rb b/actionpack/test/controller/helper_test.rb index 3ecfedefd1..feb882a2b3 100644 --- a/actionpack/test/controller/helper_test.rb +++ b/actionpack/test/controller/helper_test.rb @@ -141,20 +141,10 @@ class HelperTest < ActiveSupport::TestCase def test_helper_for_nested_controller assert_equal 'hello: Iz guuut!', call_controller(Fun::GamesController, "render_hello_world").last.body - # request = ActionController::TestRequest.new - # - # resp = Fun::GamesController.action(:render_hello_world).call(request.env) - # assert_equal 'hello: Iz guuut!', resp.last.body end def test_helper_for_acronym_controller assert_equal "test: baz", call_controller(Fun::PdfController, "test").last.body - # - # request = ActionController::TestRequest.new - # response = ActionDispatch::TestResponse.new - # request.action = 'test' - # - # assert_equal 'test: baz', Fun::PdfController.process(request, response).body end def test_default_helpers_only diff --git a/actionpack/test/controller/http_basic_authentication_test.rb b/actionpack/test/controller/http_basic_authentication_test.rb index 0a5e5402b9..adcf259317 100644 --- a/actionpack/test/controller/http_basic_authentication_test.rb +++ b/actionpack/test/controller/http_basic_authentication_test.rb @@ -5,6 +5,7 @@ class HttpBasicAuthenticationTest < ActionController::TestCase before_action :authenticate, only: :index before_action :authenticate_with_request, only: :display before_action :authenticate_long_credentials, only: :show + before_action :auth_with_special_chars, only: :special_creds http_basic_authenticate_with :name => "David", :password => "Goliath", :only => :search @@ -20,6 +21,10 @@ class HttpBasicAuthenticationTest < ActionController::TestCase render plain: 'Only for loooooong credentials' end + def special_creds + render plain: 'Only for special credentials' + end + def search render plain: 'All inline' end @@ -40,6 +45,12 @@ class HttpBasicAuthenticationTest < ActionController::TestCase end end + def auth_with_special_chars + authenticate_or_request_with_http_basic do |username, password| + username == 'login!@#$%^&*()_+{}[];"\',./<>?`~ \n\r\t' && password == 'pwd:!@#$%^&*()_+{}[];"\',./<>?`~ \n\r\t' + end + end + def authenticate_long_credentials authenticate_or_request_with_http_basic do |username, password| username == '1234567890123456789012345678901234567890' && password == '1234567890123456789012345678901234567890' @@ -100,7 +111,7 @@ class HttpBasicAuthenticationTest < ActionController::TestCase assert_no_match(/\n/, result) end - test "succesful authentication with uppercase authorization scheme" do + test "successful authentication with uppercase authorization scheme" do @request.env['HTTP_AUTHORIZATION'] = "BASIC #{::Base64.encode64("lifo:world")}" get :index @@ -133,6 +144,14 @@ class HttpBasicAuthenticationTest < ActionController::TestCase assert_equal 'Definitely Maybe', @response.body end + test "authentication request with valid credential special chars" do + @request.env['HTTP_AUTHORIZATION'] = encode_credentials('login!@#$%^&*()_+{}[];"\',./<>?`~ \n\r\t', 'pwd:!@#$%^&*()_+{}[];"\',./<>?`~ \n\r\t') + get :special_creds + + assert_response :success + assert_equal 'Only for special credentials', @response.body + end + test "authenticate with class method" do @request.env['HTTP_AUTHORIZATION'] = encode_credentials('David', 'Goliath') get :search diff --git a/actionpack/test/controller/http_token_authentication_test.rb b/actionpack/test/controller/http_token_authentication_test.rb index 9c5a01c318..98e3c891a7 100644 --- a/actionpack/test/controller/http_token_authentication_test.rb +++ b/actionpack/test/controller/http_token_authentication_test.rb @@ -94,6 +94,14 @@ class HttpTokenAuthenticationTest < ActionController::TestCase assert_response :success end + test "authentication request with tab in header" do + @request.env['HTTP_AUTHORIZATION'] = "Token\ttoken=\"lifo\"" + get :index + + assert_response :success + assert_equal 'Hello Secret', @response.body + end + test "authentication request without credential" do get :display diff --git a/actionpack/test/controller/integration_test.rb b/actionpack/test/controller/integration_test.rb index d0a1d1285f..6277407ff7 100644 --- a/actionpack/test/controller/integration_test.rb +++ b/actionpack/test/controller/integration_test.rb @@ -390,7 +390,7 @@ class IntegrationTestUsesCorrectClass < ActionDispatch::IntegrationTest reset! %w( get post head patch put delete ).each do |verb| - assert_nothing_raised("'#{verb}' should use integration test methods") { __send__(verb, '/') } + assert_nothing_raised { __send__(verb, '/') } end end end @@ -1126,3 +1126,69 @@ class IntegrationRequestsWithSessionSetup < ActionDispatch::IntegrationTest assert_equal({"user_name"=>"david"}, cookies.to_hash) end end + +class IntegrationRequestEncodersTest < ActionDispatch::IntegrationTest + class FooController < ActionController::Base + def foos_json + render json: params.permit(:foo) + end + + def foos_wibble + render plain: 'ok' + end + end + + def test_encoding_as_json + post_to_foos as: :json do + assert_response :success + assert_match 'foos_json.json', request.path + assert_equal 'application/json', request.content_type + assert_equal({ 'foo' => 'fighters' }, request.request_parameters) + assert_equal({ 'foo' => 'fighters' }, response.parsed_body) + end + end + + def test_encoding_as_without_mime_registration + assert_raise ArgumentError do + ActionDispatch::IntegrationTest.register_encoder :wibble + end + end + + def test_registering_custom_encoder + Mime::Type.register 'text/wibble', :wibble + + ActionDispatch::IntegrationTest.register_encoder(:wibble, + param_encoder: -> params { params }) + + post_to_foos as: :wibble do + assert_response :success + assert_match 'foos_wibble.wibble', request.path + assert_equal 'text/wibble', request.content_type + assert_equal Hash.new, request.request_parameters # Unregistered MIME Type can't be parsed. + assert_equal 'ok', response.parsed_body + end + ensure + Mime::Type.unregister :wibble + end + + def test_parsed_body_without_as_option + with_routing do |routes| + routes.draw { get ':action' => FooController } + + get '/foos_json.json', params: { foo: 'heyo' } + + assert_equal({ 'foo' => 'heyo' }, response.parsed_body) + end + end + + private + def post_to_foos(as:) + with_routing do |routes| + routes.draw { post ':action' => FooController } + + post "/foos_#{as}", params: { foo: 'fighters' }, as: as + + yield + end + end +end diff --git a/actionpack/test/controller/live_stream_test.rb b/actionpack/test/controller/live_stream_test.rb index 4224ac2a1b..0c3884cd38 100644 --- a/actionpack/test/controller/live_stream_test.rb +++ b/actionpack/test/controller/live_stream_test.rb @@ -1,5 +1,5 @@ require 'abstract_unit' -require 'concurrent/atomics' +require 'concurrent/atomic/count_down_latch' Thread.abort_on_exception = true module ActionController @@ -152,7 +152,6 @@ module ActionController def thread_locals tc.assert_equal 'aaron', Thread.current[:setting] - tc.assert_not_equal Thread.current.object_id, Thread.current[:originating_thread] response.headers['Content-Type'] = 'text/event-stream' %w{ hello world }.each do |word| @@ -261,6 +260,14 @@ module ActionController end end + def setup + super + + def @controller.new_controller_thread + Thread.new { yield } + end + end + def test_set_cookie get :set_cookie assert_equal({'hello' => 'world'}, @response.cookies) @@ -429,7 +436,7 @@ module ActionController end def test_stale_with_etag - @request.if_none_match = Digest::MD5.hexdigest("123") + @request.if_none_match = %(W/"#{Digest::MD5.hexdigest('123')}") get :with_stale assert_equal 304, response.status.to_i end @@ -442,3 +449,42 @@ module ActionController end end end + +class LiveStreamRouterTest < ActionDispatch::IntegrationTest + class TestController < ActionController::Base + include ActionController::Live + + def index + response.headers['Content-Type'] = 'text/event-stream' + sse = SSE.new(response.stream) + sse.write("{\"name\":\"John\"}") + sse.write({ name: "Ryan" }) + ensure + sse.close + end + end + + def self.call(env) + routes.call(env) + end + + def self.routes + @routes ||= ActionDispatch::Routing::RouteSet.new + end + + routes.draw do + get '/test' => 'live_stream_router_test/test#index' + end + + def app + self.class + end + + test "streaming served through the router" do + get "/test" + + assert_response :ok + assert_match(/data: {\"name\":\"John\"}/, response.body) + assert_match(/data: {\"name\":\"Ryan\"}/, response.body) + end +end diff --git a/actionpack/test/controller/log_subscriber_test.rb b/actionpack/test/controller/log_subscriber_test.rb index 7835d2768a..6ae33be3c8 100644 --- a/actionpack/test/controller/log_subscriber_test.rb +++ b/actionpack/test/controller/log_subscriber_test.rb @@ -170,7 +170,7 @@ class ACLogSubscriberTest < ActionController::TestCase def test_process_action_with_view_runtime get :show wait - assert_match(/Completed 200 OK in [\d]ms/, logs[1]) + assert_match(/Completed 200 OK in \d+ms/, logs[1]) end def test_append_info_to_payload_is_called_even_with_exception diff --git a/actionpack/test/controller/metal/renderers_test.rb b/actionpack/test/controller/metal/renderers_test.rb new file mode 100644 index 0000000000..007866a559 --- /dev/null +++ b/actionpack/test/controller/metal/renderers_test.rb @@ -0,0 +1,42 @@ +require 'abstract_unit' +require 'active_support/core_ext/hash/conversions' + +class MetalRenderingJsonController < MetalRenderingController + class Model + def to_json(options = {}) + { a: 'b' }.to_json(options) + end + + def to_xml(options = {}) + { a: 'b' }.to_xml(options) + end + end + + use_renderers :json + + def one + render json: Model.new + end + + def two + render xml: Model.new + end +end + +class RenderersMetalTest < ActionController::TestCase + tests MetalRenderingJsonController + + def test_render_json + get :one + assert_response :success + assert_equal({ a: 'b' }.to_json, @response.body) + assert_equal 'application/json', @response.content_type + end + + def test_render_xml + get :two + assert_response :success + assert_equal(" ", @response.body) + assert_equal 'text/plain', @response.content_type + end +end diff --git a/actionpack/test/controller/mime/respond_to_test.rb b/actionpack/test/controller/mime/respond_to_test.rb index c025c7fa00..d0c7b2e06a 100644 --- a/actionpack/test/controller/mime/respond_to_test.rb +++ b/actionpack/test/controller/mime/respond_to_test.rb @@ -160,7 +160,14 @@ class RespondToController < ActionController::Base end end - def variant_with_implicit_rendering + def variant_with_implicit_template_rendering + # This has exactly one variant template defined in the file system (+mobile.html.erb), + # which raises the regular MissingTemplate error for other variants. + end + + def variant_without_implicit_template_rendering + # This differs from the above in that it does not have any templates defined in the file + # system, which triggers the ImplicitRender (204 No Content) behavior. end def variant_with_format_and_custom_render @@ -272,6 +279,8 @@ class RespondToController < ActionController::Base end class RespondToControllerTest < ActionController::TestCase + NO_CONTENT_WARNING = "No template found for RespondToController#variant_without_implicit_template_rendering, rendering head :no_content" + def setup super @request.host = "www.example.com" @@ -616,30 +625,69 @@ class RespondToControllerTest < ActionController::TestCase end def test_invalid_variant + assert_raises(ActionController::UnknownFormat) do + get :variant_with_implicit_template_rendering, params: { v: :invalid } + end + end + + def test_variant_not_set_regular_unknown_format + assert_raises(ActionController::UnknownFormat) do + get :variant_with_implicit_template_rendering + end + end + + def test_variant_with_implicit_template_rendering + get :variant_with_implicit_template_rendering, params: { v: :mobile } + assert_equal "text/html", @response.content_type + assert_equal "mobile", @response.body + end + + def test_variant_without_implicit_rendering_from_browser + assert_raises(ActionController::UnknownFormat) do + get :variant_without_implicit_template_rendering, params: { v: :does_not_matter } + end + end + + def test_variant_variant_not_set_and_without_implicit_rendering_from_browser + assert_raises(ActionController::UnknownFormat) do + get :variant_without_implicit_template_rendering + end + end + + def test_variant_without_implicit_rendering_from_xhr logger = ActiveSupport::LogSubscriber::TestHelper::MockLogger.new old_logger, ActionController::Base.logger = ActionController::Base.logger, logger - get :variant_with_implicit_rendering, params: { v: :invalid } + get :variant_without_implicit_template_rendering, xhr: true, params: { v: :does_not_matter } assert_response :no_content - assert_equal 1, logger.logged(:info).select{ |s| s =~ /No template found/ }.size, "Implicit head :no_content not logged" + + assert_equal 1, logger.logged(:info).select{ |s| s == NO_CONTENT_WARNING }.size, "Implicit head :no_content not logged" ensure ActionController::Base.logger = old_logger end - def test_variant_not_set_regular_template_missing - get :variant_with_implicit_rendering + def test_variant_without_implicit_rendering_from_api + logger = ActiveSupport::LogSubscriber::TestHelper::MockLogger.new + old_logger, ActionController::Base.logger = ActionController::Base.logger, logger + + get :variant_without_implicit_template_rendering, format: 'json', params: { v: :does_not_matter } assert_response :no_content + + assert_equal 1, logger.logged(:info).select{ |s| s == NO_CONTENT_WARNING }.size, "Implicit head :no_content not logged" + ensure + ActionController::Base.logger = old_logger end - def test_variant_with_implicit_rendering - get :variant_with_implicit_rendering, params: { v: :implicit } + def test_variant_variant_not_set_and_without_implicit_rendering_from_xhr + logger = ActiveSupport::LogSubscriber::TestHelper::MockLogger.new + old_logger, ActionController::Base.logger = ActionController::Base.logger, logger + + get :variant_without_implicit_template_rendering, xhr: true assert_response :no_content - end - def test_variant_with_implicit_template_rendering - get :variant_with_implicit_rendering, params: { v: :mobile } - assert_equal "text/html", @response.content_type - assert_equal "mobile", @response.body + assert_equal 1, logger.logged(:info).select { |s| s == NO_CONTENT_WARNING }.size, "Implicit head :no_content not logged" + ensure + ActionController::Base.logger = old_logger end def test_variant_with_format_and_custom_render @@ -661,10 +709,6 @@ class RespondToControllerTest < ActionController::TestCase end def test_variant_inline_syntax - get :variant_inline_syntax, format: :js - assert_equal "text/javascript", @response.content_type - assert_equal "js", @response.body - get :variant_inline_syntax assert_equal "text/html", @response.content_type assert_equal "none", @response.body @@ -674,6 +718,12 @@ class RespondToControllerTest < ActionController::TestCase assert_equal "phone", @response.body end + def test_variant_inline_syntax_with_format + get :variant_inline_syntax, format: :js + assert_equal "text/javascript", @response.content_type + assert_equal "js", @response.body + end + def test_variant_inline_syntax_without_block get :variant_inline_syntax_without_block, params: { v: :phone } assert_equal "text/html", @response.content_type @@ -776,24 +826,3 @@ class RespondToControllerTest < ActionController::TestCase assert_equal "phone", @response.body end end - -class RespondToWithBlockOnDefaultRenderController < ActionController::Base - def show - default_render do - render body: 'default_render yielded' - end - end -end - -class RespondToWithBlockOnDefaultRenderControllerTest < ActionController::TestCase - def setup - super - @request.host = "www.example.com" - end - - def test_default_render_uses_block_when_no_template_exists - get :show - assert_equal "default_render yielded", @response.body - assert_equal "text/plain", @response.content_type - end -end diff --git a/actionpack/test/controller/new_base/bare_metal_test.rb b/actionpack/test/controller/new_base/bare_metal_test.rb index e61f4d241b..ee3c498b1c 100644 --- a/actionpack/test/controller/new_base/bare_metal_test.rb +++ b/actionpack/test/controller/new_base/bare_metal_test.rb @@ -26,7 +26,7 @@ module BareMetalTest test "response_body value is wrapped in an array when the value is a String" do controller = BareController.new - controller.set_request!(ActionDispatch::Request.new({})) + controller.set_request!(ActionDispatch::Request.empty) controller.set_response!(BareController.make_response!(controller.request)) controller.index assert_equal ["Hello world"], controller.response_body @@ -40,6 +40,22 @@ module BareMetalTest end end + class BareEmptyController < ActionController::Metal + def index + self.response_body = nil + end + end + + class BareEmptyTest < ActiveSupport::TestCase + test "response body is nil" do + controller = BareEmptyController.new + controller.set_request!(ActionDispatch::Request.empty) + controller.set_response!(BareController.make_response!(controller.request)) + controller.index + assert_equal nil, controller.response_body + end + end + class HeadController < ActionController::Metal include ActionController::Head diff --git a/actionpack/test/controller/parameters/accessors_test.rb b/actionpack/test/controller/parameters/accessors_test.rb index 97875c3cbb..cea265f9ab 100644 --- a/actionpack/test/controller/parameters/accessors_test.rb +++ b/actionpack/test/controller/parameters/accessors_test.rb @@ -4,6 +4,8 @@ require 'active_support/core_ext/hash/transform_values' class ParametersAccessorsTest < ActiveSupport::TestCase setup do + ActionController::Parameters.permit_all_parameters = false + @params = ActionController::Parameters.new( person: { age: '32', @@ -27,6 +29,12 @@ class ParametersAccessorsTest < ActiveSupport::TestCase assert_not @params[:person][:name].permitted? end + test "as_json returns the JSON representation of the parameters hash" do + assert_not @params.as_json.key? "parameters" + assert_not @params.as_json.key? "permitted" + assert @params.as_json.key? "person" + end + test "each carries permitted status" do @params.permit! @params.each { |key, value| assert(value.permitted?) if key == "person" } @@ -122,4 +130,68 @@ class ParametersAccessorsTest < ActiveSupport::TestCase assert_not @params.values_at(:person).first.permitted? assert_not @params[:person].values_at(:name).first.permitted? end + + test "equality with a hash is deprecated" do + hash1 = { foo: :bar } + params1 = ActionController::Parameters.new(hash1) + assert_deprecated("will be removed in Rails 5.1") do + assert(params1 == hash1) + end + end + + test "is equal to Parameters instance with same params" do + params1 = ActionController::Parameters.new(a: 1, b: 2) + params2 = ActionController::Parameters.new(a: 1, b: 2) + assert(params1 == params2) + end + + test "is equal to Parameters instance with same permitted params" do + params1 = ActionController::Parameters.new(a: 1, b: 2).permit(:a) + params2 = ActionController::Parameters.new(a: 1, b: 2).permit(:a) + assert(params1 == params2) + end + + test "is equal to Parameters instance with same different source params, but same permitted params" do + params1 = ActionController::Parameters.new(a: 1, b: 2).permit(:a) + params2 = ActionController::Parameters.new(a: 1, c: 3).permit(:a) + assert(params1 == params2) + assert(params2 == params1) + end + + test 'is not equal to an unpermitted Parameters instance with same params' do + params1 = ActionController::Parameters.new(a: 1).permit(:a) + params2 = ActionController::Parameters.new(a: 1) + assert(params1 != params2) + assert(params2 != params1) + end + + test "is not equal to Parameters instance with different permitted params" do + params1 = ActionController::Parameters.new(a: 1, b: 2).permit(:a, :b) + params2 = ActionController::Parameters.new(a: 1, b: 2).permit(:a) + assert(params1 != params2) + assert(params2 != params1) + end + + test "equality with simple types works" do + assert(@params != 'Hello') + assert(@params != 42) + assert(@params != false) + end + + test "inspect shows both class name, parameters and permitted flag" do + assert_equal( + '<ActionController::Parameters {"person"=>{"age"=>"32", '\ + '"name"=>{"first"=>"David", "last"=>"Heinemeier Hansson"}, ' \ + '"addresses"=>[{"city"=>"Chicago", "state"=>"Illinois"}]}} permitted: false>', + @params.inspect + ) + end + + test "inspect prints updated permitted flag in the output" do + assert_match(/permitted: false/, @params.inspect) + + @params.permit! + + assert_match(/permitted: true/, @params.inspect) + end end diff --git a/actionpack/test/controller/parameters/always_permitted_parameters_test.rb b/actionpack/test/controller/parameters/always_permitted_parameters_test.rb index efaf8a96c3..c5bfb10b53 100644 --- a/actionpack/test/controller/parameters/always_permitted_parameters_test.rb +++ b/actionpack/test/controller/parameters/always_permitted_parameters_test.rb @@ -12,12 +12,6 @@ class AlwaysPermittedParametersTest < ActiveSupport::TestCase ActionController::Parameters.always_permitted_parameters = %w( controller action ) end - test "shows deprecations warning on NEVER_UNPERMITTED_PARAMS" do - assert_deprecated do - ActionController::Parameters::NEVER_UNPERMITTED_PARAMS - end - end - test "returns super on missing constant other than NEVER_UNPERMITTED_PARAMS" do ActionController::Parameters.superclass.stub :const_missing, "super" do assert_equal "super", ActionController::Parameters::NON_EXISTING_CONSTANT diff --git a/actionpack/test/controller/parameters/parameters_permit_test.rb b/actionpack/test/controller/parameters/parameters_permit_test.rb index 9f7d14e85d..96048e2868 100644 --- a/actionpack/test/controller/parameters/parameters_permit_test.rb +++ b/actionpack/test/controller/parameters/parameters_permit_test.rb @@ -27,6 +27,27 @@ class ParametersPermitTest < ActiveSupport::TestCase end end + def walk_permitted params + params.each do |k,v| + case v + when ActionController::Parameters + walk_permitted v + when Array + v.each { |x| walk_permitted v } + end + end + end + + test 'iteration should not impact permit' do + hash = {"foo"=>{"bar"=>{"0"=>{"baz"=>"hello", "zot"=>"1"}}}} + params = ActionController::Parameters.new(hash) + + walk_permitted params + + sanitized = params[:foo].permit(bar: [:baz]) + assert_equal({"0"=>{"baz"=>"hello"}}, sanitized[:bar].to_unsafe_h) + end + test 'if nothing is permitted, the hash becomes empty' do params = ActionController::Parameters.new(id: '1234') permitted = params.permit @@ -256,7 +277,7 @@ class ParametersPermitTest < ActiveSupport::TestCase end test "to_h returns empty hash on unpermitted params" do - assert @params.to_h.is_a? Hash + assert @params.to_h.is_a? ActiveSupport::HashWithIndifferentAccess assert_not @params.to_h.is_a? ActionController::Parameters assert @params.to_h.empty? end @@ -264,7 +285,7 @@ class ParametersPermitTest < ActiveSupport::TestCase test "to_h returns converted hash on permitted params" do @params.permit! - assert @params.to_h.is_a? Hash + assert @params.to_h.is_a? ActiveSupport::HashWithIndifferentAccess assert_not @params.to_h.is_a? ActionController::Parameters end @@ -273,7 +294,7 @@ class ParametersPermitTest < ActiveSupport::TestCase ActionController::Parameters.permit_all_parameters = true params = ActionController::Parameters.new(crab: "Senjougahara Hitagi") - assert params.to_h.is_a? Hash + assert params.to_h.is_a? ActiveSupport::HashWithIndifferentAccess assert_not @params.to_h.is_a? ActionController::Parameters assert_equal({ "crab" => "Senjougahara Hitagi" }, params.to_h) ensure @@ -294,7 +315,49 @@ class ParametersPermitTest < ActiveSupport::TestCase end test "to_unsafe_h returns unfiltered params" do - assert @params.to_h.is_a? Hash - assert_not @params.to_h.is_a? ActionController::Parameters + assert @params.to_unsafe_h.is_a? ActiveSupport::HashWithIndifferentAccess + assert_not @params.to_unsafe_h.is_a? ActionController::Parameters + end + + test "to_unsafe_h returns unfiltered params even after accessing few keys" do + params = ActionController::Parameters.new("f"=>{"language_facet"=>["Tibetan"]}) + expected = {"f"=>{"language_facet"=>["Tibetan"]}} + + assert params['f'].is_a? ActionController::Parameters + assert_equal expected, params.to_unsafe_h + end + + test "to_h only deep dups Ruby collections" do + company = Class.new do + attr_reader :dupped + def dup; @dupped = true; end + end.new + + params = ActionController::Parameters.new(prem: { likes: %i( dancing ) }) + assert_equal({ 'prem' => { 'likes' => %i( dancing ) } }, params.permit!.to_h) + + params = ActionController::Parameters.new(companies: [ company, :acme ]) + assert_equal({ 'companies' => [ company, :acme ] }, params.permit!.to_h) + assert_not company.dupped + end + + test "to_unsafe_h only deep dups Ruby collections" do + company = Class.new do + attr_reader :dupped + def dup; @dupped = true; end + end.new + + params = ActionController::Parameters.new(prem: { likes: %i( dancing ) }) + assert_equal({ 'prem' => { 'likes' => %i( dancing ) } }, params.to_unsafe_h) + + params = ActionController::Parameters.new(companies: [ company, :acme ]) + assert_equal({ 'companies' => [ company, :acme ] }, params.to_unsafe_h) + assert_not company.dupped + end + + test "include? returns true when the key is present" do + assert @params.include? :person + assert @params.include? 'person' + assert_not @params.include? :gorilla end end diff --git a/actionpack/test/controller/redirect_test.rb b/actionpack/test/controller/redirect_test.rb index 631ff7d02a..3ea03be74a 100644 --- a/actionpack/test/controller/redirect_test.rb +++ b/actionpack/test/controller/redirect_test.rb @@ -42,6 +42,10 @@ class RedirectController < ActionController::Base redirect_to :back, :status => 307 end + def redirect_back_with_status + redirect_back(fallback_location: "/things/stuff", status: 307) + end + def host_redirect redirect_to :action => "other_host", :only_path => false, :host => 'other.test.host' end @@ -187,7 +191,11 @@ class RedirectTest < ActionController::TestCase def test_redirect_to_back_with_status @request.env["HTTP_REFERER"] = "http://www.example.com/coming/from" - get :redirect_to_back_with_status + + assert_deprecated do + get :redirect_to_back_with_status + end + assert_response 307 assert_equal "http://www.example.com/coming/from", redirect_to_url end @@ -236,7 +244,11 @@ class RedirectTest < ActionController::TestCase def test_redirect_to_back @request.env["HTTP_REFERER"] = "http://www.example.com/coming/from" - get :redirect_to_back + + assert_deprecated do + get :redirect_to_back + end + assert_response :redirect assert_equal "http://www.example.com/coming/from", redirect_to_url end @@ -244,10 +256,32 @@ class RedirectTest < ActionController::TestCase def test_redirect_to_back_with_no_referer assert_raise(ActionController::RedirectBackError) { @request.env["HTTP_REFERER"] = nil + + assert_deprecated do + get :redirect_to_back + end + get :redirect_to_back } end + def test_redirect_back + referer = "http://www.example.com/coming/from" + @request.env["HTTP_REFERER"] = referer + + get :redirect_back_with_status + + assert_response 307 + assert_equal referer, redirect_to_url + end + + def test_redirect_back_with_no_referer + get :redirect_back_with_status + + assert_response 307 + assert_equal "http://test.host/things/stuff", redirect_to_url + end + def test_redirect_to_record with_routing do |set| set.draw do @@ -273,10 +307,10 @@ class RedirectTest < ActionController::TestCase end def test_redirect_to_params - error = assert_raise(ActionController::ActionControllerError) do + error = assert_raise(ArgumentError) do get :redirect_to_params end - assert_equal "Cannot redirect to a parameter hash!", error.message + assert_equal "Generating a URL from non sanitized request parameters is insecure!", error.message end def test_redirect_to_with_block diff --git a/actionpack/test/controller/render_other_test.rb b/actionpack/test/controller/render_other_test.rb deleted file mode 100644 index 1f5215ac55..0000000000 --- a/actionpack/test/controller/render_other_test.rb +++ /dev/null @@ -1,24 +0,0 @@ -require 'abstract_unit' - - -class RenderOtherTest < ActionController::TestCase - class TestController < ActionController::Base - def render_simon_says - render :simon => "foo" - end - end - - tests TestController - - def test_using_custom_render_option - ActionController.add_renderer :simon do |says, options| - self.content_type = Mime[:text] - self.response_body = "Simon says: #{says}" - end - - get :render_simon_says - assert_equal "Simon says: foo", @response.body - ensure - ActionController.remove_renderer :simon - end -end diff --git a/actionpack/test/controller/render_test.rb b/actionpack/test/controller/render_test.rb index 256ebf6a07..83d7405e4d 100644 --- a/actionpack/test/controller/render_test.rb +++ b/actionpack/test/controller/render_test.rb @@ -26,6 +26,9 @@ end class ImplicitRenderTestController < ActionController::Base def empty_action end + + def empty_action_with_template + end end class TestController < ActionController::Base @@ -62,6 +65,20 @@ class TestController < ActionController::Base end end + def dynamic_render + render params[:id] # => String, AC::Params + end + + def dynamic_render_permit + render params[:id].permit(:file) + end + + def dynamic_render_with_file + # This is extremely bad, but should be possible to do. + file = params[:id] # => String, AC::Params + render file: file + end + class Collection def initialize(records) @records = records @@ -243,6 +260,52 @@ end class ExpiresInRenderTest < ActionController::TestCase tests TestController + def setup + super + ActionController::Base.view_paths.paths.each(&:clear_cache) + end + + def test_dynamic_render_with_file + # This is extremely bad, but should be possible to do. + assert File.exist?(File.join(File.dirname(__FILE__), '../../test/abstract_unit.rb')) + response = get :dynamic_render_with_file, params: { id: '../\\../test/abstract_unit.rb' } + assert_equal File.read(File.join(File.dirname(__FILE__), '../../test/abstract_unit.rb')), + response.body + end + + def test_dynamic_render_with_absolute_path + file = Tempfile.new('name') + file.write "secrets!" + file.flush + assert_raises ActionView::MissingTemplate do + get :dynamic_render, params: { id: file.path } + end + ensure + file.close + file.unlink + end + + def test_dynamic_render + assert File.exist?(File.join(File.dirname(__FILE__), '../../test/abstract_unit.rb')) + assert_raises ActionView::MissingTemplate do + get :dynamic_render, params: { id: '../\\../test/abstract_unit.rb' } + end + end + + def test_permitted_dynamic_render_file_hash + skip "FIXME: this test passes on 4-2-stable but not master. Why?" + assert File.exist?(File.join(File.dirname(__FILE__), '../../test/abstract_unit.rb')) + response = get :dynamic_render_permit, params: { id: { file: '../\\../test/abstract_unit.rb' } } + assert_equal File.read(File.join(File.dirname(__FILE__), '../../test/abstract_unit.rb')), + response.body + end + + def test_dynamic_render_file_hash + assert_raises ArgumentError do + get :dynamic_render, params: { id: { file: '../\\../test/abstract_unit.rb' } } + end + end + def test_expires_in_header get :conditional_hello_with_expires_in assert_equal "max-age=60, private", @response.headers["Cache-Control"] @@ -449,7 +512,7 @@ class EtagRenderTest < ActionController::TestCase begin File.write path, 'foo' - ActionView::Digestor.cache.clear + ActionView::LookupContext::DetailsKey.clear request.if_none_match = etag get :with_template @@ -461,7 +524,7 @@ class EtagRenderTest < ActionController::TestCase end def etag(record) - Digest::MD5.hexdigest(ActiveSupport::Cache.expand_cache_key(record)).inspect + %(W/"#{Digest::MD5.hexdigest(ActiveSupport::Cache.expand_cache_key(record))}") end end @@ -477,10 +540,28 @@ end class ImplicitRenderTest < ActionController::TestCase tests ImplicitRenderTestController - def test_implicit_no_content_response - get :empty_action + def test_implicit_no_content_response_as_browser + assert_raises(ActionController::UnknownFormat) do + get :empty_action + end + end + + def test_implicit_no_content_response_as_xhr + get :empty_action, xhr: true assert_response :no_content end + + def test_implicit_success_response_with_right_format + get :empty_action_with_template + assert_equal "<h1>Empty action rendered this implicitly.</h1>\n", @response.body + assert_response :success + end + + def test_implicit_unknown_format_response + assert_raises(ActionController::UnknownFormat) do + get :empty_action_with_template, format: 'json' + end + end end class HeadRenderTest < ActionController::TestCase diff --git a/actionpack/test/controller/renderers_test.rb b/actionpack/test/controller/renderers_test.rb new file mode 100644 index 0000000000..e6c2e4636e --- /dev/null +++ b/actionpack/test/controller/renderers_test.rb @@ -0,0 +1,90 @@ +require 'abstract_unit' +require 'controller/fake_models' +require 'active_support/logger' + +class RenderersTest < ActionController::TestCase + class XmlRenderable + def to_xml(options) + options[:root] ||= "i-am-xml" + "<#{options[:root]}/>" + end + end + class JsonRenderable + def as_json(options={}) + hash = { :a => :b, :c => :d, :e => :f } + hash.except!(*options[:except]) if options[:except] + hash + end + + def to_json(options = {}) + super :except => [:c, :e] + end + end + class CsvRenderable + def to_csv + "c,s,v" + end + end + class TestController < ActionController::Base + + def render_simon_says + render :simon => "foo" + end + + def respond_to_mime + respond_to do |type| + type.json do + render json: JsonRenderable.new + end + type.js { render json: 'JS', callback: 'alert' } + type.csv { render csv: CsvRenderable.new } + type.xml { render xml: XmlRenderable.new } + type.html { render body: "HTML" } + type.rss { render body: "RSS" } + type.all { render body: "Nothing" } + type.any(:js, :xml) { render body: "Either JS or XML" } + end + end + end + + tests TestController + + def setup + # enable a logger so that (e.g.) the benchmarking stuff runs, so we can get + # a more accurate simulation of what happens in "real life". + super + @controller.logger = ActiveSupport::Logger.new(nil) + end + + def test_using_custom_render_option + ActionController.add_renderer :simon do |says, options| + self.content_type = Mime[:text] + self.response_body = "Simon says: #{says}" + end + + get :render_simon_says + assert_equal "Simon says: foo", @response.body + ensure + ActionController.remove_renderer :simon + end + + def test_raises_missing_template_no_renderer + assert_raise ActionView::MissingTemplate do + get :respond_to_mime, format: 'csv' + end + assert_equal Mime[:csv], @response.content_type + assert_equal "", @response.body + end + + def test_adding_csv_rendering_via_renderers_add + ActionController::Renderers.add :csv do |value, options| + send_data value.to_csv, type: Mime[:csv] + end + @request.accept = "text/csv" + get :respond_to_mime, format: 'csv' + assert_equal Mime[:csv], @response.content_type + assert_equal "c,s,v", @response.body + ensure + ActionController::Renderers.remove :csv + end +end diff --git a/actionpack/test/controller/request_forgery_protection_test.rb b/actionpack/test/controller/request_forgery_protection_test.rb index 94ffbe3cd0..f7dcbc1984 100644 --- a/actionpack/test/controller/request_forgery_protection_test.rb +++ b/actionpack/test/controller/request_forgery_protection_test.rb @@ -128,6 +128,27 @@ class CustomAuthenticityParamController < RequestForgeryProtectionControllerUsin end end +class PerFormTokensController < ActionController::Base + protect_from_forgery :with => :exception + self.per_form_csrf_tokens = true + + def index + render inline: "<%= form_tag (params[:form_path] || '/per_form_tokens/post_one'), method: params[:form_method] %>" + end + + def button_to + render inline: "<%= button_to 'Button', (params[:form_path] || '/per_form_tokens/post_one'), method: params[:form_method] %>" + end + + def post_one + render plain: '' + end + + def post_two + render plain: '' + end +end + # common test methods module RequestForgeryProtectionTests def setup @@ -304,6 +325,41 @@ module RequestForgeryProtectionTests assert_not_blocked { put :index } end + def test_should_allow_post_with_origin_checking_and_correct_origin + forgery_protection_origin_check do + session[:_csrf_token] = @token + @controller.stub :form_authenticity_token, @token do + assert_not_blocked do + @request.set_header 'HTTP_ORIGIN', 'http://test.host' + post :index, params: { custom_authenticity_token: @token } + end + end + end + end + + def test_should_allow_post_with_origin_checking_and_no_origin + forgery_protection_origin_check do + session[:_csrf_token] = @token + @controller.stub :form_authenticity_token, @token do + assert_not_blocked do + post :index, params: { custom_authenticity_token: @token } + end + end + end + end + + def test_should_block_post_with_origin_checking_and_wrong_origin + forgery_protection_origin_check do + session[:_csrf_token] = @token + @controller.stub :form_authenticity_token, @token do + assert_blocked do + @request.set_header 'HTTP_ORIGIN', 'http://bad.host' + post :index, params: { custom_authenticity_token: @token } + end + end + end + end + def test_should_warn_on_missing_csrf_token old_logger = ActionController::Base.logger logger = ActiveSupport::LogSubscriber::TestHelper::MockLogger.new @@ -405,6 +461,16 @@ module RequestForgeryProtectionTests def assert_cross_origin_not_blocked assert_not_blocked { yield } end + + def forgery_protection_origin_check + old_setting = ActionController::Base.forgery_protection_origin_check + ActionController::Base.forgery_protection_origin_check = true + begin + yield + ensure + ActionController::Base.forgery_protection_origin_check = old_setting + end + end end # OK let's get our test on @@ -495,10 +561,10 @@ class PrependProtectForgeryBaseControllerTest < ActionController::TestCase assert_equal(expected_callback_order, @controller.called_callbacks) end - def test_verify_authenticity_token_is_prepended_by_default + def test_verify_authenticity_token_is_not_prepended_by_default @controller = PrependDefaultController.new get :index - expected_callback_order = ["verify_authenticity_token", "custom_action"] + expected_callback_order = ["custom_action", "verify_authenticity_token"] assert_equal(expected_callback_order, @controller.called_callbacks) end end @@ -578,3 +644,180 @@ class CustomAuthenticityParamControllerTest < ActionController::TestCase end end end + +class PerFormTokensControllerTest < ActionController::TestCase + def test_per_form_token_is_same_size_as_global_token + get :index + expected = ActionController::RequestForgeryProtection::AUTHENTICITY_TOKEN_LENGTH + actual = @controller.send(:per_form_csrf_token, session, '/path', 'post').size + assert_equal expected, actual + end + + def test_accepts_token_for_correct_path_and_method + get :index + + form_token = assert_presence_and_fetch_form_csrf_token + + assert_matches_session_token_on_server form_token + + # This is required because PATH_INFO isn't reset between requests. + @request.env['PATH_INFO'] = '/per_form_tokens/post_one' + assert_nothing_raised do + post :post_one, params: {custom_authenticity_token: form_token} + end + assert_response :success + end + + def test_rejects_token_for_incorrect_path + get :index + + form_token = assert_presence_and_fetch_form_csrf_token + + assert_matches_session_token_on_server form_token + + # This is required because PATH_INFO isn't reset between requests. + @request.env['PATH_INFO'] = '/per_form_tokens/post_two' + assert_raises(ActionController::InvalidAuthenticityToken) do + post :post_two, params: {custom_authenticity_token: form_token} + end + end + + def test_rejects_token_for_incorrect_method + get :index + + form_token = assert_presence_and_fetch_form_csrf_token + + assert_matches_session_token_on_server form_token + + # This is required because PATH_INFO isn't reset between requests. + @request.env['PATH_INFO'] = '/per_form_tokens/post_one' + assert_raises(ActionController::InvalidAuthenticityToken) do + patch :post_one, params: {custom_authenticity_token: form_token} + end + end + + def test_rejects_token_for_incorrect_method_button_to + get :button_to, params: { form_method: 'delete' } + + form_token = assert_presence_and_fetch_form_csrf_token + + assert_matches_session_token_on_server form_token, 'delete' + + # This is required because PATH_INFO isn't reset between requests. + @request.env['PATH_INFO'] = '/per_form_tokens/post_one' + assert_raises(ActionController::InvalidAuthenticityToken) do + patch :post_one, params: { custom_authenticity_token: form_token } + end + end + + test "Accepts proper token for implicit post method on button_to tag" do + get :button_to + + form_token = assert_presence_and_fetch_form_csrf_token + + assert_matches_session_token_on_server form_token, 'post' + + # This is required because PATH_INFO isn't reset between requests. + @request.env['PATH_INFO'] = '/per_form_tokens/post_one' + assert_nothing_raised do + post :post_one, params: { custom_authenticity_token: form_token } + end + end + + %w{delete post patch}.each do |verb| + test "Accepts proper token for #{verb} method on button_to tag" do + get :button_to, params: { form_method: verb } + + form_token = assert_presence_and_fetch_form_csrf_token + + assert_matches_session_token_on_server form_token, verb + + # This is required because PATH_INFO isn't reset between requests. + @request.env['PATH_INFO'] = '/per_form_tokens/post_one' + assert_nothing_raised do + send verb, :post_one, params: { custom_authenticity_token: form_token } + end + end + end + + def test_accepts_global_csrf_token + get :index + + token = @controller.send(:form_authenticity_token) + + # This is required because PATH_INFO isn't reset between requests. + @request.env['PATH_INFO'] = '/per_form_tokens/post_one' + assert_nothing_raised do + post :post_one, params: {custom_authenticity_token: token} + end + assert_response :success + end + + def test_ignores_params + get :index, params: {form_path: '/per_form_tokens/post_one?foo=bar'} + + form_token = assert_presence_and_fetch_form_csrf_token + + assert_matches_session_token_on_server form_token + + # This is required because PATH_INFO isn't reset between requests. + @request.env['PATH_INFO'] = '/per_form_tokens/post_one?foo=baz' + assert_nothing_raised do + post :post_one, params: {custom_authenticity_token: form_token, baz: 'foo'} + end + assert_response :success + end + + def test_ignores_trailing_slash_during_generation + get :index, params: {form_path: '/per_form_tokens/post_one/'} + + form_token = assert_presence_and_fetch_form_csrf_token + + # This is required because PATH_INFO isn't reset between requests. + @request.env['PATH_INFO'] = '/per_form_tokens/post_one' + assert_nothing_raised do + post :post_one, params: {custom_authenticity_token: form_token} + end + assert_response :success + end + + def test_ignores_trailing_slash_during_validation + get :index + + form_token = assert_presence_and_fetch_form_csrf_token + + # This is required because PATH_INFO isn't reset between requests. + @request.env['PATH_INFO'] = '/per_form_tokens/post_one/' + assert_nothing_raised do + post :post_one, params: {custom_authenticity_token: form_token} + end + assert_response :success + end + + def test_method_is_case_insensitive + get :index, params: {form_method: "POST"} + + form_token = assert_presence_and_fetch_form_csrf_token + # This is required because PATH_INFO isn't reset between requests. + @request.env['PATH_INFO'] = '/per_form_tokens/post_one/' + assert_nothing_raised do + post :post_one, params: {custom_authenticity_token: form_token} + end + assert_response :success + end + + private + def assert_presence_and_fetch_form_csrf_token + assert_select 'input[name="custom_authenticity_token"]' do |input| + form_csrf_token = input.first['value'] + assert_not_nil form_csrf_token + return form_csrf_token + end + end + + def assert_matches_session_token_on_server(form_token, method = 'post') + actual = @controller.send(:unmask_token, Base64.strict_decode64(form_token)) + expected = @controller.send(:per_form_csrf_token, session, '/per_form_tokens/post_one', method) + assert_equal expected, actual + end +end diff --git a/actionpack/test/controller/required_params_test.rb b/actionpack/test/controller/required_params_test.rb index 168f64ce41..b6efcd6f9a 100644 --- a/actionpack/test/controller/required_params_test.rb +++ b/actionpack/test/controller/required_params_test.rb @@ -65,4 +65,17 @@ class ParametersRequireTest < ActiveSupport::TestCase .require([:first_name, :title]) end end + + test "value params" do + params = ActionController::Parameters.new(foo: "bar", dog: "cinco") + assert_equal ["bar", "cinco"], params.values + assert params.has_value?("cinco") + assert params.value?("cinco") + end + + test "Deprecated methods are deprecated" do + assert_deprecated do + ActionController::Parameters.new(foo: "bar").merge!({bar: "foo"}) + end + end end diff --git a/actionpack/test/controller/routing_test.rb b/actionpack/test/controller/routing_test.rb index 4a2b02a003..a39fede5b9 100644 --- a/actionpack/test/controller/routing_test.rb +++ b/actionpack/test/controller/routing_test.rb @@ -289,12 +289,6 @@ class LegacyRouteSetTests < ActiveSupport::TestCase assert_equal({:id=>"1", :filters=>"foo", :format=>"js"}, params) end - def test_draw_with_block_arity_one_raises - assert_raise(RuntimeError) do - rs.draw { |map| map.match '/:controller(/:action(/:id))' } - end - end - def test_specific_controller_action_failure rs.draw do mount lambda {} => "/foo" diff --git a/actionpack/test/controller/test_case_test.rb b/actionpack/test/controller/test_case_test.rb index e50373a0cc..0c1393548e 100644 --- a/actionpack/test/controller/test_case_test.rb +++ b/actionpack/test/controller/test_case_test.rb @@ -137,6 +137,10 @@ XML head :created, location: 'created resource' end + def render_cookie + render plain: cookies["foo"] + end + def delete_cookie cookies.delete("foo") render plain: 'ok' @@ -172,7 +176,7 @@ XML before_action { @dynamic_opt = 'opt' } def test_url_options_reset - render plain: url_for(params) + render plain: url_for end def default_url_options @@ -829,6 +833,12 @@ XML assert_equal 'bar', cookies['foo'] end + def test_cookies_should_be_escaped_properly + cookies['foo'] = '+' + get :render_cookie + assert_equal '+', @response.body + end + def test_should_detect_if_cookie_is_deleted cookies['foo'] = 'bar' get :delete_cookie diff --git a/actionpack/test/controller/url_for_test.rb b/actionpack/test/controller/url_for_test.rb index 78e883f134..67212fea38 100644 --- a/actionpack/test/controller/url_for_test.rb +++ b/actionpack/test/controller/url_for_test.rb @@ -375,6 +375,13 @@ module AbstractController assert_equal({'query[person][position][]' => 'prof' }.to_query, params[3]) end + def test_url_action_controller_parameters + add_host! + assert_raise(ArgumentError) do + W.new.url_for(ActionController::Parameters.new(:controller => 'c', :action => 'a', protocol: 'javascript', f: '%0Aeval(name)')) + end + end + def test_path_generation_for_symbol_parameter_keys assert_generates("/image", :controller=> :image) end diff --git a/actionpack/test/controller/webservice_test.rb b/actionpack/test/controller/webservice_test.rb index 6d377c4691..daf17558aa 100644 --- a/actionpack/test/controller/webservice_test.rb +++ b/actionpack/test/controller/webservice_test.rb @@ -99,7 +99,7 @@ class WebServiceTest < ActionDispatch::IntegrationTest def test_parsing_json_doesnot_rescue_exception req = Class.new(ActionDispatch::Request) do def params_parsers - { Mime[:json] => Proc.new { |data| raise Interrupt } } + { json: Proc.new { |data| raise Interrupt } } end def content_length; get_header('rack.input').length; end diff --git a/actionpack/test/dispatch/cookies_test.rb b/actionpack/test/dispatch/cookies_test.rb index 84c244c72a..dfcef14344 100644 --- a/actionpack/test/dispatch/cookies_test.rb +++ b/actionpack/test/dispatch/cookies_test.rb @@ -7,7 +7,7 @@ class CookieJarTest < ActiveSupport::TestCase attr_reader :request def setup - @request = ActionDispatch::Request.new({}) + @request = ActionDispatch::Request.empty end def test_fetch diff --git a/actionpack/test/dispatch/debug_exceptions_test.rb b/actionpack/test/dispatch/debug_exceptions_test.rb index 89c3e75a50..159bf10545 100644 --- a/actionpack/test/dispatch/debug_exceptions_test.rb +++ b/actionpack/test/dispatch/debug_exceptions_test.rb @@ -75,6 +75,13 @@ class DebugExceptionsTest < ActionDispatch::IntegrationTest end end + class BoomerAPI < Boomer + def call(env) + env['action_dispatch.show_detailed_exceptions'] = @detailed + raise "puke!" + end + end + RoutesApp = Struct.new(:routes).new(SharedTestRoutes) ProductionApp = ActionDispatch::DebugExceptions.new(Boomer.new(false), RoutesApp) DevelopmentApp = ActionDispatch::DebugExceptions.new(Boomer.new(true), RoutesApp) @@ -166,6 +173,14 @@ class DebugExceptionsTest < ActionDispatch::IntegrationTest assert_equal "text/plain", response.content_type assert_match(/RuntimeError\npuke/, body) + Rails.stub :root, Pathname.new('.') do + get "/", headers: xhr_request_env + + assert_response 500 + assert_match 'Extracted source (around line #', body + assert_select 'pre', { count: 0 }, body + end + get "/not_found", headers: xhr_request_env assert_response 404 assert_no_match(/<body>/, body) @@ -197,6 +212,68 @@ class DebugExceptionsTest < ActionDispatch::IntegrationTest assert_match(/ActionController::ParameterMissing/, body) end + test "rescue with json error for API request" do + @app = ActionDispatch::DebugExceptions.new(Boomer.new(true), RoutesApp, :api) + + get "/", headers: { 'action_dispatch.show_exceptions' => true } + assert_response 500 + assert_no_match(/<header>/, body) + assert_no_match(/<body>/, body) + assert_equal "application/json", response.content_type + assert_match(/RuntimeError: puke/, body) + + get "/not_found", headers: { 'action_dispatch.show_exceptions' => true } + assert_response 404 + assert_no_match(/<body>/, body) + assert_equal "application/json", response.content_type + assert_match(/#{AbstractController::ActionNotFound.name}/, body) + + get "/method_not_allowed", headers: { 'action_dispatch.show_exceptions' => true } + assert_response 405 + assert_no_match(/<body>/, body) + assert_equal "application/json", response.content_type + assert_match(/ActionController::MethodNotAllowed/, body) + + get "/unknown_http_method", headers: { 'action_dispatch.show_exceptions' => true } + assert_response 405 + assert_no_match(/<body>/, body) + assert_equal "application/json", response.content_type + assert_match(/ActionController::UnknownHttpMethod/, body) + + get "/bad_request", headers: { 'action_dispatch.show_exceptions' => true } + assert_response 400 + assert_no_match(/<body>/, body) + assert_equal "application/json", response.content_type + assert_match(/ActionController::BadRequest/, body) + + get "/parameter_missing", headers: { 'action_dispatch.show_exceptions' => true } + assert_response 400 + assert_no_match(/<body>/, body) + assert_equal "application/json", response.content_type + assert_match(/ActionController::ParameterMissing/, body) + end + + test "rescue with json on API request returns only allowed formats or json as a fallback" do + @app = ActionDispatch::DebugExceptions.new(Boomer.new(true), RoutesApp, :api) + + get "/index.json", headers: { 'action_dispatch.show_exceptions' => true } + assert_response 500 + assert_equal "application/json", response.content_type + assert_match(/RuntimeError: puke/, body) + + get "/index.html", headers: { 'action_dispatch.show_exceptions' => true } + assert_response 500 + assert_no_match(/<header>/, body) + assert_no_match(/<body>/, body) + assert_equal "application/json", response.content_type + assert_match(/RuntimeError: puke/, body) + + get "/index.xml", headers: { 'action_dispatch.show_exceptions' => true } + assert_response 500 + assert_equal "application/xml", response.content_type + assert_match(/RuntimeError: puke/, body) + end + test "does not show filtered parameters" do @app = DevelopmentApp @@ -364,7 +441,7 @@ class DebugExceptionsTest < ActionDispatch::IntegrationTest assert_select 'pre code a:first', %r{test/dispatch/debug_exceptions_test\.rb:\d+:in `call} end - # assert framework trace that that threw the error is first + # assert framework trace that threw the error is first assert_select '#Framework-Trace' do assert_select 'pre code a:first', /method_that_raises/ end diff --git a/actionpack/test/dispatch/live_response_test.rb b/actionpack/test/dispatch/live_response_test.rb index 55becc1c91..e4475f4233 100644 --- a/actionpack/test/dispatch/live_response_test.rb +++ b/actionpack/test/dispatch/live_response_test.rb @@ -1,12 +1,12 @@ require 'abstract_unit' -require 'concurrent/atomics' +require 'concurrent/atomic/count_down_latch' module ActionController module Live class ResponseTest < ActiveSupport::TestCase def setup @response = Live::Response.new - @response.request = ActionDispatch::Request.new({}) #yolo + @response.request = ActionDispatch::Request.empty end def test_header_merge diff --git a/actionpack/test/dispatch/mapper_test.rb b/actionpack/test/dispatch/mapper_test.rb index e783df855e..df27e41997 100644 --- a/actionpack/test/dispatch/mapper_test.rb +++ b/actionpack/test/dispatch/mapper_test.rb @@ -158,7 +158,7 @@ module ActionDispatch assert_equal '/*path.:format', fakeset.asts.first.to_s end - def test_raising_helpful_error_on_invalid_arguments + def test_raising_error_when_path_is_not_passed fakeset = FakeSet.new mapper = Mapper.new fakeset app = lambda { |env| [200, {}, [""]] } @@ -166,6 +166,18 @@ module ActionDispatch mapper.mount app end end + + def test_raising_error_when_rack_app_is_not_passed + fakeset = FakeSet.new + mapper = Mapper.new fakeset + assert_raises ArgumentError do + mapper.mount 10, as: "exciting" + end + + assert_raises ArgumentError do + mapper.mount as: "exciting" + end + end end end end diff --git a/actionpack/test/dispatch/middleware_stack_test.rb b/actionpack/test/dispatch/middleware_stack_test.rb index 33aa616474..a8c8e0784f 100644 --- a/actionpack/test/dispatch/middleware_stack_test.rb +++ b/actionpack/test/dispatch/middleware_stack_test.rb @@ -131,4 +131,20 @@ class MiddlewareStackTest < ActiveSupport::TestCase assert_equal BazMiddleware, @stack.last.klass end end + + test "can check if Middleware are equal - Class" do + assert_equal @stack.last, BarMiddleware + end + + test "includes a class" do + assert_equal true, @stack.include?(BarMiddleware) + end + + test "can check if Middleware are equal - Middleware" do + assert_equal @stack.last, @stack.last + end + + test "includes a middleware" do + assert_equal true, @stack.include?(ActionDispatch::MiddlewareStack::Middleware.new(BarMiddleware, nil, nil)) + end end diff --git a/actionpack/test/dispatch/mime_type_test.rb b/actionpack/test/dispatch/mime_type_test.rb index 149e37bf3d..672b272590 100644 --- a/actionpack/test/dispatch/mime_type_test.rb +++ b/actionpack/test/dispatch/mime_type_test.rb @@ -49,7 +49,7 @@ class MimeTypeTest < ActiveSupport::TestCase test "parse application with trailing star" do accept = "application/*" - expect = [Mime[:html], Mime[:js], Mime[:xml], Mime[:rss], Mime[:atom], Mime[:yaml], Mime[:url_encoded_form], Mime[:json], Mime[:pdf], Mime[:zip]] + expect = [Mime[:html], Mime[:js], Mime[:xml], Mime[:rss], Mime[:atom], Mime[:yaml], Mime[:url_encoded_form], Mime[:json], Mime[:pdf], Mime[:zip], Mime[:gzip]] parsed = Mime::Type.parse(accept) assert_equal expect, parsed end diff --git a/actionpack/test/dispatch/request/json_params_parsing_test.rb b/actionpack/test/dispatch/request/json_params_parsing_test.rb index a3992ad008..3655c7f570 100644 --- a/actionpack/test/dispatch/request/json_params_parsing_test.rb +++ b/actionpack/test/dispatch/request/json_params_parsing_test.rb @@ -37,9 +37,9 @@ class JsonParamsParsingTest < ActionDispatch::IntegrationTest ) end - test "parses json params for application/vnd.api+json" do + test "does not parse unregistered media types such as application/vnd.api+json" do assert_parses( - {"person" => {"name" => "David"}}, + {}, "{\"person\": {\"name\": \"David\"}}", { 'CONTENT_TYPE' => 'application/vnd.api+json' } ) end @@ -143,13 +143,6 @@ class RootLessJSONParamsParsingTest < ActionDispatch::IntegrationTest ) end - test "parses json params for application/vnd.api+json" do - assert_parses( - {"user" => {"username" => "sikachu"}, "username" => "sikachu"}, - "{\"username\": \"sikachu\"}", { 'CONTENT_TYPE' => 'application/vnd.api+json' } - ) - end - test "parses json with non-object JSON content" do assert_parses( {"user" => {"_json" => "string content" }, "_json" => "string content" }, @@ -157,6 +150,34 @@ class RootLessJSONParamsParsingTest < ActionDispatch::IntegrationTest ) end + test "parses json params after custom json mime type registered" do + begin + Mime::Type.unregister :json + Mime::Type.register "application/json", :json, %w(application/vnd.api+json) + assert_parses( + {"user" => {"username" => "meinac"}, "username" => "meinac"}, + "{\"username\": \"meinac\"}", { 'CONTENT_TYPE' => 'application/json' } + ) + ensure + Mime::Type.unregister :json + Mime::Type.register "application/json", :json, %w( text/x-json application/jsonrequest ) + end + end + + test "parses json params after custom json mime type registered with synonym" do + begin + Mime::Type.unregister :json + Mime::Type.register "application/json", :json, %w(application/vnd.api+json) + assert_parses( + {"user" => {"username" => "meinac"}, "username" => "meinac"}, + "{\"username\": \"meinac\"}", { 'CONTENT_TYPE' => 'application/vnd.api+json' } + ) + ensure + Mime::Type.unregister :json + Mime::Type.register "application/json", :json, %w( text/x-json application/jsonrequest ) + end + end + private def assert_parses(expected, actual, headers = {}) with_test_routing(UsersController) do diff --git a/actionpack/test/dispatch/request/session_test.rb b/actionpack/test/dispatch/request/session_test.rb index ae0e7e93ed..7dcbcc5c21 100644 --- a/actionpack/test/dispatch/request/session_test.rb +++ b/actionpack/test/dispatch/request/session_test.rb @@ -7,7 +7,7 @@ module ActionDispatch attr_reader :req def setup - @req = ActionDispatch::Request.new({}) + @req = ActionDispatch::Request.empty end def test_create_adds_itself_to_env diff --git a/actionpack/test/dispatch/request_test.rb b/actionpack/test/dispatch/request_test.rb index 22240699d9..0edad72fd9 100644 --- a/actionpack/test/dispatch/request_test.rb +++ b/actionpack/test/dispatch/request_test.rb @@ -4,9 +4,13 @@ class BaseRequestTest < ActiveSupport::TestCase def setup @env = { :ip_spoofing_check => true, - :tld_length => 1, "rack.input" => "foo" } + @original_tld_length = ActionDispatch::Http::URL.tld_length + end + + def teardown + ActionDispatch::Http::URL.tld_length = @original_tld_length end def url_for(options = {}) @@ -19,9 +23,9 @@ class BaseRequestTest < ActiveSupport::TestCase ip_spoofing_check = env.key?(:ip_spoofing_check) ? env.delete(:ip_spoofing_check) : true @trusted_proxies ||= nil ip_app = ActionDispatch::RemoteIp.new(Proc.new { }, ip_spoofing_check, @trusted_proxies) - tld_length = env.key?(:tld_length) ? env.delete(:tld_length) : 1 + ActionDispatch::Http::URL.tld_length = env.delete(:tld_length) if env.key?(:tld_length) + ip_app.call(env) - ActionDispatch::Http::URL.tld_length = tld_length env = @env.merge(env) ActionDispatch::Request.new(env) @@ -254,15 +258,6 @@ end class RequestDomain < BaseRequestTest test "domains" do - request = stub_request 'HTTP_HOST' => 'www.rubyonrails.org' - assert_equal "rubyonrails.org", request.domain - - request = stub_request 'HTTP_HOST' => "www.rubyonrails.co.uk" - assert_equal "rubyonrails.co.uk", request.domain(2) - - request = stub_request 'HTTP_HOST' => "www.rubyonrails.co.uk", :tld_length => 2 - assert_equal "rubyonrails.co.uk", request.domain - request = stub_request 'HTTP_HOST' => "192.168.1.200" assert_nil request.domain @@ -271,25 +266,18 @@ class RequestDomain < BaseRequestTest request = stub_request 'HTTP_HOST' => "192.168.1.200.com" assert_equal "200.com", request.domain - end - test "subdomains" do - request = stub_request 'HTTP_HOST' => "www.rubyonrails.org" - assert_equal %w( www ), request.subdomains - assert_equal "www", request.subdomain + request = stub_request 'HTTP_HOST' => 'www.rubyonrails.org' + assert_equal "rubyonrails.org", request.domain request = stub_request 'HTTP_HOST' => "www.rubyonrails.co.uk" - assert_equal %w( www ), request.subdomains(2) - assert_equal "www", request.subdomain(2) - - request = stub_request 'HTTP_HOST' => "dev.www.rubyonrails.co.uk" - assert_equal %w( dev www ), request.subdomains(2) - assert_equal "dev.www", request.subdomain(2) + assert_equal "rubyonrails.co.uk", request.domain(2) - request = stub_request 'HTTP_HOST' => "dev.www.rubyonrails.co.uk", :tld_length => 2 - assert_equal %w( dev www ), request.subdomains - assert_equal "dev.www", request.subdomain + request = stub_request 'HTTP_HOST' => "www.rubyonrails.co.uk", :tld_length => 2 + assert_equal "rubyonrails.co.uk", request.domain + end + test "subdomains" do request = stub_request 'HTTP_HOST' => "foobar.foobar.com" assert_equal %w( foobar ), request.subdomains assert_equal "foobar", request.subdomain @@ -309,6 +297,22 @@ class RequestDomain < BaseRequestTest request = stub_request 'HTTP_HOST' => nil assert_equal [], request.subdomains assert_equal "", request.subdomain + + request = stub_request 'HTTP_HOST' => "www.rubyonrails.org" + assert_equal %w( www ), request.subdomains + assert_equal "www", request.subdomain + + request = stub_request 'HTTP_HOST' => "www.rubyonrails.co.uk" + assert_equal %w( www ), request.subdomains(2) + assert_equal "www", request.subdomain(2) + + request = stub_request 'HTTP_HOST' => "dev.www.rubyonrails.co.uk" + assert_equal %w( dev www ), request.subdomains(2) + assert_equal "dev.www", request.subdomain(2) + + request = stub_request 'HTTP_HOST' => "dev.www.rubyonrails.co.uk", :tld_length => 2 + assert_equal %w( dev www ), request.subdomains + assert_equal "dev.www", request.subdomain end end @@ -893,6 +897,27 @@ class RequestFormat < BaseRequestTest ActionDispatch::Request.ignore_accept_header = old_ignore_accept_header end end + + test "format taken from the path extension" do + request = stub_request 'PATH_INFO' => '/foo.xml' + assert_called(request, :parameters, times: 1, returns: {}) do + assert_equal [Mime[:xml]], request.formats + end + + request = stub_request 'PATH_INFO' => '/foo.123' + assert_called(request, :parameters, times: 1, returns: {}) do + assert_equal [Mime[:html]], request.formats + end + end + + test "formats from accept headers have higher precedence than path extension" do + request = stub_request 'HTTP_ACCEPT' => 'application/json', + 'PATH_INFO' => '/foo.xml' + + assert_called(request, :parameters, times: 1, returns: {}) do + assert_equal [Mime[:json]], request.formats + end + end end class RequestMimeType < BaseRequestTest @@ -1212,3 +1237,23 @@ class RequestVariant < BaseRequestTest end end end + +class RequestFormData < BaseRequestTest + test 'media_type is from the FORM_DATA_MEDIA_TYPES array' do + assert stub_request('CONTENT_TYPE' => 'application/x-www-form-urlencoded').form_data? + assert stub_request('CONTENT_TYPE' => 'multipart/form-data').form_data? + end + + test 'media_type is not from the FORM_DATA_MEDIA_TYPES array' do + assert !stub_request('CONTENT_TYPE' => 'application/xml').form_data? + assert !stub_request('CONTENT_TYPE' => 'multipart/related').form_data? + end + + test 'no Content-Type header is provided and the request_method is POST' do + request = stub_request('REQUEST_METHOD' => 'POST') + + assert_equal '', request.media_type + assert_equal 'POST', request.request_method + assert !request.form_data? + end +end diff --git a/actionpack/test/dispatch/response_test.rb b/actionpack/test/dispatch/response_test.rb index 981d820ccf..cd385982d9 100644 --- a/actionpack/test/dispatch/response_test.rb +++ b/actionpack/test/dispatch/response_test.rb @@ -5,6 +5,7 @@ require 'rack/content_length' class ResponseTest < ActiveSupport::TestCase def setup @response = ActionDispatch::Response.create + @response.request = ActionDispatch::Request.empty end def test_can_wait_until_commit @@ -36,9 +37,31 @@ class ResponseTest < ActiveSupport::TestCase assert_equal "closed stream", e.message end + def test_read_body_during_action + @response.body = "Hello, World!" + + # even though there's no explicitly set content-type, + assert_equal nil, @response.content_type + + # after the action reads back @response.body, + assert_equal "Hello, World!", @response.body + + # the response can be built. + status, headers, body = @response.to_a + assert_equal 200, status + assert_equal({ + "Content-Type" => "text/html; charset=utf-8" + }, headers) + + parts = [] + body.each { |part| parts << part } + assert_equal ["Hello, World!"], parts + end + def test_response_body_encoding body = ["hello".encode(Encoding::UTF_8)] response = ActionDispatch::Response.new 200, {}, body + response.request = ActionDispatch::Request.empty assert_equal Encoding::UTF_8, response.body.encoding end @@ -174,11 +197,11 @@ class ResponseTest < ActiveSupport::TestCase } resp.to_a - assert_equal('"202cb962ac59075b964b07152d234b70"', resp.etag) + assert_equal('W/"202cb962ac59075b964b07152d234b70"', resp.etag) assert_equal({:public => true}, resp.cache_control) assert_equal('public', resp.headers['Cache-Control']) - assert_equal('"202cb962ac59075b964b07152d234b70"', resp.headers['ETag']) + assert_equal('W/"202cb962ac59075b964b07152d234b70"', resp.headers['ETag']) end test "read charset and content type" do @@ -261,6 +284,7 @@ class ResponseTest < ActiveSupport::TestCase test "can be explicitly destructured into status, headers and an enumerable body" do response = ActionDispatch::Response.new(404, { 'Content-Type' => 'text/plain' }, ['Not Found']) + response.request = ActionDispatch::Request.empty status, headers, body = *response assert_equal 404, status @@ -356,6 +380,7 @@ class ResponseIntegrationTest < ActionDispatch::IntegrationTest resp.cache_control[:public] = true resp.etag = '123' resp.body = 'Hello' + resp.request = ActionDispatch::Request.empty }.to_a } @@ -363,16 +388,16 @@ class ResponseIntegrationTest < ActionDispatch::IntegrationTest assert_response :success assert_equal('public', @response.headers['Cache-Control']) - assert_equal('"202cb962ac59075b964b07152d234b70"', @response.headers['ETag']) + assert_equal('W/"202cb962ac59075b964b07152d234b70"', @response.headers['ETag']) - assert_equal('"202cb962ac59075b964b07152d234b70"', @response.etag) + assert_equal('W/"202cb962ac59075b964b07152d234b70"', @response.etag) assert_equal({:public => true}, @response.cache_control) end test "response cache control from rackish app" do @app = lambda { |env| [200, - {'ETag' => '"202cb962ac59075b964b07152d234b70"', + {'ETag' => 'W/"202cb962ac59075b964b07152d234b70"', 'Cache-Control' => 'public'}, ['Hello']] } @@ -380,9 +405,9 @@ class ResponseIntegrationTest < ActionDispatch::IntegrationTest assert_response :success assert_equal('public', @response.headers['Cache-Control']) - assert_equal('"202cb962ac59075b964b07152d234b70"', @response.headers['ETag']) + assert_equal('W/"202cb962ac59075b964b07152d234b70"', @response.headers['ETag']) - assert_equal('"202cb962ac59075b964b07152d234b70"', @response.etag) + assert_equal('W/"202cb962ac59075b964b07152d234b70"', @response.etag) assert_equal({:public => true}, @response.cache_control) end @@ -392,6 +417,7 @@ class ResponseIntegrationTest < ActionDispatch::IntegrationTest resp.charset = 'utf-16' resp.content_type = Mime[:xml] resp.body = 'Hello' + resp.request = ActionDispatch::Request.empty }.to_a } @@ -419,4 +445,12 @@ class ResponseIntegrationTest < ActionDispatch::IntegrationTest assert_equal('application/xml; charset=utf-16', @response.headers['Content-Type']) end + + test "we can set strong ETag by directly adding it as header" do + @response = ActionDispatch::Response.create + @response.add_header "ETag", '"202cb962ac59075b964b07152d234b70"' + + assert_equal('"202cb962ac59075b964b07152d234b70"', @response.etag) + assert_equal('"202cb962ac59075b964b07152d234b70"', @response.headers['ETag']) + end end diff --git a/actionpack/test/dispatch/routing/inspector_test.rb b/actionpack/test/dispatch/routing/inspector_test.rb index a17d07c40b..fd85cc6e9f 100644 --- a/actionpack/test/dispatch/routing/inspector_test.rb +++ b/actionpack/test/dispatch/routing/inspector_test.rb @@ -7,6 +7,9 @@ class MountedRackApp end end +class Rails::DummyController +end + module ActionDispatch module Routing class RoutesInspectorTest < ActiveSupport::TestCase @@ -14,10 +17,10 @@ module ActionDispatch @set = ActionDispatch::Routing::RouteSet.new end - def draw(options = {}, &block) + def draw(options = nil, &block) @set.draw(&block) inspector = ActionDispatch::Routing::RoutesInspector.new(@set.routes) - inspector.format(ActionDispatch::Routing::ConsoleFormatter.new, options[:filter]).split("\n") + inspector.format(ActionDispatch::Routing::ConsoleFormatter.new, options).split("\n") end def test_displaying_routes_for_engines @@ -294,7 +297,7 @@ module ActionDispatch end def test_routes_can_be_filtered - output = draw(filter: 'posts') do + output = draw('posts') do resources :articles resources :posts end @@ -310,6 +313,26 @@ module ActionDispatch " DELETE /posts/:id(.:format) posts#destroy"], output end + def test_routes_can_be_filtered_with_namespaced_controllers + output = draw('admin/posts') do + resources :articles + namespace :admin do + resources :posts + end + end + + assert_equal [" Prefix Verb URI Pattern Controller#Action", + " admin_posts GET /admin/posts(.:format) admin/posts#index", + " POST /admin/posts(.:format) admin/posts#create", + " new_admin_post GET /admin/posts/new(.:format) admin/posts#new", + "edit_admin_post GET /admin/posts/:id/edit(.:format) admin/posts#edit", + " admin_post GET /admin/posts/:id(.:format) admin/posts#show", + " PATCH /admin/posts/:id(.:format) admin/posts#update", + " PUT /admin/posts/:id(.:format) admin/posts#update", + " DELETE /admin/posts/:id(.:format) admin/posts#destroy"], output + end + + def test_regression_route_with_controller_regexp output = draw do get ':controller(/:action)', controller: /api\/[^\/]+/, format: false @@ -331,6 +354,64 @@ module ActionDispatch " cart GET /cart(.:format) cart#show" ], output end + + def test_routes_with_undefined_filter + output = draw(controller: 'Rails::MissingController') do + get 'photos/:id' => 'photos#show', :id => /[A-Z]\d{5}/ + end + + assert_equal [ + "No routes were found for this controller", + "For more information about routes, see the Rails guide: http://guides.rubyonrails.org/routing.html." + ], output + end + + def test_no_routes_matched_filter + output = draw('rails/dummy') do + get 'photos/:id' => 'photos#show', :id => /[A-Z]\d{5}/ + end + + assert_equal [ + "No routes were found for this controller", + "For more information about routes, see the Rails guide: http://guides.rubyonrails.org/routing.html." + ], output + end + + def test_no_routes_were_defined + output = draw('Rails::DummyController') {} + + assert_equal [ + "You don't have any routes defined!", + "", + "Please add some routes in config/routes.rb.", + "", + "For more information about routes, see the Rails guide: http://guides.rubyonrails.org/routing.html." + ], output + end + + def test_displaying_routes_for_internal_engines + engine = Class.new(Rails::Engine) do + def self.inspect + "Blog::Engine" + end + end + engine.routes.draw do + get '/cart', to: 'cart#show' + post '/cart', to: 'cart#create' + patch '/cart', to: 'cart#update' + end + + output = draw do + get '/custom/assets', to: 'custom_assets#show' + mount engine => "/blog", as: "blog", internal: true + end + + assert_equal [ + " Prefix Verb URI Pattern Controller#Action", + "custom_assets GET /custom/assets(.:format) custom_assets#show", + ], output + end + end end end diff --git a/actionpack/test/dispatch/routing_test.rb b/actionpack/test/dispatch/routing_test.rb index 8972f3e74d..5ead9357ae 100644 --- a/actionpack/test/dispatch/routing_test.rb +++ b/actionpack/test/dispatch/routing_test.rb @@ -3578,6 +3578,27 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest assert_equal 'HEAD', @response.body end + def test_passing_action_parameters_to_url_helpers_raises_error_if_parameters_are_not_permitted + draw do + root :to => 'projects#index' + end + params = ActionController::Parameters.new(id: '1') + + assert_raises ArgumentError do + root_path(params) + end + end + + def test_passing_action_parameters_to_url_helpers_is_allowed_if_parameters_are_permitted + draw do + root :to => 'projects#index' + end + params = ActionController::Parameters.new(id: '1') + params.permit! + + assert_equal '/?id=1', root_path(params) + end + private def draw(&block) @@ -4592,3 +4613,107 @@ class TestDefaultUrlOptions < ActionDispatch::IntegrationTest assert_equal '/en/posts/2014/12/13', archived_posts_path(2014, 12, 13) end end + +class TestErrorsInController < ActionDispatch::IntegrationTest + class ::PostsController < ActionController::Base + def foo + nil.i_do_not_exist + end + + def bar + NonExistingClass.new + end + end + + Routes = ActionDispatch::Routing::RouteSet.new + Routes.draw do + get '/:controller(/:action)' + end + + APP = build_app Routes + + def app + APP + end + + def test_legit_no_method_errors_are_not_caught + get '/posts/foo' + assert_equal 500, response.status + end + + def test_legit_name_errors_are_not_caught + get '/posts/bar' + assert_equal 500, response.status + end + + def test_legit_routing_not_found_responses + get '/posts/baz' + assert_equal 404, response.status + + get '/i_do_not_exist' + assert_equal 404, response.status + end +end + +class TestPartialDynamicPathSegments < ActionDispatch::IntegrationTest + Routes = ActionDispatch::Routing::RouteSet.new + Routes.draw do + ok = lambda { |env| [200, { 'Content-Type' => 'text/plain' }, []] } + + get '/songs/song-:song', to: ok + get '/songs/:song-song', to: ok + get '/:artist/song-:song', to: ok + get '/:artist/:song-song', to: ok + + get '/optional/songs(/song-:song)', to: ok + get '/optional/songs(/:song-song)', to: ok + get '/optional/:artist(/song-:song)', to: ok + get '/optional/:artist(/:song-song)', to: ok + end + + APP = build_app Routes + + def app + APP + end + + def test_paths_with_partial_dynamic_segments_are_recognised + get '/david-bowie/changes-song' + assert_equal 200, response.status + assert_params artist: 'david-bowie', song: 'changes' + + get '/david-bowie/song-changes' + assert_equal 200, response.status + assert_params artist: 'david-bowie', song: 'changes' + + get '/songs/song-changes' + assert_equal 200, response.status + assert_params song: 'changes' + + get '/songs/changes-song' + assert_equal 200, response.status + assert_params song: 'changes' + + get '/optional/songs/song-changes' + assert_equal 200, response.status + assert_params song: 'changes' + + get '/optional/songs/changes-song' + assert_equal 200, response.status + assert_params song: 'changes' + + get '/optional/david-bowie/changes-song' + assert_equal 200, response.status + assert_params artist: 'david-bowie', song: 'changes' + + get '/optional/david-bowie/song-changes' + assert_equal 200, response.status + assert_params artist: 'david-bowie', song: 'changes' + end + + private + + def assert_params(params) + assert_equal(params, request.path_parameters) + end +end diff --git a/actionpack/test/dispatch/show_exceptions_test.rb b/actionpack/test/dispatch/show_exceptions_test.rb index ffdf775836..14894d4b82 100644 --- a/actionpack/test/dispatch/show_exceptions_test.rb +++ b/actionpack/test/dispatch/show_exceptions_test.rb @@ -8,7 +8,7 @@ class ShowExceptionsTest < ActionDispatch::IntegrationTest case req.path when "/not_found" raise AbstractController::ActionNotFound - when "/bad_params" + when "/bad_params", "/bad_params.json" begin raise StandardError.new rescue @@ -120,4 +120,18 @@ class ShowExceptionsTest < ActionDispatch::IntegrationTest assert_response 405 assert_equal "", body end + + test "bad params exception is returned in the correct format" do + @app = ProductionApp + + get "/bad_params", headers: { 'action_dispatch.show_exceptions' => true } + assert_equal "text/html; charset=utf-8", response.headers["Content-Type"] + assert_response 400 + assert_match(/400 error/, body) + + get "/bad_params.json", headers: { 'action_dispatch.show_exceptions' => true } + assert_equal "application/json; charset=utf-8", response.headers["Content-Type"] + assert_response 400 + assert_equal("{\"status\":400,\"error\":\"Bad Request\"}", body) + end end diff --git a/actionpack/test/dispatch/ssl_test.rb b/actionpack/test/dispatch/ssl_test.rb index 7a5b8393dc..bb2125e485 100644 --- a/actionpack/test/dispatch/ssl_test.rb +++ b/actionpack/test/dispatch/ssl_test.rb @@ -7,30 +7,43 @@ class SSLTest < ActionDispatch::IntegrationTest def build_app(headers: {}, ssl_options: {}) headers = HEADERS.merge(headers) - ActionDispatch::SSL.new lambda { |env| [200, headers, []] }, ssl_options + ActionDispatch::SSL.new lambda { |env| [200, headers, []] }, ssl_options.reverse_merge(hsts: { subdomains: true }) end end class RedirectSSLTest < SSLTest - def assert_not_redirected(url, headers: {}) - self.app = build_app + + def assert_not_redirected(url, headers: {}, redirect: {}, deprecated_host: nil, + deprecated_port: nil) + + self.app = build_app ssl_options: { redirect: redirect, + host: deprecated_host, port: deprecated_port + } + get url, headers: headers assert_response :ok end - def assert_redirected(host: nil, port: nil, status: 301, body: [], - deprecated_host: nil, deprecated_port: nil, + def assert_redirected(redirect: {}, deprecated_host: nil, deprecated_port: nil, from: 'http://a/b?c=d', to: from.sub('http', 'https')) - self.app = build_app ssl_options: { - redirect: { host: host, port: port, status: status, body: body }, + redirect = { status: 301, body: [] }.merge(redirect) + + self.app = build_app ssl_options: { redirect: redirect, host: deprecated_host, port: deprecated_port } get from - assert_response status + assert_response redirect[:status] || 301 assert_redirected_to to - assert_equal body.join, @response.body + assert_equal redirect[:body].join, @response.body + end + + test 'constrain to can avoid redirect' do + constraining = { constrain_to: -> request { request.path !~ /healthcheck/ } } + + assert_not_redirected 'http://example.org/healthcheck', redirect: constraining + assert_redirected from: 'http://example.org/', redirect: constraining end test 'https is not redirected' do @@ -46,31 +59,31 @@ class RedirectSSLTest < SSLTest end test 'redirect with non-301 status' do - assert_redirected status: 307 + assert_redirected redirect: { status: 307 } end test 'redirect with custom body' do - assert_redirected body: ['foo'] + assert_redirected redirect: { body: ['foo'] } end test 'redirect to specific host' do - assert_redirected host: 'ssl', to: 'https://ssl/b?c=d' + assert_redirected redirect: { host: 'ssl' }, to: 'https://ssl/b?c=d' end test 'redirect to default port' do - assert_redirected port: 443 + assert_redirected redirect: { port: 443 } end test 'redirect to non-default port' do - assert_redirected port: 8443, to: 'https://a:8443/b?c=d' + assert_redirected redirect: { port: 8443 }, to: 'https://a:8443/b?c=d' end test 'redirect to different host and non-default port' do - assert_redirected host: 'ssl', port: 8443, to: 'https://ssl:8443/b?c=d' + assert_redirected redirect: { host: 'ssl', port: 8443 }, to: 'https://ssl:8443/b?c=d' end test 'redirect to different host including port' do - assert_redirected host: 'ssl:443', to: 'https://ssl:443/b?c=d' + assert_redirected redirect: { host: 'ssl:443' }, to: 'https://ssl:443/b?c=d' end test ':host is deprecated, moved within redirect: { host: … }' do @@ -84,19 +97,24 @@ class RedirectSSLTest < SSLTest assert_redirected deprecated_port: 1, to: 'https://a:1/b?c=d' end end + + test 'no redirect with redirect set to false' do + assert_not_redirected 'http://example.org', redirect: false + end end class StrictTransportSecurityTest < SSLTest EXPECTED = 'max-age=15552000' + EXPECTED_WITH_SUBDOMAINS = 'max-age=15552000; includeSubDomains' - def assert_hsts(expected, url: 'https://example.org', hsts: {}, headers: {}) + def assert_hsts(expected, url: 'https://example.org', hsts: { subdomains: true }, headers: {}) self.app = build_app ssl_options: { hsts: hsts }, headers: headers get url assert_equal expected, response.headers['Strict-Transport-Security'] end test 'enabled by default' do - assert_hsts EXPECTED + assert_hsts EXPECTED_WITH_SUBDOMAINS end test 'not sent with http:// responses' do @@ -116,11 +134,15 @@ class StrictTransportSecurityTest < SSLTest end test ':expires sets max-age' do - assert_hsts 'max-age=500', hsts: { expires: 500 } + assert_deprecated do + assert_hsts 'max-age=500', hsts: { expires: 500 } + end end test ':expires supports AS::Duration arguments' do - assert_hsts 'max-age=31557600', hsts: { expires: 1.year } + assert_deprecated do + assert_hsts 'max-age=31557600', hsts: { expires: 1.year } + end end test 'include subdomains' do @@ -132,11 +154,15 @@ class StrictTransportSecurityTest < SSLTest end test 'opt in to browser preload lists' do - assert_hsts "#{EXPECTED}; preload", hsts: { preload: true } + assert_deprecated do + assert_hsts "#{EXPECTED}; preload", hsts: { preload: true } + end end test 'opt out of browser preload lists' do - assert_hsts EXPECTED, hsts: { preload: false } + assert_deprecated do + assert_hsts EXPECTED, hsts: { preload: false } + end end end @@ -187,6 +213,11 @@ class SecureCookiesTest < SSLTest assert_cookies 'problem=def; path=/; Secure; HttpOnly' end + def test_cookies_as_not_secure_with_secure_cookies_disabled + get headers: { 'Set-Cookie' => DEFAULT }, ssl_options: { secure_cookies: false } + assert_cookies(*DEFAULT.split("\n")) + end + def test_no_cookies get assert_nil response.headers['Set-Cookie'] diff --git a/actionpack/test/dispatch/static_test.rb b/actionpack/test/dispatch/static_test.rb index 1da57ab50b..ea8b5e904e 100644 --- a/actionpack/test/dispatch/static_test.rb +++ b/actionpack/test/dispatch/static_test.rb @@ -40,6 +40,10 @@ module StaticTests assert_equal "Hello, World!", get("/doorkeeper%E3E4".force_encoding('ASCII-8BIT')).body end + def test_handles_urls_with_null_byte + assert_equal "Hello, World!", get("/doorkeeper%00").body + end + def test_sets_cache_control app = assert_deprecated do ActionDispatch::Static.new(DummyApp, @root, "public, max-age=60") diff --git a/actionpack/test/dispatch/url_generation_test.rb b/actionpack/test/dispatch/url_generation_test.rb index fd4ede4d1b..8c9782bb90 100644 --- a/actionpack/test/dispatch/url_generation_test.rb +++ b/actionpack/test/dispatch/url_generation_test.rb @@ -129,6 +129,13 @@ module TestUrlGeneration ) end + test "generating URLS with empty querystring" do + assert_equal "/bars.json", bars_path( + a: {}, + format: 'json' + ) + end + end end diff --git a/actionpack/test/fixtures/collection_cache/index.html.erb b/actionpack/test/fixtures/collection_cache/index.html.erb index 521b1450df..853e501ab4 100644 --- a/actionpack/test/fixtures/collection_cache/index.html.erb +++ b/actionpack/test/fixtures/collection_cache/index.html.erb @@ -1 +1 @@ -<%= render @customers %>
\ No newline at end of file +<%= render partial: 'customers/customer', collection: @customers, cached: true %> diff --git a/actionpack/test/fixtures/functional_caching/fragment_cached_with_options.html.erb b/actionpack/test/fixtures/functional_caching/fragment_cached_with_options.html.erb new file mode 100644 index 0000000000..01453323ef --- /dev/null +++ b/actionpack/test/fixtures/functional_caching/fragment_cached_with_options.html.erb @@ -0,0 +1,3 @@ +<body> +<%= cache 'with_options', skip_digest: true, expires_in: 1.minute do %><p>ERB</p><% end %> +</body> diff --git a/actionpack/test/fixtures/implicit_render_test/empty_action_with_mobile_variant.html+mobile.erb b/actionpack/test/fixtures/implicit_render_test/empty_action_with_mobile_variant.html+mobile.erb new file mode 100644 index 0000000000..ded99ba52d --- /dev/null +++ b/actionpack/test/fixtures/implicit_render_test/empty_action_with_mobile_variant.html+mobile.erb @@ -0,0 +1 @@ +mobile diff --git a/actionpack/test/fixtures/implicit_render_test/empty_action_with_template.html.erb b/actionpack/test/fixtures/implicit_render_test/empty_action_with_template.html.erb new file mode 100644 index 0000000000..dd294f8cf6 --- /dev/null +++ b/actionpack/test/fixtures/implicit_render_test/empty_action_with_template.html.erb @@ -0,0 +1 @@ +<h1>Empty action rendered this implicitly.</h1> diff --git a/actionpack/test/fixtures/respond_to/variant_with_implicit_rendering.html+mobile.erb b/actionpack/test/fixtures/respond_to/variant_with_implicit_template_rendering.html+mobile.erb index 317801ad30..317801ad30 100644 --- a/actionpack/test/fixtures/respond_to/variant_with_implicit_rendering.html+mobile.erb +++ b/actionpack/test/fixtures/respond_to/variant_with_implicit_template_rendering.html+mobile.erb |