diff options
Diffstat (limited to 'actionpack')
35 files changed, 1146 insertions, 172 deletions
diff --git a/actionpack/CHANGELOG.md b/actionpack/CHANGELOG.md index b63fb4a9b6..b05aa21f95 100644 --- a/actionpack/CHANGELOG.md +++ b/actionpack/CHANGELOG.md @@ -1,22 +1,74 @@ -* Do not discard query parameters that form a hash with the same root key as - the `wrapper_key` for a request using `wrap_parameters`. +* Introduce `render :html` as an option to render HTML content with a content + type of `text/html`. This rendering option calls `ERB::Util.html_escape` + internally to escape unsafe HTML string, so you will have to mark your + string as html safe if you have any HTML tag in it. - *Josh Jordan* + Please see #12374 for more detail. + + *Prem Sichanugrist* + +* Introduce `render :plain` as an option to render content with a content type + of `text/plain`. This is the preferred option if you are planning to render + a plain text content. + + Please see #12374 for more detail. + + *Prem Sichanugrist* + +* Introduce `render :body` as an option for sending a raw content back to + browser. Note that this rendering option will unset the default content type + and does not include "Content-Type" header back in the response. + + You should only use this option if you are expecting the "Content-Type" + header to not be set. More information on "Content-Type" header can be found + on RFC 2616, section 7.2.1. + + Please see #12374 for more detail. + + *Prem Sichanugrist* + +* Set stream status to 500 (or 400 on BadRequest) when an error is thrown + before commiting. + + Fixes #12552. -* Add `:serializer` option for `config.session_store :cookie_store`. This - changes default serializer when using `:cookie_store`. + *Kevin Casey* - It is possible to pass: +* Add new config option `config.action_dispatch.cookies_serializer` for + specifying a serializer for the signed and encrypted cookie jars. - * `:json` which is a secure wrapper on JSON using `JSON.parse` and - `JSON.generate` methods with quirks mode; - * `:marshal` which is a wrapper on Marshal; - * serializer class with `load` and `dump` methods defined. + The possible values are: - For new apps `:json` option is added by default and :marshal is used - when no option is specified. + * `:json` - serialize cookie values with `JSON` + * `:marshal` - serialize cookie values with `Marshal` + * `:hybrid` - transparently migrate existing `Marshal` cookie values to `JSON` - *Łukasz Sarnacki*, *Matt Aimonetti* + For new apps `:json` option is added by default and `:marshal` is used + when no option is specified to maintain backwards compatibility. + + *Łukasz Sarnacki*, *Matt Aimonetti*, *Guillermo Iguaran*, *Godfrey Chan*, *Rafael Mendonça França* + +* `FlashHash` now behaves like a `HashWithIndifferentAccess`. + + *Guillermo Iguaran* + +* Set the `:shallow_path` scope option as each scope is generated rather than + waiting until the `shallow` option is set. Also make the behavior of the + `:shallow` resource option consistent with the behavior of the `shallow` method. + + Fixes #12498. + + *Andrew White*, *Aleksi Aalto* + +* Properly require `action_view` in `AbstractController::Rendering` to prevent + uninitialized constant error for `ENCODING_FLAG`. + + *Philipe Fatio* + +* Do not discard query parameters that form a hash with the same root key as + the `wrapper_key` for a request using `wrap_parameters`. + + *Josh Jordan* * Ensure that `request.filtered_parameters` is reset between calls to `process` in `ActionController::TestCase`. diff --git a/actionpack/RUNNING_UNIT_TESTS.rdoc b/actionpack/RUNNING_UNIT_TESTS.rdoc index 08767ae133..ad1448f61b 100644 --- a/actionpack/RUNNING_UNIT_TESTS.rdoc +++ b/actionpack/RUNNING_UNIT_TESTS.rdoc @@ -1,10 +1,10 @@ == Running with Rake The easiest way to run the unit tests is through Rake. The default task runs -the entire test suite for all classes. For more information, checkout the -full array of rake tasks with "rake -T" +the entire test suite for all classes. For more information, check out the +full array of rake tasks with "rake -T". -Rake can be found at http://rake.rubyforge.org +Rake can be found at http://rake.rubyforge.org. == Running by hand diff --git a/actionpack/lib/abstract_controller/rendering.rb b/actionpack/lib/abstract_controller/rendering.rb index 7be61d94c9..9d10140ed2 100644 --- a/actionpack/lib/abstract_controller/rendering.rb +++ b/actionpack/lib/abstract_controller/rendering.rb @@ -1,5 +1,6 @@ require 'active_support/concern' require 'active_support/core_ext/class/attribute' +require 'action_view' require 'action_view/view_paths' require 'set' @@ -22,7 +23,7 @@ module AbstractController def render(*args, &block) options = _normalize_render(*args, &block) self.response_body = render_to_body(options) - _process_format(rendered_format) if rendered_format + _process_format(rendered_format, options) if rendered_format self.response_body end @@ -97,7 +98,7 @@ module AbstractController # Process the rendered format. # :api: private - def _process_format(format) + def _process_format(format, options = {}) end # Normalize args and options. @@ -105,7 +106,9 @@ module AbstractController def _normalize_render(*args, &block) options = _normalize_args(*args, &block) #TODO: remove defined? when we restore AP <=> AV dependency - options[:variant] = request.variant if defined?(request) && request.variant.present? + if defined?(request) && request && request.variant.present? + options[:variant] = request.variant + end _normalize_options(options) options end diff --git a/actionpack/lib/action_controller/base.rb b/actionpack/lib/action_controller/base.rb index c0f10da23a..e6fe6b0b00 100644 --- a/actionpack/lib/action_controller/base.rb +++ b/actionpack/lib/action_controller/base.rb @@ -45,7 +45,7 @@ module ActionController # # def server_ip # location = request.env["SERVER_ADDR"] - # render text: "This server hosted at #{location}" + # render plain: "This server hosted at #{location}" # end # # == Parameters diff --git a/actionpack/lib/action_controller/metal/data_streaming.rb b/actionpack/lib/action_controller/metal/data_streaming.rb index 75c4d3ef99..1abd8d3a33 100644 --- a/actionpack/lib/action_controller/metal/data_streaming.rb +++ b/actionpack/lib/action_controller/metal/data_streaming.rb @@ -96,7 +96,7 @@ module ActionController #:nodoc: end # Sends the given binary data to the browser. This method is similar to - # <tt>render text: data</tt>, but also allows you to specify whether + # <tt>render plain: data</tt>, but also allows you to specify whether # the browser should display the response as a file attachment (i.e. in a # download dialog) or as inline data. You may also set the content type, # the apparent file name, and other things. diff --git a/actionpack/lib/action_controller/metal/http_authentication.rb b/actionpack/lib/action_controller/metal/http_authentication.rb index 158d552ec7..1acc19d74b 100644 --- a/actionpack/lib/action_controller/metal/http_authentication.rb +++ b/actionpack/lib/action_controller/metal/http_authentication.rb @@ -11,11 +11,11 @@ module ActionController # http_basic_authenticate_with name: "dhh", password: "secret", except: :index # # def index - # render text: "Everyone can see me!" + # render plain: "Everyone can see me!" # end # # def edit - # render text: "I'm only accessible if you know the password" + # render plain: "I'm only accessible if you know the password" # end # end # @@ -127,11 +127,11 @@ module ActionController # before_action :authenticate, except: [:index] # # def index - # render text: "Everyone can see me!" + # render plain: "Everyone can see me!" # end # # def edit - # render text: "I'm only accessible if you know the password" + # render plain: "I'm only accessible if you know the password" # end # # private @@ -321,11 +321,11 @@ module ActionController # before_action :authenticate, except: [ :index ] # # def index - # render text: "Everyone can see me!" + # render plain: "Everyone can see me!" # end # # def edit - # render text: "I'm only accessible if you know the password" + # render plain: "I'm only accessible if you know the password" # end # # private diff --git a/actionpack/lib/action_controller/metal/live.rb b/actionpack/lib/action_controller/metal/live.rb index 33014b97ca..fdf4ef293d 100644 --- a/actionpack/lib/action_controller/metal/live.rb +++ b/actionpack/lib/action_controller/metal/live.rb @@ -205,6 +205,8 @@ module ActionController begin super(name) rescue => e + @_response.status = 500 unless @_response.committed? + @_response.status = 400 if e.class == ActionController::BadRequest begin @_response.stream.write(ActionView::Base.streaming_completion_on_exception) if request.format == :html @_response.stream.call_on_error diff --git a/actionpack/lib/action_controller/metal/mime_responds.rb b/actionpack/lib/action_controller/metal/mime_responds.rb index d5e08b7034..1974bbf529 100644 --- a/actionpack/lib/action_controller/metal/mime_responds.rb +++ b/actionpack/lib/action_controller/metal/mime_responds.rb @@ -236,6 +236,18 @@ module ActionController #:nodoc: # end # end # + # You can also set an array of variants: + # + # request.variant = [:tablet, :phone] + # + # which will work similarly to formats and MIME types negotiation. If there will be no + # :tablet variant declared, :phone variant will be picked: + # + # respond_to do |format| + # format.html.none + # format.html.phone # this gets rendered + # end + # # Be sure to check the documentation of +respond_with+ and # <tt>ActionController::MimeResponds.respond_to</tt> for more examples. def respond_to(*mimes, &block) @@ -488,7 +500,7 @@ module ActionController #:nodoc: response else # `format.html{ |variant| variant.phone }` - variant block syntax variant_collector = VariantCollector.new(@variant) - response.call(variant_collector) #call format block with variants collector + response.call(variant_collector) # call format block with variants collector variant_collector.variant end end @@ -519,15 +531,15 @@ module ActionController #:nodoc: end def variant - key = if @variant.nil? - :none - elsif @variants.has_key?(@variant) - @variant + if @variant.nil? + @variants[:none] || @variants[:any] + elsif (@variants.keys & @variant).any? + @variant.each do |v| + return @variants[v] if @variants.key?(v) + end else - :any + @variants[:any] end - - @variants[key] end end end diff --git a/actionpack/lib/action_controller/metal/rack_delegation.rb b/actionpack/lib/action_controller/metal/rack_delegation.rb index bdf6e88699..e1bee9e60c 100644 --- a/actionpack/lib/action_controller/metal/rack_delegation.rb +++ b/actionpack/lib/action_controller/metal/rack_delegation.rb @@ -5,8 +5,8 @@ module ActionController module RackDelegation extend ActiveSupport::Concern - delegate :headers, :status=, :location=, :content_type=, - :status, :location, :content_type, :to => "@_response" + delegate :headers, :status=, :location=, :content_type=, :no_content_type=, + :status, :location, :content_type, :no_content_type, :to => "@_response" def dispatch(action, request) set_response!(request) diff --git a/actionpack/lib/action_controller/metal/rendering.rb b/actionpack/lib/action_controller/metal/rendering.rb index 5c48b4ab98..3c4ef596c7 100644 --- a/actionpack/lib/action_controller/metal/rendering.rb +++ b/actionpack/lib/action_controller/metal/rendering.rb @@ -2,6 +2,8 @@ module ActionController module Rendering extend ActiveSupport::Concern + RENDER_FORMATS_IN_PRIORITY = [:body, :text, :plain, :html] + # Before processing, set the request formats in current controller formats. def process_action(*) #:nodoc: self.formats = request.formats.map(&:ref).compact @@ -27,14 +29,29 @@ module ActionController end def render_to_body(options = {}) - super || options[:text].presence || ' ' + super || _render_in_priorities(options) || ' ' end private - def _process_format(format) + def _render_in_priorities(options) + RENDER_FORMATS_IN_PRIORITY.each do |format| + return options[format] if options.key?(format) + end + + nil + end + + def _process_format(format, options = {}) super - self.content_type ||= format.to_s + + if options[:body] + self.headers.delete "Content-Type" + elsif options[:plain] + self.content_type = Mime::TEXT + else + self.content_type ||= format.to_s + end end # Normalize arguments by catching blocks and setting them on :update. @@ -46,12 +63,14 @@ module ActionController # Normalize both text and status options. def _normalize_options(options) #:nodoc: - if options.key?(:text) && options[:text].respond_to?(:to_text) - options[:text] = options[:text].to_text + _normalize_text(options) + + if options[:html] + options[:html] = ERB::Util.html_escape(options[:html]) end - if options.delete(:nothing) || (options.key?(:text) && options[:text].nil?) - options[:text] = " " + if options.delete(:nothing) || _any_render_format_is_nil?(options) + options[:body] = " " end if options[:status] @@ -61,6 +80,18 @@ module ActionController super end + def _normalize_text(options) + RENDER_FORMATS_IN_PRIORITY.each do |format| + if options.key?(format) && options[format].respond_to?(:to_text) + options[format] = options[format].to_text + end + end + end + + def _any_render_format_is_nil?(options) + RENDER_FORMATS_IN_PRIORITY.any? { |format| options.key?(format) && options[format].nil? } + end + # Process controller specific options, as status, content-type and location. def _process_options(options) #:nodoc: status, content_type, location = options.values_at(:status, :content_type, :location) diff --git a/actionpack/lib/action_dispatch.rb b/actionpack/lib/action_dispatch.rb index 9b26845190..3dd2e2a45c 100644 --- a/actionpack/lib/action_dispatch.rb +++ b/actionpack/lib/action_dispatch.rb @@ -74,7 +74,6 @@ module ActionDispatch autoload :MimeNegotiation autoload :Parameters autoload :ParameterFilter - autoload :FilterParameters autoload :Upload autoload :UploadedFile, 'action_dispatch/http/upload' autoload :URL @@ -85,8 +84,6 @@ module ActionDispatch autoload :CookieStore, 'action_dispatch/middleware/session/cookie_store' autoload :MemCacheStore, 'action_dispatch/middleware/session/mem_cache_store' autoload :CacheStore, 'action_dispatch/middleware/session/cache_store' - autoload :JsonSerializer, 'action_dispatch/middleware/session/json_serializer' - autoload :MarshalSerializer, 'action_dispatch/middleware/session/marshal_serializer' end mattr_accessor :test_app diff --git a/actionpack/lib/action_dispatch/http/mime_negotiation.rb b/actionpack/lib/action_dispatch/http/mime_negotiation.rb index c33ba201e1..b803ce8b6f 100644 --- a/actionpack/lib/action_dispatch/http/mime_negotiation.rb +++ b/actionpack/lib/action_dispatch/http/mime_negotiation.rb @@ -68,10 +68,12 @@ module ActionDispatch # Sets the \variant for template. def variant=(variant) - if variant.is_a? Symbol + if variant.is_a?(Symbol) + @variant = [variant] + elsif variant.is_a?(Array) && variant.any? && variant.all?{ |v| v.is_a?(Symbol) } @variant = variant else - raise ArgumentError, "request.variant must be set to a Symbol, not a #{variant.class}. " \ + raise ArgumentError, "request.variant must be set to a Symbol or an Array of Symbols, not a #{variant.class}. " \ "For security reasons, never directly set the variant to a user-provided value, " \ "like params[:variant].to_sym. Check user-provided value against a whitelist first, " \ "then set the variant: request.variant = :tablet if params[:variant] == 'tablet'" diff --git a/actionpack/lib/action_dispatch/http/response.rb b/actionpack/lib/action_dispatch/http/response.rb index bc13ee00f1..f14ca1ea44 100644 --- a/actionpack/lib/action_dispatch/http/response.rb +++ b/actionpack/lib/action_dispatch/http/response.rb @@ -63,6 +63,8 @@ module ActionDispatch # :nodoc: # content you're giving them, so we need to send that along. attr_accessor :charset + attr_accessor :no_content_type # :nodoc: + CONTENT_TYPE = "Content-Type".freeze SET_COOKIE = "Set-Cookie".freeze LOCATION = "Location".freeze @@ -303,8 +305,17 @@ module ActionDispatch # :nodoc: !@sending_file && @charset != false end + def remove_content_type! + headers.delete CONTENT_TYPE + end + def rack_response(status, header) - assign_default_content_type_and_charset!(header) + if no_content_type + remove_content_type! + else + assign_default_content_type_and_charset!(header) + end + handle_conditional_get! header[SET_COOKIE] = header[SET_COOKIE].join("\n") if header[SET_COOKIE].respond_to?(:join) @@ -313,7 +324,7 @@ module ActionDispatch # :nodoc: header.delete CONTENT_TYPE [status, header, []] else - [status, header, self] + [status, header, Rack::BodyProxy.new(self){}] end end end diff --git a/actionpack/lib/action_dispatch/middleware/cookies.rb b/actionpack/lib/action_dispatch/middleware/cookies.rb index 23d0ecd529..18e64704f6 100644 --- a/actionpack/lib/action_dispatch/middleware/cookies.rb +++ b/actionpack/lib/action_dispatch/middleware/cookies.rb @@ -23,15 +23,15 @@ module ActionDispatch # # This cookie will be deleted when the user's browser is closed. # cookies[:user_name] = "david" # - # # Assign an array of values to a cookie. - # cookies[:lat_lon] = [47.68, -122.37] + # # Cookie values are String based. Other data types need to be serialized. + # cookies[:lat_lon] = JSON.generate([47.68, -122.37]) # # # Sets a cookie that expires in 1 hour. # cookies[:login] = { value: "XJ-122", expires: 1.hour.from_now } # # # Sets a signed cookie, which prevents users from tampering with its value. - # # The cookie is signed by your app's <tt>secrets.secret_key_base</tt> value. - # # It can be read using the signed method <tt>cookies.signed[:name]</tt> + # # The cookie is signed by your app's `secrets.secret_key_base` value. + # # It can be read using the signed method `cookies.signed[:name]` # cookies.signed[:user_id] = current_user.id # # # Sets a "permanent" cookie (which expires in 20 years from now). @@ -42,10 +42,10 @@ module ActionDispatch # # Examples of reading: # - # cookies[:user_name] # => "david" - # cookies.size # => 2 - # cookies[:lat_lon] # => [47.68, -122.37] - # cookies.signed[:login] # => "XJ-122" + # cookies[:user_name] # => "david" + # cookies.size # => 2 + # JSON.parse(cookies[:lat_lon]) # => [47.68, -122.37] + # cookies.signed[:login] # => "XJ-122" # # Example for deleting: # @@ -63,7 +63,7 @@ module ActionDispatch # # The option symbols for setting cookies are: # - # * <tt>:value</tt> - The cookie's value or list of values (as an array). + # * <tt>:value</tt> - The cookie's value. # * <tt>:path</tt> - The path for which this cookie applies. Defaults to the root # of the application. # * <tt>:domain</tt> - The domain for which this cookie applies so you can @@ -89,7 +89,7 @@ module ActionDispatch ENCRYPTED_SIGNED_COOKIE_SALT = "action_dispatch.encrypted_signed_cookie_salt".freeze SECRET_TOKEN = "action_dispatch.secret_token".freeze SECRET_KEY_BASE = "action_dispatch.secret_key_base".freeze - SESSION_SERIALIZER = "action_dispatch.session_serializer".freeze + COOKIES_SERIALIZER = "action_dispatch.cookies_serializer".freeze # Cookies can typically store 4096 bytes. MAX_COOKIE_SIZE = 4096 @@ -181,7 +181,7 @@ module ActionDispatch def verify_and_upgrade_legacy_signed_message(name, signed_message) @legacy_verifier.verify(signed_message).tap do |value| - self[name] = value + self[name] = { value: value } end rescue ActiveSupport::MessageVerifier::InvalidSignature nil @@ -212,7 +212,7 @@ module ActionDispatch secret_token: env[SECRET_TOKEN], secret_key_base: env[SECRET_KEY_BASE], upgrade_legacy_signed_cookies: env[SECRET_TOKEN].present? && env[SECRET_KEY_BASE].present?, - session_serializer: env[SESSION_SERIALIZER] + serializer: env[COOKIES_SERIALIZER] } end @@ -374,28 +374,89 @@ module ActionDispatch end end + class JsonSerializer + def self.load(value) + JSON.parse(value, quirks_mode: true) + end + + def self.dump(value) + JSON.generate(value, quirks_mode: true) + end + end + + # Passing the NullSerializer downstream to the Message{Encryptor,Verifier} + # allows us to handle the (de)serialization step within the cookie jar, + # which gives us the opportunity to detect and migrate legacy cookies. + class NullSerializer + def self.load(value) + value + end + + def self.dump(value) + value + end + end + + module SerializedCookieJars + MARSHAL_SIGNATURE = "\x04\x08".freeze + + protected + def needs_migration?(value) + @options[:serializer] == :hybrid && value.start_with?(MARSHAL_SIGNATURE) + end + + def serialize(name, value) + serializer.dump(value) + end + + def deserialize(name, value) + if value + if needs_migration?(value) + Marshal.load(value).tap do |v| + self[name] = { value: v } + end + else + serializer.load(value) + end + end + end + + def serializer + serializer = @options[:serializer] || :marshal + case serializer + when :marshal + Marshal + when :json, :hybrid + JsonSerializer + else + serializer + end + end + end + class SignedCookieJar #:nodoc: include ChainedCookieJars + include SerializedCookieJars def initialize(parent_jar, key_generator, options = {}) @parent_jar = parent_jar @options = options secret = key_generator.generate_key(@options[:signed_cookie_salt]) - @verifier = ActiveSupport::MessageVerifier.new(secret) + @verifier = ActiveSupport::MessageVerifier.new(secret, serializer: NullSerializer) end def [](name) if signed_message = @parent_jar[name] - verify(signed_message) + deserialize name, verify(signed_message) end end def []=(name, options) if options.is_a?(Hash) options.symbolize_keys! - options[:value] = @verifier.generate(options[:value]) + options[:value] = @verifier.generate(serialize(name, options[:value])) else - options = { :value => @verifier.generate(options) } + options = { :value => @verifier.generate(serialize(name, options)) } end raise CookieOverflow if options[:value].size > MAX_COOKIE_SIZE @@ -419,13 +480,14 @@ module ActionDispatch def [](name) if signed_message = @parent_jar[name] - verify(signed_message) || verify_and_upgrade_legacy_signed_message(name, signed_message) + deserialize(name, verify(signed_message)) || verify_and_upgrade_legacy_signed_message(name, signed_message) end end end class EncryptedCookieJar #:nodoc: include ChainedCookieJars + include SerializedCookieJars def initialize(parent_jar, key_generator, options = {}) if ActiveSupport::LegacyKeyGenerator === key_generator @@ -437,12 +499,12 @@ module ActionDispatch @options = options secret = key_generator.generate_key(@options[:encrypted_cookie_salt]) sign_secret = key_generator.generate_key(@options[:encrypted_signed_cookie_salt]) - @encryptor = ActiveSupport::MessageEncryptor.new(secret, sign_secret, serializer: serializer) + @encryptor = ActiveSupport::MessageEncryptor.new(secret, sign_secret, serializer: NullSerializer) end def [](name) if encrypted_message = @parent_jar[name] - decrypt_and_verify(encrypted_message) + deserialize name, decrypt_and_verify(encrypted_message) end end @@ -452,7 +514,8 @@ module ActionDispatch else options = { :value => options } end - options[:value] = @encryptor.encrypt_and_sign(options[:value]) + + options[:value] = @encryptor.encrypt_and_sign(serialize(name, options[:value])) raise CookieOverflow if options[:value].size > MAX_COOKIE_SIZE @parent_jar[name] = options @@ -464,18 +527,6 @@ module ActionDispatch rescue ActiveSupport::MessageVerifier::InvalidSignature, ActiveSupport::MessageEncryptor::InvalidMessage nil end - - def serializer - serializer = @options[:session_serializer] || :marshal - case serializer - when :marshal - ActionDispatch::Session::MarshalSerializer - when :json - ActionDispatch::Session::JsonSerializer - else - serializer - end - end end # UpgradeLegacyEncryptedCookieJar is used by ActionDispatch::Session::CookieStore @@ -487,7 +538,7 @@ module ActionDispatch def [](name) if encrypted_or_signed_message = @parent_jar[name] - decrypt_and_verify(encrypted_or_signed_message) || verify_and_upgrade_legacy_signed_message(name, encrypted_or_signed_message) + deserialize(name, decrypt_and_verify(encrypted_or_signed_message)) || verify_and_upgrade_legacy_signed_message(name, encrypted_or_signed_message) end end end diff --git a/actionpack/lib/action_dispatch/middleware/flash.rb b/actionpack/lib/action_dispatch/middleware/flash.rb index 89003e7a5e..4821d2a899 100644 --- a/actionpack/lib/action_dispatch/middleware/flash.rb +++ b/actionpack/lib/action_dispatch/middleware/flash.rb @@ -1,3 +1,5 @@ +require 'active_support/core_ext/hash/keys' + module ActionDispatch class Request < Rack::Request # Access the contents of the flash. Use <tt>flash["notice"]</tt> to @@ -50,13 +52,14 @@ module ActionDispatch end def []=(k, v) + k = k.to_s @flash[k] = v @flash.discard(k) v end def [](k) - @flash[k] + @flash[k.to_s] end # Convenience accessor for <tt>flash.now[:alert]=</tt>. @@ -92,8 +95,8 @@ module ActionDispatch end def initialize(flashes = {}, discard = []) #:nodoc: - @discard = Set.new(discard) - @flashes = flashes + @discard = Set.new(stringify_array(discard)) + @flashes = flashes.stringify_keys @now = nil end @@ -106,17 +109,18 @@ module ActionDispatch end def []=(k, v) + k = k.to_s @discard.delete k @flashes[k] = v end def [](k) - @flashes[k] + @flashes[k.to_s] end def update(h) #:nodoc: - @discard.subtract h.keys - @flashes.update h + @discard.subtract stringify_array(h.keys) + @flashes.update h.stringify_keys self end @@ -129,6 +133,7 @@ module ActionDispatch end def delete(key) + key = key.to_s @discard.delete key @flashes.delete key self @@ -155,7 +160,7 @@ module ActionDispatch def replace(h) #:nodoc: @discard.clear - @flashes.replace h + @flashes.replace h.stringify_keys self end @@ -186,6 +191,7 @@ module ActionDispatch # flash.keep # keeps the entire flash # flash.keep(:notice) # keeps only the "notice" entry, the rest of the flash is discarded def keep(k = nil) + k = k.to_s if k @discard.subtract Array(k || keys) k ? self[k] : self end @@ -195,6 +201,7 @@ module ActionDispatch # flash.discard # discard the entire flash at the end of the current action # flash.discard(:warning) # discard only the "warning" entry at the end of the current action def discard(k = nil) + k = k.to_s if k @discard.merge Array(k || keys) k ? self[k] : self end @@ -231,6 +238,12 @@ module ActionDispatch def now_is_loaded? @now end + + def stringify_array(array) + array.map do |item| + item.kind_of?(Symbol) ? item.to_s : item + end + end end def initialize(app) diff --git a/actionpack/lib/action_dispatch/middleware/remote_ip.rb b/actionpack/lib/action_dispatch/middleware/remote_ip.rb index 57bc6d5cd0..c1df518b14 100644 --- a/actionpack/lib/action_dispatch/middleware/remote_ip.rb +++ b/actionpack/lib/action_dispatch/middleware/remote_ip.rb @@ -47,12 +47,12 @@ module ActionDispatch # clients (like WAP devices), or behind proxies that set headers in an # incorrect or confusing way (like AWS ELB). # - # The +custom_trusted+ argument can take a regex, which will be used + # The +custom_proxies+ argument can take a regex, which will be used # instead of +TRUSTED_PROXIES+, or a string, which will be used in addition # to +TRUSTED_PROXIES+. Any proxy setup will put the value you want in the # middle (or at the beginning) of the X-Forwarded-For list, with your proxy # servers after it. If your proxies aren't removed, pass them in via the - # +custom_trusted+ parameter. That way, the middleware will ignore those + # +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) @app = app diff --git a/actionpack/lib/action_dispatch/middleware/session/json_serializer.rb b/actionpack/lib/action_dispatch/middleware/session/json_serializer.rb deleted file mode 100644 index d341853f7a..0000000000 --- a/actionpack/lib/action_dispatch/middleware/session/json_serializer.rb +++ /dev/null @@ -1,13 +0,0 @@ -module ActionDispatch - module Session - class JsonSerializer - def self.load(value) - JSON.parse(value, quirks_mode: true) - end - - def self.dump(value) - JSON.generate(value, quirks_mode: true) - end - end - end -end diff --git a/actionpack/lib/action_dispatch/middleware/session/marshal_serializer.rb b/actionpack/lib/action_dispatch/middleware/session/marshal_serializer.rb deleted file mode 100644 index 26622f682d..0000000000 --- a/actionpack/lib/action_dispatch/middleware/session/marshal_serializer.rb +++ /dev/null @@ -1,14 +0,0 @@ -module ActionDispatch - module Session - class MarshalSerializer - def self.load(value) - Marshal.load(value) - end - - def self.dump(value) - Marshal.dump(value) - end - end - end -end - diff --git a/actionpack/lib/action_dispatch/routing/mapper.rb b/actionpack/lib/action_dispatch/routing/mapper.rb index d5eb770cb1..0b762aa9a4 100644 --- a/actionpack/lib/action_dispatch/routing/mapper.rb +++ b/actionpack/lib/action_dispatch/routing/mapper.rb @@ -707,6 +707,10 @@ module ActionDispatch options[:path] = args.flatten.join('/') if args.any? options[:constraints] ||= {} + unless shallow? + options[:shallow_path] = options[:path] if args.any? + end + if options[:constraints].is_a?(Hash) defaults = options[:constraints].select do |k, v| URL_OPTIONS.include?(k) && (v.is_a?(String) || v.is_a?(Fixnum)) @@ -1369,7 +1373,7 @@ module ActionDispatch end def shallow - scope(:shallow => true, :shallow_path => @scope[:path]) do + scope(:shallow => true) do yield end end @@ -1490,6 +1494,13 @@ module ActionDispatch return true end + if options.delete(:shallow) + shallow do + send(method, resources.pop, options, &block) + end + return true + end + if resource_scope? nested { send(method, resources.pop, options, &block) } return true diff --git a/actionpack/lib/action_pack/version.rb b/actionpack/lib/action_pack/version.rb index a51f6a434a..8da3069c8b 100644 --- a/actionpack/lib/action_pack/version.rb +++ b/actionpack/lib/action_pack/version.rb @@ -1,7 +1,7 @@ module ActionPack # Returns the version of the currently loaded ActionPack as a Gem::Version def self.version - Gem::Version.new "4.1.0.beta1" + Gem::Version.new "4.1.0.beta2" end module VERSION #:nodoc: diff --git a/actionpack/test/controller/flash_hash_test.rb b/actionpack/test/controller/flash_hash_test.rb index 5490d9394b..50b36a0567 100644 --- a/actionpack/test/controller/flash_hash_test.rb +++ b/actionpack/test/controller/flash_hash_test.rb @@ -67,6 +67,16 @@ module ActionDispatch assert_equal({'flashes' => {'message' => 'Hello'}, 'discard' => %w[message]}, hash.to_session_value) end + def test_from_session_value_on_json_serializer + decrypted_data = "{ \"session_id\":\"d98bdf6d129618fc2548c354c161cfb5\", \"flash\":{\"discard\":[], \"flashes\":{\"message\":\"hey you\"}} }" + session = ActionDispatch::Cookies::JsonSerializer.load(decrypted_data) + hash = Flash::FlashHash.from_session_value(session['flash']) + + assert_equal({'discard' => %w[message], 'flashes' => { 'message' => 'hey you'}}, hash.to_session_value) + assert_equal "hey you", hash[:message] + assert_equal "hey you", hash["message"] + end + def test_empty? assert @hash.empty? @hash['zomg'] = 'bears' diff --git a/actionpack/test/controller/flash_test.rb b/actionpack/test/controller/flash_test.rb index 9ceab91e42..25a4857eba 100644 --- a/actionpack/test/controller/flash_test.rb +++ b/actionpack/test/controller/flash_test.rb @@ -175,13 +175,13 @@ class FlashTest < ActionController::TestCase assert_equal(:foo_indeed, flash.discard(:foo)) # valid key passed assert_nil flash.discard(:unknown) # non existent key passed - assert_equal({:foo => :foo_indeed, :bar => :bar_indeed}, flash.discard().to_hash) # nothing passed - assert_equal({:foo => :foo_indeed, :bar => :bar_indeed}, flash.discard(nil).to_hash) # nothing passed + assert_equal({"foo" => :foo_indeed, "bar" => :bar_indeed}, flash.discard().to_hash) # nothing passed + assert_equal({"foo" => :foo_indeed, "bar" => :bar_indeed}, flash.discard(nil).to_hash) # nothing passed assert_equal(:foo_indeed, flash.keep(:foo)) # valid key passed assert_nil flash.keep(:unknown) # non existent key passed - assert_equal({:foo => :foo_indeed, :bar => :bar_indeed}, flash.keep().to_hash) # nothing passed - assert_equal({:foo => :foo_indeed, :bar => :bar_indeed}, flash.keep(nil).to_hash) # nothing passed + assert_equal({"foo" => :foo_indeed, "bar" => :bar_indeed}, flash.keep().to_hash) # nothing passed + assert_equal({"foo" => :foo_indeed, "bar" => :bar_indeed}, flash.keep(nil).to_hash) # nothing passed end def test_redirect_to_with_alert diff --git a/actionpack/test/controller/live_stream_test.rb b/actionpack/test/controller/live_stream_test.rb index 0a431270b5..fb6a750089 100644 --- a/actionpack/test/controller/live_stream_test.rb +++ b/actionpack/test/controller/live_stream_test.rb @@ -156,6 +156,14 @@ module ActionController raise 'An exception occurred...' end + def exception_in_controller + raise 'Exception in controller' + end + + def bad_request_error + raise ActionController::BadRequest + end + def exception_in_exception_callback response.headers['Content-Type'] = 'text/event-stream' response.stream.on_error do @@ -275,6 +283,16 @@ module ActionController end end + def test_exception_in_controller_before_streaming + response = get :exception_in_controller, format: 'text/event-stream' + assert_equal 500, response.status + end + + def test_bad_request_in_controller_before_streaming + response = get :bad_request_error, format: 'text/event-stream' + assert_equal 400, response.status + end + def test_exceptions_raised_handling_exceptions capture_log_output do |output| get :exception_in_exception_callback, format: 'text/event-stream' diff --git a/actionpack/test/controller/mime/respond_to_test.rb b/actionpack/test/controller/mime/respond_to_test.rb index 84e4936f31..499c62cc35 100644 --- a/actionpack/test/controller/mime/respond_to_test.rb +++ b/actionpack/test/controller/mime/respond_to_test.rb @@ -671,6 +671,10 @@ class RespondToControllerTest < ActionController::TestCase end def test_variant_any_any + get :variant_any_any + assert_equal "text/html", @response.content_type + assert_equal "any", @response.body + @request.variant = :phone get :variant_any_any assert_equal "text/html", @response.content_type @@ -740,4 +744,25 @@ class RespondToControllerTest < ActionController::TestCase assert_equal "text/javascript", @response.content_type assert_equal "tablet", @response.body end + + def test_variant_negotiation_inline_syntax + @request.variant = [:tablet, :phone] + get :variant_inline_syntax_without_block + assert_equal "text/html", @response.content_type + assert_equal "phone", @response.body + end + + def test_variant_negotiation_block_syntax + @request.variant = [:tablet, :phone] + get :variant_plus_none_for_format + assert_equal "text/html", @response.content_type + assert_equal "phone", @response.body + end + + def test_variant_negotiation_without_block + @request.variant = [:tablet, :phone] + get :variant_inline_syntax_without_block + assert_equal "text/html", @response.content_type + assert_equal "phone", @response.body + end end diff --git a/actionpack/test/controller/new_base/render_body_test.rb b/actionpack/test/controller/new_base/render_body_test.rb new file mode 100644 index 0000000000..a7e4f87bd9 --- /dev/null +++ b/actionpack/test/controller/new_base/render_body_test.rb @@ -0,0 +1,175 @@ +require 'abstract_unit' + +module RenderBody + class MinimalController < ActionController::Metal + include AbstractController::Rendering + include ActionController::Rendering + + def index + render body: "Hello World!" + end + end + + class SimpleController < ActionController::Base + self.view_paths = [ActionView::FixtureResolver.new] + + def index + render body: "hello david" + end + end + + class WithLayoutController < ::ApplicationController + self.view_paths = [ActionView::FixtureResolver.new( + "layouts/application.erb" => "<%= yield %>, I'm here!", + "layouts/greetings.erb" => "<%= yield %>, I wish thee well.", + "layouts/ivar.erb" => "<%= yield %>, <%= @ivar %>" + )] + + def index + render body: "hello david" + end + + def custom_code + render body: "hello world", status: 404 + end + + def with_custom_code_as_string + render body: "hello world", status: "404 Not Found" + end + + def with_nil + render body: nil + end + + def with_nil_and_status + render body: nil, status: 403 + end + + def with_false + render body: false + end + + def with_layout_true + render body: "hello world", layout: true + end + + def with_layout_false + render body: "hello world", layout: false + end + + def with_layout_nil + render body: "hello world", layout: nil + end + + def with_custom_layout + render body: "hello world", layout: "greetings" + end + + def with_ivar_in_layout + @ivar = "hello world" + render body: "hello world", layout: "ivar" + end + end + + class RenderBodyTest < Rack::TestCase + test "rendering body from a minimal controller" do + get "/render_body/minimal/index" + assert_body "Hello World!" + assert_status 200 + end + + test "rendering body from an action with default options renders the body with the layout" do + with_routing do |set| + set.draw { get ':controller', action: 'index' } + + get "/render_body/simple" + assert_body "hello david" + assert_status 200 + end + end + + test "rendering body from an action with default options renders the body without the layout" do + with_routing do |set| + set.draw { get ':controller', action: 'index' } + + get "/render_body/with_layout" + + assert_body "hello david" + assert_status 200 + end + end + + test "rendering body, while also providing a custom status code" do + get "/render_body/with_layout/custom_code" + + assert_body "hello world" + assert_status 404 + end + + test "rendering body with nil returns an empty body padded for Safari" do + get "/render_body/with_layout/with_nil" + + assert_body " " + assert_status 200 + end + + test "Rendering body with nil and custom status code returns an empty body padded for Safari and the status" do + get "/render_body/with_layout/with_nil_and_status" + + assert_body " " + assert_status 403 + end + + test "rendering body with false returns the string 'false'" do + get "/render_body/with_layout/with_false" + + assert_body "false" + assert_status 200 + end + + test "rendering body with layout: true" do + get "/render_body/with_layout/with_layout_true" + + assert_body "hello world, I'm here!" + assert_status 200 + end + + test "rendering body with layout: 'greetings'" do + get "/render_body/with_layout/with_custom_layout" + + assert_body "hello world, I wish thee well." + assert_status 200 + end + + test "rendering body with layout: false" do + get "/render_body/with_layout/with_layout_false" + + assert_body "hello world" + assert_status 200 + end + + test "rendering body with layout: nil" do + get "/render_body/with_layout/with_layout_nil" + + assert_body "hello world" + assert_status 200 + end + + test "rendering from minimal controller returns response with no content type" do + get "/render_body/minimal/index" + + assert_header_no_content_type + end + + test "rendering from normal controller returns response with no content type" do + get "/render_body/simple/index" + + assert_header_no_content_type + end + + def assert_header_no_content_type + assert_not response.headers.has_key?("Content-Type"), + %(Expect response not to have Content-Type header, got "#{response.headers["Content-Type"]}") + end + end +end diff --git a/actionpack/test/controller/new_base/render_html_test.rb b/actionpack/test/controller/new_base/render_html_test.rb new file mode 100644 index 0000000000..bfe0271df7 --- /dev/null +++ b/actionpack/test/controller/new_base/render_html_test.rb @@ -0,0 +1,190 @@ +require 'abstract_unit' + +module RenderHtml + class MinimalController < ActionController::Metal + include AbstractController::Rendering + include ActionController::Rendering + + def index + render html: "Hello World!" + end + end + + class SimpleController < ActionController::Base + self.view_paths = [ActionView::FixtureResolver.new] + + def index + render html: "hello david" + end + end + + class WithLayoutController < ::ApplicationController + self.view_paths = [ActionView::FixtureResolver.new( + "layouts/application.html.erb" => "<%= yield %>, I'm here!", + "layouts/greetings.html.erb" => "<%= yield %>, I wish thee well.", + "layouts/ivar.html.erb" => "<%= yield %>, <%= @ivar %>" + )] + + def index + render html: "hello david" + end + + def custom_code + render html: "hello world", status: 404 + end + + def with_custom_code_as_string + render html: "hello world", status: "404 Not Found" + end + + def with_nil + render html: nil + end + + def with_nil_and_status + render html: nil, status: 403 + end + + def with_false + render html: false + end + + def with_layout_true + render html: "hello world", layout: true + end + + def with_layout_false + render html: "hello world", layout: false + end + + def with_layout_nil + render html: "hello world", layout: nil + end + + def with_custom_layout + render html: "hello world", layout: "greetings" + end + + def with_ivar_in_layout + @ivar = "hello world" + render html: "hello world", layout: "ivar" + end + + def with_unsafe_html_tag + render html: "<p>hello world</p>", layout: nil + end + + def with_safe_html_tag + render html: "<p>hello world</p>".html_safe, layout: nil + end + end + + class RenderHtmlTest < Rack::TestCase + test "rendering text from a minimal controller" do + get "/render_html/minimal/index" + assert_body "Hello World!" + assert_status 200 + end + + test "rendering text from an action with default options renders the text with the layout" do + with_routing do |set| + set.draw { get ':controller', action: 'index' } + + get "/render_html/simple" + assert_body "hello david" + assert_status 200 + end + end + + test "rendering text from an action with default options renders the text without the layout" do + with_routing do |set| + set.draw { get ':controller', action: 'index' } + + get "/render_html/with_layout" + + assert_body "hello david" + assert_status 200 + end + end + + test "rendering text, while also providing a custom status code" do + get "/render_html/with_layout/custom_code" + + assert_body "hello world" + assert_status 404 + end + + test "rendering text with nil returns an empty body padded for Safari" do + get "/render_html/with_layout/with_nil" + + assert_body " " + assert_status 200 + end + + test "Rendering text with nil and custom status code returns an empty body padded for Safari and the status" do + get "/render_html/with_layout/with_nil_and_status" + + assert_body " " + assert_status 403 + end + + test "rendering text with false returns the string 'false'" do + get "/render_html/with_layout/with_false" + + assert_body "false" + assert_status 200 + end + + test "rendering text with layout: true" do + get "/render_html/with_layout/with_layout_true" + + assert_body "hello world, I'm here!" + assert_status 200 + end + + test "rendering text with layout: 'greetings'" do + get "/render_html/with_layout/with_custom_layout" + + assert_body "hello world, I wish thee well." + assert_status 200 + end + + test "rendering text with layout: false" do + get "/render_html/with_layout/with_layout_false" + + assert_body "hello world" + assert_status 200 + end + + test "rendering text with layout: nil" do + get "/render_html/with_layout/with_layout_nil" + + assert_body "hello world" + assert_status 200 + end + + test "rendering html should escape the string if it is not html safe" do + get "/render_html/with_layout/with_unsafe_html_tag" + + assert_body "<p>hello world</p>" + assert_status 200 + end + + test "rendering html should not escape the string if it is html safe" do + get "/render_html/with_layout/with_safe_html_tag" + + assert_body "<p>hello world</p>" + assert_status 200 + end + + test "rendering from minimal controller returns response with text/html content type" do + get "/render_html/minimal/index" + assert_content_type "text/html" + end + + test "rendering from normal controller returns response with text/html content type" do + get "/render_html/simple/index" + assert_content_type "text/html; charset=utf-8" + end + end +end diff --git a/actionpack/test/controller/new_base/render_plain_test.rb b/actionpack/test/controller/new_base/render_plain_test.rb new file mode 100644 index 0000000000..dba2e9f13e --- /dev/null +++ b/actionpack/test/controller/new_base/render_plain_test.rb @@ -0,0 +1,168 @@ +require 'abstract_unit' + +module RenderPlain + class MinimalController < ActionController::Metal + include AbstractController::Rendering + include ActionController::Rendering + + def index + render plain: "Hello World!" + end + end + + class SimpleController < ActionController::Base + self.view_paths = [ActionView::FixtureResolver.new] + + def index + render plain: "hello david" + end + end + + class WithLayoutController < ::ApplicationController + self.view_paths = [ActionView::FixtureResolver.new( + "layouts/application.text.erb" => "<%= yield %>, I'm here!", + "layouts/greetings.text.erb" => "<%= yield %>, I wish thee well.", + "layouts/ivar.text.erb" => "<%= yield %>, <%= @ivar %>" + )] + + def index + render plain: "hello david" + end + + def custom_code + render plain: "hello world", status: 404 + end + + def with_custom_code_as_string + render plain: "hello world", status: "404 Not Found" + end + + def with_nil + render plain: nil + end + + def with_nil_and_status + render plain: nil, status: 403 + end + + def with_false + render plain: false + end + + def with_layout_true + render plain: "hello world", layout: true + end + + def with_layout_false + render plain: "hello world", layout: false + end + + def with_layout_nil + render plain: "hello world", layout: nil + end + + def with_custom_layout + render plain: "hello world", layout: "greetings" + end + + def with_ivar_in_layout + @ivar = "hello world" + render plain: "hello world", layout: "ivar" + end + end + + class RenderPlainTest < Rack::TestCase + test "rendering text from a minimal controller" do + get "/render_plain/minimal/index" + assert_body "Hello World!" + assert_status 200 + end + + test "rendering text from an action with default options renders the text with the layout" do + with_routing do |set| + set.draw { get ':controller', action: 'index' } + + get "/render_plain/simple" + assert_body "hello david" + assert_status 200 + end + end + + test "rendering text from an action with default options renders the text without the layout" do + with_routing do |set| + set.draw { get ':controller', action: 'index' } + + get "/render_plain/with_layout" + + assert_body "hello david" + assert_status 200 + end + end + + test "rendering text, while also providing a custom status code" do + get "/render_plain/with_layout/custom_code" + + assert_body "hello world" + assert_status 404 + end + + test "rendering text with nil returns an empty body padded for Safari" do + get "/render_plain/with_layout/with_nil" + + assert_body " " + assert_status 200 + end + + test "Rendering text with nil and custom status code returns an empty body padded for Safari and the status" do + get "/render_plain/with_layout/with_nil_and_status" + + assert_body " " + assert_status 403 + end + + test "rendering text with false returns the string 'false'" do + get "/render_plain/with_layout/with_false" + + assert_body "false" + assert_status 200 + end + + test "rendering text with layout: true" do + get "/render_plain/with_layout/with_layout_true" + + assert_body "hello world, I'm here!" + assert_status 200 + end + + test "rendering text with layout: 'greetings'" do + get "/render_plain/with_layout/with_custom_layout" + + assert_body "hello world, I wish thee well." + assert_status 200 + end + + test "rendering text with layout: false" do + get "/render_plain/with_layout/with_layout_false" + + assert_body "hello world" + assert_status 200 + end + + test "rendering text with layout: nil" do + get "/render_plain/with_layout/with_layout_nil" + + assert_body "hello world" + assert_status 200 + end + + test "rendering from minimal controller returns response with text/plain content type" do + get "/render_plain/minimal/index" + assert_content_type "text/plain" + end + + test "rendering from normal controller returns response with text/plain content type" do + get "/render_plain/simple/index" + assert_content_type "text/plain; charset=utf-8" + end + end +end diff --git a/actionpack/test/controller/new_base/render_text_test.rb b/actionpack/test/controller/new_base/render_text_test.rb index 2a253799f3..abb81d7e71 100644 --- a/actionpack/test/controller/new_base/render_text_test.rb +++ b/actionpack/test/controller/new_base/render_text_test.rb @@ -14,7 +14,7 @@ module RenderText self.view_paths = [ActionView::FixtureResolver.new] def index - render :text => "hello david" + render text: "hello david" end end @@ -26,48 +26,48 @@ module RenderText )] def index - render :text => "hello david" + render text: "hello david" end def custom_code - render :text => "hello world", :status => 404 + render text: "hello world", status: 404 end def with_custom_code_as_string - render :text => "hello world", :status => "404 Not Found" + render text: "hello world", status: "404 Not Found" end def with_nil - render :text => nil + render text: nil end def with_nil_and_status - render :text => nil, :status => 403 + render text: nil, status: 403 end def with_false - render :text => false + render text: false end def with_layout_true - render :text => "hello world", :layout => true + render text: "hello world", layout: true end def with_layout_false - render :text => "hello world", :layout => false + render text: "hello world", layout: false end def with_layout_nil - render :text => "hello world", :layout => nil + render text: "hello world", layout: nil end def with_custom_layout - render :text => "hello world", :layout => "greetings" + render text: "hello world", layout: "greetings" end def with_ivar_in_layout @ivar = "hello world" - render :text => "hello world", :layout => "ivar" + render text: "hello world", layout: "ivar" end end @@ -80,7 +80,7 @@ module RenderText test "rendering text from an action with default options renders the text with the layout" do with_routing do |set| - set.draw { get ':controller', :action => 'index' } + set.draw { get ':controller', action: 'index' } get "/render_text/simple" assert_body "hello david" @@ -90,7 +90,7 @@ module RenderText test "rendering text from an action with default options renders the text without the layout" do with_routing do |set| - set.draw { get ':controller', :action => 'index' } + set.draw { get ':controller', action: 'index' } get "/render_text/with_layout" @@ -127,28 +127,28 @@ module RenderText assert_status 200 end - test "rendering text with :layout => true" do + test "rendering text with layout: true" do get "/render_text/with_layout/with_layout_true" assert_body "hello world, I'm here!" assert_status 200 end - test "rendering text with :layout => 'greetings'" do + test "rendering text with layout: 'greetings'" do get "/render_text/with_layout/with_custom_layout" assert_body "hello world, I wish thee well." assert_status 200 end - test "rendering text with :layout => false" do + test "rendering text with layout: false" do get "/render_text/with_layout/with_layout_false" assert_body "hello world" assert_status 200 end - test "rendering text with :layout => nil" do + test "rendering text with layout: nil" do get "/render_text/with_layout/with_layout_nil" assert_body "hello world" diff --git a/actionpack/test/controller/url_for_test.rb b/actionpack/test/controller/url_for_test.rb index d2b4952759..a8035e5bd7 100644 --- a/actionpack/test/controller/url_for_test.rb +++ b/actionpack/test/controller/url_for_test.rb @@ -204,9 +204,6 @@ module AbstractController end def test_relative_url_root_is_respected - # ROUTES TODO: Tests should not have to pass :relative_url_root directly. This - # should probably come from routes. - add_host! assert_equal('https://www.basecamphq.com/subdir/c/a/i', W.new.url_for(:controller => 'c', :action => 'a', :id => 'i', :protocol => 'https', :script_name => '/subdir') diff --git a/actionpack/test/dispatch/cookies_test.rb b/actionpack/test/dispatch/cookies_test.rb index 6101acdc25..ba7aaa338d 100644 --- a/actionpack/test/dispatch/cookies_test.rb +++ b/actionpack/test/dispatch/cookies_test.rb @@ -11,6 +11,16 @@ require 'active_support/key_generator' require 'active_support/message_verifier' class CookiesTest < ActionController::TestCase + class CustomSerializer + def self.load(value) + value.to_s + " and loaded" + end + + def self.dump(value) + value.to_s + " was dumped" + end + end + class TestController < ActionController::Base def authenticate cookies["user_name"] = "david" @@ -359,9 +369,72 @@ class CookiesTest < ActionController::TestCase assert_equal 'Jamie', @controller.send(:cookies).permanent[:user_name] end - def test_signed_cookie + def test_signed_cookie_using_default_serializer get :set_signed_cookie - assert_equal 45, @controller.send(:cookies).signed[:user_id] + cookies = @controller.send :cookies + assert_not_equal 45, cookies[:user_id] + assert_equal 45, cookies.signed[:user_id] + end + + def test_signed_cookie_using_marshal_serializer + @request.env["action_dispatch.cookies_serializer"] = :marshal + get :set_signed_cookie + cookies = @controller.send :cookies + assert_not_equal 45, cookies[:user_id] + assert_equal 45, cookies.signed[:user_id] + end + + def test_signed_cookie_using_json_serializer + @request.env["action_dispatch.cookies_serializer"] = :json + get :set_signed_cookie + cookies = @controller.send :cookies + assert_not_equal 45, cookies[:user_id] + assert_equal 45, cookies.signed[:user_id] + end + + def test_signed_cookie_using_custom_serializer + @request.env["action_dispatch.cookies_serializer"] = CustomSerializer + get :set_signed_cookie + assert_not_equal 45, cookies[:user_id] + assert_equal '45 was dumped and loaded', cookies.signed[:user_id] + end + + def test_signed_cookie_using_hybrid_serializer_can_migrate_marshal_dumped_value_to_json + @request.env["action_dispatch.cookies_serializer"] = :hybrid + + key_generator = @request.env["action_dispatch.key_generator"] + signed_cookie_salt = @request.env["action_dispatch.signed_cookie_salt"] + secret = key_generator.generate_key(signed_cookie_salt) + + marshal_value = ActiveSupport::MessageVerifier.new(secret, serializer: Marshal).generate(45) + @request.headers["Cookie"] = "user_id=#{marshal_value}" + + get :get_signed_cookie + + cookies = @controller.send :cookies + assert_not_equal 45, cookies[:user_id] + assert_equal 45, cookies.signed[:user_id] + + verifier = ActiveSupport::MessageVerifier.new(secret, serializer: JSON) + assert_equal 45, verifier.verify(@response.cookies['user_id']) + end + + def test_signed_cookie_using_hybrid_serializer_can_read_from_json_dumped_value + @request.env["action_dispatch.cookies_serializer"] = :hybrid + + key_generator = @request.env["action_dispatch.key_generator"] + signed_cookie_salt = @request.env["action_dispatch.signed_cookie_salt"] + secret = key_generator.generate_key(signed_cookie_salt) + json_value = ActiveSupport::MessageVerifier.new(secret, serializer: JSON).generate(45) + @request.headers["Cookie"] = "user_id=#{json_value}" + + get :get_signed_cookie + + cookies = @controller.send :cookies + assert_not_equal 45, cookies[:user_id] + assert_equal 45, cookies.signed[:user_id] + + assert_nil @response.cookies["user_id"] end def test_accessing_nonexistant_signed_cookie_should_not_raise_an_invalid_signature @@ -369,43 +442,87 @@ class CookiesTest < ActionController::TestCase assert_nil @controller.send(:cookies).signed[:non_existant_attribute] end - def test_encrypted_cookie + def test_encrypted_cookie_using_default_serializer get :set_encrypted_cookie cookies = @controller.send :cookies assert_not_equal 'bar', cookies[:foo] - assert_raises TypeError do + assert_raise TypeError do cookies.signed[:foo] end assert_equal 'bar', cookies.encrypted[:foo] end - class CustomJsonSerializer - def self.load(value) - JSON.load(value) + " and loaded" - end - - def self.dump(value) - JSON.dump(value + " was dumped") - end - end - - def test_encrypted_cookie_using_serializer_object - @request.env["action_dispatch.session_serializer"] = CustomJsonSerializer + def test_encrypted_cookie_using_marshal_serializer + @request.env["action_dispatch.cookies_serializer"] = :marshal get :set_encrypted_cookie - assert_equal 'bar was dumped and loaded', cookies.encrypted[:foo] + cookies = @controller.send :cookies + assert_not_equal 'bar', cookies[:foo] + assert_raises TypeError do + cookies.signed[:foo] + end + assert_equal 'bar', cookies.encrypted[:foo] end def test_encrypted_cookie_using_json_serializer - @request.env["action_dispatch.session_serializer"] = :json + @request.env["action_dispatch.cookies_serializer"] = :json get :set_encrypted_cookie cookies = @controller.send :cookies assert_not_equal 'bar', cookies[:foo] - assert_raises TypeError do + assert_raises ::JSON::ParserError do cookies.signed[:foo] end assert_equal 'bar', cookies.encrypted[:foo] end + def test_encrypted_cookie_using_custom_serializer + @request.env["action_dispatch.cookies_serializer"] = CustomSerializer + get :set_encrypted_cookie + assert_not_equal 'bar', cookies.encrypted[:foo] + assert_equal 'bar was dumped and loaded', cookies.encrypted[:foo] + end + + def test_encrypted_cookie_using_hybrid_serializer_can_migrate_marshal_dumped_value_to_json + @request.env["action_dispatch.cookies_serializer"] = :hybrid + + key_generator = @request.env["action_dispatch.key_generator"] + encrypted_cookie_salt = @request.env["action_dispatch.encrypted_cookie_salt"] + encrypted_signed_cookie_salt = @request.env["action_dispatch.encrypted_signed_cookie_salt"] + secret = key_generator.generate_key(encrypted_cookie_salt) + sign_secret = key_generator.generate_key(encrypted_signed_cookie_salt) + + marshal_value = ActiveSupport::MessageEncryptor.new(secret, sign_secret, serializer: Marshal).encrypt_and_sign("bar") + @request.headers["Cookie"] = "foo=#{marshal_value}" + + get :get_encrypted_cookie + + cookies = @controller.send :cookies + assert_not_equal "bar", cookies[:foo] + assert_equal "bar", cookies.encrypted[:foo] + + encryptor = ActiveSupport::MessageEncryptor.new(secret, sign_secret, serializer: JSON) + assert_equal "bar", encryptor.decrypt_and_verify(@response.cookies["foo"]) + end + + def test_encrypted_cookie_using_hybrid_serializer_can_read_from_json_dumped_value + @request.env["action_dispatch.cookies_serializer"] = :hybrid + + key_generator = @request.env["action_dispatch.key_generator"] + encrypted_cookie_salt = @request.env["action_dispatch.encrypted_cookie_salt"] + encrypted_signed_cookie_salt = @request.env["action_dispatch.encrypted_signed_cookie_salt"] + secret = key_generator.generate_key(encrypted_cookie_salt) + sign_secret = key_generator.generate_key(encrypted_signed_cookie_salt) + json_value = ActiveSupport::MessageEncryptor.new(secret, sign_secret, serializer: JSON).encrypt_and_sign("bar") + @request.headers["Cookie"] = "foo=#{json_value}" + + get :get_encrypted_cookie + + cookies = @controller.send :cookies + assert_not_equal "bar", cookies[:foo] + assert_equal "bar", cookies.encrypted[:foo] + + assert_nil @response.cookies["foo"] + end + def test_accessing_nonexistant_encrypted_cookie_should_not_raise_invalid_message get :set_encrypted_cookie assert_nil @controller.send(:cookies).encrypted[:non_existant_attribute] @@ -721,8 +838,6 @@ class CookiesTest < ActionController::TestCase assert_equal "dhh", cookies['user_name'] end - - def test_setting_request_cookies_is_indifferent_access cookies.clear cookies[:user_name] = "andrew" diff --git a/actionpack/test/dispatch/rack_test.rb b/actionpack/test/dispatch/rack_test.rb index 42067854ee..ef1964fd19 100644 --- a/actionpack/test/dispatch/rack_test.rb +++ b/actionpack/test/dispatch/rack_test.rb @@ -119,7 +119,7 @@ class RackRequestTest < BaseRackTest assert_equal "[2001:1234:5678:9abc:def0::dead:beef]", @request.host end - test "cgi environment variables" do + test "CGI environment variables" do assert_equal "Basic", @request.auth_type assert_equal 0, @request.content_length assert_equal nil, @request.content_mime_type diff --git a/actionpack/test/dispatch/request_test.rb b/actionpack/test/dispatch/request_test.rb index f79fe47897..40e32cb4d3 100644 --- a/actionpack/test/dispatch/request_test.rb +++ b/actionpack/test/dispatch/request_test.rb @@ -846,8 +846,20 @@ class RequestTest < ActiveSupport::TestCase test "setting variant" do request = stub_request + request.variant = :mobile - assert_equal :mobile, request.variant + assert_equal [:mobile], request.variant + + request.variant = [:phone, :tablet] + assert_equal [:phone, :tablet], request.variant + + assert_raise ArgumentError do + request.variant = [:phone, "tablet"] + end + + assert_raise ArgumentError do + request.variant = "yolo" + end end test "setting variant with non symbol value" do diff --git a/actionpack/test/dispatch/response_test.rb b/actionpack/test/dispatch/response_test.rb index 4501ea095c..1360ede3f8 100644 --- a/actionpack/test/dispatch/response_test.rb +++ b/actionpack/test/dispatch/response_test.rb @@ -217,6 +217,32 @@ class ResponseTest < ActiveSupport::TestCase assert_not @response.respond_to?(:method_missing) assert @response.respond_to?(:method_missing, true) end + + test "can be destructured into status, headers and an enumerable body" do + response = ActionDispatch::Response.new(404, { 'Content-Type' => 'text/plain' }, ['Not Found']) + status, headers, body = response + + assert_equal 404, status + assert_equal({ 'Content-Type' => 'text/plain' }, headers) + assert_equal ['Not Found'], body.each.to_a + end + + test "[response].flatten does not recurse infinitely" do + Timeout.timeout(1) do # use a timeout to prevent it stalling indefinitely + status, headers, body = [@response].flatten + assert_equal @response.status, status + assert_equal @response.headers, headers + assert_equal @response.body, body.each.to_a.join + end + end + + test "does not add default content-type if Content-Type is none" do + resp = ActionDispatch::Response.new.tap { |response| + response.no_content_type = true + } + + assert_not resp.headers.has_key?('Content-Type') + end end class ResponseIntegrationTest < ActionDispatch::IntegrationTest diff --git a/actionpack/test/dispatch/routing_test.rb b/actionpack/test/dispatch/routing_test.rb index 26821bdb56..1fa2cc6cf2 100644 --- a/actionpack/test/dispatch/routing_test.rb +++ b/actionpack/test/dispatch/routing_test.rb @@ -1889,6 +1889,65 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest assert_equal 'notes#destroy', @response.body end + def test_shallow_option_nested_resources_within_scope + draw do + scope '/hello' do + resources :notes, :shallow => true do + resources :trackbacks + end + end + end + + get '/hello/notes/1/trackbacks' + assert_equal 'trackbacks#index', @response.body + assert_equal '/hello/notes/1/trackbacks', note_trackbacks_path(:note_id => 1) + + get '/hello/notes/1/edit' + assert_equal 'notes#edit', @response.body + assert_equal '/hello/notes/1/edit', edit_note_path(:id => '1') + + get '/hello/notes/1/trackbacks/new' + assert_equal 'trackbacks#new', @response.body + assert_equal '/hello/notes/1/trackbacks/new', new_note_trackback_path(:note_id => 1) + + get '/hello/trackbacks/1' + assert_equal 'trackbacks#show', @response.body + assert_equal '/hello/trackbacks/1', trackback_path(:id => '1') + + get '/hello/trackbacks/1/edit' + assert_equal 'trackbacks#edit', @response.body + assert_equal '/hello/trackbacks/1/edit', edit_trackback_path(:id => '1') + + put '/hello/trackbacks/1' + assert_equal 'trackbacks#update', @response.body + + post '/hello/notes/1/trackbacks' + assert_equal 'trackbacks#create', @response.body + + delete '/hello/trackbacks/1' + assert_equal 'trackbacks#destroy', @response.body + + get '/hello/notes' + assert_equal 'notes#index', @response.body + + post '/hello/notes' + assert_equal 'notes#create', @response.body + + get '/hello/notes/new' + assert_equal 'notes#new', @response.body + assert_equal '/hello/notes/new', new_note_path + + get '/hello/notes/1' + assert_equal 'notes#show', @response.body + assert_equal '/hello/notes/1', note_path(:id => 1) + + put '/hello/notes/1' + assert_equal 'notes#update', @response.body + + delete '/hello/notes/1' + assert_equal 'notes#destroy', @response.body + end + def test_custom_resource_routes_are_scoped draw do resources :customers do @@ -2958,6 +3017,22 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest assert_equal '/photos/1', photo_path('1') end + def test_shallow_path_inside_namespace_is_not_added_twice + draw do + namespace :admin do + shallow do + resources :posts do + resources :comments + end + end + end + end + + get '/admin/posts/1/comments' + assert_equal 'admin/comments#index', @response.body + assert_equal '/admin/posts/1/comments', admin_post_comments_path('1') + end + private def draw(&block) diff --git a/actionpack/test/dispatch/static_test.rb b/actionpack/test/dispatch/static_test.rb index 5bd1806b21..afdda70748 100644 --- a/actionpack/test/dispatch/static_test.rb +++ b/actionpack/test/dispatch/static_test.rb @@ -136,10 +136,15 @@ module StaticTests def with_static_file(file) path = "#{FIXTURE_LOAD_PATH}/#{public_path}" + file - File.open(path, "wb+") { |f| f.write(file) } + begin + File.open(path, "wb+") { |f| f.write(file) } + rescue Errno::EPROTO + skip "Couldn't create a file #{path}" + end + yield file ensure - File.delete(path) + File.delete(path) if File.exist? path end end |