aboutsummaryrefslogtreecommitdiffstats
path: root/actionpack/lib
diff options
context:
space:
mode:
Diffstat (limited to 'actionpack/lib')
-rw-r--r--actionpack/lib/action_controller/metal/request_forgery_protection.rb68
-rw-r--r--actionpack/lib/action_controller/metal/strong_parameters.rb87
-rw-r--r--actionpack/lib/action_dispatch/http/request.rb10
-rw-r--r--actionpack/lib/action_dispatch/journey/router.rb40
-rw-r--r--actionpack/lib/action_dispatch/middleware/remote_ip.rb90
-rw-r--r--actionpack/lib/action_dispatch/middleware/static.rb64
-rw-r--r--actionpack/lib/action_dispatch/routing/mapper.rb2
-rw-r--r--actionpack/lib/action_dispatch/routing/polymorphic_routes.rb3
-rw-r--r--actionpack/lib/action_pack/gem_version.rb4
9 files changed, 264 insertions, 104 deletions
diff --git a/actionpack/lib/action_controller/metal/request_forgery_protection.rb b/actionpack/lib/action_controller/metal/request_forgery_protection.rb
index 7afbd767ce..a4f376816f 100644
--- a/actionpack/lib/action_controller/metal/request_forgery_protection.rb
+++ b/actionpack/lib/action_controller/metal/request_forgery_protection.rb
@@ -240,6 +240,8 @@ module ActionController #:nodoc:
content_type =~ %r(\Atext/javascript) && !request.xhr?
end
+ AUTHENTICITY_TOKEN_LENGTH = 32
+
# Returns true or false if a request is verified. Checks:
#
# * is it a GET or HEAD request? Gets should be safe and idempotent
@@ -247,13 +249,73 @@ module ActionController #:nodoc:
# * Does the X-CSRF-Token header match the form_authenticity_token
def verified_request?
!protect_against_forgery? || request.get? || request.head? ||
- form_authenticity_token == form_authenticity_param ||
- form_authenticity_token == request.headers['X-CSRF-Token']
+ valid_authenticity_token?(session, form_authenticity_param) ||
+ valid_authenticity_token?(session, request.headers['X-CSRF-Token'])
end
# Sets the token value for the current session.
def form_authenticity_token
- session[:_csrf_token] ||= SecureRandom.base64(32)
+ masked_authenticity_token(session)
+ end
+
+ # Creates a masked version of the authenticity token that varies
+ # on each request. The masking is used to mitigate SSL attacks
+ # like BREACH.
+ def masked_authenticity_token(session)
+ one_time_pad = SecureRandom.random_bytes(AUTHENTICITY_TOKEN_LENGTH)
+ encrypted_csrf_token = xor_byte_strings(one_time_pad, real_csrf_token(session))
+ masked_token = one_time_pad + encrypted_csrf_token
+ Base64.strict_encode64(masked_token)
+ end
+
+ # Checks the client's masked token to see if it matches the
+ # session token. Essentially the inverse of
+ # +masked_authenticity_token+.
+ def valid_authenticity_token?(session, encoded_masked_token)
+ return false if encoded_masked_token.nil? || encoded_masked_token.empty?
+
+ begin
+ masked_token = Base64.strict_decode64(encoded_masked_token)
+ rescue ArgumentError # encoded_masked_token is invalid Base64
+ return false
+ end
+
+ # See if it's actually a masked token or not. In order to
+ # deploy this code, we should be able to handle any unmasked
+ # tokens that we've issued without error.
+
+ if masked_token.length == AUTHENTICITY_TOKEN_LENGTH
+ # This is actually an unmasked token. This is expected if
+ # you have just upgraded to masked tokens, but should stop
+ # happening shortly after installing this gem
+ compare_with_real_token masked_token, session
+
+ elsif masked_token.length == AUTHENTICITY_TOKEN_LENGTH * 2
+ # Split the token into the one-time pad and the encrypted
+ # value and decrypt it
+ one_time_pad = masked_token[0...AUTHENTICITY_TOKEN_LENGTH]
+ encrypted_csrf_token = masked_token[AUTHENTICITY_TOKEN_LENGTH..-1]
+ csrf_token = xor_byte_strings(one_time_pad, encrypted_csrf_token)
+
+ compare_with_real_token csrf_token, session
+
+ else
+ false # Token is malformed
+ end
+ end
+
+ def compare_with_real_token(token, session)
+ # Borrow a constant-time comparison from Rack
+ Rack::Utils.secure_compare(token, real_csrf_token(session))
+ end
+
+ def real_csrf_token(session)
+ session[:_csrf_token] ||= SecureRandom.base64(AUTHENTICITY_TOKEN_LENGTH)
+ Base64.strict_decode64(session[:_csrf_token])
+ end
+
+ def xor_byte_strings(s1, s2)
+ s1.bytes.zip(s2.bytes).map { |(c1,c2)| c1 ^ c2 }.pack('c*')
end
# The form's authenticity parameter. Override to provide your own.
diff --git a/actionpack/lib/action_controller/metal/strong_parameters.rb b/actionpack/lib/action_controller/metal/strong_parameters.rb
index bc27ecaa20..7038f0997f 100644
--- a/actionpack/lib/action_controller/metal/strong_parameters.rb
+++ b/actionpack/lib/action_controller/metal/strong_parameters.rb
@@ -141,6 +141,37 @@ module ActionController
@permitted = self.class.permit_all_parameters
end
+ # Returns a safe +Hash+ representation of this parameter with all
+ # unpermitted keys removed.
+ #
+ # params = ActionController::Parameters.new({
+ # name: 'Senjougahara Hitagi',
+ # oddity: 'Heavy stone crab'
+ # })
+ # params.to_h # => {}
+ #
+ # safe_params = params.permit(:name)
+ # safe_params.to_h # => {"name"=>"Senjougahara Hitagi"}
+ def to_h
+ if permitted?
+ to_hash
+ else
+ slice(*self.class.always_permitted_parameters).permit!.to_h
+ end
+ end
+
+ # Convert all hashes in values into parameters, then yield each pair like
+ # the same way as <tt>Hash#each_pair</tt>
+ def each_pair(&block)
+ super do |key, value|
+ convert_hashes_to_parameters(key, value)
+ end
+
+ super
+ end
+
+ alias_method :each, :each_pair
+
# Attribute that keeps track of converted arrays, if any, to avoid double
# looping in the common use case permit + mass-assignment. Defined in a
# method to instantiate it only if needed.
@@ -176,7 +207,6 @@ module ActionController
# Person.new(params) # => #<Person id: nil, name: "Francesco">
def permit!
each_pair do |key, value|
- value = convert_hashes_to_parameters(key, value)
Array.wrap(value).each do |v|
v.permit! if v.respond_to? :permit!
end
@@ -331,11 +361,56 @@ module ActionController
# params.slice(:a, :b) # => {"a"=>1, "b"=>2}
# params.slice(:d) # => {}
def slice(*keys)
- self.class.new(super).tap do |new_instance|
- new_instance.permitted = @permitted
+ new_instance_with_inherited_permitted_status(super)
+ end
+
+ # Removes and returns the key/value pairs matching the given keys.
+ #
+ # params = ActionController::Parameters.new(a: 1, b: 2, c: 3)
+ # params.extract!(:a, :b) # => {"a"=>1, "b"=>2}
+ # params # => {"c"=>3}
+ def extract!(*keys)
+ new_instance_with_inherited_permitted_status(super)
+ end
+
+ # Returns a new <tt>ActionController::Parameters</tt> with the results of
+ # running +block+ once for every value. The keys are unchanged.
+ #
+ # params = ActionController::Parameters.new(a: 1, b: 2, c: 3)
+ # params.transform_values { |x| x * 2 }
+ # # => {"a"=>2, "b"=>4, "c"=>6}
+ def transform_values
+ if block_given?
+ new_instance_with_inherited_permitted_status(super)
+ else
+ super
+ end
+ end
+
+ # This method is here only to make sure that the returned object has the
+ # correct +permitted+ status. It should not matter since the parent of
+ # this object is +HashWithIndifferentAccess+
+ def transform_keys # :nodoc:
+ if block_given?
+ new_instance_with_inherited_permitted_status(super)
+ else
+ super
end
end
+ # Deletes and returns a key-value pair from +Parameters+ whose key is equal
+ # to key. If the key is not found, returns the default value. If the
+ # optional code block is given and the key is not found, pass in the key
+ # and return the result of block.
+ def delete(key, &block)
+ convert_hashes_to_parameters(key, super, false)
+ end
+
+ # Equivalent to Hash#keep_if, but returns nil if no changes were made.
+ def select!(&block)
+ convert_value_to_parameters(super)
+ end
+
# Returns an exact copy of the <tt>ActionController::Parameters</tt>
# instance. +permitted+ state is kept on the duped object.
#
@@ -356,6 +431,12 @@ module ActionController
end
private
+ def new_instance_with_inherited_permitted_status(hash)
+ self.class.new(hash).tap do |new_instance|
+ new_instance.permitted = @permitted
+ end
+ end
+
def convert_hashes_to_parameters(key, value, assign_if_converted=true)
converted = convert_value_to_parameters(value)
self[key] = converted if assign_if_converted && !converted.equal?(value)
diff --git a/actionpack/lib/action_dispatch/http/request.rb b/actionpack/lib/action_dispatch/http/request.rb
index 8c035c3c6c..c441f7f95b 100644
--- a/actionpack/lib/action_dispatch/http/request.rb
+++ b/actionpack/lib/action_dispatch/http/request.rb
@@ -105,6 +105,12 @@ module ActionDispatch
@request_method ||= check_method(env["REQUEST_METHOD"])
end
+ def request_method=(request_method) #:nodoc:
+ if check_method(request_method)
+ @request_method = env["REQUEST_METHOD"] = request_method
+ end
+ end
+
# Returns a symbol form of the #request_method
def request_method_symbol
HTTP_METHOD_LOOKUP[request_method]
@@ -292,7 +298,7 @@ module ActionDispatch
# Override Rack's GET method to support indifferent access
def GET
@env["action_dispatch.request.query_parameters"] ||= Utils.deep_munge(normalize_encode_params(super || {}))
- rescue TypeError => e
+ rescue TypeError, Rack::Utils::InvalidParameterError => e
raise ActionController::BadRequest.new(:query, e)
end
alias :query_parameters :GET
@@ -300,7 +306,7 @@ module ActionDispatch
# Override Rack's POST method to support indifferent access
def POST
@env["action_dispatch.request.request_parameters"] ||= Utils.deep_munge(normalize_encode_params(super || {}))
- rescue TypeError => e
+ rescue TypeError, Rack::Utils::InvalidParameterError => e
raise ActionController::BadRequest.new(:request, e)
end
alias :request_parameters :POST
diff --git a/actionpack/lib/action_dispatch/journey/router.rb b/actionpack/lib/action_dispatch/journey/router.rb
index 21817b374c..9131b65380 100644
--- a/actionpack/lib/action_dispatch/journey/router.rb
+++ b/actionpack/lib/action_dispatch/journey/router.rb
@@ -101,11 +101,13 @@ module ActionDispatch
r.path.match(req.path_info)
}
- if req.env["REQUEST_METHOD"] === "HEAD"
- routes.concat get_routes_as_head(routes)
- end
+ routes =
+ if req.request_method == "HEAD"
+ match_head_routes(routes, req)
+ else
+ match_routes(routes, req)
+ end
- routes.select! { |r| r.matches?(req) }
routes.sort_by!(&:precedence)
routes.map! { |r|
@@ -118,19 +120,23 @@ module ActionDispatch
}
end
- def get_routes_as_head(routes)
- precedence = (routes.map(&:precedence).max || 0) + 1
- routes.select { |r|
- r.verb === "GET" && !(r.verb === "HEAD")
- }.map! { |r|
- Route.new(r.name,
- r.app,
- r.path,
- r.conditions.merge(request_method: "HEAD"),
- r.defaults).tap do |route|
- route.precedence = r.precedence + precedence
- end
- }
+ def match_head_routes(routes, req)
+ head_routes = match_routes(routes, req)
+
+ if head_routes.empty?
+ begin
+ req.request_method = "GET"
+ match_routes(routes, req)
+ ensure
+ req.request_method = "HEAD"
+ end
+ else
+ head_routes
+ end
+ end
+
+ def match_routes(routes, req)
+ routes.select { |r| r.matches?(req) }
end
end
end
diff --git a/actionpack/lib/action_dispatch/middleware/remote_ip.rb b/actionpack/lib/action_dispatch/middleware/remote_ip.rb
index 6a79b4e859..7c4236518d 100644
--- a/actionpack/lib/action_dispatch/middleware/remote_ip.rb
+++ b/actionpack/lib/action_dispatch/middleware/remote_ip.rb
@@ -1,3 +1,5 @@
+require 'ipaddr'
+
module ActionDispatch
# This middleware calculates the IP address of the remote client that is
# making the request. It does this by checking various headers that could
@@ -28,14 +30,14 @@ module ActionDispatch
# guaranteed by the IP specification to be private addresses. Those will
# not be the ultimate client IP in production, and so are discarded. See
# http://en.wikipedia.org/wiki/Private_network for details.
- TRUSTED_PROXIES = %r{
- ^127\.0\.0\.1$ | # localhost IPv4
- ^::1$ | # localhost IPv6
- ^[fF][cCdD] | # private IPv6 range fc00::/7
- ^10\. | # private IPv4 range 10.x.x.x
- ^172\.(1[6-9]|2[0-9]|3[0-1])\.| # private IPv4 range 172.16.0.0 .. 172.31.255.255
- ^192\.168\. # private IPv4 range 192.168.x.x
- }x
+ TRUSTED_PROXIES = [
+ "127.0.0.1", # localhost IPv4
+ "::1", # localhost IPv6
+ "fc00::/7", # private IPv6 range fc00::/7
+ "10.0.0.0/8", # private IPv4 range 10.x.x.x
+ "172.16.0.0/12", # private IPv4 range 172.16.0.0 .. 172.31.255.255
+ "192.168.0.0/16", # private IPv4 range 192.168.x.x
+ ].map { |proxy| IPAddr.new(proxy) }
attr_reader :check_ip, :proxies
@@ -47,24 +49,24 @@ module ActionDispatch
# clients (like WAP devices), or behind proxies that set headers in an
# incorrect or confusing way (like AWS ELB).
#
- # 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_proxies+ parameter. That way, the middleware will ignore those
- # IP addresses, and return the one that you want.
+ # The +custom_proxies+ argument can take an Array of string, IPAddr, or
+ # Regexp objects which will be used instead of +TRUSTED_PROXIES+. If a
+ # single string, IPAddr, or Regexp object is provided, it 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_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
@check_ip = check_ip_spoofing
- @proxies = case custom_proxies
- when Regexp
- custom_proxies
- when nil
- TRUSTED_PROXIES
- else
- Regexp.union(TRUSTED_PROXIES, custom_proxies)
- end
+ @proxies = if custom_proxies.blank?
+ TRUSTED_PROXIES
+ elsif custom_proxies.respond_to?(:any?)
+ custom_proxies
+ else
+ Array(custom_proxies) + TRUSTED_PROXIES
+ end
end
# Since the IP address may not be needed, we store the object here
@@ -80,32 +82,6 @@ module ActionDispatch
# into an actual IP address. If the ActionDispatch::Request#remote_ip method
# is called, this class will calculate the value and then memoize it.
class GetIp
-
- # This constant contains a regular expression that validates every known
- # form of IP v4 and v6 address, with or without abbreviations, adapted
- # from {this gist}[https://gist.github.com/gazay/1289635].
- VALID_IP = %r{
- (^(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[0-9]{1,2})(\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[0-9]{1,2})){3}$) | # ip v4
- (^(
- (([0-9A-Fa-f]{1,4}:){7}[0-9A-Fa-f]{1,4}) | # ip v6 not abbreviated
- (([0-9A-Fa-f]{1,4}:){6}:[0-9A-Fa-f]{1,4}) | # ip v6 with double colon in the end
- (([0-9A-Fa-f]{1,4}:){5}:([0-9A-Fa-f]{1,4}:)?[0-9A-Fa-f]{1,4}) | # - ip addresses v6
- (([0-9A-Fa-f]{1,4}:){4}:([0-9A-Fa-f]{1,4}:){0,2}[0-9A-Fa-f]{1,4}) | # - with
- (([0-9A-Fa-f]{1,4}:){3}:([0-9A-Fa-f]{1,4}:){0,3}[0-9A-Fa-f]{1,4}) | # - double colon
- (([0-9A-Fa-f]{1,4}:){2}:([0-9A-Fa-f]{1,4}:){0,4}[0-9A-Fa-f]{1,4}) | # - in the middle
- (([0-9A-Fa-f]{1,4}:){6} ((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)\.){3} (\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)) | # ip v6 with compatible to v4
- (([0-9A-Fa-f]{1,4}:){1,5}:((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)\.){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)) | # ip v6 with compatible to v4
- (([0-9A-Fa-f]{1,4}:){1}:([0-9A-Fa-f]{1,4}:){0,4}((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)\.){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)) | # ip v6 with compatible to v4
- (([0-9A-Fa-f]{1,4}:){0,2}:([0-9A-Fa-f]{1,4}:){0,3}((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)\.){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)) | # ip v6 with compatible to v4
- (([0-9A-Fa-f]{1,4}:){0,3}:([0-9A-Fa-f]{1,4}:){0,2}((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)\.){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)) | # ip v6 with compatible to v4
- (([0-9A-Fa-f]{1,4}:){0,4}:([0-9A-Fa-f]{1,4}:){1}((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)\.){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)) | # ip v6 with compatible to v4
- (::([0-9A-Fa-f]{1,4}:){0,5}((\b((25[0-5])|(1\d{2})|(2[0-4]\d) |(\d{1,2}))\b)\.){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)) | # ip v6 with compatible to v4
- ([0-9A-Fa-f]{1,4}::([0-9A-Fa-f]{1,4}:){0,5}[0-9A-Fa-f]{1,4}) | # ip v6 with compatible to v4
- (::([0-9A-Fa-f]{1,4}:){0,6}[0-9A-Fa-f]{1,4}) | # ip v6 with double colon at the beginning
- (([0-9A-Fa-f]{1,4}:){1,7}:) # ip v6 without ending
- )$)
- }x
-
def initialize(env, middleware)
@env = env
@check_ip = middleware.check_ip
@@ -173,12 +149,22 @@ module ActionDispatch
def ips_from(header)
# Split the comma-separated list into an array of strings
ips = @env[header] ? @env[header].strip.split(/[,\s]+/) : []
- # Only return IPs that are valid according to the regex
- ips.select{ |ip| ip =~ VALID_IP }
+ ips.select do |ip|
+ begin
+ # Only return IPs that are valid according to the IPAddr#new method
+ range = IPAddr.new(ip).to_range
+ # we want to make sure nobody is sneaking a netmask in
+ range.begin == range.end
+ rescue ArgumentError
+ nil
+ end
+ end
end
def filter_proxies(ips)
- ips.reject { |ip| ip =~ @proxies }
+ ips.reject do |ip|
+ @proxies.any? { |proxy| proxy === ip }
+ end
end
end
diff --git a/actionpack/lib/action_dispatch/middleware/static.rb b/actionpack/lib/action_dispatch/middleware/static.rb
index 25e32cdef8..e66c21ef85 100644
--- a/actionpack/lib/action_dispatch/middleware/static.rb
+++ b/actionpack/lib/action_dispatch/middleware/static.rb
@@ -16,43 +16,63 @@ module ActionDispatch
def initialize(root, cache_control)
@root = root.chomp('/')
@compiled_root = /^#{Regexp.escape(root)}/
- headers = cache_control && { 'Cache-Control' => cache_control }
- @file_server = ::Rack::File.new(@root, headers)
+ headers = cache_control && { 'Cache-Control' => cache_control }
+ @file_server = ::Rack::File.new(@root, headers)
end
def match?(path)
- path = unescape_path(path)
+ path = URI.parser.unescape(path)
return false unless path.valid_encoding?
- full_path = path.empty? ? @root : File.join(@root, escape_glob_chars(path))
- paths = "#{full_path}#{ext}"
+ paths = [path, "#{path}#{ext}", "#{path}/index#{ext}"]
- matches = Dir[paths]
- match = matches.detect { |m| File.file?(m) }
- if match
- match.sub!(@compiled_root, '')
- ::Rack::Utils.escape(match)
+ if match = paths.detect {|p| File.file?(File.join(@root, p)) }
+ return ::Rack::Utils.escape(match)
end
end
def call(env)
- @file_server.call(env)
- end
+ path = env['PATH_INFO']
+ gzip_path = gzip_file_path(path)
- def ext
- @ext ||= begin
- ext = ::ActionController::Base.default_static_extension
- "{,#{ext},/index#{ext}}"
+ if gzip_path && gzip_encoding_accepted?(env)
+ env['PATH_INFO'] = gzip_path
+ status, headers, body = @file_server.call(env)
+ headers['Content-Encoding'] = 'gzip'
+ headers['Content-Type'] = content_type(path)
+ else
+ status, headers, body = @file_server.call(env)
end
- end
- def unescape_path(path)
- URI.parser.unescape(path)
- end
+ headers['Vary'] = 'Accept-Encoding' if gzip_path
- def escape_glob_chars(path)
- path.gsub(/[*?{}\[\]]/, "\\\\\\&")
+ return [status, headers, body]
+ ensure
+ env['PATH_INFO'] = path
end
+
+ private
+ def ext
+ ::ActionController::Base.default_static_extension
+ end
+
+ def content_type(path)
+ ::Rack::Mime.mime_type(::File.extname(path), 'text/plain')
+ end
+
+ def gzip_encoding_accepted?(env)
+ env['HTTP_ACCEPT_ENCODING'] =~ /\bgzip\b/i
+ end
+
+ def gzip_file_path(path)
+ can_gzip_mime = content_type(path) =~ /\A(?:text\/|application\/javascript)/
+ gzip_path = "#{path}.gz"
+ if can_gzip_mime && File.exist?(File.join(@root, ::Rack::Utils.unescape(gzip_path)))
+ gzip_path
+ else
+ false
+ end
+ end
end
# This middleware will attempt to return the contents of a file's body from
diff --git a/actionpack/lib/action_dispatch/routing/mapper.rb b/actionpack/lib/action_dispatch/routing/mapper.rb
index e92baa5aa7..fc28740828 100644
--- a/actionpack/lib/action_dispatch/routing/mapper.rb
+++ b/actionpack/lib/action_dispatch/routing/mapper.rb
@@ -241,8 +241,6 @@ module ActionDispatch
end
def app(blocks)
- return to if Redirect === to
-
if to.respond_to?(:call)
Constraints.new(to, blocks, false)
else
diff --git a/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb b/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb
index 427a5674bd..f15868d37e 100644
--- a/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb
+++ b/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb
@@ -197,7 +197,8 @@ module ActionDispatch
case record_or_hash_or_array
when Array
- if record_or_hash_or_array.empty? || record_or_hash_or_array.include?(nil)
+ record_or_hash_or_array = record_or_hash_or_array.compact
+ if record_or_hash_or_array.empty?
raise ArgumentError, "Nil location provided. Can't build URI."
end
if record_or_hash_or_array.first.is_a?(ActionDispatch::Routing::RoutesProxy)
diff --git a/actionpack/lib/action_pack/gem_version.rb b/actionpack/lib/action_pack/gem_version.rb
index beaf35d3da..280d35adcb 100644
--- a/actionpack/lib/action_pack/gem_version.rb
+++ b/actionpack/lib/action_pack/gem_version.rb
@@ -1,5 +1,5 @@
module ActionPack
- # Returns the version of the currently loaded ActionPack as a <tt>Gem::Version</tt>
+ # Returns the version of the currently loaded Action Pack as a <tt>Gem::Version</tt>
def self.gem_version
Gem::Version.new VERSION::STRING
end
@@ -8,7 +8,7 @@ module ActionPack
MAJOR = 4
MINOR = 2
TINY = 0
- PRE = "alpha"
+ PRE = "beta1"
STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
end