aboutsummaryrefslogtreecommitdiffstats
path: root/actionpack/lib/action_dispatch
diff options
context:
space:
mode:
Diffstat (limited to 'actionpack/lib/action_dispatch')
-rw-r--r--actionpack/lib/action_dispatch/http/cache.rb9
-rw-r--r--actionpack/lib/action_dispatch/http/headers.rb29
-rw-r--r--actionpack/lib/action_dispatch/http/mime_negotiation.rb13
-rw-r--r--actionpack/lib/action_dispatch/http/mime_type.rb6
-rw-r--r--actionpack/lib/action_dispatch/http/parameter_filter.rb2
-rw-r--r--actionpack/lib/action_dispatch/http/parameters.rb19
-rw-r--r--actionpack/lib/action_dispatch/http/request.rb53
-rw-r--r--actionpack/lib/action_dispatch/http/response.rb80
-rw-r--r--actionpack/lib/action_dispatch/http/upload.rb10
-rw-r--r--actionpack/lib/action_dispatch/http/url.rb245
-rw-r--r--actionpack/lib/action_dispatch/journey/formatter.rb8
-rw-r--r--actionpack/lib/action_dispatch/journey/gtg/transition_table.rb6
-rw-r--r--actionpack/lib/action_dispatch/journey/nfa/transition_table.rb47
-rw-r--r--actionpack/lib/action_dispatch/journey/parser.rb54
-rw-r--r--actionpack/lib/action_dispatch/journey/parser.y22
-rw-r--r--actionpack/lib/action_dispatch/journey/path/pattern.rb9
-rw-r--r--actionpack/lib/action_dispatch/journey/route.rb8
-rw-r--r--actionpack/lib/action_dispatch/journey/router.rb47
-rw-r--r--actionpack/lib/action_dispatch/journey/router/utils.rb12
-rw-r--r--actionpack/lib/action_dispatch/journey/scanner.rb10
-rw-r--r--actionpack/lib/action_dispatch/journey/visualizer/fsm.css4
-rw-r--r--actionpack/lib/action_dispatch/middleware/callbacks.rb2
-rw-r--r--actionpack/lib/action_dispatch/middleware/cookies.rb68
-rw-r--r--actionpack/lib/action_dispatch/middleware/debug_exceptions.rb51
-rw-r--r--actionpack/lib/action_dispatch/middleware/exception_wrapper.rb77
-rw-r--r--actionpack/lib/action_dispatch/middleware/flash.rb48
-rw-r--r--actionpack/lib/action_dispatch/middleware/params_parser.rb2
-rw-r--r--actionpack/lib/action_dispatch/middleware/public_exceptions.rb10
-rw-r--r--actionpack/lib/action_dispatch/middleware/remote_ip.rb94
-rw-r--r--actionpack/lib/action_dispatch/middleware/request_id.rb16
-rw-r--r--actionpack/lib/action_dispatch/middleware/session/cache_store.rb6
-rw-r--r--actionpack/lib/action_dispatch/middleware/session/cookie_store.rb2
-rw-r--r--actionpack/lib/action_dispatch/middleware/show_exceptions.rb1
-rw-r--r--actionpack/lib/action_dispatch/middleware/static.rb92
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/_request_and_response.html.erb16
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/_source.erb40
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb46
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.text.erb10
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/layout.erb6
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/missing_template.html.erb4
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/routing_error.html.erb2
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb25
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.text.erb1
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/routes/_table.html.erb2
-rw-r--r--actionpack/lib/action_dispatch/request/utils.rb4
-rw-r--r--actionpack/lib/action_dispatch/routing/inspector.rb6
-rw-r--r--actionpack/lib/action_dispatch/routing/mapper.rb471
-rw-r--r--actionpack/lib/action_dispatch/routing/polymorphic_routes.rb51
-rw-r--r--actionpack/lib/action_dispatch/routing/redirection.rb2
-rw-r--r--actionpack/lib/action_dispatch/routing/route_set.rb269
-rw-r--r--actionpack/lib/action_dispatch/routing/routes_proxy.rb9
-rw-r--r--actionpack/lib/action_dispatch/routing/url_for.rb13
-rw-r--r--actionpack/lib/action_dispatch/testing/assertions.rb18
-rw-r--r--actionpack/lib/action_dispatch/testing/assertions/dom.rb27
-rw-r--r--actionpack/lib/action_dispatch/testing/assertions/response.rb9
-rw-r--r--actionpack/lib/action_dispatch/testing/assertions/routing.rb46
-rw-r--r--actionpack/lib/action_dispatch/testing/assertions/selector.rb430
-rw-r--r--actionpack/lib/action_dispatch/testing/assertions/tag.rb135
-rw-r--r--actionpack/lib/action_dispatch/testing/integration.rb310
-rw-r--r--actionpack/lib/action_dispatch/testing/test_request.rb4
-rw-r--r--actionpack/lib/action_dispatch/testing/test_response.rb6
61 files changed, 1564 insertions, 1560 deletions
diff --git a/actionpack/lib/action_dispatch/http/cache.rb b/actionpack/lib/action_dispatch/http/cache.rb
index f9b278349e..747d295261 100644
--- a/actionpack/lib/action_dispatch/http/cache.rb
+++ b/actionpack/lib/action_dispatch/http/cache.rb
@@ -69,17 +69,17 @@ module ActionDispatch
end
def date
- if date_header = headers['Date']
+ if date_header = headers[DATE]
Time.httpdate(date_header)
end
end
def date?
- headers.include?('Date')
+ headers.include?(DATE)
end
def date=(utc_time)
- headers['Date'] = utc_time.httpdate
+ headers[DATE] = utc_time.httpdate
end
def etag=(etag)
@@ -89,10 +89,11 @@ module ActionDispatch
private
+ DATE = 'Date'.freeze
LAST_MODIFIED = "Last-Modified".freeze
ETAG = "ETag".freeze
CACHE_CONTROL = "Cache-Control".freeze
- SPECIAL_KEYS = %w[extras no-cache max-age public must-revalidate]
+ SPECIAL_KEYS = Set.new(%w[extras no-cache max-age public must-revalidate])
def cache_control_segments
if cache_control = self[CACHE_CONTROL]
diff --git a/actionpack/lib/action_dispatch/http/headers.rb b/actionpack/lib/action_dispatch/http/headers.rb
index 3e607bbde1..bc5410dc38 100644
--- a/actionpack/lib/action_dispatch/http/headers.rb
+++ b/actionpack/lib/action_dispatch/http/headers.rb
@@ -6,14 +6,27 @@ module ActionDispatch
# headers = ActionDispatch::Http::Headers.new(env)
# headers["Content-Type"] # => "text/plain"
class Headers
- CGI_VARIABLES = %w(
- CONTENT_TYPE CONTENT_LENGTH
- HTTPS AUTH_TYPE GATEWAY_INTERFACE
- PATH_INFO PATH_TRANSLATED QUERY_STRING
- REMOTE_ADDR REMOTE_HOST REMOTE_IDENT REMOTE_USER
- REQUEST_METHOD SCRIPT_NAME
- SERVER_NAME SERVER_PORT SERVER_PROTOCOL SERVER_SOFTWARE
- )
+ CGI_VARIABLES = Set.new(%W[
+ AUTH_TYPE
+ CONTENT_LENGTH
+ CONTENT_TYPE
+ GATEWAY_INTERFACE
+ HTTPS
+ PATH_INFO
+ PATH_TRANSLATED
+ QUERY_STRING
+ REMOTE_ADDR
+ REMOTE_HOST
+ REMOTE_IDENT
+ REMOTE_USER
+ REQUEST_METHOD
+ SCRIPT_NAME
+ SERVER_NAME
+ SERVER_PORT
+ SERVER_PROTOCOL
+ SERVER_SOFTWARE
+ ]).freeze
+
HTTP_HEADER = /\A[A-Za-z0-9-]+\z/
include Enumerable
diff --git a/actionpack/lib/action_dispatch/http/mime_negotiation.rb b/actionpack/lib/action_dispatch/http/mime_negotiation.rb
index 0b2b60d2e4..53a98c5d0a 100644
--- a/actionpack/lib/action_dispatch/http/mime_negotiation.rb
+++ b/actionpack/lib/action_dispatch/http/mime_negotiation.rb
@@ -54,8 +54,14 @@ module ActionDispatch
end
def formats
- @env["action_dispatch.request.formats"] ||=
- if parameters[:format]
+ @env["action_dispatch.request.formats"] ||= begin
+ params_readable = begin
+ parameters[:format]
+ rescue ActionController::BadRequest
+ false
+ end
+
+ if params_readable
Array(Mime[parameters[:format]])
elsif use_accept_header && valid_accept_header
accepts
@@ -64,13 +70,14 @@ module ActionDispatch
else
[Mime::HTML]
end
+ end
end
# Sets the \variant for template.
def variant=(variant)
if variant.is_a?(Symbol)
@variant = [variant]
- elsif variant.is_a?(Array) && variant.any? && variant.all?{ |v| v.is_a?(Symbol) }
+ elsif variant.nil? || 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 or an Array of Symbols, not a #{variant.class}. " \
diff --git a/actionpack/lib/action_dispatch/http/mime_type.rb b/actionpack/lib/action_dispatch/http/mime_type.rb
index 9450be838c..7e585aa244 100644
--- a/actionpack/lib/action_dispatch/http/mime_type.rb
+++ b/actionpack/lib/action_dispatch/http/mime_type.rb
@@ -6,7 +6,7 @@ require 'active_support/core_ext/string/starts_ends_with'
module Mime
class Mimes < Array
def symbols
- @symbols ||= map { |m| m.to_sym }
+ @symbols ||= map(&:to_sym)
end
%w(<< concat shift unshift push pop []= clear compact! collect!
@@ -45,8 +45,8 @@ module Mime
#
# respond_to do |format|
# format.html
- # format.ics { render text: post.to_ics, mime_type: Mime::Type["text/calendar"] }
- # format.xml { render xml: @people }
+ # format.ics { render text: @post.to_ics, mime_type: Mime::Type.lookup("text/calendar") }
+ # format.xml { render xml: @post }
# end
# end
# end
diff --git a/actionpack/lib/action_dispatch/http/parameter_filter.rb b/actionpack/lib/action_dispatch/http/parameter_filter.rb
index b655a54865..df4b073a17 100644
--- a/actionpack/lib/action_dispatch/http/parameter_filter.rb
+++ b/actionpack/lib/action_dispatch/http/parameter_filter.rb
@@ -56,7 +56,7 @@ module ActionDispatch
elsif value.is_a?(Array)
value = value.map { |v| v.is_a?(Hash) ? call(v) : v }
elsif blocks.any?
- key = key.dup
+ key = key.dup if key.duplicable?
value = value.dup if value.duplicable?
blocks.each { |b| b.call(key, value) }
end
diff --git a/actionpack/lib/action_dispatch/http/parameters.rb b/actionpack/lib/action_dispatch/http/parameters.rb
index 1ab11392ce..c2f05ecc86 100644
--- a/actionpack/lib/action_dispatch/http/parameters.rb
+++ b/actionpack/lib/action_dispatch/http/parameters.rb
@@ -15,7 +15,6 @@ module ActionDispatch
query_parameters.dup
end
params.merge!(path_parameters)
- params.with_indifferent_access
end
end
alias :params :parameters
@@ -25,40 +24,26 @@ module ActionDispatch
@env[PARAMETERS_KEY] = parameters
end
- # The same as <tt>path_parameters</tt> with explicitly symbolized keys.
- def symbolized_path_parameters
- path_parameters
- end
-
# Returns a hash with the \parameters used to form the \path of the request.
# Returned hash keys are strings:
#
# {'action' => 'my_action', 'controller' => 'my_controller'}
- #
- # See <tt>symbolized_path_parameters</tt> for symbolized keys.
def path_parameters
@env[PARAMETERS_KEY] ||= {}
end
private
- # Convert nested Hash to HashWithIndifferentAccess
- # and UTF-8 encode both keys and values in nested Hash.
+ # Convert nested Hash to HashWithIndifferentAccess.
#
- # TODO: Validate that the characters are UTF-8. If they aren't,
- # you'll get a weird error down the road, but our form handling
- # should really prevent that from happening
def normalize_encode_params(params)
case params
- when String
- params.force_encoding(Encoding::UTF_8).encode!
when Hash
if params.has_key?(:tempfile)
UploadedFile.new(params)
else
params.each_with_object({}) do |(key, val), new_hash|
- new_key = key.is_a?(String) ? key.dup.force_encoding(Encoding::UTF_8).encode! : key
- new_hash[new_key] = if val.is_a?(Array)
+ new_hash[key] = if val.is_a?(Array)
val.map! { |el| normalize_encode_params(el) }
else
normalize_encode_params(val)
diff --git a/actionpack/lib/action_dispatch/http/request.rb b/actionpack/lib/action_dispatch/http/request.rb
index dfe258e463..07b3814ca4 100644
--- a/actionpack/lib/action_dispatch/http/request.rb
+++ b/actionpack/lib/action_dispatch/http/request.rb
@@ -23,7 +23,7 @@ module ActionDispatch
autoload :Session, 'action_dispatch/request/session'
autoload :Utils, 'action_dispatch/request/utils'
- LOCALHOST = Regexp.union [/^127\.0\.0\.\d{1,3}$/, /^::1$/, /^0:0:0:0:0:0:0:1(%.*)?$/]
+ LOCALHOST = Regexp.union [/^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/, /^::1$/, /^0:0:0:0:0:0:0:1(%.*)?$/]
ENV_METHODS = %w[ AUTH_TYPE GATEWAY_INTERFACE
PATH_TRANSLATED REMOTE_HOST
@@ -50,7 +50,7 @@ module ActionDispatch
@original_fullpath = nil
@fullpath = nil
@ip = nil
- @uuid = nil
+ @request_id = nil
end
def check_path_parameters!
@@ -105,6 +105,24 @@ module ActionDispatch
@request_method ||= check_method(env["REQUEST_METHOD"])
end
+ def routes # :nodoc:
+ env["action_dispatch.routes".freeze]
+ end
+
+ def original_script_name # :nodoc:
+ env['ORIGINAL_SCRIPT_NAME'.freeze]
+ end
+
+ def engine_script_name(_routes) # :nodoc:
+ env["ROUTES_#{_routes.object_id}_SCRIPT_NAME"]
+ 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]
@@ -209,8 +227,8 @@ module ActionDispatch
end
# Returns true if the "X-Requested-With" header contains "XMLHttpRequest"
- # (case-insensitive). All major JavaScript libraries send this header with
- # every Ajax request.
+ # (case-insensitive), which may need to be manually added depending on the
+ # choice of JavaScript libraries and frameworks.
def xml_http_request?
@env['HTTP_X_REQUESTED_WITH'] =~ /XMLHttpRequest/i
end
@@ -225,16 +243,18 @@ module ActionDispatch
@remote_ip ||= (@env["action_dispatch.remote_ip"] || ip).to_s
end
- # Returns the unique request id, which is based off either the X-Request-Id header that can
+ # Returns the unique request id, which is based on either the X-Request-Id header that can
# be generated by a firewall, load balancer, or web server or by the RequestId middleware
# (which sets the action_dispatch.request_id environment variable).
#
# This unique ID is useful for tracing a request from end-to-end as part of logging or debugging.
# This relies on the rack variable set by the ActionDispatch::RequestId middleware.
- def uuid
- @uuid ||= env["action_dispatch.request_id"]
+ def request_id
+ @request_id ||= env["action_dispatch.request_id"]
end
+ alias_method :uuid, :request_id
+
# Returns the lowercase name of the HTTP server software.
def server_software
(@env['SERVER_SOFTWARE'] && /^([a-zA-Z]+)/ =~ @env['SERVER_SOFTWARE']) ? $1.downcase : nil
@@ -291,16 +311,16 @@ 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
+ @env["action_dispatch.request.query_parameters"] ||= Utils.deep_munge(normalize_encode_params(super || {}))
+ rescue Rack::Utils::ParameterTypeError, Rack::Utils::InvalidParameterError => e
raise ActionController::BadRequest.new(:query, e)
end
alias :query_parameters :GET
# 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
+ @env["action_dispatch.request.request_parameters"] ||= Utils.deep_munge(normalize_encode_params(super || {}))
+ rescue Rack::Utils::ParameterTypeError, Rack::Utils::InvalidParameterError => e
raise ActionController::BadRequest.new(:request, e)
end
alias :request_parameters :POST
@@ -319,15 +339,6 @@ module ActionDispatch
LOCALHOST =~ remote_addr && LOCALHOST =~ remote_ip
end
- # Extracted into ActionDispatch::Request::Utils.deep_munge, but kept here for backwards compatibility.
- def deep_munge(hash)
- ActiveSupport::Deprecation.warn(
- "This method has been extracted into ActionDispatch::Request::Utils.deep_munge. Please start using that instead."
- )
-
- Utils.deep_munge(hash)
- end
-
protected
def parse_query(qs)
Utils.deep_munge(super)
@@ -335,7 +346,7 @@ module ActionDispatch
private
def check_method(name)
- HTTP_METHOD_LOOKUP[name] || raise(ActionController::UnknownHttpMethod, "#{name}, accepted HTTP methods are #{HTTP_METHODS.to_sentence(:locale => :en)}")
+ HTTP_METHOD_LOOKUP[name] || raise(ActionController::UnknownHttpMethod, "#{name}, accepted HTTP methods are #{HTTP_METHODS[0...-1].join(', ')}, and #{HTTP_METHODS[-1]}")
name
end
end
diff --git a/actionpack/lib/action_dispatch/http/response.rb b/actionpack/lib/action_dispatch/http/response.rb
index eaea93b730..a895d1ab18 100644
--- a/actionpack/lib/action_dispatch/http/response.rb
+++ b/actionpack/lib/action_dispatch/http/response.rb
@@ -97,6 +97,9 @@ module ActionDispatch # :nodoc:
x
end
+ def abort
+ end
+
def close
@response.commit!
@closed = true
@@ -110,10 +113,10 @@ module ActionDispatch # :nodoc:
# The underlying body, as a streamable object.
attr_reader :stream
- def initialize(status = 200, header = {}, body = [])
+ def initialize(status = 200, header = {}, body = [], default_headers: self.class.default_headers)
super()
- header = merge_default_headers(header, self.class.default_headers)
+ header = merge_default_headers(header, default_headers)
self.body, self.header, self.status = body, header, status
@@ -207,18 +210,6 @@ module ActionDispatch # :nodoc:
end
alias_method :status_message, :message
- def respond_to?(method, include_private = false)
- if method.to_s == 'to_path'
- stream.respond_to?(method)
- else
- super
- end
- end
-
- def to_path
- stream.to_path
- end
-
# Returns the content of the response as a string. This contains the contents
# of any calls to <tt>render</tt>.
def body
@@ -271,13 +262,25 @@ module ActionDispatch # :nodoc:
stream.close if stream.respond_to?(:close)
end
+ def abort
+ if stream.respond_to?(:abort)
+ stream.abort
+ elsif stream.respond_to?(:close)
+ # `stream.close` should really be reserved for a close from the
+ # other direction, but we must fall back to it for
+ # compatibility.
+ stream.close
+ end
+ end
+
# Turns the Response into a Rack-compatible array of the status, headers,
- # and body.
+ # and body. Allows explict splatting:
+ #
+ # status, headers, body = *response
def to_a
rack_response @status, @header.to_hash
end
alias prepare! to_a
- alias to_ary to_a
# Returns the response cookies, converted to a Hash of (name => value) pairs
#
@@ -296,9 +299,6 @@ module ActionDispatch # :nodoc:
cookies
end
- def _status_code
- @status
- end
private
def before_committed
@@ -308,9 +308,7 @@ module ActionDispatch # :nodoc:
end
def merge_default_headers(original, default)
- return original unless default.respond_to?(:merge)
-
- default.merge(original)
+ default.respond_to?(:merge) ? default.merge(original) : original
end
def build_buffer(response, body)
@@ -337,6 +335,42 @@ module ActionDispatch # :nodoc:
!@sending_file && @charset != false
end
+ class RackBody
+ def initialize(response)
+ @response = response
+ end
+
+ def each(*args, &block)
+ @response.each(*args, &block)
+ end
+
+ def close
+ # Rack "close" maps to Response#abort, and *not* Response#close
+ # (which is used when the controller's finished writing)
+ @response.abort
+ end
+
+ def body
+ @response.body
+ end
+
+ def respond_to?(method, include_private = false)
+ if method.to_s == 'to_path'
+ @response.stream.respond_to?(method)
+ else
+ super
+ end
+ end
+
+ def to_path
+ @response.stream.to_path
+ end
+
+ def to_ary
+ nil
+ end
+ end
+
def rack_response(status, header)
assign_default_content_type_and_charset!(header)
handle_conditional_get!
@@ -347,7 +381,7 @@ module ActionDispatch # :nodoc:
header.delete CONTENT_TYPE
[status, header, []]
else
- [status, header, Rack::BodyProxy.new(self){}]
+ [status, header, RackBody.new(self)]
end
end
end
diff --git a/actionpack/lib/action_dispatch/http/upload.rb b/actionpack/lib/action_dispatch/http/upload.rb
index 45bf751d09..540e11a4a0 100644
--- a/actionpack/lib/action_dispatch/http/upload.rb
+++ b/actionpack/lib/action_dispatch/http/upload.rb
@@ -27,7 +27,8 @@ module ActionDispatch
@tempfile = hash[:tempfile]
raise(ArgumentError, ':tempfile is required') unless @tempfile
- @original_filename = encode_filename(hash[:filename])
+ @original_filename = hash[:filename]
+ @original_filename &&= @original_filename.encode "UTF-8"
@content_type = hash[:type]
@headers = hash[:head]
end
@@ -66,13 +67,6 @@ module ActionDispatch
def eof?
@tempfile.eof?
end
-
- private
-
- def encode_filename(filename)
- # Encode the filename in the utf8 encoding, unless it is nil
- filename.force_encoding(Encoding::UTF_8).encode! if filename
- end
end
end
end
diff --git a/actionpack/lib/action_dispatch/http/url.rb b/actionpack/lib/action_dispatch/http/url.rb
index 4cba4f5f37..001b14ec97 100644
--- a/actionpack/lib/action_dispatch/http/url.rb
+++ b/actionpack/lib/action_dispatch/http/url.rb
@@ -5,60 +5,100 @@ module ActionDispatch
module Http
module URL
IP_HOST_REGEXP = /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/
- HOST_REGEXP = /(^.*:\/\/)?([^:]+)(?::(\d+$))?/
+ HOST_REGEXP = /(^[^:]+:\/\/)?([^:]+)(?::(\d+$))?/
PROTOCOL_REGEXP = /^([^:]+)(:)?(\/\/)?$/
mattr_accessor :tld_length
self.tld_length = 1
class << self
- def extract_domain(host, tld_length = @@tld_length)
- host.split('.').last(1 + tld_length).join('.') if named_host?(host)
+ # Returns the domain part of a host given the domain level.
+ #
+ # # Top-level domain example
+ # extract_domain('www.example.com', 1) # => "example.com"
+ # # Second-level domain example
+ # extract_domain('dev.www.example.co.uk', 2) # => "example.co.uk"
+ def extract_domain(host, tld_length)
+ extract_domain_from(host, tld_length) if named_host?(host)
end
- def extract_subdomains(host, tld_length = @@tld_length)
+ # Returns the subdomains of a host as an Array given the domain level.
+ #
+ # # Top-level domain example
+ # extract_subdomains('www.example.com', 1) # => ["www"]
+ # # Second-level domain example
+ # extract_subdomains('dev.www.example.co.uk', 2) # => ["dev", "www"]
+ def extract_subdomains(host, tld_length)
if named_host?(host)
- parts = host.split('.')
- parts[0..-(tld_length + 2)]
+ extract_subdomains_from(host, tld_length)
else
[]
end
end
- def extract_subdomain(host, tld_length = @@tld_length)
+ # Returns the subdomains of a host as a String given the domain level.
+ #
+ # # Top-level domain example
+ # extract_subdomain('www.example.com', 1) # => "www"
+ # # Second-level domain example
+ # extract_subdomain('dev.www.example.co.uk', 2) # => "dev.www"
+ def extract_subdomain(host, tld_length)
extract_subdomains(host, tld_length).join('.')
end
def url_for(options)
- unless options[:host] || options[:only_path]
+ if options[:only_path]
+ path_for options
+ else
+ full_url_for options
+ end
+ end
+
+ def full_url_for(options)
+ host = options[:host]
+ protocol = options[:protocol]
+ port = options[:port]
+
+ unless host
raise ArgumentError, 'Missing host to link to! Please provide the :host parameter, set default_url_options[:host], or set :only_path to true'
end
+ build_host_url(host, port, protocol, options, path_for(options))
+ end
+
+ def path_for(options)
path = options[:script_name].to_s.chomp("/")
- path << options[:path].to_s
+ path << options[:path] if options.key?(:path)
add_trailing_slash(path) if options[:trailing_slash]
+ add_params(path, options[:params]) if options.key?(:params)
+ add_anchor(path, options[:anchor]) if options.key?(:anchor)
- result = path
+ path
+ end
- unless options[:only_path]
- result.prepend build_host_url(options)
- end
+ private
- if options.key? :params
- params = options[:params].is_a?(Hash) ?
- options[:params] :
- { params: options[:params] }
+ 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?
+ end
- params.reject! { |_,v| v.to_param.nil? }
- result << "?#{params.to_query}" unless params.empty?
+ def add_anchor(path, anchor)
+ if anchor
+ path << "##{Journey::Router::Utils.escape_fragment(anchor.to_param)}"
end
+ end
- result << "##{Journey::Router::Utils.escape_fragment(options[:anchor].to_param.to_s)}" if options[:anchor]
- result
+ def extract_domain_from(host, tld_length)
+ host.split('.').last(1 + tld_length).join('.')
end
- private
+ def extract_subdomains_from(host, tld_length)
+ parts = host.split('.')
+ parts[0..-(tld_length + 2)]
+ end
def add_trailing_slash(path)
# includes querysting
@@ -68,43 +108,38 @@ module ActionDispatch
elsif !path.include?(".")
path.sub!(/[^\/]\z|\A\z/, '\&/')
end
-
- path
end
- def build_host_url(options)
- if match = options[:host].match(HOST_REGEXP)
- options[:protocol] ||= match[1] unless options[:protocol] == false
- options[:host] = match[2]
- options[:port] = match[3] unless options.key?(:port)
+ def build_host_url(host, port, protocol, options, path)
+ if match = host.match(HOST_REGEXP)
+ protocol ||= match[1] unless protocol == false
+ host = match[2]
+ port = match[3] unless options.key? :port
end
- options[:protocol] = normalize_protocol(options)
- options[:host] = normalize_host(options)
- options[:port] = normalize_port(options)
+ protocol = normalize_protocol protocol
+ host = normalize_host(host, options)
- result = options[:protocol]
+ result = protocol.dup
if options[:user] && options[:password]
result << "#{Rack::Utils.escape(options[:user])}:#{Rack::Utils.escape(options[:password])}@"
end
- result << options[:host]
- result << ":#{options[:port]}" if options[:port]
+ result << host
+ normalize_port(port, protocol) { |normalized_port|
+ result << ":#{normalized_port}"
+ }
- result
+ result.concat path
end
def named_host?(host)
- host && IP_HOST_REGEXP !~ host
+ IP_HOST_REGEXP !~ host
end
- def same_host?(options)
- (options[:subdomain] == true || !options.key?(:subdomain)) && options[:domain].nil?
- end
-
- def normalize_protocol(options)
- case options[:protocol]
+ def normalize_protocol(protocol)
+ case protocol
when nil
"http://"
when false, "//"
@@ -112,36 +147,39 @@ module ActionDispatch
when PROTOCOL_REGEXP
"#{$1}://"
else
- raise ArgumentError, "Invalid :protocol option: #{options[:protocol].inspect}"
+ raise ArgumentError, "Invalid :protocol option: #{protocol.inspect}"
end
end
- def normalize_host(options)
- return options[:host] if !named_host?(options[:host]) || same_host?(options)
+ def normalize_host(_host, options)
+ return _host unless named_host?(_host)
tld_length = options[:tld_length] || @@tld_length
+ subdomain = options.fetch :subdomain, true
+ domain = options[:domain]
host = ""
- if options[:subdomain] == true || !options.key?(:subdomain)
- host << extract_subdomain(options[:host], tld_length).to_param
- elsif options[:subdomain].present?
- host << options[:subdomain].to_param
+ if subdomain == true
+ return _host if domain.nil?
+
+ host << extract_subdomains_from(_host, tld_length).join('.')
+ elsif subdomain
+ host << subdomain.to_param
end
host << "." unless host.empty?
- host << (options[:domain] || extract_domain(options[:host], tld_length))
+ host << (domain || extract_domain_from(_host, tld_length))
host
end
- def normalize_port(options)
- return nil if options[:port].nil? || options[:port] == false
+ def normalize_port(port, protocol)
+ return unless port
- case options[:protocol]
- when "//"
- options[:port]
+ case protocol
+ when "//" then yield port
when "https://"
- options[:port].to_i == 443 ? nil : options[:port]
+ yield port unless port.to_i == 443
else
- options[:port].to_i == 80 ? nil : options[:port]
+ yield port unless port.to_i == 80
end
end
end
@@ -153,16 +191,43 @@ module ActionDispatch
end
# Returns the complete URL used for this request.
+ #
+ # class Request < Rack::Request
+ # include ActionDispatch::Http::URL
+ # end
+ #
+ # req = Request.new 'HTTP_HOST' => 'example.com'
+ # req.url # => "http://example.com"
def url
protocol + host_with_port + fullpath
end
# Returns 'https://' if this is an SSL request and 'http://' otherwise.
+ #
+ # class Request < Rack::Request
+ # include ActionDispatch::Http::URL
+ # end
+ #
+ # req = Request.new 'HTTP_HOST' => 'example.com'
+ # req.protocol # => "http://"
+ #
+ # req = Request.new 'HTTP_HOST' => 'example.com', 'HTTPS' => 'on'
+ # req.protocol # => "https://"
def protocol
@protocol ||= ssl? ? 'https://' : 'http://'
end
# Returns the \host for this request, such as "example.com".
+ #
+ # class Request < Rack::Request
+ # include ActionDispatch::Http::URL
+ # end
+ #
+ # req = Request.new 'HTTP_HOST' => 'example.com'
+ # req.raw_host_with_port # => "example.com"
+ #
+ # req = Request.new 'HTTP_HOST' => 'example.com:8080'
+ # req.raw_host_with_port # => "example.com:8080"
def raw_host_with_port
if forwarded = env["HTTP_X_FORWARDED_HOST"]
forwarded.split(/,\s?/).last
@@ -172,17 +237,44 @@ module ActionDispatch
end
# Returns the host for this request, such as example.com.
+ #
+ # class Request < Rack::Request
+ # include ActionDispatch::Http::URL
+ # end
+ #
+ # req = Request.new 'HTTP_HOST' => 'example.com:8080'
+ # req.host # => "example.com"
def host
raw_host_with_port.sub(/:\d+$/, '')
end
# Returns a \host:\port string for this request, such as "example.com" or
# "example.com:8080".
+ #
+ # class Request < Rack::Request
+ # include ActionDispatch::Http::URL
+ # end
+ #
+ # req = Request.new 'HTTP_HOST' => 'example.com:80'
+ # req.host_with_port # => "example.com"
+ #
+ # req = Request.new 'HTTP_HOST' => 'example.com:8080'
+ # req.host_with_port # => "example.com:8080"
def host_with_port
"#{host}#{port_string}"
end
# Returns the port number of this request as an integer.
+ #
+ # class Request < Rack::Request
+ # include ActionDispatch::Http::URL
+ # end
+ #
+ # req = Request.new 'HTTP_HOST' => 'example.com'
+ # req.port # => 80
+ #
+ # req = Request.new 'HTTP_HOST' => 'example.com:8080'
+ # req.port # => 8080
def port
@port ||= begin
if raw_host_with_port =~ /:(\d+)$/
@@ -194,6 +286,13 @@ module ActionDispatch
end
# Returns the standard \port number for this request's protocol.
+ #
+ # class Request < Rack::Request
+ # include ActionDispatch::Http::URL
+ # end
+ #
+ # req = Request.new 'HTTP_HOST' => 'example.com:8080'
+ # req.standard_port # => 80
def standard_port
case protocol
when 'https://' then 443
@@ -202,18 +301,48 @@ module ActionDispatch
end
# Returns whether this request is using the standard port
+ #
+ # class Request < Rack::Request
+ # include ActionDispatch::Http::URL
+ # end
+ #
+ # req = Request.new 'HTTP_HOST' => 'example.com:80'
+ # req.standard_port? # => true
+ #
+ # req = Request.new 'HTTP_HOST' => 'example.com:8080'
+ # req.standard_port? # => false
def standard_port?
port == standard_port
end
# Returns a number \port suffix like 8080 if the \port number of this request
# is not the default HTTP \port 80 or HTTPS \port 443.
+ #
+ # class Request < Rack::Request
+ # include ActionDispatch::Http::URL
+ # end
+ #
+ # req = Request.new 'HTTP_HOST' => 'example.com:80'
+ # req.optional_port # => nil
+ #
+ # req = Request.new 'HTTP_HOST' => 'example.com:8080'
+ # req.optional_port # => 8080
def optional_port
standard_port? ? nil : port
end
# Returns a string \port suffix, including colon, like ":8080" if the \port
# number of this request is not the default HTTP \port 80 or HTTPS \port 443.
+ #
+ # class Request < Rack::Request
+ # include ActionDispatch::Http::URL
+ # end
+ #
+ # req = Request.new 'HTTP_HOST' => 'example.com:80'
+ # req.port_string # => ""
+ #
+ # req = Request.new 'HTTP_HOST' => 'example.com:8080'
+ # req.port_string # => ":8080"
def port_string
standard_port? ? '' : ":#{port}"
end
diff --git a/actionpack/lib/action_dispatch/journey/formatter.rb b/actionpack/lib/action_dispatch/journey/formatter.rb
index 6d58323789..992c1a9efe 100644
--- a/actionpack/lib/action_dispatch/journey/formatter.rb
+++ b/actionpack/lib/action_dispatch/journey/formatter.rb
@@ -12,12 +12,12 @@ module ActionDispatch
@cache = nil
end
- def generate(name, options, recall = {}, parameterize = nil)
- constraints = recall.merge(options)
+ def generate(name, options, path_parameters, parameterize = nil)
+ constraints = path_parameters.merge(options)
missing_keys = []
match_route(name, constraints) do |route|
- parameterized_parts = extract_parameterized_parts(route, options, recall, parameterize)
+ parameterized_parts = extract_parameterized_parts(route, options, path_parameters, parameterize)
# Skip this route unless a name has been provided or it is a
# standard Rails route since we can't determine whether an options
@@ -40,7 +40,7 @@ module ActionDispatch
end
message = "No route matches #{Hash[constraints.sort].inspect}"
- message << " missing required keys: #{missing_keys.sort.inspect}" if name
+ message << " missing required keys: #{missing_keys.sort.inspect}" unless missing_keys.empty?
raise ActionController::UrlGenerationError, message
end
diff --git a/actionpack/lib/action_dispatch/journey/gtg/transition_table.rb b/actionpack/lib/action_dispatch/journey/gtg/transition_table.rb
index 990d2127ee..d7ce6042c2 100644
--- a/actionpack/lib/action_dispatch/journey/gtg/transition_table.rb
+++ b/actionpack/lib/action_dispatch/journey/gtg/transition_table.rb
@@ -88,13 +88,13 @@ module ActionDispatch
erb = File.read File.join(viz_dir, 'index.html.erb')
states = "function tt() { return #{to_json}; }"
- fun_routes = paths.shuffle.first(3).map do |ast|
+ fun_routes = paths.sample(3).map do |ast|
ast.map { |n|
case n
when Nodes::Symbol
case n.left
when ':id' then rand(100).to_s
- when ':format' then %w{ xml json }.shuffle.first
+ when ':format' then %w{ xml json }.sample
else
'omg'
end
@@ -109,7 +109,7 @@ module ActionDispatch
svg = to_svg
javascripts = [states, fsm_js]
- # Annoying hack for 1.9 warnings
+ # Annoying hack warnings
fun_routes = fun_routes
stylesheets = stylesheets
svg = svg
diff --git a/actionpack/lib/action_dispatch/journey/nfa/transition_table.rb b/actionpack/lib/action_dispatch/journey/nfa/transition_table.rb
index 66e414213a..0ccab21801 100644
--- a/actionpack/lib/action_dispatch/journey/nfa/transition_table.rb
+++ b/actionpack/lib/action_dispatch/journey/nfa/transition_table.rb
@@ -45,51 +45,6 @@ module ActionDispatch
(@table.keys + @table.values.flat_map(&:keys)).uniq
end
- # Returns a generalized transition graph with reduced states. The states
- # are reduced like a DFA, but the table must be simulated like an NFA.
- #
- # Edges of the GTG are regular expressions.
- def generalized_table
- gt = GTG::TransitionTable.new
- marked = {}
- state_id = Hash.new { |h,k| h[k] = h.length }
- alphabet = self.alphabet
-
- stack = [eclosure(0)]
-
- until stack.empty?
- state = stack.pop
- next if marked[state] || state.empty?
-
- marked[state] = true
-
- alphabet.each do |alpha|
- next_state = eclosure(following_states(state, alpha))
- next if next_state.empty?
-
- gt[state_id[state], state_id[next_state]] = alpha
- stack << next_state
- end
- end
-
- final_groups = state_id.keys.find_all { |s|
- s.sort.last == accepting
- }
-
- final_groups.each do |states|
- id = state_id[states]
-
- gt.add_accepting(id)
- save = states.find { |s|
- @memos.key?(s) && eclosure(s).sort.last == accepting
- }
-
- gt.add_memo(id, memo(save))
- end
-
- gt
- end
-
# Returns set of NFA states to which there is a transition on ast symbol
# +a+ from some state +s+ in +t+.
def following_states(t, a)
@@ -107,7 +62,7 @@ module ActionDispatch
end
def alphabet
- inverted.values.flat_map(&:keys).compact.uniq.sort_by { |x| x.to_s }
+ inverted.values.flat_map(&:keys).compact.uniq.sort_by(&:to_s)
end
# Returns a set of NFA states reachable from some NFA state +s+ in set
diff --git a/actionpack/lib/action_dispatch/journey/parser.rb b/actionpack/lib/action_dispatch/journey/parser.rb
index d129ba7e16..9012297400 100644
--- a/actionpack/lib/action_dispatch/journey/parser.rb
+++ b/actionpack/lib/action_dispatch/journey/parser.rb
@@ -86,7 +86,7 @@ racc_token_table = {
racc_nt_base = 10
-racc_use_result_var = true
+racc_use_result_var = false
Racc_arg = [
racc_action_table,
@@ -133,14 +133,12 @@ Racc_debug_parser = false
# reduce 0 omitted
-def _reduce_1(val, _values, result)
- result = Cat.new(val.first, val.last)
- result
+def _reduce_1(val, _values)
+ Cat.new(val.first, val.last)
end
-def _reduce_2(val, _values, result)
- result = val.first
- result
+def _reduce_2(val, _values)
+ val.first
end
# reduce 3 omitted
@@ -151,24 +149,20 @@ end
# reduce 6 omitted
-def _reduce_7(val, _values, result)
- result = Group.new(val[1])
- result
+def _reduce_7(val, _values)
+ Group.new(val[1])
end
-def _reduce_8(val, _values, result)
- result = Or.new([val.first, val.last])
- result
+def _reduce_8(val, _values)
+ Or.new([val.first, val.last])
end
-def _reduce_9(val, _values, result)
- result = Or.new([val.first, val.last])
- result
+def _reduce_9(val, _values)
+ Or.new([val.first, val.last])
end
-def _reduce_10(val, _values, result)
- result = Star.new(Symbol.new(val.last))
- result
+def _reduce_10(val, _values)
+ Star.new(Symbol.new(val.last))
end
# reduce 11 omitted
@@ -179,27 +173,23 @@ end
# reduce 14 omitted
-def _reduce_15(val, _values, result)
- result = Slash.new('/')
- result
+def _reduce_15(val, _values)
+ Slash.new('/')
end
-def _reduce_16(val, _values, result)
- result = Symbol.new(val.first)
- result
+def _reduce_16(val, _values)
+ Symbol.new(val.first)
end
-def _reduce_17(val, _values, result)
- result = Literal.new(val.first)
- result
+def _reduce_17(val, _values)
+ Literal.new(val.first)
end
-def _reduce_18(val, _values, result)
- result = Dot.new(val.first)
- result
+def _reduce_18(val, _values)
+ Dot.new(val.first)
end
-def _reduce_none(val, _values, result)
+def _reduce_none(val, _values)
val[0]
end
diff --git a/actionpack/lib/action_dispatch/journey/parser.y b/actionpack/lib/action_dispatch/journey/parser.y
index 0ead222551..d3f7c4d765 100644
--- a/actionpack/lib/action_dispatch/journey/parser.y
+++ b/actionpack/lib/action_dispatch/journey/parser.y
@@ -1,11 +1,11 @@
class ActionDispatch::Journey::Parser
-
+ options no_result_var
token SLASH LITERAL SYMBOL LPAREN RPAREN DOT STAR OR
rule
expressions
- : expression expressions { result = Cat.new(val.first, val.last) }
- | expression { result = val.first }
+ : expression expressions { Cat.new(val.first, val.last) }
+ | expression { val.first }
| or
;
expression
@@ -14,14 +14,14 @@ rule
| star
;
group
- : LPAREN expressions RPAREN { result = Group.new(val[1]) }
+ : LPAREN expressions RPAREN { Group.new(val[1]) }
;
or
- : expression OR expression { result = Or.new([val.first, val.last]) }
- | expression OR or { result = Or.new([val.first, val.last]) }
+ : expression OR expression { Or.new([val.first, val.last]) }
+ | expression OR or { Or.new([val.first, val.last]) }
;
star
- : STAR { result = Star.new(Symbol.new(val.last)) }
+ : STAR { Star.new(Symbol.new(val.last)) }
;
terminal
: symbol
@@ -30,16 +30,16 @@ rule
| dot
;
slash
- : SLASH { result = Slash.new('/') }
+ : SLASH { Slash.new('/') }
;
symbol
- : SYMBOL { result = Symbol.new(val.first) }
+ : SYMBOL { Symbol.new(val.first) }
;
literal
- : LITERAL { result = Literal.new(val.first) }
+ : LITERAL { Literal.new(val.first) }
;
dot
- : DOT { result = Dot.new(val.first) }
+ : DOT { Dot.new(val.first) }
;
end
diff --git a/actionpack/lib/action_dispatch/journey/path/pattern.rb b/actionpack/lib/action_dispatch/journey/path/pattern.rb
index 3af940a02f..64b48ca45f 100644
--- a/actionpack/lib/action_dispatch/journey/path/pattern.rb
+++ b/actionpack/lib/action_dispatch/journey/path/pattern.rb
@@ -42,7 +42,7 @@ module ActionDispatch
end
def names
- @names ||= spec.grep(Nodes::Symbol).map { |n| n.name }
+ @names ||= spec.grep(Nodes::Symbol).map(&:name)
end
def required_names
@@ -52,7 +52,7 @@ module ActionDispatch
def optional_names
@optional_names ||= spec.grep(Nodes::Group).flat_map { |group|
group.grep(Nodes::Symbol)
- }.map { |n| n.name }.uniq
+ }.map(&:name).uniq
end
class RegexpOffsets < Journey::Visitors::Visitor # :nodoc:
@@ -122,6 +122,11 @@ module ActionDispatch
re = @matchers[node.left.to_sym] || '.+'
"(#{re})"
end
+
+ def visit_OR(node)
+ children = node.children.map { |n| visit n }
+ "(?:#{children.join(?|)})"
+ end
end
class UnanchoredRegexp < AnchoredRegexp # :nodoc:
diff --git a/actionpack/lib/action_dispatch/journey/route.rb b/actionpack/lib/action_dispatch/journey/route.rb
index 9f0a3af902..4d5c18984a 100644
--- a/actionpack/lib/action_dispatch/journey/route.rb
+++ b/actionpack/lib/action_dispatch/journey/route.rb
@@ -60,7 +60,7 @@ module ActionDispatch
end
def parts
- @parts ||= segments.map { |n| n.to_sym }
+ @parts ||= segments.map(&:to_sym)
end
alias :segment_keys :parts
@@ -68,12 +68,8 @@ module ActionDispatch
@path_formatter.evaluate path_options
end
- def optional_parts
- path.optional_names.map { |n| n.to_sym }
- end
-
def required_parts
- @required_parts ||= path.required_names.map { |n| n.to_sym }
+ @required_parts ||= path.required_names.map(&:to_sym)
end
def required_default?(key)
diff --git a/actionpack/lib/action_dispatch/journey/router.rb b/actionpack/lib/action_dispatch/journey/router.rb
index 74fa9ee3a2..e9df984c86 100644
--- a/actionpack/lib/action_dispatch/journey/router.rb
+++ b/actionpack/lib/action_dispatch/journey/router.rb
@@ -35,6 +35,7 @@ module ActionDispatch
unless route.path.anchored
req.script_name = (script_name.to_s + match.to_s).chomp('/')
req.path_info = match.post_match
+ req.path_info = "/" + req.path_info unless req.path_info.start_with? "/"
end
req.path_parameters = set_params.merge parameters
@@ -67,8 +68,8 @@ module ActionDispatch
def visualizer
tt = GTG::Builder.new(ast).transition_table
- groups = partitioned_routes.first.map(&:ast).group_by { |a| a.to_s }
- asts = groups.values.map { |v| v.first }
+ groups = partitioned_routes.first.map(&:ast).group_by(&:to_s)
+ asts = groups.values.map(&:first)
tt.visualizer(asts)
end
@@ -100,11 +101,14 @@ 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.sort_by!(&:precedence).select! { |r| r.matches?(req) }
+ routes.sort_by!(&:precedence)
routes.map! { |r|
match_data = r.path.match(req.path_info)
@@ -116,19 +120,24 @@ 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)
+ routes.delete_if { |route| route.verb == // }
+ 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/journey/router/utils.rb b/actionpack/lib/action_dispatch/journey/router/utils.rb
index ac4ecb1e65..2b0a6575d4 100644
--- a/actionpack/lib/action_dispatch/journey/router/utils.rb
+++ b/actionpack/lib/action_dispatch/journey/router/utils.rb
@@ -25,9 +25,10 @@ module ActionDispatch
# http://tools.ietf.org/html/rfc3986
class UriEncoder # :nodoc:
ENCODE = "%%%02X".freeze
- ENCODING = Encoding::US_ASCII
- EMPTY = "".force_encoding(ENCODING).freeze
- DEC2HEX = (0..255).to_a.map{ |i| ENCODE % i }.map{ |s| s.force_encoding(ENCODING) }
+ US_ASCII = Encoding::US_ASCII
+ UTF_8 = Encoding::UTF_8
+ EMPTY = "".force_encoding(US_ASCII).freeze
+ DEC2HEX = (0..255).to_a.map{ |i| ENCODE % i }.map{ |s| s.force_encoding(US_ASCII) }
ALPHA = "a-zA-Z".freeze
DIGIT = "0-9".freeze
@@ -53,12 +54,13 @@ module ActionDispatch
end
def unescape_uri(uri)
- uri.gsub(ESCAPED) { [$&[1, 2].hex].pack('C') }.force_encoding(uri.encoding)
+ encoding = uri.encoding == US_ASCII ? UTF_8 : uri.encoding
+ uri.gsub(ESCAPED) { [$&[1, 2].hex].pack('C') }.force_encoding(encoding)
end
protected
def escape(component, pattern)
- component.gsub(pattern){ |unsafe| percent_encode(unsafe) }.force_encoding(ENCODING)
+ component.gsub(pattern){ |unsafe| percent_encode(unsafe) }.force_encoding(US_ASCII)
end
def percent_encode(unsafe)
diff --git a/actionpack/lib/action_dispatch/journey/scanner.rb b/actionpack/lib/action_dispatch/journey/scanner.rb
index 633be11a2d..19e0bc03d6 100644
--- a/actionpack/lib/action_dispatch/journey/scanner.rb
+++ b/actionpack/lib/action_dispatch/journey/scanner.rb
@@ -39,18 +39,18 @@ module ActionDispatch
[:SLASH, text]
when text = @ss.scan(/\*\w+/)
[:STAR, text]
- when text = @ss.scan(/\(/)
+ when text = @ss.scan(/(?<!\\)\(/)
[:LPAREN, text]
- when text = @ss.scan(/\)/)
+ when text = @ss.scan(/(?<!\\)\)/)
[:RPAREN, text]
when text = @ss.scan(/\|/)
[:OR, text]
when text = @ss.scan(/\./)
[:DOT, text]
- when text = @ss.scan(/:\w+/)
+ when text = @ss.scan(/(?<!\\):\w+/)
[:SYMBOL, text]
- when text = @ss.scan(/[\w%\-~]+/)
- [:LITERAL, text]
+ when text = @ss.scan(/(?:[\w%\-~!$&'*+,;=@]|\\:|\\\(|\\\))+/)
+ [:LITERAL, text.tr('\\', '')]
# any char
when text = @ss.scan(/./)
[:LITERAL, text]
diff --git a/actionpack/lib/action_dispatch/journey/visualizer/fsm.css b/actionpack/lib/action_dispatch/journey/visualizer/fsm.css
index 50caebaa18..403e16a7bb 100644
--- a/actionpack/lib/action_dispatch/journey/visualizer/fsm.css
+++ b/actionpack/lib/action_dispatch/journey/visualizer/fsm.css
@@ -16,10 +16,6 @@ h2 {
font-size: 0.5em;
}
-div#chart-2 {
- height: 350px;
-}
-
.clearfix {display: inline-block; }
.input { overflow: show;}
.instruction { color: #666; padding: 0 30px 20px; font-size: 0.9em}
diff --git a/actionpack/lib/action_dispatch/middleware/callbacks.rb b/actionpack/lib/action_dispatch/middleware/callbacks.rb
index baf9d5779e..f80df78582 100644
--- a/actionpack/lib/action_dispatch/middleware/callbacks.rb
+++ b/actionpack/lib/action_dispatch/middleware/callbacks.rb
@@ -1,6 +1,6 @@
module ActionDispatch
- # Provide callbacks to be executed before and after the request dispatch.
+ # Provides callbacks to be executed before and after dispatching the request.
class Callbacks
include ActiveSupport::Callbacks
diff --git a/actionpack/lib/action_dispatch/middleware/cookies.rb b/actionpack/lib/action_dispatch/middleware/cookies.rb
index 22b16b628d..b7687ca100 100644
--- a/actionpack/lib/action_dispatch/middleware/cookies.rb
+++ b/actionpack/lib/action_dispatch/middleware/cookies.rb
@@ -3,6 +3,7 @@ require 'active_support/core_ext/module/attribute_accessors'
require 'active_support/core_ext/object/blank'
require 'active_support/key_generator'
require 'active_support/message_verifier'
+require 'active_support/json'
module ActionDispatch
class Request < Rack::Request
@@ -70,11 +71,13 @@ module ActionDispatch
# restrict to the domain level. If you use a schema like www.example.com
# and want to share session with user.example.com set <tt>:domain</tt>
# to <tt>:all</tt>. Make sure to specify the <tt>:domain</tt> option with
- # <tt>:all</tt> again when deleting cookies.
+ # <tt>:all</tt> or <tt>Array</tt> again when deleting cookies.
#
- # domain: nil # Does not sets cookie domain. (default)
+ # domain: nil # Does not set cookie domain. (default)
# domain: :all # Allow the cookie for the top most level
# # domain and subdomains.
+ # domain: %w(.example.com .example.org) # Allow the cookie
+ # # for concrete domain names.
#
# * <tt>:expires</tt> - The time at which this cookie expires, as a \Time object.
# * <tt>:secure</tt> - Whether this cookie is only transmitted to HTTPS servers.
@@ -90,6 +93,7 @@ module ActionDispatch
SECRET_TOKEN = "action_dispatch.secret_token".freeze
SECRET_KEY_BASE = "action_dispatch.secret_key_base".freeze
COOKIES_SERIALIZER = "action_dispatch.cookies_serializer".freeze
+ COOKIES_DIGEST = "action_dispatch.cookies_digest".freeze
# Cookies can typically store 4096 bytes.
MAX_COOKIE_SIZE = 4096
@@ -118,7 +122,7 @@ module ActionDispatch
# the cookie again. This is useful for creating cookies with values that the user is not supposed to change. If a signed
# cookie was tampered with by the user (or a 3rd party), nil will be returned.
#
- # If +secrets.secret_key_base+ and +config.secret_token+ (deprecated) are both set,
+ # If +secrets.secret_key_base+ and +secrets.secret_token+ (deprecated) are both set,
# legacy cookies signed with the old key generator will be transparently upgraded.
#
# This jar requires that you set a suitable secret for the verification on your app's +secrets.secret_key_base+.
@@ -141,7 +145,7 @@ module ActionDispatch
# Returns a jar that'll automatically encrypt cookie values before sending them to the client and will decrypt them for read.
# If the cookie was tampered with by the user (or a 3rd party), nil will be returned.
#
- # If +secrets.secret_key_base+ and +config.secret_token+ (deprecated) are both set,
+ # If +secrets.secret_key_base+ and +secrets.secret_token+ (deprecated) are both set,
# legacy cookies signed with the old key generator will be transparently upgraded.
#
# This jar requires that you set a suitable secret for the verification on your app's +secrets.secret_key_base+.
@@ -173,10 +177,14 @@ module ActionDispatch
end
end
+ # Passing the ActiveSupport::MessageEncryptor::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.
module VerifyAndUpgradeLegacySignedMessage
def initialize(*args)
super
- @legacy_verifier = ActiveSupport::MessageVerifier.new(@options[:secret_token], serializer: NullSerializer)
+ @legacy_verifier = ActiveSupport::MessageVerifier.new(@options[:secret_token], serializer: ActiveSupport::MessageEncryptor::NullSerializer)
end
def verify_and_upgrade_legacy_signed_message(name, signed_message)
@@ -212,7 +220,8 @@ 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?,
- serializer: env[COOKIES_SERIALIZER]
+ serializer: env[COOKIES_SERIALIZER],
+ digest: env[COOKIES_DIGEST]
}
end
@@ -274,7 +283,7 @@ module ActionDispatch
def handle_options(options) #:nodoc:
options[:path] ||= "/"
- if options[:domain] == :all
+ if options[:domain] == :all || options[:domain] == 'all'
# if there is a provided tld length then we use it otherwise default domain regexp
domain_regexp = options[:tld_length] ? /([^.]+\.?){#{options[:tld_length]}}$/ : DOMAIN_REGEXP
@@ -289,8 +298,8 @@ module ActionDispatch
end
end
- # Sets the cookie named +name+. The second argument may be the very cookie
- # value, or a hash of options as documented above.
+ # Sets the cookie named +name+. The second argument may be the cookie's
+ # value or a hash of options as documented above.
def []=(name, options)
if options.is_a?(Hash)
options.symbolize_keys!
@@ -385,24 +394,11 @@ module ActionDispatch
class JsonSerializer
def self.load(value)
- JSON.parse(value, quirks_mode: true)
+ ActiveSupport::JSON.decode(value)
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
+ ActiveSupport::JSON.encode(value)
end
end
@@ -414,7 +410,7 @@ module ActionDispatch
@options[:serializer] == :hybrid && value.start_with?(MARSHAL_SIGNATURE)
end
- def serialize(name, value)
+ def serialize(value)
serializer.dump(value)
end
@@ -441,6 +437,10 @@ module ActionDispatch
serializer
end
end
+
+ def digest
+ @options[:digest] || 'SHA1'
+ end
end
class SignedCookieJar #:nodoc:
@@ -451,7 +451,7 @@ module ActionDispatch
@parent_jar = parent_jar
@options = options
secret = key_generator.generate_key(@options[:signed_cookie_salt])
- @verifier = ActiveSupport::MessageVerifier.new(secret, serializer: NullSerializer)
+ @verifier = ActiveSupport::MessageVerifier.new(secret, digest: digest, serializer: ActiveSupport::MessageEncryptor::NullSerializer)
end
def [](name)
@@ -463,12 +463,12 @@ module ActionDispatch
def []=(name, options)
if options.is_a?(Hash)
options.symbolize_keys!
- options[:value] = @verifier.generate(serialize(name, options[:value]))
+ options[:value] = @verifier.generate(serialize(options[:value]))
else
- options = { :value => @verifier.generate(serialize(name, options)) }
+ options = { :value => @verifier.generate(serialize(options)) }
end
- raise CookieOverflow if options[:value].size > MAX_COOKIE_SIZE
+ raise CookieOverflow if options[:value].bytesize > MAX_COOKIE_SIZE
@parent_jar[name] = options
end
@@ -481,7 +481,7 @@ module ActionDispatch
end
# UpgradeLegacySignedCookieJar is used instead of SignedCookieJar if
- # config.secret_token and secrets.secret_key_base are both set. It reads
+ # secrets.secret_token and secrets.secret_key_base are both set. It reads
# legacy cookies signed with the old dummy key generator and re-saves
# them using the new key generator to provide a smooth upgrade path.
class UpgradeLegacySignedCookieJar < SignedCookieJar #:nodoc:
@@ -508,7 +508,7 @@ 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: NullSerializer)
+ @encryptor = ActiveSupport::MessageEncryptor.new(secret, sign_secret, digest: digest, serializer: ActiveSupport::MessageEncryptor::NullSerializer)
end
def [](name)
@@ -524,9 +524,9 @@ module ActionDispatch
options = { :value => options }
end
- options[:value] = @encryptor.encrypt_and_sign(serialize(name, options[:value]))
+ options[:value] = @encryptor.encrypt_and_sign(serialize(options[:value]))
- raise CookieOverflow if options[:value].size > MAX_COOKIE_SIZE
+ raise CookieOverflow if options[:value].bytesize > MAX_COOKIE_SIZE
@parent_jar[name] = options
end
@@ -539,7 +539,7 @@ module ActionDispatch
end
# UpgradeLegacyEncryptedCookieJar is used by ActionDispatch::Session::CookieStore
- # instead of EncryptedCookieJar if config.secret_token and secrets.secret_key_base
+ # instead of EncryptedCookieJar if secrets.secret_token and secrets.secret_key_base
# are both set. It reads legacy cookies signed with the old dummy key generator and
# encrypts and re-saves them using the new key generator to provide a smooth upgrade path.
class UpgradeLegacyEncryptedCookieJar < EncryptedCookieJar #:nodoc:
diff --git a/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb b/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb
index 0ca1a87645..9082aac271 100644
--- a/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb
+++ b/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb
@@ -1,6 +1,10 @@
require 'action_dispatch/http/request'
require 'action_dispatch/middleware/exception_wrapper'
require 'action_dispatch/routing/inspector'
+require 'action_view'
+require 'action_view/base'
+
+require 'pp'
module ActionDispatch
# This middleware is responsible for logging exceptions and
@@ -8,6 +12,32 @@ module ActionDispatch
class DebugExceptions
RESCUES_TEMPLATE_PATH = File.expand_path('../templates', __FILE__)
+ class DebugView < ActionView::Base
+ def debug_params(params)
+ clean_params = params.clone
+ clean_params.delete("action")
+ clean_params.delete("controller")
+
+ if clean_params.empty?
+ 'None'
+ else
+ PP.pp(clean_params, "", 200)
+ end
+ end
+
+ def debug_headers(headers)
+ if headers.present?
+ headers.inspect.gsub(',', ",\n")
+ else
+ 'None'
+ end
+ end
+
+ def debug_hash(object)
+ object.to_hash.sort_by { |k, _| k.to_s }.map { |k, v| "#{k}: #{v.inspect rescue $!.message}" }.join("\n")
+ end
+ end
+
def initialize(app, routes_app = nil)
@app = app
@routes_app = routes_app
@@ -35,14 +65,25 @@ module ActionDispatch
if env['action_dispatch.show_detailed_exceptions']
request = Request.new(env)
- template = ActionView::Base.new([RESCUES_TEMPLATE_PATH],
+ 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
+
+ template = DebugView.new([RESCUES_TEMPLATE_PATH],
request: request,
exception: wrapper.exception,
- application_trace: wrapper.application_trace,
- framework_trace: wrapper.framework_trace,
- full_trace: wrapper.full_trace,
+ traces: traces,
+ show_source_idx: source_to_show_id,
+ trace_to_show: trace_to_show,
routes_inspector: routes_inspector(exception),
- source_extract: wrapper.source_extract,
+ source_extracts: wrapper.source_extracts,
line_number: wrapper.line_number,
file: wrapper.file
)
diff --git a/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb b/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb
index 2326bb043a..d176a73633 100644
--- a/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb
+++ b/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb
@@ -1,21 +1,23 @@
require 'action_controller/metal/exceptions'
require 'active_support/core_ext/module/attribute_accessors'
+require 'rack/utils'
module ActionDispatch
class ExceptionWrapper
cattr_accessor :rescue_responses
@@rescue_responses = Hash.new(:internal_server_error)
@@rescue_responses.merge!(
- 'ActionController::RoutingError' => :not_found,
- 'AbstractController::ActionNotFound' => :not_found,
- 'ActionController::MethodNotAllowed' => :method_not_allowed,
- 'ActionController::UnknownHttpMethod' => :method_not_allowed,
- 'ActionController::NotImplemented' => :not_implemented,
- 'ActionController::UnknownFormat' => :not_acceptable,
- 'ActionController::InvalidAuthenticityToken' => :unprocessable_entity,
- 'ActionDispatch::ParamsParser::ParseError' => :bad_request,
- 'ActionController::BadRequest' => :bad_request,
- 'ActionController::ParameterMissing' => :bad_request
+ 'ActionController::RoutingError' => :not_found,
+ 'AbstractController::ActionNotFound' => :not_found,
+ 'ActionController::MethodNotAllowed' => :method_not_allowed,
+ 'ActionController::UnknownHttpMethod' => :method_not_allowed,
+ 'ActionController::NotImplemented' => :not_implemented,
+ 'ActionController::UnknownFormat' => :not_acceptable,
+ 'ActionController::InvalidAuthenticityToken' => :unprocessable_entity,
+ 'ActionController::InvalidCrossOriginRequest' => :unprocessable_entity,
+ 'ActionDispatch::ParamsParser::ParseError' => :bad_request,
+ 'ActionController::BadRequest' => :bad_request,
+ 'ActionController::ParameterMissing' => :bad_request
)
cattr_accessor :rescue_templates
@@ -56,21 +58,51 @@ module ActionDispatch
clean_backtrace(:all)
end
+ def traces
+ appplication_trace_with_ids = []
+ framework_trace_with_ids = []
+ full_trace_with_ids = []
+
+ full_trace.each_with_index do |trace, idx|
+ trace_with_id = { id: idx, trace: trace }
+
+ if application_trace.include?(trace)
+ appplication_trace_with_ids << trace_with_id
+ else
+ framework_trace_with_ids << trace_with_id
+ end
+
+ full_trace_with_ids << trace_with_id
+ end
+
+ {
+ "Application Trace" => appplication_trace_with_ids,
+ "Framework Trace" => framework_trace_with_ids,
+ "Full Trace" => full_trace_with_ids
+ }
+ end
+
def self.status_code_for_exception(class_name)
Rack::Utils.status_code(@@rescue_responses[class_name])
end
- def source_extract
- if application_trace && trace = application_trace.first
- file, line, _ = trace.split(":")
- @file = file
- @line_number = line.to_i
- source_fragment(@file, @line_number)
+ def source_extracts
+ backtrace.map do |trace|
+ file, line_number = extract_file_and_line_number(trace)
+
+ {
+ code: source_fragment(file, line_number),
+ line_number: line_number
+ }
end
end
private
+ def backtrace
+ Array(@exception.backtrace)
+ end
+
def original_exception(exception)
if registered_original_exception?(exception)
exception.original_exception
@@ -85,9 +117,9 @@ module ActionDispatch
def clean_backtrace(*args)
if backtrace_cleaner
- backtrace_cleaner.clean(@exception.backtrace, *args)
+ backtrace_cleaner.clean(backtrace, *args)
else
- @exception.backtrace
+ backtrace
end
end
@@ -107,10 +139,17 @@ module ActionDispatch
end
end
+ def extract_file_and_line_number(trace)
+ # Split by the first colon followed by some digits, which works for both
+ # Windows and Unix path styles.
+ file, line = trace.match(/^(.+?):(\d+).*$/, &:captures) || trace
+ [file, line.to_i]
+ end
+
def expand_backtrace
@exception.backtrace.unshift(
@exception.to_s.split("\n")
- ).flatten!
+ ).flatten!
end
end
end
diff --git a/actionpack/lib/action_dispatch/middleware/flash.rb b/actionpack/lib/action_dispatch/middleware/flash.rb
index 4821d2a899..59639a010e 100644
--- a/actionpack/lib/action_dispatch/middleware/flash.rb
+++ b/actionpack/lib/action_dispatch/middleware/flash.rb
@@ -10,7 +10,7 @@ module ActionDispatch
end
end
- # The flash provides a way to pass temporary objects between actions. Anything you place in the flash will be exposed
+ # The flash provides a way to pass temporary primitive-types (String, Array, Hash) between actions. Anything you place in the flash will be exposed
# to the very next action and then cleared out. This is a great way of doing notices and alerts, such as a create
# action that sets <tt>flash[:notice] = "Post successfully created"</tt> before redirecting to a display action that can
# then expose the flash to its template. Actually, that exposure is automatically done.
@@ -37,8 +37,11 @@ module ActionDispatch
# flash.alert = "You must be logged in"
# flash.notice = "Post successfully created"
#
- # This example just places a string in the flash, but you can put any object in there. And of course, you can put as
- # many as you like at a time too. Just remember: They'll be gone by the time the next action has been performed.
+ # This example places a string in the flash. And of course, you can put as many as you like at a time too. If you want to pass
+ # non-primitive types, you will have to handle that in your application. Example: To show messages with links, you will have to
+ # use sanitize helper.
+ #
+ # Just remember: They'll be gone by the time the next action has been performed.
#
# See docs on the FlashHash class for more details about the flash.
class Flash
@@ -76,22 +79,31 @@ module ActionDispatch
class FlashHash
include Enumerable
- def self.from_session_value(value)
- flash = case value
- when FlashHash # Rails 3.1, 3.2
- new(value.instance_variable_get(:@flashes), value.instance_variable_get(:@used))
- when Hash # Rails 4.0
- new(value['flashes'], value['discard'])
- else
- new
- end
-
- flash.tap(&:sweep)
+ def self.from_session_value(value) #:nodoc:
+ case value
+ when FlashHash # Rails 3.1, 3.2
+ flashes = value.instance_variable_get(:@flashes)
+ if discard = value.instance_variable_get(:@used)
+ flashes.except!(*discard)
+ end
+ new(flashes, flashes.keys)
+ when Hash # Rails 4.0
+ flashes = value['flashes']
+ if discard = value['discard']
+ flashes.except!(*discard)
+ end
+ new(flashes, flashes.keys)
+ else
+ new
+ end
end
- def to_session_value
- return nil if empty?
- {'discard' => @discard.to_a, 'flashes' => @flashes}
+ # Builds a hash containing the flashes to keep for the next request.
+ # If there are none to keep, returns nil.
+ def to_session_value #:nodoc:
+ flashes_to_keep = @flashes.except(*@discard)
+ return nil if flashes_to_keep.empty?
+ {'flashes' => flashes_to_keep}
end
def initialize(flashes = {}, discard = []) #:nodoc:
@@ -129,7 +141,7 @@ module ActionDispatch
end
def key?(name)
- @flashes.key? name
+ @flashes.key? name.to_s
end
def delete(key)
diff --git a/actionpack/lib/action_dispatch/middleware/params_parser.rb b/actionpack/lib/action_dispatch/middleware/params_parser.rb
index b426183488..29d43faeed 100644
--- a/actionpack/lib/action_dispatch/middleware/params_parser.rb
+++ b/actionpack/lib/action_dispatch/middleware/params_parser.rb
@@ -47,7 +47,7 @@ module ActionDispatch
else
false
end
- rescue Exception => e # JSON or Ruby code block errors
+ rescue => e # JSON or Ruby code block errors
logger(env).debug "Error occurred while parsing request parameters.\nContents:\n\n#{request.raw_post}"
raise ParseError.new(e.message, e)
diff --git a/actionpack/lib/action_dispatch/middleware/public_exceptions.rb b/actionpack/lib/action_dispatch/middleware/public_exceptions.rb
index 6c8944e067..040cb215b7 100644
--- a/actionpack/lib/action_dispatch/middleware/public_exceptions.rb
+++ b/actionpack/lib/action_dispatch/middleware/public_exceptions.rb
@@ -1,4 +1,14 @@
module ActionDispatch
+ # When called, this middleware renders an error page. By default if an HTML
+ # response is expected it will render static error pages from the `/public`
+ # directory. For example when this middleware receives a 500 response it will
+ # render the template found in `/public/500.html`.
+ # If an internationalized locale is set, this middleware will attempt to render
+ # the template in `/public/500.<locale>.html`. If an internationalized template
+ # is not found it will fall back on `/public/500.html`.
+ #
+ # When a request with a content type other than HTML is made, this middleware
+ # will attempt to convert error information into the appropriate response type.
class PublicExceptions
attr_accessor :public_path
diff --git a/actionpack/lib/action_dispatch/middleware/remote_ip.rb b/actionpack/lib/action_dispatch/middleware/remote_ip.rb
index cbb066b092..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
@@ -11,7 +13,7 @@ module ActionDispatch
# Some Rack servers concatenate repeated headers, like {HTTP RFC 2616}[http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2]
# requires. Some Rack servers simply drop preceding headers, and only report
# the value that was {given in the last header}[http://andre.arko.net/2011/12/26/repeated-headers-and-ruby-web-servers].
- # If you are behind multiple proxy servers (like Nginx to HAProxy to Unicorn)
+ # If you are behind multiple proxy servers (like NGINX to HAProxy to Unicorn)
# then you should test your Rack server to make sure your data is good.
#
# IF YOU DON'T USE A PROXY, THIS MAKES YOU VULNERABLE TO IP SPOOFING.
@@ -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
@@ -118,7 +94,7 @@ module ActionDispatch
#
# REMOTE_ADDR will be correct if the request is made directly against the
# Ruby process, on e.g. Heroku. When the request is proxied by another
- # server like HAProxy or Nginx, the IP address that made the original
+ # server like HAProxy or NGINX, the IP address that made the original
# request will be put in an X-Forwarded-For header. If there are multiple
# proxies, that header may contain a list of IPs. Other proxy services
# set the Client-Ip header instead, so we check that too.
@@ -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/request_id.rb b/actionpack/lib/action_dispatch/middleware/request_id.rb
index 5d1740d0d4..b9ca524309 100644
--- a/actionpack/lib/action_dispatch/middleware/request_id.rb
+++ b/actionpack/lib/action_dispatch/middleware/request_id.rb
@@ -3,28 +3,32 @@ require 'active_support/core_ext/string/access'
module ActionDispatch
# Makes a unique request id available to the action_dispatch.request_id env variable (which is then accessible through
- # ActionDispatch::Request#uuid) and sends the same id to the client via the X-Request-Id header.
+ # ActionDispatch::Request#uuid or the alias ActionDispatch::Request#request_id) and sends the same id to the client via the X-Request-Id header.
#
- # The unique request id is either based off the X-Request-Id header in the request, which would typically be generated
+ # The unique request id is either based on the X-Request-Id header in the request, which would typically be generated
# by a firewall, load balancer, or the web server, or, if this header is not available, a random uuid. If the
# header is accepted from the outside world, we sanitize it to a max of 255 chars and alphanumeric and dashes only.
#
# The unique request id can be used to trace a request end-to-end and would typically end up being part of log files
# from multiple pieces of the stack.
class RequestId
+ X_REQUEST_ID = "X-Request-Id".freeze # :nodoc:
+ ACTION_DISPATCH_REQUEST_ID = "action_dispatch.request_id".freeze # :nodoc:
+ HTTP_X_REQUEST_ID = "HTTP_X_REQUEST_ID".freeze # :nodoc:
+
def initialize(app)
@app = app
end
def call(env)
- env["action_dispatch.request_id"] = external_request_id(env) || internal_request_id
- @app.call(env).tap { |_status, headers, _body| headers["X-Request-Id"] = env["action_dispatch.request_id"] }
+ env[ACTION_DISPATCH_REQUEST_ID] = external_request_id(env) || internal_request_id
+ @app.call(env).tap { |_status, headers, _body| headers[X_REQUEST_ID] = env[ACTION_DISPATCH_REQUEST_ID] }
end
private
def external_request_id(env)
- if request_id = env["HTTP_X_REQUEST_ID"].presence
- request_id.gsub(/[^\w\-]/, "").first(255)
+ if request_id = env[HTTP_X_REQUEST_ID].presence
+ request_id.gsub(/[^\w\-]/, "".freeze).first(255)
end
end
diff --git a/actionpack/lib/action_dispatch/middleware/session/cache_store.rb b/actionpack/lib/action_dispatch/middleware/session/cache_store.rb
index 1db6194271..625050dc4b 100644
--- a/actionpack/lib/action_dispatch/middleware/session/cache_store.rb
+++ b/actionpack/lib/action_dispatch/middleware/session/cache_store.rb
@@ -16,9 +16,9 @@ module ActionDispatch
# Get a session from the cache.
def get_session(env, sid)
- sid ||= generate_sid
- session = @cache.read(cache_key(sid))
- session ||= {}
+ unless sid and session = @cache.read(cache_key(sid))
+ sid, session = generate_sid, {}
+ end
[sid, session]
end
diff --git a/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb b/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb
index 0864e7ef2a..ed25c67ae5 100644
--- a/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb
+++ b/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb
@@ -49,7 +49,7 @@ module ActionDispatch
# reasonably sure that your upgrade is otherwise complete. Additionally,
# you should take care to make sure you are not relying on the ability to
# decode signed cookies generated by your app in external applications or
- # Javascript before upgrading.
+ # JavaScript before upgrading.
#
# Note that changing the secret key will invalidate all existing sessions!
class CookieStore < Rack::Session::Abstract::ID
diff --git a/actionpack/lib/action_dispatch/middleware/show_exceptions.rb b/actionpack/lib/action_dispatch/middleware/show_exceptions.rb
index 1d4f0f89a6..f0779279c1 100644
--- a/actionpack/lib/action_dispatch/middleware/show_exceptions.rb
+++ b/actionpack/lib/action_dispatch/middleware/show_exceptions.rb
@@ -42,6 +42,7 @@ module ActionDispatch
wrapper = ExceptionWrapper.new(env, exception)
status = wrapper.status_code
env["action_dispatch.exception"] = wrapper.exception
+ env["action_dispatch.original_path"] = env["PATH_INFO"]
env["PATH_INFO"] = "/#{status}"
response = @exceptions_app.call(env)
response[1]['X-Cascade'] == 'pass' ? pass_response(status) : response
diff --git a/actionpack/lib/action_dispatch/middleware/static.rb b/actionpack/lib/action_dispatch/middleware/static.rb
index 2764584fe9..2e1bd45c3d 100644
--- a/actionpack/lib/action_dispatch/middleware/static.rb
+++ b/actionpack/lib/action_dispatch/middleware/static.rb
@@ -2,49 +2,97 @@ require 'rack/utils'
require 'active_support/core_ext/uri'
module ActionDispatch
+ # This middleware returns a file's contents from disk in the body response.
+ # When initialized it can accept an optional 'Cache-Control' header which
+ # will be set when a response containing a file's contents is delivered.
+ #
+ # This middleware will render the file specified in `env["PATH_INFO"]`
+ # where the base path is in the +root+ directory. For example if the +root+
+ # is set to `public/` then a request with `env["PATH_INFO"]` of
+ # `assets/application.js` will return a response with contents of a file
+ # located at `public/assets/application.js` if the file exists. If the file
+ # does not exist a 404 "File not Found" response will be returned.
class FileHandler
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?
+ path = Rack::Utils.clean_path_info path
- 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|
+ path = File.join(@root, p)
+ begin
+ File.file?(path) && File.readable?(path)
+ rescue SystemCallError
+ false
+ end
+
+ }
+ 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
+ # disk in the response. If a file is not found on disk, the request will be
+ # delegated to the application stack. This middleware is commonly initialized
+ # to serve assets from a server's `public/` directory.
+ #
+ # This middleware verifies the path to ensure that only files
+ # living in the root directory can be rendered. A request cannot
+ # produce a directory traversal using this middleware. Only 'GET' and 'HEAD'
+ # requests will result in a file being returned.
class Static
def initialize(app, path, cache_control=nil)
@app = app
diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/_request_and_response.html.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/_request_and_response.html.erb
index db219c8fa9..49b1e83551 100644
--- a/actionpack/lib/action_dispatch/middleware/templates/rescues/_request_and_response.html.erb
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/_request_and_response.html.erb
@@ -5,20 +5,8 @@
<pre id="blame_trace" <%='style="display:none"' if hide %>><code><%= @exception.describe_blame %></code></pre>
<% end %>
-<%
- clean_params = @request.filtered_parameters.clone
- clean_params.delete("action")
- clean_params.delete("controller")
-
- request_dump = clean_params.empty? ? 'None' : clean_params.inspect.gsub(',', ",\n")
-
- def debug_hash(object)
- object.to_hash.sort_by { |k, _| k.to_s }.map { |k, v| "#{k}: #{v.inspect rescue $!.message}" }.join("\n")
- end unless self.class.method_defined?(:debug_hash)
-%>
-
<h2 style="margin-top: 30px">Request</h2>
-<p><b>Parameters</b>:</p> <pre><%= request_dump %></pre>
+<p><b>Parameters</b>:</p> <pre><%= debug_params(@request.filtered_parameters) %></pre>
<div class="details">
<div class="summary"><a href="#" onclick="return toggleSessionDump()">Toggle session dump</a></div>
@@ -31,4 +19,4 @@
</div>
<h2 style="margin-top: 30px">Response</h2>
-<p><b>Headers</b>:</p> <pre><%= defined?(@response) ? @response.headers.inspect.gsub(',', ",\n") : 'None' %></pre>
+<p><b>Headers</b>:</p> <pre><%= debug_headers(defined?(@response) ? @response.headers : {}) %></pre>
diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/_source.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/_source.erb
index 38429cb78e..e7b913bbe4 100644
--- a/actionpack/lib/action_dispatch/middleware/templates/rescues/_source.erb
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/_source.erb
@@ -1,25 +1,27 @@
-<% if @source_extract %>
-<div class="source">
-<div class="info">
- Extracted source (around line <strong>#<%= @line_number %></strong>):
-</div>
-<div class="data">
- <table cellpadding="0" cellspacing="0" class="lines">
- <tr>
- <td>
- <pre class="line_numbers">
- <% @source_extract.keys.each do |line_number| %>
+<% @source_extracts.each_with_index do |source_extract, index| %>
+ <% if source_extract[:code] %>
+ <div class="source <%="hidden" if @show_source_idx != index%>" id="frame-source-<%=index%>">
+ <div class="info">
+ Extracted source (around line <strong>#<%= source_extract[:line_number] %></strong>):
+ </div>
+ <div class="data">
+ <table cellpadding="0" cellspacing="0" class="lines">
+ <tr>
+ <td>
+ <pre class="line_numbers">
+ <% source_extract[:code].each_key do |line_number| %>
<span><%= line_number -%></span>
- <% end %>
- </pre>
- </td>
+ <% end %>
+ </pre>
+ </td>
<td width="100%">
<pre>
-<% @source_extract.each do |line, source| -%><div class="line<%= " active" if line == @line_number -%>"><%= source -%></div><% end -%>
+<% source_extract[:code].each do |line, source| -%><div class="line<%= " active" if line == source_extract[:line_number] -%>"><%= source -%></div><% end -%>
</pre>
</td>
- </tr>
- </table>
-</div>
-</div>
+ </tr>
+ </table>
+ </div>
+ </div>
+ <% end %>
<% end %>
diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb
index b181909bff..ab57b11c7d 100644
--- a/actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb
@@ -1,9 +1,4 @@
-<%
- traces = { "Application Trace" => @application_trace,
- "Framework Trace" => @framework_trace,
- "Full Trace" => @full_trace }
- names = traces.keys
-%>
+<% names = @traces.keys %>
<p><code>Rails.root: <%= defined?(Rails) && Rails.respond_to?(:root) ? Rails.root : "unset" %></code></p>
@@ -16,9 +11,42 @@
<a href="#" onclick="<%= hide.join %><%= show %>; return false;"><%= name %></a> <%= '|' unless names.last == name %>
<% end %>
- <% traces.each do |name, trace| %>
- <div id="<%= name.gsub(/\s/, '-') %>" style="display: <%= (name == "Application Trace") ? 'block' : 'none' %>;">
- <pre><code><%= trace.join "\n" %></code></pre>
+ <% @traces.each do |name, trace| %>
+ <div id="<%= name.gsub(/\s/, '-') %>" style="display: <%= (name == @trace_to_show) ? 'block' : 'none' %>;">
+ <pre><code><% trace.each do |frame| %><a class="trace-frames" data-frame-id="<%= frame[:id] %>" href="#"><%= frame[:trace] %></a><br><% end %></code></pre>
</div>
<% end %>
+
+ <script type="text/javascript">
+ var traceFrames = document.getElementsByClassName('trace-frames');
+ var selectedFrame, currentSource = document.getElementById('frame-source-0');
+
+ // Add click listeners for all stack frames
+ for (var i = 0; i < traceFrames.length; i++) {
+ traceFrames[i].addEventListener('click', function(e) {
+ e.preventDefault();
+ var target = e.target;
+ var frame_id = target.dataset.frameId;
+
+ if (selectedFrame) {
+ selectedFrame.className = selectedFrame.className.replace("selected", "");
+ }
+
+ target.className += " selected";
+ selectedFrame = target;
+
+ // Change the extracted source code
+ changeSourceExtract(frame_id);
+ });
+
+ function changeSourceExtract(frame_id) {
+ var el = document.getElementById('frame-source-' + frame_id);
+ if (currentSource && el) {
+ currentSource.className += " hidden";
+ el.className = el.className.replace(" hidden", "");
+ currentSource = el;
+ }
+ }
+ }
+ </script>
</div>
diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.text.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.text.erb
index d4af5c9b06..c0b53068f7 100644
--- a/actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.text.erb
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.text.erb
@@ -1,15 +1,9 @@
-<%
- traces = { "Application Trace" => @application_trace,
- "Framework Trace" => @framework_trace,
- "Full Trace" => @full_trace }
-%>
-
Rails.root: <%= defined?(Rails) && Rails.respond_to?(:root) ? Rails.root : "unset" %>
-<% traces.each do |name, trace| %>
+<% @traces.each do |name, trace| %>
<% if trace.any? %>
<%= name %>
-<%= trace.join("\n") %>
+<%= trace.map { |t| t[:trace] }.join("\n") %>
<% end %>
<% end %>
diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/layout.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/layout.erb
index bc5d03dc10..e0509f56f4 100644
--- a/actionpack/lib/action_dispatch/middleware/templates/rescues/layout.erb
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/layout.erb
@@ -116,9 +116,15 @@
background-color: #FFCCCC;
}
+ .hidden {
+ display: none;
+ }
+
a { color: #980905; }
a:visited { color: #666; }
+ a.trace-frames { color: #666; }
a:hover { color: #C52F24; }
+ a.trace-frames.selected { color: #C52F24 }
<%= yield :style %>
</style>
diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/missing_template.html.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/missing_template.html.erb
index 5c016e544e..2a65fd06ad 100644
--- a/actionpack/lib/action_dispatch/middleware/templates/rescues/missing_template.html.erb
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/missing_template.html.erb
@@ -4,4 +4,8 @@
<div id="container">
<h2><%= h @exception.message %></h2>
+
+ <%= render template: "rescues/_source" %>
+ <%= render template: "rescues/_trace" %>
+ <%= render template: "rescues/_request_and_response" %>
</div>
diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/routing_error.html.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/routing_error.html.erb
index 7e9cedb95e..55dd5ddc7b 100644
--- a/actionpack/lib/action_dispatch/middleware/templates/rescues/routing_error.html.erb
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/routing_error.html.erb
@@ -27,4 +27,6 @@
<%= @routes_inspector.format(ActionDispatch::Routing::HtmlTableFormatter.new(self)) %>
<% end %>
+
+ <%= render template: "rescues/_request_and_response" %>
</div>
diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb
index 027a0f5b3e..c1e8b6cae3 100644
--- a/actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb
@@ -1,4 +1,3 @@
-<% @source_extract = @exception.source_extract(0, :html) %>
<header>
<h1>
<%= @exception.original_exception.class.to_s %> in
@@ -12,29 +11,7 @@
</p>
<pre><code><%= h @exception.message %></code></pre>
- <div class="source">
- <div class="info">
- <p>Extracted source (around line <strong>#<%= @exception.line_number %></strong>):</p>
- </div>
- <div class="data">
- <table cellpadding="0" cellspacing="0" class="lines">
- <tr>
- <td>
- <pre class="line_numbers">
- <% @source_extract.keys.each do |line_number| %>
-<span><%= line_number -%></span>
- <% end %>
- </pre>
- </td>
-<td width="100%">
-<pre>
-<% @source_extract.each do |line, source| -%><div class="line<%= " active" if line == @exception.line_number -%>"><%= source -%></div><% end -%>
-</pre>
-</td>
- </tr>
- </table>
-</div>
-</div>
+ <%= render template: "rescues/_source" %>
<p><%= @exception.sub_template_message %></p>
diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.text.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.text.erb
index 5da21d9784..77bcd26726 100644
--- a/actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.text.erb
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.text.erb
@@ -1,4 +1,3 @@
-<% @source_extract = @exception.source_extract(0, :html) %>
<%= @exception.original_exception.class.to_s %> in <%= @request.parameters["controller"].camelize if @request.parameters["controller"] %>#<%= @request.parameters["action"] %>
Showing <%= @exception.file_name %> where line #<%= @exception.line_number %> raised:
diff --git a/actionpack/lib/action_dispatch/middleware/templates/routes/_table.html.erb b/actionpack/lib/action_dispatch/middleware/templates/routes/_table.html.erb
index 6ffa242da4..5cee0b5932 100644
--- a/actionpack/lib/action_dispatch/middleware/templates/routes/_table.html.erb
+++ b/actionpack/lib/action_dispatch/middleware/templates/routes/_table.html.erb
@@ -1,6 +1,6 @@
<% content_for :style do %>
#route_table {
- margin: 0 auto 0;
+ margin: 0;
border-collapse: collapse;
}
diff --git a/actionpack/lib/action_dispatch/request/utils.rb b/actionpack/lib/action_dispatch/request/utils.rb
index 9d4f1aa3c5..1c9371d89c 100644
--- a/actionpack/lib/action_dispatch/request/utils.rb
+++ b/actionpack/lib/action_dispatch/request/utils.rb
@@ -16,10 +16,6 @@ module ActionDispatch
when Array
v.grep(Hash) { |x| deep_munge(x, keys) }
v.compact!
- if v.empty?
- hash[k] = nil
- ActiveSupport::Notifications.instrument("deep_munge.action_controller", keys: keys)
- end
when Hash
deep_munge(v, keys)
end
diff --git a/actionpack/lib/action_dispatch/routing/inspector.rb b/actionpack/lib/action_dispatch/routing/inspector.rb
index ea3b2f419d..df5ebb6751 100644
--- a/actionpack/lib/action_dispatch/routing/inspector.rb
+++ b/actionpack/lib/action_dispatch/routing/inspector.rb
@@ -48,7 +48,7 @@ module ActionDispatch
def reqs
@reqs ||= begin
reqs = endpoint
- reqs += " #{constraints.to_s}" unless constraints.empty?
+ reqs += " #{constraints}" unless constraints.empty?
reqs
end
end
@@ -114,9 +114,7 @@ module ActionDispatch
def collect_routes(routes)
routes.collect do |route|
RouteWrapper.new(route)
- end.reject do |route|
- route.internal?
- end.collect do |route|
+ end.reject(&:internal?).collect do |route|
collect_engine_routes(route)
{ name: route.name,
diff --git a/actionpack/lib/action_dispatch/routing/mapper.rb b/actionpack/lib/action_dispatch/routing/mapper.rb
index 3bc578b379..34b5b48f3a 100644
--- a/actionpack/lib/action_dispatch/routing/mapper.rb
+++ b/actionpack/lib/action_dispatch/routing/mapper.rb
@@ -12,9 +12,6 @@ module ActionDispatch
module Routing
class Mapper
URL_OPTIONS = [:protocol, :subdomain, :domain, :host, :port]
- SCOPE_OPTIONS = [:path, :shallow_path, :as, :shallow_prefix, :module,
- :controller, :action, :path_names, :constraints,
- :shallow, :blocks, :defaults, :options]
class Constraints < Endpoint #:nodoc:
attr_reader :app, :constraints
@@ -60,32 +57,74 @@ module ActionDispatch
end
class Mapping #:nodoc:
- IGNORE_OPTIONS = [:to, :as, :via, :on, :constraints, :defaults, :only, :except, :anchor, :shallow, :shallow_path, :shallow_prefix, :format]
ANCHOR_CHARACTERS_REGEX = %r{\A(\\A|\^)|(\\Z|\\z|\$)\Z}
- attr_reader :scope, :options, :requirements, :conditions, :defaults
- attr_reader :to, :default_controller, :default_action
-
- def initialize(scope, path, options)
- @scope = scope
- @requirements, @conditions, @defaults = {}, {}, {}
+ attr_reader :requirements, :conditions, :defaults
+ attr_reader :to, :default_controller, :default_action, :as, :anchor
+ def self.build(scope, set, path, as, options)
options = scope[:options].merge(options) if scope[:options]
- @to = options[:to]
- @default_controller = options[:controller] || scope[:controller]
- @default_action = options[:action] || scope[:action]
- path = normalize_path! path, options[:format]
+ options.delete :only
+ options.delete :except
+ options.delete :shallow_path
+ options.delete :shallow_prefix
+ options.delete :shallow
+
+ defaults = (scope[:defaults] || {}).merge options.delete(:defaults) || {}
+
+ new scope, set, path, defaults, as, options
+ end
+
+ def initialize(scope, set, path, defaults, as, options)
+ @requirements, @conditions = {}, {}
+ @defaults = defaults
+ @set = set
+
+ @to = options.delete :to
+ @default_controller = options.delete(:controller) || scope[:controller]
+ @default_action = options.delete(:action) || scope[:action]
+ @as = as
+ @anchor = options.delete :anchor
+
+ formatted = options.delete :format
+ via = Array(options.delete(:via) { [] })
+ options_constraints = options.delete :constraints
+
+ path = normalize_path! path, formatted
ast = path_ast path
path_params = path_params ast
- @options = normalize_options!(options, path_params, ast)
- normalize_requirements!(path_params)
- normalize_conditions!(path_params, path, ast)
- normalize_defaults!
+
+ options = normalize_options!(options, formatted, path_params, ast, scope[:module])
+
+
+ split_constraints(path_params, scope[:constraints]) if scope[:constraints]
+ constraints = constraints(options, path_params)
+
+ split_constraints path_params, constraints
+
+ @blocks = blocks(options_constraints, scope[:blocks])
+
+ if options_constraints.is_a?(Hash)
+ split_constraints path_params, options_constraints
+ options_constraints.each do |key, default|
+ if URL_OPTIONS.include?(key) && (String === default || Fixnum === default)
+ @defaults[key] ||= default
+ end
+ end
+ end
+
+ normalize_format!(formatted)
+
+ @conditions[:path_info] = path
+ @conditions[:parsed_path_info] = ast
+
+ add_request_method(via, @conditions)
+ normalize_defaults!(options)
end
def to_route
- [ app, conditions, requirements, defaults, options[:as], options[:anchor] ]
+ [ app(@blocks), conditions, requirements, defaults, as, anchor ]
end
private
@@ -106,17 +145,17 @@ module ActionDispatch
format != false && !path.include?(':format') && !path.end_with?('/')
end
- def normalize_options!(options, path_params, path_ast)
+ def normalize_options!(options, formatted, path_params, path_ast, modyoule)
# Add a constraint for wildcard route to make it non-greedy and match the
# optional format part of the route by default
- if options[:format] != false
+ if formatted != false
path_ast.grep(Journey::Nodes::Star) do |node|
options[node.name.to_sym] ||= /.+?/
end
end
if path_params.include?(:controller)
- raise ArgumentError, ":controller segment is not allowed within a namespace block" if scope[:module]
+ raise ArgumentError, ":controller segment is not allowed within a namespace block" if modyoule
# Add a default constraint for :controller path segments that matches namespaced
# controllers with default routes like :controller/:action/:id(.:format), e.g:
@@ -128,23 +167,36 @@ module ActionDispatch
if to.respond_to? :call
options
else
- options.merge!(default_controller_and_action(path_params))
+ to_endpoint = split_to to
+ controller = to_endpoint[0] || default_controller
+ action = to_endpoint[1] || default_action
+
+ controller = add_controller_module(controller, modyoule)
+
+ options.merge! check_controller_and_action(path_params, controller, action)
end
end
- def normalize_requirements!(path_params)
- constraints.each do |key, requirement|
- next unless path_params.include?(key) || key == :controller
- verify_regexp_requirement(requirement) if requirement.is_a?(Regexp)
- @requirements[key] = requirement
+ def split_constraints(path_params, constraints)
+ constraints.each_pair do |key, requirement|
+ if path_params.include?(key) || key == :controller
+ verify_regexp_requirement(requirement) if requirement.is_a?(Regexp)
+ @requirements[key] = requirement
+ else
+ @conditions[key] = requirement
+ end
end
+ end
- if options[:format] == true
+ def normalize_format!(formatted)
+ if formatted == true
@requirements[:format] ||= /.+/
- elsif Regexp === options[:format]
- @requirements[:format] = options[:format]
- elsif String === options[:format]
- @requirements[:format] = Regexp.compile(options[:format])
+ elsif Regexp === formatted
+ @requirements[:format] = formatted
+ @defaults[:format] = nil
+ elsif String === formatted
+ @requirements[:format] = Regexp.compile(formatted)
+ @defaults[:format] = formatted
end
end
@@ -158,31 +210,12 @@ module ActionDispatch
end
end
- def normalize_defaults!
- @defaults.merge!(scope[:defaults]) if scope[:defaults]
- @defaults.merge!(options[:defaults]) if options[:defaults]
-
- options.each do |key, default|
- unless Regexp === default || IGNORE_OPTIONS.include?(key)
+ def normalize_defaults!(options)
+ options.each_pair do |key, default|
+ unless Regexp === default
@defaults[key] = default
end
end
-
- if options[:constraints].is_a?(Hash)
- options[:constraints].each do |key, default|
- if URL_OPTIONS.include?(key) && (String === default || Fixnum === default)
- @defaults[key] ||= default
- end
- end
- elsif options[:constraints]
- verify_callable_constraint(options[:constraints])
- end
-
- if Regexp === options[:format]
- @defaults[:format] = nil
- elsif String === options[:format]
- @defaults[:format] = options[:format]
- end
end
def verify_callable_constraint(callable_constraint)
@@ -191,61 +224,32 @@ module ActionDispatch
end
end
- def normalize_conditions!(path_params, path, ast)
- @conditions[:path_info] = path
- @conditions[:parsed_path_info] = ast
+ def add_request_method(via, conditions)
+ return if via == [:all]
- constraints.each do |key, condition|
- unless path_params.include?(key) || key == :controller
- @conditions[key] = condition
- end
- end
-
- required_defaults = []
- options.each do |key, required_default|
- unless path_params.include?(key) || IGNORE_OPTIONS.include?(key) || Regexp === required_default
- required_defaults << key
- end
- end
- @conditions[:required_defaults] = required_defaults
-
- via_all = options.delete(:via) if options[:via] == :all
-
- if !via_all && options[:via].blank?
+ if via.empty?
msg = "You should not use the `match` method in your router without specifying an HTTP method.\n" \
"If you want to expose your action to both GET and POST, add `via: [:get, :post]` option.\n" \
"If you want to expose your action to GET, use `get` in the router:\n" \
" Instead of: match \"controller#action\"\n" \
" Do: get \"controller#action\""
- raise msg
+ raise ArgumentError, msg
end
- if via = options[:via]
- @conditions[:request_method] = Array(via).map { |m| m.to_s.dasherize.upcase }
- end
+ conditions[:request_method] = via.map { |m| m.to_s.dasherize.upcase }
end
- def app
- return to if Redirect === to
-
+ def app(blocks)
if to.respond_to?(:call)
Constraints.new(to, blocks, false)
+ elsif blocks.any?
+ Constraints.new(dispatcher(defaults), blocks, true)
else
- if blocks.any?
- Constraints.new(dispatcher, blocks, true)
- else
- dispatcher
- end
+ dispatcher(defaults)
end
end
- def default_controller_and_action(path_params)
- controller, action = get_controller_and_action(default_controller,
- default_action,
- to,
- @scope[:module]
- )
-
+ def check_controller_and_action(path_params, controller, action)
hash = check_part(:controller, controller, path_params, {}) do |part|
translate_controller(part) {
message = "'#{part}' is not a supported controller name. This can lead to potential routing problems."
@@ -272,21 +276,24 @@ module ActionDispatch
hash
end
- def get_controller_and_action(controller, action, to, modyoule)
- case to
- when Symbol then action = to.to_s
- when /#/ then controller, action = to.split('#')
- when String then controller = to
+ def split_to(to)
+ if to =~ /#/
+ to.split('#')
+ else
+ []
end
+ end
+ def add_controller_module(controller, modyoule)
if modyoule && !controller.is_a?(Regexp)
if controller =~ %r{\A/}
- controller = controller[1..-1]
+ controller[1..-1]
else
- controller = [modyoule, controller].compact.join("/")
+ [modyoule, controller].compact.join("/")
end
+ else
+ controller
end
- [controller, action]
end
def translate_controller(controller)
@@ -296,24 +303,27 @@ module ActionDispatch
yield
end
- def blocks
- if options[:constraints].present? && !options[:constraints].is_a?(Hash)
- [options[:constraints]]
+ def blocks(options_constraints, scope_blocks)
+ if options_constraints && !options_constraints.is_a?(Hash)
+ verify_callable_constraint(options_constraints)
+ [options_constraints]
else
- scope[:blocks] || []
+ scope_blocks || []
end
end
- def constraints
- @constraints ||= {}.tap do |constraints|
- constraints.merge!(scope[:constraints]) if scope[:constraints]
-
- options.except(*IGNORE_OPTIONS).each do |key, option|
- constraints[key] = option if Regexp === option
+ def constraints(options, path_params)
+ constraints = {}
+ required_defaults = []
+ options.each_pair do |key, option|
+ if Regexp === option
+ constraints[key] = option
+ else
+ required_defaults << key unless path_params.include?(key)
end
-
- constraints.merge!(options[:constraints]) if options[:constraints].is_a?(Hash)
end
+ @conditions[:required_defaults] = required_defaults
+ constraints
end
def path_params(ast)
@@ -325,8 +335,8 @@ module ActionDispatch
parser.parse path
end
- def dispatcher
- Routing::RouteSet::Dispatcher.new(defaults)
+ def dispatcher(defaults)
+ @set.dispatcher defaults
end
end
@@ -363,7 +373,7 @@ module ActionDispatch
# Matches a url pattern to one or more routes.
#
- # You should not use the `match` method in your router
+ # You should not use the +match+ method in your router
# without specifying an HTTP method.
#
# If you want to expose your action to both GET and POST, use:
@@ -374,7 +384,7 @@ module ActionDispatch
# Note that +:controller+, +:action+ and +:id+ are interpreted as url
# query parameters and thus available through +params+ in an action.
#
- # If you want to expose your action to GET, use `get` in the router:
+ # If you want to expose your action to GET, use +get+ in the router:
#
# Instead of:
#
@@ -415,7 +425,7 @@ module ActionDispatch
#
# Because requesting various HTTP verbs with a single action has security
# implications, you must either specify the actions in
- # the via options or use one of the HtttpHelpers[rdoc-ref:HttpHelpers]
+ # the via options or use one of the HttpHelpers[rdoc-ref:HttpHelpers]
# instead +match+
#
# === Options
@@ -429,7 +439,7 @@ module ActionDispatch
# The route's action.
#
# [:param]
- # Overrides the default resource identifier `:id` (name of the
+ # Overrides the default resource identifier +:id+ (name of the
# dynamic segment used to generate the routes).
# You can access that segment from your controller using
# <tt>params[<:param>]</tt>.
@@ -553,13 +563,15 @@ module ActionDispatch
raise "A rack application must be specified" unless path
- options[:as] ||= app_name(app)
+ rails_app = rails_app? app
+ options[:as] ||= app_name(app, rails_app)
+
target_as = name_for_action(options[:as], path)
options[:via] ||= :all
match(path, options.merge(:to => app, :anchor => false, :format => false))
- define_generate_prefix(app, target_as)
+ define_generate_prefix(app, target_as) if rails_app
self
end
@@ -580,31 +592,33 @@ module ActionDispatch
end
private
- def app_name(app)
- return unless app.respond_to?(:routes)
+ def rails_app?(app)
+ app.is_a?(Class) && app < Rails::Railtie
+ end
- if app.respond_to?(:railtie_name)
+ def app_name(app, rails_app)
+ if rails_app
app.railtie_name
- else
- class_name = app.class.is_a?(Class) ? app.name : app.class.name
+ elsif app.is_a?(Class)
+ class_name = app.name
ActiveSupport::Inflector.underscore(class_name).tr("/", "_")
end
end
def define_generate_prefix(app, name)
- return unless app.respond_to?(:routes) && app.routes.respond_to?(:define_mounted_helper)
-
- _route = @set.named_routes.routes[name.to_sym]
+ _route = @set.named_routes.get name
_routes = @set
app.routes.define_mounted_helper(name)
app.routes.extend Module.new {
- def mounted?; true; end
+ def optimize_routes_generation?; false; end
define_method :find_script_name do |options|
- super(options) || begin
- prefix_options = options.slice(*_route.segment_keys)
- # we must actually delete prefix segment keys to avoid passing them to next url_for
- _route.segment_keys.each { |k| options.delete(k) }
- _routes.url_helpers.send("#{name}_path", prefix_options)
+ if options.key? :script_name
+ super(options)
+ else
+ prefix_options = options.slice(*_route.segment_keys)
+ # we must actually delete prefix segment keys to avoid passing them to next url_for
+ _route.segment_keys.each { |k| options.delete(k) }
+ _routes.url_helpers.send("#{name}_path", prefix_options)
end
end
}
@@ -694,7 +708,7 @@ module ActionDispatch
# resources :posts, module: "admin"
#
# If you want to route /admin/posts to +PostsController+
- # (without the Admin:: module prefix), you could use
+ # (without the <tt>Admin::</tt> module prefix), you could use
#
# scope "/admin" do
# resources :posts
@@ -748,7 +762,7 @@ module ActionDispatch
# end
def scope(*args)
options = args.extract_options!.dup
- recover = {}
+ scope = {}
options[:path] = args.flatten.join('/') if args.any?
options[:constraints] ||= {}
@@ -768,7 +782,7 @@ module ActionDispatch
block, options[:constraints] = options[:constraints], {}
end
- SCOPE_OPTIONS.each do |option|
+ @scope.options.each do |option|
if option == :blocks
value = block
elsif option == :options
@@ -778,15 +792,15 @@ module ActionDispatch
end
if value
- recover[option] = @scope[option]
- @scope[option] = send("merge_#{option}_scope", @scope[option], value)
+ scope[option] = send("merge_#{option}_scope", @scope[option], value)
end
end
+ @scope = @scope.new scope
yield
self
ensure
- @scope.merge!(recover)
+ @scope = @scope.parent
end
# Scopes routes to a specific controller
@@ -1024,8 +1038,6 @@ module ActionDispatch
VALID_ON_OPTIONS = [:new, :collection, :member]
RESOURCE_OPTIONS = [:as, :controller, :path, :only, :except, :param, :concerns]
CANONICAL_ACTIONS = %w(index create new show update destroy)
- RESOURCE_METHOD_SCOPES = [:collection, :member, :new]
- RESOURCE_SCOPES = [:resource, :resources]
class Resource #:nodoc:
attr_reader :controller, :path, :options, :param
@@ -1411,7 +1423,7 @@ module ActionDispatch
end
with_scope_level(:nested) do
- if shallow? && shallow_nesting_depth > 1
+ if shallow? && shallow_nesting_depth >= 1
shallow_scope(parent_resource.nested_scope, nested_options) { yield }
else
scope(parent_resource.nested_scope, nested_options) { yield }
@@ -1445,7 +1457,20 @@ module ActionDispatch
if rest.empty? && Hash === path
options = path
path, to = options.find { |name, _value| name.is_a?(String) }
- options[:to] = to
+
+ case to
+ when Symbol
+ options[:action] = to
+ when String
+ if to =~ /#/
+ options[:to] = to
+ else
+ options[:controller] = to
+ end
+ else
+ options[:to] = to
+ end
+
options.delete(path)
paths = [path]
else
@@ -1479,14 +1504,14 @@ module ActionDispatch
end
def using_match_shorthand?(path, options)
- path && (options[:to] || options[:action]).nil? && path =~ %r{/[\w/]+$}
+ path && (options[:to] || options[:action]).nil? && path =~ %r{^/?[-\w]+/[-\w/]+$}
end
def decomposed_match(path, options) # :nodoc:
if on = options.delete(:on)
send(on) { decomposed_match(path, options) }
else
- case @scope[:scope_level]
+ case @scope.scope_level
when :resources
nested { decomposed_match(path, options) }
when :resource
@@ -1509,13 +1534,13 @@ module ActionDispatch
action = nil
end
- if !options.fetch(:as, true)
- options.delete(:as)
- else
- options[:as] = name_for_action(options[:as], action)
- end
+ as = if !options.fetch(:as, true) # if it's set to nil or false
+ options.delete(:as)
+ else
+ name_for_action(options.delete(:as), action)
+ end
- mapping = Mapping.new(@scope, URI.parser.escape(path), options)
+ mapping = Mapping.build(@scope, @set, URI.parser.escape(path), as, options)
app, conditions, requirements, defaults, as, anchor = mapping.to_route
@set.add_route(app, conditions, requirements, defaults, as, anchor)
end
@@ -1529,7 +1554,7 @@ module ActionDispatch
raise ArgumentError, "must be called with a path and/or options"
end
- if @scope[:scope_level] == :resources
+ if @scope.resources?
with_scope_level(:root) do
scope(parent_resource.path) do
super(options)
@@ -1596,40 +1621,39 @@ module ActionDispatch
end
def resource_scope? #:nodoc:
- RESOURCE_SCOPES.include? @scope[:scope_level]
+ @scope.resource_scope?
end
def resource_method_scope? #:nodoc:
- RESOURCE_METHOD_SCOPES.include? @scope[:scope_level]
+ @scope.resource_method_scope?
end
def nested_scope? #:nodoc:
- @scope[:scope_level] == :nested
+ @scope.nested?
end
def with_exclusive_scope
begin
- old_name_prefix, old_path = @scope[:as], @scope[:path]
- @scope[:as], @scope[:path] = nil, nil
+ @scope = @scope.new(:as => nil, :path => nil)
with_scope_level(:exclusive) do
yield
end
ensure
- @scope[:as], @scope[:path] = old_name_prefix, old_path
+ @scope = @scope.parent
end
end
def with_scope_level(kind)
- old, @scope[:scope_level] = @scope[:scope_level], kind
+ @scope = @scope.new_level(kind)
yield
ensure
- @scope[:scope_level] = old
+ @scope = @scope.parent
end
def resource_scope(kind, resource) #:nodoc:
resource.shallow = @scope[:shallow]
- old_resource, @scope[:scope_level_resource] = @scope[:scope_level_resource], resource
+ @scope = @scope.new(:scope_level_resource => resource)
@nesting.push(resource)
with_scope_level(kind) do
@@ -1637,7 +1661,7 @@ module ActionDispatch
end
ensure
@nesting.pop
- @scope[:scope_level_resource] = old_resource
+ @scope = @scope.parent
end
def nested_options #:nodoc:
@@ -1665,21 +1689,22 @@ module ActionDispatch
@scope[:constraints][parent_resource.param]
end
- def canonical_action?(action, flag) #:nodoc:
- flag && resource_method_scope? && CANONICAL_ACTIONS.include?(action.to_s)
+ def canonical_action?(action) #:nodoc:
+ resource_method_scope? && CANONICAL_ACTIONS.include?(action.to_s)
end
def shallow_scope(path, options = {}) #:nodoc:
- old_name_prefix, old_path = @scope[:as], @scope[:path]
- @scope[:as], @scope[:path] = @scope[:shallow_prefix], @scope[:shallow_path]
+ scope = { :as => @scope[:shallow_prefix],
+ :path => @scope[:shallow_path] }
+ @scope = @scope.new scope
scope(path, options) { yield }
ensure
- @scope[:as], @scope[:path] = old_name_prefix, old_path
+ @scope = @scope.parent
end
def path_for_action(action, path) #:nodoc:
- if canonical_action?(action, path.blank?)
+ if path.blank? && canonical_action?(action)
@scope[:path].to_s
else
"#{@scope[:path]}/#{action_path(action, path)}"
@@ -1694,15 +1719,17 @@ module ActionDispatch
def prefix_name_for_action(as, action) #:nodoc:
if as
prefix = as
- elsif !canonical_action?(action, @scope[:scope_level])
+ elsif !canonical_action?(action)
prefix = action
end
- prefix.to_s.tr('-', '_') if prefix
+
+ if prefix && prefix != '/' && !prefix.empty?
+ Mapper.normalize_name prefix.to_s.tr('-', '_')
+ end
end
def name_for_action(as, action) #:nodoc:
prefix = prefix_name_for_action(as, action)
- prefix = Mapper.normalize_name(prefix) if prefix
name_prefix = @scope[:as]
if parent_resource
@@ -1712,27 +1739,15 @@ module ActionDispatch
member_name = parent_resource.member_name
end
- name = case @scope[:scope_level]
- when :nested
- [name_prefix, prefix]
- when :collection
- [prefix, name_prefix, collection_name]
- when :new
- [prefix, :new, name_prefix, member_name]
- when :member
- [prefix, name_prefix, member_name]
- when :root
- [name_prefix, collection_name, prefix]
- else
- [name_prefix, member_name, prefix]
- end
+ action_name = @scope.action_name(name_prefix, prefix, collection_name, member_name)
+ candidate = action_name.select(&:present?).join('_')
- if candidate = name.select(&:present?).join("_").presence
+ unless candidate.empty?
# If a name was not explicitly given, we check if it is valid
# and return nil in case it isn't. Otherwise, we pass the invalid name
# forward so the underlying router engine treats it and raises an exception.
if as.nil?
- candidate unless @set.routes.find { |r| r.name == candidate } || candidate !~ /\A[_a-z]/i
+ candidate unless candidate !~ /\A[_a-z]/i || @set.named_routes.key?(candidate)
else
candidate
end
@@ -1857,9 +1872,83 @@ module ActionDispatch
end
end
+ class Scope # :nodoc:
+ OPTIONS = [:path, :shallow_path, :as, :shallow_prefix, :module,
+ :controller, :action, :path_names, :constraints,
+ :shallow, :blocks, :defaults, :options]
+
+ RESOURCE_SCOPES = [:resource, :resources]
+ RESOURCE_METHOD_SCOPES = [:collection, :member, :new]
+
+ attr_reader :parent, :scope_level
+
+ def initialize(hash, parent = {}, scope_level = nil)
+ @hash = hash
+ @parent = parent
+ @scope_level = scope_level
+ end
+
+ def nested?
+ scope_level == :nested
+ end
+
+ def resources?
+ scope_level == :resources
+ end
+
+ def resource_method_scope?
+ RESOURCE_METHOD_SCOPES.include? scope_level
+ end
+
+ def action_name(name_prefix, prefix, collection_name, member_name)
+ case scope_level
+ when :nested
+ [name_prefix, prefix]
+ when :collection
+ [prefix, name_prefix, collection_name]
+ when :new
+ [prefix, :new, name_prefix, member_name]
+ when :member
+ [prefix, name_prefix, member_name]
+ when :root
+ [name_prefix, collection_name, prefix]
+ else
+ [name_prefix, member_name, prefix]
+ end
+ end
+
+ def resource_scope?
+ RESOURCE_SCOPES.include? scope_level
+ end
+
+ def options
+ OPTIONS
+ end
+
+ def new(hash)
+ self.class.new hash, self, scope_level
+ end
+
+ def new_level(level)
+ self.class.new(self, self, level)
+ end
+
+ def fetch(key, &block)
+ @hash.fetch(key, &block)
+ end
+
+ def [](key)
+ @hash.fetch(key) { @parent[key] }
+ end
+
+ def []=(k,v)
+ @hash[k] = v
+ end
+ end
+
def initialize(set) #:nodoc:
@set = set
- @scope = { :path_names => @set.resources_path_names }
+ @scope = Scope.new({ :path_names => @set.resources_path_names })
@concerns = {}
@nesting = []
end
diff --git a/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb b/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb
index bd3696cda1..2e116ea9cd 100644
--- a/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb
+++ b/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb
@@ -1,5 +1,3 @@
-require 'action_controller/model_naming'
-
module ActionDispatch
module Routing
# Polymorphic URL helpers are methods for smart resolution to a named route call when
@@ -55,8 +53,6 @@ module ActionDispatch
# form_for([blog, @post]) # => "/blog/posts/1"
#
module PolymorphicRoutes
- include ActionController::ModelNaming
-
# Constructs a call to a named RESTful route for the given record and returns the
# resulting URL string. For example:
#
@@ -116,7 +112,6 @@ module ActionDispatch
action,
type,
opts
-
end
# Returns the path component of a URL for the given record. It uses
@@ -142,22 +137,26 @@ module ActionDispatch
%w(edit new).each do |action|
module_eval <<-EOT, __FILE__, __LINE__ + 1
- def #{action}_polymorphic_url(record_or_hash, options = {}) # def edit_polymorphic_url(record_or_hash, options = {})
- polymorphic_url( # polymorphic_url(
- record_or_hash, # record_or_hash,
- options.merge(:action => "#{action}")) # options.merge(:action => "edit"))
- end # end
- #
- def #{action}_polymorphic_path(record_or_hash, options = {}) # def edit_polymorphic_path(record_or_hash, options = {})
- polymorphic_url( # polymorphic_url(
- record_or_hash, # record_or_hash,
- options.merge(:action => "#{action}", :routing_type => :path)) # options.merge(:action => "edit", :routing_type => :path))
- end # end
+ def #{action}_polymorphic_url(record_or_hash, options = {})
+ polymorphic_url_for_action("#{action}", record_or_hash, options)
+ end
+
+ def #{action}_polymorphic_path(record_or_hash, options = {})
+ polymorphic_path_for_action("#{action}", record_or_hash, options)
+ end
EOT
end
private
+ def polymorphic_url_for_action(action, record_or_hash, options)
+ polymorphic_url(record_or_hash, options.merge(:action => action))
+ end
+
+ def polymorphic_path_for_action(action, record_or_hash, options)
+ polymorphic_path(record_or_hash, options.merge(:action => action))
+ end
+
class HelperMethodBuilder # :nodoc:
CACHE = { 'path' => {}, 'url' => {} }
@@ -192,7 +191,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)
@@ -247,11 +247,11 @@ module ActionDispatch
args = []
model = record.to_model
- name = if record.persisted?
+ name = if model.persisted?
args << model
- model.class.model_name.singular_route_key
+ model.model_name.singular_route_key
else
- @key_strategy.call model.class.model_name
+ @key_strategy.call model.model_name
end
named_route = prefix + "#{name}_#{suffix}"
@@ -279,7 +279,7 @@ module ActionDispatch
parent.model_name.singular_route_key
else
args << parent.to_model
- parent.to_model.class.model_name.singular_route_key
+ parent.to_model.model_name.singular_route_key
end
}
@@ -290,11 +290,12 @@ module ActionDispatch
when Class
@key_strategy.call record.model_name
else
- if record.persisted?
- args << record.to_model
- record.to_model.class.model_name.singular_route_key
+ model = record.to_model
+ if model.persisted?
+ args << model
+ model.model_name.singular_route_key
else
- @key_strategy.call record.to_model.class.model_name
+ @key_strategy.call model.model_name
end
end
diff --git a/actionpack/lib/action_dispatch/routing/redirection.rb b/actionpack/lib/action_dispatch/routing/redirection.rb
index b1b39a5496..3c1c4fadf6 100644
--- a/actionpack/lib/action_dispatch/routing/redirection.rb
+++ b/actionpack/lib/action_dispatch/routing/redirection.rb
@@ -37,7 +37,7 @@ module ActionDispatch
uri.host ||= req.host
uri.port ||= req.port unless req.standard_port?
- body = %(<html><body>You are being <a href="#{ERB::Util.h(uri.to_s)}">redirected</a>.</body></html>)
+ body = %(<html><body>You are being <a href="#{ERB::Util.unwrapped_html_escape(uri.to_s)}">redirected</a>.</body></html>)
headers = {
'Location' => uri.to_s,
diff --git a/actionpack/lib/action_dispatch/routing/route_set.rb b/actionpack/lib/action_dispatch/routing/route_set.rb
index bdda802195..ce04f0b08a 100644
--- a/actionpack/lib/action_dispatch/routing/route_set.rb
+++ b/actionpack/lib/action_dispatch/routing/route_set.rb
@@ -12,14 +12,15 @@ require 'action_dispatch/routing/endpoint'
module ActionDispatch
module Routing
- class RouteSet #:nodoc:
+ # :stopdoc:
+ class RouteSet
# Since the router holds references to many parts of the system
# like engines, controllers and the application itself, inspecting
# the route set can actually be really slow, therefore we default
# alias inspect to to_s.
alias inspect to_s
- class Dispatcher < Routing::Endpoint #:nodoc:
+ class Dispatcher < Routing::Endpoint
def initialize(defaults)
@defaults = defaults
@controller_class_names = ThreadSafe::Cache.new
@@ -84,38 +85,66 @@ module ActionDispatch
# A NamedRouteCollection instance is a collection of named routes, and also
# maintains an anonymous module that can be used to install helpers for the
# named routes.
- class NamedRouteCollection #:nodoc:
+ class NamedRouteCollection
include Enumerable
- attr_reader :routes, :helpers, :module
+ attr_reader :routes, :url_helpers_module, :path_helpers_module
def initialize
@routes = {}
- @helpers = []
- @module = Module.new
+ @path_helpers = Set.new
+ @url_helpers = Set.new
+ @url_helpers_module = Module.new
+ @path_helpers_module = Module.new
+ end
+
+ def route_defined?(name)
+ key = name.to_sym
+ @path_helpers.include?(key) || @url_helpers.include?(key)
end
def helper_names
- @helpers.map(&:to_s)
+ @path_helpers.map(&:to_s) + @url_helpers.map(&:to_s)
end
def clear!
- @helpers.each do |helper|
- @module.remove_possible_method helper
+ @path_helpers.each do |helper|
+ @path_helpers_module.send :undef_method, helper
+ end
+
+ @url_helpers.each do |helper|
+ @url_helpers_module.send :undef_method, helper
end
@routes.clear
- @helpers.clear
+ @path_helpers.clear
+ @url_helpers.clear
end
def add(name, route)
- routes[name.to_sym] = route
- define_named_route_methods(name, route)
+ key = name.to_sym
+ path_name = :"#{name}_path"
+ url_name = :"#{name}_url"
+
+ if routes.key? key
+ @path_helpers_module.send :undef_method, path_name
+ @url_helpers_module.send :undef_method, url_name
+ end
+ routes[key] = route
+ define_url_helper @path_helpers_module, route, path_name, route.defaults, name, PATH
+ define_url_helper @url_helpers_module, route, url_name, route.defaults, name, UNKNOWN
+
+ @path_helpers << path_name
+ @url_helpers << url_name
end
def get(name)
routes[name.to_sym]
end
+ def key?(name)
+ routes.key? name.to_sym
+ end
+
alias []= add
alias [] get
alias clear clear!
@@ -133,12 +162,12 @@ module ActionDispatch
routes.length
end
- class UrlHelper # :nodoc:
- def self.create(route, options)
+ class UrlHelper
+ def self.create(route, options, route_name, url_strategy)
if optimize_helper?(route)
- OptimizedUrlHelper.new(route, options)
+ OptimizedUrlHelper.new(route, options, route_name, url_strategy)
else
- new route, options
+ new route, options, route_name, url_strategy
end
end
@@ -146,20 +175,22 @@ module ActionDispatch
!route.glob? && route.path.requirements.empty?
end
- class OptimizedUrlHelper < UrlHelper # :nodoc:
+ attr_reader :url_strategy, :route_name
+
+ class OptimizedUrlHelper < UrlHelper
attr_reader :arg_size
- def initialize(route, options)
+ def initialize(route, options, route_name, url_strategy)
super
@required_parts = @route.required_parts
@arg_size = @required_parts.size
end
- def call(t, args)
- if args.size == arg_size && !args.last.is_a?(Hash) && optimize_routes_generation?(t)
+ def call(t, args, inner_options)
+ if args.size == arg_size && !inner_options && optimize_routes_generation?(t)
options = t.url_options.merge @options
options[:path] = optimized_helper(args)
- ActionDispatch::Http::URL.url_for(options)
+ url_strategy.call options
else
super
end
@@ -201,30 +232,43 @@ module ActionDispatch
end
end
- def initialize(route, options)
+ def initialize(route, options, route_name, url_strategy)
@options = options
@segment_keys = route.segment_keys.uniq
@route = route
+ @url_strategy = url_strategy
+ @route_name = route_name
end
- def call(t, args)
+ def call(t, args, inner_options)
controller_options = t.url_options
options = controller_options.merge @options
- hash = handle_positional_args(controller_options, args, options, @segment_keys)
- t._routes.url_for(hash)
- end
+ hash = handle_positional_args(controller_options,
+ inner_options || {},
+ args,
+ options,
+ @segment_keys)
- def handle_positional_args(controller_options, args, result, path_params)
- inner_options = args.extract_options!
+ t._routes.url_for(hash, route_name, url_strategy)
+ end
+ def handle_positional_args(controller_options, inner_options, args, result, path_params)
if args.size > 0
- if args.size < path_params.size - 1 # take format into account
+ # take format into account
+ if path_params.include?(:format)
+ path_params_size = path_params.size - 1
+ else
+ path_params_size = path_params.size
+ end
+
+ if args.size < path_params_size
path_params -= controller_options.keys
path_params -= result.keys
end
- path_params.each { |param|
- result[param] = inner_options[param] || args.shift
- }
+ path_params -= inner_options.keys
+ path_params.take(args.size).each do |param|
+ result[param] = args.shift
+ end
end
result.merge!(inner_options)
@@ -245,27 +289,22 @@ module ActionDispatch
#
# foo_url(bar, baz, bang, sort_by: 'baz')
#
- def define_url_helper(route, name, options)
- helper = UrlHelper.create(route, options.dup)
-
- @module.remove_possible_method name
- @module.module_eval do
+ def define_url_helper(mod, route, name, opts, route_key, url_strategy)
+ helper = UrlHelper.create(route, opts, route_key, url_strategy)
+ mod.module_eval do
define_method(name) do |*args|
- helper.call self, args
+ options = nil
+ options = args.pop if args.last.is_a? Hash
+ helper.call self, args, options
end
end
-
- helpers << name
- end
-
- def define_named_route_methods(name, route)
- define_url_helper route, :"#{name}_path",
- route.defaults.merge(:use_route => name, :only_path => true)
- define_url_helper route, :"#{name}_url",
- route.defaults.merge(:use_route => name, :only_path => false)
end
end
+ # strategy for building urls to send to the client
+ PATH = ->(options) { ActionDispatch::Http::URL.path_for(options) }
+ UNKNOWN = ->(options) { ActionDispatch::Http::URL.url_for(options) }
+
attr_accessor :formatter, :set, :named_routes, :default_scope, :router
attr_accessor :disable_clear_and_finalize, :resources_path_names
attr_accessor :default_url_options, :request_class
@@ -278,7 +317,7 @@ module ActionDispatch
def initialize(request_class = ActionDispatch::Request)
self.named_routes = NamedRouteCollection.new
- self.resources_path_names = self.class.default_resources_path_names.dup
+ self.resources_path_names = self.class.default_resources_path_names
self.default_url_options = {}
self.request_class = request_class
@@ -319,6 +358,7 @@ module ActionDispatch
mapper.instance_exec(&block)
end
end
+ private :eval_block
def finalize!
return if @finalized
@@ -334,7 +374,11 @@ module ActionDispatch
@prepend.each { |blk| eval_block(blk) }
end
- module MountedHelpers #:nodoc:
+ def dispatcher(defaults)
+ Routing::RouteSet::Dispatcher.new(defaults)
+ end
+
+ module MountedHelpers
extend ActiveSupport::Concern
include UrlFor
end
@@ -351,9 +395,11 @@ module ActionDispatch
return if MountedHelpers.method_defined?(name)
routes = self
+ helpers = routes.url_helpers
+
MountedHelpers.class_eval do
define_method "_#{name}" do
- RoutesProxy.new(routes, _routes_context)
+ RoutesProxy.new(routes, _routes_context, helpers)
end
end
@@ -364,42 +410,62 @@ module ActionDispatch
RUBY
end
- def url_helpers
- @url_helpers ||= begin
- routes = self
-
- Module.new do
- extend ActiveSupport::Concern
- include UrlFor
-
- # Define url_for in the singleton level so one can do:
- # Rails.application.routes.url_helpers.url_for(args)
- @_routes = routes
- class << self
- delegate :url_for, :optimize_routes_generation?, :to => '@_routes'
- attr_reader :_routes
- def url_options; {}; end
- end
+ def url_helpers(supports_path = true)
+ routes = self
- # Make named_routes available in the module singleton
- # as well, so one can do:
- # Rails.application.routes.url_helpers.posts_path
- extend routes.named_routes.module
+ Module.new do
+ extend ActiveSupport::Concern
+ include UrlFor
- # Any class that includes this module will get all
- # named routes...
- include routes.named_routes.module
+ # Define url_for in the singleton level so one can do:
+ # Rails.application.routes.url_helpers.url_for(args)
+ @_routes = routes
+ class << self
+ def url_for(options)
+ @_routes.url_for(options)
+ end
- # plus a singleton class method called _routes ...
- included do
- singleton_class.send(:redefine_method, :_routes) { routes }
+ def optimize_routes_generation?
+ @_routes.optimize_routes_generation?
end
- # And an instance method _routes. Note that
- # UrlFor (included in this module) add extra
- # conveniences for working with @_routes.
- define_method(:_routes) { @_routes || routes }
+ attr_reader :_routes
+ def url_options; {}; end
+ end
+
+ url_helpers = routes.named_routes.url_helpers_module
+
+ # Make named_routes available in the module singleton
+ # as well, so one can do:
+ # Rails.application.routes.url_helpers.posts_path
+ extend url_helpers
+
+ # Any class that includes this module will get all
+ # named routes...
+ include url_helpers
+
+ if supports_path
+ path_helpers = routes.named_routes.path_helpers_module
+
+ include path_helpers
+ extend path_helpers
end
+
+ # plus a singleton class method called _routes ...
+ included do
+ singleton_class.send(:redefine_method, :_routes) { routes }
+ end
+
+ # And an instance method _routes. Note that
+ # UrlFor (included in this module) add extra
+ # conveniences for working with @_routes.
+ define_method(:_routes) { @_routes || routes }
+
+ define_method(:_generate_paths_by_default) do
+ supports_path
+ end
+
+ private :_generate_paths_by_default
end
end
@@ -420,15 +486,15 @@ module ActionDispatch
path = conditions.delete :path_info
ast = conditions.delete :parsed_path_info
- path = build_path(path, ast, requirements, SEPARATORS, anchor)
- conditions = build_conditions(conditions, path.names.map { |x| x.to_sym })
+ path = build_path(path, ast, requirements, anchor)
+ conditions = build_conditions(conditions, path.names.map(&:to_sym))
route = @set.add_route(app, path, conditions, defaults, name)
named_routes[name] = route if name
route
end
- def build_path(path, ast, requirements, separators, anchor)
+ def build_path(path, ast, requirements, anchor)
strexp = Journey::Router::Strexp.new(
ast,
path,
@@ -478,12 +544,12 @@ module ActionDispatch
end
private :build_conditions
- class Generator #:nodoc:
+ class Generator
PARAMETERIZE = lambda do |name, value|
if name == :controller
value
elsif value.is_a?(Array)
- value.map { |v| v.to_param }.join('/')
+ value.map(&:to_param).join('/')
elsif param = value.to_param
param
end
@@ -491,8 +557,8 @@ module ActionDispatch
attr_reader :options, :recall, :set, :named_route
- def initialize(options, recall, set)
- @named_route = options.delete(:use_route)
+ def initialize(named_route, options, recall, set)
+ @named_route = named_route
@options = options.dup
@recall = recall.dup
@set = set
@@ -608,32 +674,34 @@ module ActionDispatch
end
def generate_extras(options, recall={})
- path, params = generate(options, recall)
+ route_key = options.delete :use_route
+ path, params = generate(route_key, options, recall)
return path, params.keys
end
- def generate(options, recall = {})
- Generator.new(options, recall, self).generate
+ def generate(route_key, options, recall = {})
+ Generator.new(route_key, options, recall, self).generate
end
+ private :generate
RESERVED_OPTIONS = [:host, :protocol, :port, :subdomain, :domain, :tld_length,
:trailing_slash, :anchor, :params, :only_path, :script_name,
:original_script_name]
- def mounted?
- false
- end
-
def optimize_routes_generation?
- !mounted? && default_url_options.empty?
+ default_url_options.empty?
end
def find_script_name(options)
- options.delete :script_name
+ options.delete(:script_name) || ''
+ end
+
+ def path_for(options, route_name = nil)
+ url_for(options, route_name, PATH)
end
# The +options+ argument must be a hash whose keys are *symbols*.
- def url_for(options)
+ def url_for(options, route_name = nil, url_strategy = UNKNOWN)
options = default_url_options.merge options
user = password = nil
@@ -648,14 +716,14 @@ module ActionDispatch
original_script_name = options.delete(:original_script_name)
script_name = find_script_name options
- if script_name && original_script_name
+ if original_script_name
script_name = original_script_name + script_name
end
path_options = options.dup
RESERVED_OPTIONS.each { |ro| path_options.delete ro }
- path, params = generate(path_options, recall)
+ path, params = generate(route_name, path_options, recall)
if options.key? :params
params.merge! options[:params]
@@ -667,7 +735,7 @@ module ActionDispatch
options[:user] = user
options[:password] = password
- ActionDispatch::Http::URL.url_for(options)
+ url_strategy.call options
end
def call(env)
@@ -714,5 +782,6 @@ module ActionDispatch
raise ActionController::RoutingError, "No route matches #{path.inspect}"
end
end
+ # :startdoc:
end
end
diff --git a/actionpack/lib/action_dispatch/routing/routes_proxy.rb b/actionpack/lib/action_dispatch/routing/routes_proxy.rb
index e2393d3799..040ea04046 100644
--- a/actionpack/lib/action_dispatch/routing/routes_proxy.rb
+++ b/actionpack/lib/action_dispatch/routing/routes_proxy.rb
@@ -8,8 +8,9 @@ module ActionDispatch
attr_accessor :scope, :routes
alias :_routes :routes
- def initialize(routes, scope)
+ def initialize(routes, scope, helpers)
@routes, @scope = routes, scope
+ @helpers = helpers
end
def url_options
@@ -19,16 +20,16 @@ module ActionDispatch
end
def respond_to?(method, include_private = false)
- super || routes.url_helpers.respond_to?(method)
+ super || @helpers.respond_to?(method)
end
def method_missing(method, *args)
- if routes.url_helpers.respond_to?(method)
+ if @helpers.respond_to?(method)
self.class.class_eval <<-RUBY, __FILE__, __LINE__ + 1
def #{method}(*args)
options = args.extract_options!
args << url_options.merge((options || {}).symbolize_keys)
- routes.url_helpers.#{method}(*args)
+ @helpers.#{method}(*args)
end
RUBY
send(method, *args)
diff --git a/actionpack/lib/action_dispatch/routing/url_for.rb b/actionpack/lib/action_dispatch/routing/url_for.rb
index e624fe3c4a..dca86858cc 100644
--- a/actionpack/lib/action_dispatch/routing/url_for.rb
+++ b/actionpack/lib/action_dispatch/routing/url_for.rb
@@ -152,7 +152,9 @@ module ActionDispatch
when nil
_routes.url_for(url_options.symbolize_keys)
when Hash
- _routes.url_for(options.symbolize_keys.reverse_merge!(url_options))
+ route_name = options.delete :use_route
+ _routes.url_for(options.symbolize_keys.reverse_merge!(url_options),
+ route_name)
when String
options
when Symbol
@@ -169,8 +171,7 @@ module ActionDispatch
protected
def optimize_routes_generation?
- return @_optimized_routes if defined?(@_optimized_routes)
- @_optimized_routes = _routes.optimize_routes_generation? && default_url_options.empty?
+ _routes.optimize_routes_generation? && default_url_options.empty?
end
def _with_routes(routes)
@@ -183,6 +184,12 @@ module ActionDispatch
def _routes_context
self
end
+
+ private
+
+ def _generate_paths_by_default
+ true
+ end
end
end
end
diff --git a/actionpack/lib/action_dispatch/testing/assertions.rb b/actionpack/lib/action_dispatch/testing/assertions.rb
index 226baf9ad0..f325c35b57 100644
--- a/actionpack/lib/action_dispatch/testing/assertions.rb
+++ b/actionpack/lib/action_dispatch/testing/assertions.rb
@@ -1,18 +1,22 @@
+require 'rails-dom-testing'
+
module ActionDispatch
module Assertions
- autoload :DomAssertions, 'action_dispatch/testing/assertions/dom'
autoload :ResponseAssertions, 'action_dispatch/testing/assertions/response'
autoload :RoutingAssertions, 'action_dispatch/testing/assertions/routing'
- autoload :SelectorAssertions, 'action_dispatch/testing/assertions/selector'
- autoload :TagAssertions, 'action_dispatch/testing/assertions/tag'
extend ActiveSupport::Concern
- include DomAssertions
include ResponseAssertions
include RoutingAssertions
- include SelectorAssertions
- include TagAssertions
+ include Rails::Dom::Testing::Assertions
+
+ def html_document
+ @html_document ||= if @response.content_type =~ /xml$/
+ Nokogiri::XML::Document.parse(@response.body)
+ else
+ Nokogiri::HTML::Document.parse(@response.body)
+ end
+ end
end
end
-
diff --git a/actionpack/lib/action_dispatch/testing/assertions/dom.rb b/actionpack/lib/action_dispatch/testing/assertions/dom.rb
deleted file mode 100644
index 241a39393a..0000000000
--- a/actionpack/lib/action_dispatch/testing/assertions/dom.rb
+++ /dev/null
@@ -1,27 +0,0 @@
-require 'action_view/vendor/html-scanner'
-
-module ActionDispatch
- module Assertions
- module DomAssertions
- # \Test two HTML strings for equivalency (e.g., identical up to reordering of attributes)
- #
- # # assert that the referenced method generates the appropriate HTML string
- # assert_dom_equal '<a href="http://www.example.com">Apples</a>', link_to("Apples", "http://www.example.com")
- def assert_dom_equal(expected, actual, message = nil)
- expected_dom = HTML::Document.new(expected).root
- actual_dom = HTML::Document.new(actual).root
- assert_equal expected_dom, actual_dom, message
- end
-
- # The negated form of +assert_dom_equivalent+.
- #
- # # assert that the referenced method does not generate the specified HTML string
- # assert_dom_not_equal '<a href="http://www.example.com">Apples</a>', link_to("Oranges", "http://www.example.com")
- def assert_dom_not_equal(expected, actual, message = nil)
- expected_dom = HTML::Document.new(expected).root
- actual_dom = HTML::Document.new(actual).root
- assert_not_equal expected_dom, actual_dom, message
- end
- end
- end
-end
diff --git a/actionpack/lib/action_dispatch/testing/assertions/response.rb b/actionpack/lib/action_dispatch/testing/assertions/response.rb
index 0adc6c84ff..13a72220b3 100644
--- a/actionpack/lib/action_dispatch/testing/assertions/response.rb
+++ b/actionpack/lib/action_dispatch/testing/assertions/response.rb
@@ -73,13 +73,8 @@ module ActionDispatch
if Regexp === fragment
fragment
else
- handle = @controller || Class.new(ActionController::Metal) do
- include ActionController::Redirecting
- def initialize(request)
- @_request = request
- end
- end.new(@request)
- handle._compute_redirect_to_location(fragment)
+ handle = @controller || ActionController::Redirecting
+ handle._compute_redirect_to_location(@request, fragment)
end
end
end
diff --git a/actionpack/lib/action_dispatch/testing/assertions/routing.rb b/actionpack/lib/action_dispatch/testing/assertions/routing.rb
index f1f998d932..c94eea9134 100644
--- a/actionpack/lib/action_dispatch/testing/assertions/routing.rb
+++ b/actionpack/lib/action_dispatch/testing/assertions/routing.rb
@@ -38,18 +38,24 @@ module ActionDispatch
# # Test a custom route
# assert_recognizes({controller: 'items', action: 'show', id: '1'}, 'view/item1')
def assert_recognizes(expected_options, path, extras={}, msg=nil)
- request = recognized_request_for(path, extras)
+ if path.is_a?(Hash) && path[:method].to_s == "all"
+ [:get, :post, :put, :delete].each do |method|
+ assert_recognizes(expected_options, path.merge(method: method), extras, msg)
+ end
+ else
+ request = recognized_request_for(path, extras, msg)
- expected_options = expected_options.clone
+ expected_options = expected_options.clone
- expected_options.stringify_keys!
+ expected_options.stringify_keys!
- msg = message(msg, "") {
- sprintf("The recognized options <%s> did not match <%s>, difference:",
- request.path_parameters, expected_options)
- }
+ msg = message(msg, "") {
+ sprintf("The recognized options <%s> did not match <%s>, difference:",
+ request.path_parameters, expected_options)
+ }
- assert_equal(expected_options, request.path_parameters, msg)
+ assert_equal(expected_options, request.path_parameters, msg)
+ end
end
# Asserts that the provided options can be used to generate the provided path. This is the inverse of +assert_recognizes+.
@@ -69,9 +75,9 @@ module ActionDispatch
#
# # Asserts that the generated route gives us our custom route
# assert_generates "changesets/12", { controller: 'scm', action: 'show_diff', revision: "12" }
- def assert_generates(expected_path, options, defaults={}, extras = {}, message=nil)
+ def assert_generates(expected_path, options, defaults={}, extras={}, message=nil)
if expected_path =~ %r{://}
- fail_on(URI::InvalidURIError) do
+ fail_on(URI::InvalidURIError, message) do
uri = URI.parse(expected_path)
expected_path = uri.path.to_s.empty? ? "/" : uri.path
end
@@ -144,13 +150,7 @@ module ActionDispatch
old_controller, @controller = @controller, @controller.clone
_routes = @routes
- # Unfortunately, there is currently an abstraction leak between AC::Base
- # and AV::Base which requires having the URL helpers in both AC and AV.
- # To do this safely at runtime for tests, we need to bump up the helper serial
- # to that the old AV subclass isn't cached.
- #
- # TODO: Make this unnecessary
- @controller.singleton_class.send(:include, _routes.url_helpers)
+ @controller.singleton_class.include(_routes.url_helpers)
@controller.view_context_class = Class.new(@controller.view_context_class) do
include _routes.url_helpers
end
@@ -165,7 +165,7 @@ module ActionDispatch
# ROUTES TODO: These assertions should really work in an integration context
def method_missing(selector, *args, &block)
- if defined?(@controller) && @controller && @routes && @routes.named_routes.helpers.include?(selector)
+ if defined?(@controller) && @controller && @routes && @routes.named_routes.route_defined?(selector)
@controller.send(selector, *args, &block)
else
super
@@ -174,7 +174,7 @@ module ActionDispatch
private
# Recognizes the route for a given path.
- def recognized_request_for(path, extras = {})
+ def recognized_request_for(path, extras = {}, msg)
if path.is_a?(Hash)
method = path[:method]
path = path[:path]
@@ -186,7 +186,7 @@ module ActionDispatch
request = ActionController::TestRequest.new
if path =~ %r{://}
- fail_on(URI::InvalidURIError) do
+ fail_on(URI::InvalidURIError, msg) do
uri = URI.parse(path)
request.env["rack.url_scheme"] = uri.scheme || "http"
request.host = uri.host if uri.host
@@ -200,7 +200,7 @@ module ActionDispatch
request.request_method = method if method
- params = fail_on(ActionController::RoutingError) do
+ params = fail_on(ActionController::RoutingError, msg) do
@routes.recognize_path(path, { :method => method, :extras => extras })
end
request.path_parameters = params.with_indifferent_access
@@ -208,10 +208,10 @@ module ActionDispatch
request
end
- def fail_on(exception_class)
+ def fail_on(exception_class, message)
yield
rescue exception_class => e
- raise Minitest::Assertion, e.message
+ raise Minitest::Assertion, message || e.message
end
end
end
diff --git a/actionpack/lib/action_dispatch/testing/assertions/selector.rb b/actionpack/lib/action_dispatch/testing/assertions/selector.rb
deleted file mode 100644
index 12023e6f77..0000000000
--- a/actionpack/lib/action_dispatch/testing/assertions/selector.rb
+++ /dev/null
@@ -1,430 +0,0 @@
-require 'action_view/vendor/html-scanner'
-require 'active_support/core_ext/object/inclusion'
-
-#--
-# Copyright (c) 2006 Assaf Arkin (http://labnotes.org)
-# Under MIT and/or CC By license.
-#++
-
-module ActionDispatch
- module Assertions
- NO_STRIP = %w{pre script style textarea}
-
- # Adds the +assert_select+ method for use in Rails functional
- # test cases, which can be used to make assertions on the response HTML of a controller
- # action. You can also call +assert_select+ within another +assert_select+ to
- # make assertions on elements selected by the enclosing assertion.
- #
- # Use +css_select+ to select elements without making an assertions, either
- # from the response HTML or elements selected by the enclosing assertion.
- #
- # In addition to HTML responses, you can make the following assertions:
- #
- # * +assert_select_encoded+ - Assertions on HTML encoded inside XML, for example for dealing with feed item descriptions.
- # * +assert_select_email+ - Assertions on the HTML body of an e-mail.
- #
- # Also see HTML::Selector to learn how to use selectors.
- module SelectorAssertions
- # Select and return all matching elements.
- #
- # If called with a single argument, uses that argument as a selector
- # to match all elements of the current page. Returns an empty array
- # if no match is found.
- #
- # If called with two arguments, uses the first argument as the base
- # element and the second argument as the selector. Attempts to match the
- # base element and any of its children. Returns an empty array if no
- # match is found.
- #
- # The selector may be a CSS selector expression (String), an expression
- # with substitution values (Array) or an HTML::Selector object.
- #
- # # Selects all div tags
- # divs = css_select("div")
- #
- # # Selects all paragraph tags and does something interesting
- # pars = css_select("p")
- # pars.each do |par|
- # # Do something fun with paragraphs here...
- # end
- #
- # # Selects all list items in unordered lists
- # items = css_select("ul>li")
- #
- # # Selects all form tags and then all inputs inside the form
- # forms = css_select("form")
- # forms.each do |form|
- # inputs = css_select(form, "input")
- # ...
- # end
- def css_select(*args)
- # See assert_select to understand what's going on here.
- arg = args.shift
-
- if arg.is_a?(HTML::Node)
- root = arg
- arg = args.shift
- elsif arg == nil
- raise ArgumentError, "First argument is either selector or element to select, but nil found. Perhaps you called assert_select with an element that does not exist?"
- elsif defined?(@selected) && @selected
- matches = []
-
- @selected.each do |selected|
- subset = css_select(selected, HTML::Selector.new(arg.dup, args.dup))
- subset.each do |match|
- matches << match unless matches.any? { |m| m.equal?(match) }
- end
- end
-
- return matches
- else
- root = response_from_page
- end
-
- case arg
- when String
- selector = HTML::Selector.new(arg, args)
- when Array
- selector = HTML::Selector.new(*arg)
- when HTML::Selector
- selector = arg
- else raise ArgumentError, "Expecting a selector as the first argument"
- end
-
- selector.select(root)
- end
-
- # An assertion that selects elements and makes one or more equality tests.
- #
- # If the first argument is an element, selects all matching elements
- # starting from (and including) that element and all its children in
- # depth-first order.
- #
- # If no element if specified, calling +assert_select+ selects from the
- # response HTML unless +assert_select+ is called from within an +assert_select+ block.
- #
- # When called with a block +assert_select+ passes an array of selected elements
- # to the block. Calling +assert_select+ from the block, with no element specified,
- # runs the assertion on the complete set of elements selected by the enclosing assertion.
- # Alternatively the array may be iterated through so that +assert_select+ can be called
- # separately for each element.
- #
- #
- # ==== Example
- # If the response contains two ordered lists, each with four list elements then:
- # assert_select "ol" do |elements|
- # elements.each do |element|
- # assert_select element, "li", 4
- # end
- # end
- #
- # will pass, as will:
- # assert_select "ol" do
- # assert_select "li", 8
- # end
- #
- # The selector may be a CSS selector expression (String), an expression
- # with substitution values, or an HTML::Selector object.
- #
- # === Equality Tests
- #
- # The equality test may be one of the following:
- # * <tt>true</tt> - Assertion is true if at least one element selected.
- # * <tt>false</tt> - Assertion is true if no element selected.
- # * <tt>String/Regexp</tt> - Assertion is true if the text value of at least
- # one element matches the string or regular expression.
- # * <tt>Integer</tt> - Assertion is true if exactly that number of
- # elements are selected.
- # * <tt>Range</tt> - Assertion is true if the number of selected
- # elements fit the range.
- # If no equality test specified, the assertion is true if at least one
- # element selected.
- #
- # To perform more than one equality tests, use a hash with the following keys:
- # * <tt>:text</tt> - Narrow the selection to elements that have this text
- # value (string or regexp).
- # * <tt>:html</tt> - Narrow the selection to elements that have this HTML
- # content (string or regexp).
- # * <tt>:count</tt> - Assertion is true if the number of selected elements
- # is equal to this value.
- # * <tt>:minimum</tt> - Assertion is true if the number of selected
- # elements is at least this value.
- # * <tt>:maximum</tt> - Assertion is true if the number of selected
- # elements is at most this value.
- #
- # If the method is called with a block, once all equality tests are
- # evaluated the block is called with an array of all matched elements.
- #
- # # At least one form element
- # assert_select "form"
- #
- # # Form element includes four input fields
- # assert_select "form input", 4
- #
- # # Page title is "Welcome"
- # assert_select "title", "Welcome"
- #
- # # Page title is "Welcome" and there is only one title element
- # assert_select "title", {count: 1, text: "Welcome"},
- # "Wrong title or more than one title element"
- #
- # # Page contains no forms
- # assert_select "form", false, "This page must contain no forms"
- #
- # # Test the content and style
- # assert_select "body div.header ul.menu"
- #
- # # Use substitution values
- # assert_select "ol>li#?", /item-\d+/
- #
- # # All input fields in the form have a name
- # assert_select "form input" do
- # assert_select "[name=?]", /.+/ # Not empty
- # end
- def assert_select(*args, &block)
- # Start with optional element followed by mandatory selector.
- arg = args.shift
- @selected ||= nil
-
- if arg.is_a?(HTML::Node)
- # First argument is a node (tag or text, but also HTML root),
- # so we know what we're selecting from.
- root = arg
- arg = args.shift
- elsif arg == nil
- # This usually happens when passing a node/element that
- # happens to be nil.
- raise ArgumentError, "First argument is either selector or element to select, but nil found. Perhaps you called assert_select with an element that does not exist?"
- elsif @selected
- root = HTML::Node.new(nil)
- root.children.concat @selected
- else
- # Otherwise just operate on the response document.
- root = response_from_page
- end
-
- # First or second argument is the selector: string and we pass
- # all remaining arguments. Array and we pass the argument. Also
- # accepts selector itself.
- case arg
- when String
- selector = HTML::Selector.new(arg, args)
- when Array
- selector = HTML::Selector.new(*arg)
- when HTML::Selector
- selector = arg
- else raise ArgumentError, "Expecting a selector as the first argument"
- end
-
- # Next argument is used for equality tests.
- equals = {}
- case arg = args.shift
- when Hash
- equals = arg
- when String, Regexp
- equals[:text] = arg
- when Integer
- equals[:count] = arg
- when Range
- equals[:minimum] = arg.begin
- equals[:maximum] = arg.end
- when FalseClass
- equals[:count] = 0
- when NilClass, TrueClass
- equals[:minimum] = 1
- else raise ArgumentError, "I don't understand what you're trying to match"
- end
-
- # By default we're looking for at least one match.
- if equals[:count]
- equals[:minimum] = equals[:maximum] = equals[:count]
- else
- equals[:minimum] = 1 unless equals[:minimum]
- end
-
- # Last argument is the message we use if the assertion fails.
- message = args.shift
- #- message = "No match made with selector #{selector.inspect}" unless message
- if args.shift
- raise ArgumentError, "Not expecting that last argument, you either have too many arguments, or they're the wrong type"
- end
-
- matches = selector.select(root)
- # If text/html, narrow down to those elements that match it.
- content_mismatch = nil
- if match_with = equals[:text]
- matches.delete_if do |match|
- text = ""
- stack = match.children.reverse
- while node = stack.pop
- if node.tag?
- stack.concat node.children.reverse
- else
- content = node.content
- text << content
- end
- end
- text.strip! unless NO_STRIP.include?(match.name)
- text.sub!(/\A\n/, '') if match.name == "textarea"
- unless match_with.is_a?(Regexp) ? (text =~ match_with) : (text == match_with.to_s)
- content_mismatch ||= sprintf("<%s> expected but was\n<%s>", match_with, text)
- true
- end
- end
- elsif match_with = equals[:html]
- matches.delete_if do |match|
- html = match.children.map(&:to_s).join
- html.strip! unless NO_STRIP.include?(match.name)
- unless match_with.is_a?(Regexp) ? (html =~ match_with) : (html == match_with.to_s)
- content_mismatch ||= sprintf("<%s> expected but was\n<%s>", match_with, html)
- true
- end
- end
- end
- # Expecting foo found bar element only if found zero, not if
- # found one but expecting two.
- message ||= content_mismatch if matches.empty?
- # Test minimum/maximum occurrence.
- min, max, count = equals[:minimum], equals[:maximum], equals[:count]
-
- # FIXME: minitest provides messaging when we use assert_operator,
- # so is this custom message really needed?
- message = message || %(Expected #{count_description(min, max, count)} matching "#{selector.to_s}", found #{matches.size})
- if count
- assert_equal count, matches.size, message
- else
- assert_operator matches.size, :>=, min, message if min
- assert_operator matches.size, :<=, max, message if max
- end
-
- # If a block is given call that block. Set @selected to allow
- # nested assert_select, which can be nested several levels deep.
- if block_given? && !matches.empty?
- begin
- in_scope, @selected = @selected, matches
- yield matches
- ensure
- @selected = in_scope
- end
- end
-
- # Returns all matches elements.
- matches
- end
-
- def count_description(min, max, count) #:nodoc:
- pluralize = lambda {|word, quantity| word << (quantity == 1 ? '' : 's')}
-
- if min && max && (max != min)
- "between #{min} and #{max} elements"
- elsif min && max && max == min && count
- "exactly #{count} #{pluralize['element', min]}"
- elsif min && !(min == 1 && max == 1)
- "at least #{min} #{pluralize['element', min]}"
- elsif max
- "at most #{max} #{pluralize['element', max]}"
- end
- end
-
- # Extracts the content of an element, treats it as encoded HTML and runs
- # nested assertion on it.
- #
- # You typically call this method within another assertion to operate on
- # all currently selected elements. You can also pass an element or array
- # of elements.
- #
- # The content of each element is un-encoded, and wrapped in the root
- # element +encoded+. It then calls the block with all un-encoded elements.
- #
- # # Selects all bold tags from within the title of an Atom feed's entries (perhaps to nab a section name prefix)
- # assert_select "feed[xmlns='http://www.w3.org/2005/Atom']" do
- # # Select each entry item and then the title item
- # assert_select "entry>title" do
- # # Run assertions on the encoded title elements
- # assert_select_encoded do
- # assert_select "b"
- # end
- # end
- # end
- #
- #
- # # Selects all paragraph tags from within the description of an RSS feed
- # assert_select "rss[version=2.0]" do
- # # Select description element of each feed item.
- # assert_select "channel>item>description" do
- # # Run assertions on the encoded elements.
- # assert_select_encoded do
- # assert_select "p"
- # end
- # end
- # end
- def assert_select_encoded(element = nil, &block)
- case element
- when Array
- elements = element
- when HTML::Node
- elements = [element]
- when nil
- unless elements = @selected
- raise ArgumentError, "First argument is optional, but must be called from a nested assert_select"
- end
- else
- raise ArgumentError, "Argument is optional, and may be node or array of nodes"
- end
-
- fix_content = lambda do |node|
- # Gets around a bug in the Rails 1.1 HTML parser.
- node.content.gsub(/<!\[CDATA\[(.*)(\]\]>)?/m) { Rack::Utils.escapeHTML($1) }
- end
-
- selected = elements.map do |elem|
- text = elem.children.select{ |c| not c.tag? }.map{ |c| fix_content[c] }.join
- root = HTML::Document.new(CGI.unescapeHTML("<encoded>#{text}</encoded>")).root
- css_select(root, "encoded:root", &block)[0]
- end
-
- begin
- old_selected, @selected = @selected, selected
- assert_select ":root", &block
- ensure
- @selected = old_selected
- end
- end
-
- # Extracts the body of an email and runs nested assertions on it.
- #
- # You must enable deliveries for this assertion to work, use:
- # ActionMailer::Base.perform_deliveries = true
- #
- # assert_select_email do
- # assert_select "h1", "Email alert"
- # end
- #
- # assert_select_email do
- # items = assert_select "ol>li"
- # items.each do
- # # Work with items here...
- # end
- # end
- def assert_select_email(&block)
- deliveries = ActionMailer::Base.deliveries
- assert !deliveries.empty?, "No e-mail in delivery list"
-
- deliveries.each do |delivery|
- (delivery.parts.empty? ? [delivery] : delivery.parts).each do |part|
- if part["Content-Type"].to_s =~ /^text\/html\W/
- root = HTML::Document.new(part.body.to_s).root
- assert_select root, ":root", &block
- end
- end
- end
- end
-
- protected
- # +assert_select+ and +css_select+ call this to obtain the content in the HTML page.
- def response_from_page
- html_document.root
- end
- end
- end
-end
diff --git a/actionpack/lib/action_dispatch/testing/assertions/tag.rb b/actionpack/lib/action_dispatch/testing/assertions/tag.rb
deleted file mode 100644
index e5fe30ba82..0000000000
--- a/actionpack/lib/action_dispatch/testing/assertions/tag.rb
+++ /dev/null
@@ -1,135 +0,0 @@
-require 'action_view/vendor/html-scanner'
-
-module ActionDispatch
- module Assertions
- # Pair of assertions to testing elements in the HTML output of the response.
- module TagAssertions
- # Asserts that there is a tag/node/element in the body of the response
- # that meets all of the given conditions. The +conditions+ parameter must
- # be a hash of any of the following keys (all are optional):
- #
- # * <tt>:tag</tt>: the node type must match the corresponding value
- # * <tt>:attributes</tt>: a hash. The node's attributes must match the
- # corresponding values in the hash.
- # * <tt>:parent</tt>: a hash. The node's parent must match the
- # corresponding hash.
- # * <tt>:child</tt>: a hash. At least one of the node's immediate children
- # must meet the criteria described by the hash.
- # * <tt>:ancestor</tt>: a hash. At least one of the node's ancestors must
- # meet the criteria described by the hash.
- # * <tt>:descendant</tt>: a hash. At least one of the node's descendants
- # must meet the criteria described by the hash.
- # * <tt>:sibling</tt>: a hash. At least one of the node's siblings must
- # meet the criteria described by the hash.
- # * <tt>:after</tt>: a hash. The node must be after any sibling meeting
- # the criteria described by the hash, and at least one sibling must match.
- # * <tt>:before</tt>: a hash. The node must be before any sibling meeting
- # the criteria described by the hash, and at least one sibling must match.
- # * <tt>:children</tt>: a hash, for counting children of a node. Accepts
- # the keys:
- # * <tt>:count</tt>: either a number or a range which must equal (or
- # include) the number of children that match.
- # * <tt>:less_than</tt>: the number of matching children must be less
- # than this number.
- # * <tt>:greater_than</tt>: the number of matching children must be
- # greater than this number.
- # * <tt>:only</tt>: another hash consisting of the keys to use
- # to match on the children, and only matching children will be
- # counted.
- # * <tt>:content</tt>: the textual content of the node must match the
- # given value. This will not match HTML tags in the body of a
- # tag--only text.
- #
- # Conditions are matched using the following algorithm:
- #
- # * if the condition is a string, it must be a substring of the value.
- # * if the condition is a regexp, it must match the value.
- # * if the condition is a number, the value must match number.to_s.
- # * if the condition is +true+, the value must not be +nil+.
- # * if the condition is +false+ or +nil+, the value must be +nil+.
- #
- # # Assert that there is a "span" tag
- # assert_tag tag: "span"
- #
- # # Assert that there is a "span" tag with id="x"
- # assert_tag tag: "span", attributes: { id: "x" }
- #
- # # Assert that there is a "span" tag using the short-hand
- # assert_tag :span
- #
- # # Assert that there is a "span" tag with id="x" using the short-hand
- # assert_tag :span, attributes: { id: "x" }
- #
- # # Assert that there is a "span" inside of a "div"
- # assert_tag tag: "span", parent: { tag: "div" }
- #
- # # Assert that there is a "span" somewhere inside a table
- # assert_tag tag: "span", ancestor: { tag: "table" }
- #
- # # Assert that there is a "span" with at least one "em" child
- # assert_tag tag: "span", child: { tag: "em" }
- #
- # # Assert that there is a "span" containing a (possibly nested)
- # # "strong" tag.
- # assert_tag tag: "span", descendant: { tag: "strong" }
- #
- # # Assert that there is a "span" containing between 2 and 4 "em" tags
- # # as immediate children
- # assert_tag tag: "span",
- # children: { count: 2..4, only: { tag: "em" } }
- #
- # # Get funky: assert that there is a "div", with an "ul" ancestor
- # # and an "li" parent (with "class" = "enum"), and containing a
- # # "span" descendant that contains text matching /hello world/
- # assert_tag tag: "div",
- # ancestor: { tag: "ul" },
- # parent: { tag: "li",
- # attributes: { class: "enum" } },
- # descendant: { tag: "span",
- # child: /hello world/ }
- #
- # <b>Please note</b>: +assert_tag+ and +assert_no_tag+ only work
- # with well-formed XHTML. They recognize a few tags as implicitly self-closing
- # (like br and hr and such) but will not work correctly with tags
- # that allow optional closing tags (p, li, td). <em>You must explicitly
- # close all of your tags to use these assertions.</em>
- def assert_tag(*opts)
- opts = opts.size > 1 ? opts.last.merge({ :tag => opts.first.to_s }) : opts.first
- tag = find_tag(opts)
- assert tag, "expected tag, but no tag found matching #{opts.inspect} in:\n#{@response.body.inspect}"
- end
-
- # Identical to +assert_tag+, but asserts that a matching tag does _not_
- # exist. (See +assert_tag+ for a full discussion of the syntax.)
- #
- # # Assert that there is not a "div" containing a "p"
- # assert_no_tag tag: "div", descendant: { tag: "p" }
- #
- # # Assert that an unordered list is empty
- # assert_no_tag tag: "ul", descendant: { tag: "li" }
- #
- # # Assert that there is not a "p" tag with between 1 to 3 "img" tags
- # # as immediate children
- # assert_no_tag tag: "p",
- # children: { count: 1..3, only: { tag: "img" } }
- def assert_no_tag(*opts)
- opts = opts.size > 1 ? opts.last.merge({ :tag => opts.first.to_s }) : opts.first
- tag = find_tag(opts)
- assert !tag, "expected no tag, but found tag matching #{opts.inspect} in:\n#{@response.body.inspect}"
- end
-
- def find_tag(conditions)
- html_document.find(conditions)
- end
-
- def find_all_tag(conditions)
- html_document.find_all(conditions)
- end
-
- def html_document
- xml = @response.content_type =~ /xml$/
- @html_document ||= HTML::Document.new(@response.body, false, xml)
- end
- end
- end
-end
diff --git a/actionpack/lib/action_dispatch/testing/integration.rb b/actionpack/lib/action_dispatch/testing/integration.rb
index af3bc26691..2fe37c5bd4 100644
--- a/actionpack/lib/action_dispatch/testing/integration.rb
+++ b/actionpack/lib/action_dispatch/testing/integration.rb
@@ -3,6 +3,7 @@ require 'uri'
require 'active_support/core_ext/kernel/singleton_class'
require 'active_support/core_ext/object/try'
require 'rack/test'
+require 'minitest'
module ActionDispatch
module Integration #:nodoc:
@@ -11,12 +12,14 @@ module ActionDispatch
#
# - +path+: The URI (as a String) on which you want to perform a GET
# request.
- # - +parameters+: The HTTP parameters that you want to pass. This may
+ # - +params+: The HTTP parameters that you want to pass. This may
# be +nil+,
# a Hash, or a String that is appropriately encoded
# (<tt>application/x-www-form-urlencoded</tt> or
# <tt>multipart/form-data</tt>).
- # - +headers_or_env+: Additional headers to pass, as a Hash. The headers will be
+ # - +headers+: Additional headers to pass, as a Hash. The headers will be
+ # merged into the Rack env hash.
+ # - +env+: Additional env to pass, as a Hash. The headers will be
# merged into the Rack env hash.
#
# This method returns a Response object, which one can use to
@@ -27,38 +30,43 @@ module ActionDispatch
#
# You can also perform POST, PATCH, PUT, DELETE, and HEAD requests with
# +#post+, +#patch+, +#put+, +#delete+, and +#head+.
- def get(path, parameters = nil, headers_or_env = nil)
- process :get, path, parameters, headers_or_env
+ #
+ # Example:
+ #
+ # get '/feed', params: { since: 201501011400 }
+ # post '/profile', headers: { "X-Test-Header" => "testvalue" }
+ def get(path, *args)
+ process_with_kwargs(:get, path, *args)
end
# Performs a POST request with the given parameters. See +#get+ for more
# details.
- def post(path, parameters = nil, headers_or_env = nil)
- process :post, path, parameters, headers_or_env
+ def post(path, *args)
+ process_with_kwargs(:post, path, *args)
end
# Performs a PATCH request with the given parameters. See +#get+ for more
# details.
- def patch(path, parameters = nil, headers_or_env = nil)
- process :patch, path, parameters, headers_or_env
+ def patch(path, *args)
+ process_with_kwargs(:patch, path, *args)
end
# Performs a PUT request with the given parameters. See +#get+ for more
# details.
- def put(path, parameters = nil, headers_or_env = nil)
- process :put, path, parameters, headers_or_env
+ def put(path, *args)
+ process_with_kwargs(:put, path, *args)
end
# Performs a DELETE request with the given parameters. See +#get+ for
# more details.
- def delete(path, parameters = nil, headers_or_env = nil)
- process :delete, path, parameters, headers_or_env
+ def delete(path, *args)
+ process_with_kwargs(:delete, path, *args)
end
# Performs a HEAD request with the given parameters. See +#get+ for more
# details.
- def head(path, parameters = nil, headers_or_env = nil)
- process :head, path, parameters, headers_or_env
+ def head(path, *args)
+ process_with_kwargs(:head, path, *args)
end
# Performs an XMLHttpRequest request with the given parameters, mirroring
@@ -67,11 +75,29 @@ module ActionDispatch
# The request_method is +:get+, +:post+, +:patch+, +:put+, +:delete+ or
# +:head+; the parameters are +nil+, a hash, or a url-encoded or multipart
# string; the headers are a hash.
- def xml_http_request(request_method, path, parameters = nil, headers_or_env = nil)
- headers_or_env ||= {}
- headers_or_env['HTTP_X_REQUESTED_WITH'] = 'XMLHttpRequest'
- headers_or_env['HTTP_ACCEPT'] ||= [Mime::JS, Mime::HTML, Mime::XML, 'text/xml', Mime::ALL].join(', ')
- process(request_method, path, parameters, headers_or_env)
+ #
+ # Example:
+ #
+ # xhr :get, '/feed', params: { since: 201501011400 }
+ def xml_http_request(request_method, path, *args)
+ if kwarg_request?(*args)
+ params, headers, env = args.first.values_at(:params, :headers, :env)
+ else
+ params = args[0]
+ headers = args[1]
+ env = {}
+
+ if params.present? || headers.present?
+ non_kwarg_request_warning
+ end
+ end
+
+ ActiveSupport::Deprecation.warn(<<-MSG.strip_heredoc)
+ xhr and xml_http_request methods are deprecated in favor of
+ `get "/posts", xhr: true` and `post "/posts/1", xhr: true`
+ MSG
+
+ process(request_method, path, params: params, headers: headers, xhr: true)
end
alias xhr :xml_http_request
@@ -88,40 +114,52 @@ module ActionDispatch
# redirect. Note that the redirects are followed until the response is
# not a redirect--this means you may run into an infinite loop if your
# redirect loops back to itself.
- def request_via_redirect(http_method, path, parameters = nil, headers_or_env = nil)
- process(http_method, path, parameters, headers_or_env)
+ #
+ # Example:
+ #
+ # request_via_redirect :post, '/welcome',
+ # params: { ref_id: 14 },
+ # headers: { "X-Test-Header" => "testvalue" }
+ def request_via_redirect(http_method, path, *args)
+ process_with_kwargs(http_method, path, *args)
+
follow_redirect! while redirect?
status
end
# Performs a GET request, following any subsequent redirect.
# See +request_via_redirect+ for more information.
- def get_via_redirect(path, parameters = nil, headers_or_env = nil)
- request_via_redirect(:get, path, parameters, headers_or_env)
+ def get_via_redirect(path, *args)
+ ActiveSupport::Deprecation.warn('`get_via_redirect` is deprecated and will be removed in the next version of Rails. Please use follow_redirect! manually after the request call for the same behavior.')
+ request_via_redirect(:get, path, *args)
end
# Performs a POST request, following any subsequent redirect.
# See +request_via_redirect+ for more information.
- def post_via_redirect(path, parameters = nil, headers_or_env = nil)
- request_via_redirect(:post, path, parameters, headers_or_env)
+ def post_via_redirect(path, *args)
+ ActiveSupport::Deprecation.warn('`post_via_redirect` is deprecated and will be removed in the next version of Rails. Please use follow_redirect! manually after the request call for the same behavior.')
+ request_via_redirect(:post, path, *args)
end
# Performs a PATCH request, following any subsequent redirect.
# See +request_via_redirect+ for more information.
- def patch_via_redirect(path, parameters = nil, headers_or_env = nil)
- request_via_redirect(:patch, path, parameters, headers_or_env)
+ def patch_via_redirect(path, *args)
+ ActiveSupport::Deprecation.warn('`patch_via_redirect` is deprecated and will be removed in the next version of Rails. Please use follow_redirect! manually after the request call for the same behavior.')
+ request_via_redirect(:patch, path, *args)
end
# Performs a PUT request, following any subsequent redirect.
# See +request_via_redirect+ for more information.
- def put_via_redirect(path, parameters = nil, headers_or_env = nil)
- request_via_redirect(:put, path, parameters, headers_or_env)
+ def put_via_redirect(path, *args)
+ ActiveSupport::Deprecation.warn('`put_via_redirect` is deprecated and will be removed in the next version of Rails. Please use follow_redirect! manually after the request call for the same behavior.')
+ request_via_redirect(:put, path, *args)
end
# Performs a DELETE request, following any subsequent redirect.
# See +request_via_redirect+ for more information.
- def delete_via_redirect(path, parameters = nil, headers_or_env = nil)
- request_via_redirect(:delete, path, parameters, headers_or_env)
+ def delete_via_redirect(path, *args)
+ ActiveSupport::Deprecation.warn('`delete_via_redirect` is deprecated and will be removed in the next version of Rails. Please use follow_redirect! manually after the request call for the same behavior.')
+ request_via_redirect(:delete, path, *args)
end
end
@@ -184,15 +222,6 @@ module ActionDispatch
super()
@app = app
- # If the app is a Rails app, make url_helpers available on the session
- # This makes app.url_for and app.foo_path available in the console
- if app.respond_to?(:routes)
- singleton_class.class_eval do
- include app.routes.url_helpers if app.routes.respond_to?(:url_helpers)
- include app.routes.mounted_helpers if app.routes.respond_to?(:mounted_helpers)
- end
- end
-
reset!
end
@@ -200,7 +229,7 @@ module ActionDispatch
@url_options ||= default_url_options.dup.tap do |url_options|
url_options.reverse_merge!(controller.url_options) if controller
- if @app.respond_to?(:routes) && @app.routes.respond_to?(:default_url_options)
+ if @app.respond_to?(:routes)
url_options.reverse_merge!(@app.routes.default_url_options)
end
@@ -260,8 +289,38 @@ module ActionDispatch
@_mock_session ||= Rack::MockSession.new(@app, host)
end
+ def process_with_kwargs(http_method, path, *args)
+ if kwarg_request?(*args)
+ process(http_method, path, *args)
+ else
+ non_kwarg_request_warning if args.present?
+ process(http_method, path, { params: args[0], headers: args[1] })
+ end
+ end
+
+ REQUEST_KWARGS = %i(params headers env xhr)
+ def kwarg_request?(*args)
+ args[0].respond_to?(:keys) && args[0].keys.any? { |k| REQUEST_KWARGS.include?(k) }
+ end
+
+ def non_kwarg_request_warning
+ ActiveSupport::Deprecation.warn(<<-MSG.strip_heredoc)
+ ActionDispatch::IntegrationTest HTTP request methods will accept only
+ the following keyword arguments in future Rails versions:
+ #{REQUEST_KWARGS.join(', ')}
+
+ Examples:
+
+ get '/profile',
+ params: { id: 1 },
+ headers: { 'X-Extra-Header' => '123' },
+ env: { 'action_dispatch.custom' => 'custom' },
+ xhr: true
+ MSG
+ end
+
# Performs the actual request.
- def process(method, path, parameters = nil, headers_or_env = nil)
+ def process(method, path, params: nil, headers: nil, env: nil, xhr: false)
if path =~ %r{://}
location = URI.parse(path)
https! URI::HTTPS === location if location.scheme
@@ -271,9 +330,9 @@ module ActionDispatch
hostname, port = host.split(':')
- env = {
+ request_env = {
:method => method,
- :params => parameters,
+ :params => params,
"SERVER_NAME" => hostname,
"SERVER_PORT" => port || (https? ? "443" : "80"),
@@ -286,25 +345,37 @@ module ActionDispatch
"CONTENT_TYPE" => "application/x-www-form-urlencoded",
"HTTP_ACCEPT" => accept
}
- # this modifies the passed env directly
- Http::Headers.new(env).merge!(headers_or_env || {})
+
+ if xhr
+ headers ||= {}
+ headers['HTTP_X_REQUESTED_WITH'] = 'XMLHttpRequest'
+ headers['HTTP_ACCEPT'] ||= [Mime::JS, Mime::HTML, Mime::XML, 'text/xml', Mime::ALL].join(', ')
+ end
+
+ # this modifies the passed request_env directly
+ if headers.present?
+ Http::Headers.new(request_env).merge!(headers)
+ end
+ if env.present?
+ Http::Headers.new(request_env).merge!(env)
+ end
session = Rack::Test::Session.new(_mock_session)
# NOTE: rack-test v0.5 doesn't build a default uri correctly
# Make sure requested path is always a full uri
- session.request(build_full_uri(path, env), env)
+ session.request(build_full_uri(path, request_env), request_env)
@request_count += 1
@request = ActionDispatch::Request.new(session.last_request.env)
response = _mock_session.last_response
- @response = ActionDispatch::TestResponse.new(response.status, response.headers, response.body)
+ @response = ActionDispatch::TestResponse.from_response(response)
@html_document = nil
@url_options = nil
@controller = session.last_request.env['action_controller.instance']
- return response.status
+ response.status
end
def build_full_uri(path, env)
@@ -315,22 +386,51 @@ module ActionDispatch
module Runner
include ActionDispatch::Assertions
- def app
- @app ||= nil
+ APP_SESSIONS = {}
+
+ attr_reader :app
+
+ def before_setup
+ super
+ @app = nil
+ @integration_session = nil
+ end
+
+ def integration_session
+ @integration_session ||= create_session(app)
end
# Reset the current session. This is useful for testing multiple sessions
# in a single test case.
def reset!
- @integration_session = Integration::Session.new(app)
+ @integration_session = create_session(app)
+ end
+
+ def create_session(app)
+ klass = APP_SESSIONS[app] ||= Class.new(Integration::Session) {
+ # If the app is a Rails app, make url_helpers available on the session
+ # This makes app.url_for and app.foo_path available in the console
+ if app.respond_to?(:routes)
+ include app.routes.url_helpers
+ include app.routes.mounted_helpers
+ end
+ }
+ klass.new(app)
+ end
+
+ def remove! # :nodoc:
+ @integration_session = nil
end
%w(get post patch put head delete cookies assigns
xml_http_request xhr get_via_redirect post_via_redirect).each do |method|
define_method(method) do |*args|
- reset! unless integration_session
- # reset the html_document variable, but only for new get/post calls
- @html_document = nil unless method == 'cookies' || method == 'assigns'
+ # reset the html_document variable, except for cookies/assigns calls
+ unless method == 'cookies' || method == 'assigns'
+ @html_document = nil
+ reset_template_assertion
+ end
+
integration_session.__send__(method, *args).tap do
copy_session_variables!
end
@@ -347,7 +447,7 @@ module ActionDispatch
# By default, a single session is automatically created for you, but you
# can use this method to open multiple sessions that ought to be tested
# simultaneously.
- def open_session(app = nil)
+ def open_session
dup.tap do |session|
yield session if block_given?
end
@@ -356,19 +456,16 @@ module ActionDispatch
# Copy the instance variables from the current session instance into the
# test instance.
def copy_session_variables! #:nodoc:
- return unless integration_session
- %w(controller response request).each do |var|
- instance_variable_set("@#{var}", @integration_session.__send__(var))
- end
+ @controller = @integration_session.controller
+ @response = @integration_session.response
+ @request = @integration_session.request
end
def default_url_options
- reset! unless integration_session
integration_session.default_url_options
end
def default_url_options=(options)
- reset! unless integration_session
integration_session.default_url_options = options
end
@@ -378,7 +475,6 @@ module ActionDispatch
# Delegate unhandled messages to the current session instance.
def method_missing(sym, *args, &block)
- reset! unless integration_session
if integration_session.respond_to?(sym)
integration_session.__send__(sym, *args, &block).tap do
copy_session_variables!
@@ -387,11 +483,6 @@ module ActionDispatch
super
end
end
-
- private
- def integration_session
- @integration_session ||= nil
- end
end
end
@@ -470,6 +561,84 @@ module ActionDispatch
# end
# end
# end
+ #
+ # Another longer example would be:
+ #
+ # A simple integration test that exercises multiple controllers:
+ #
+ # require 'test_helper'
+ #
+ # class UserFlowsTest < ActionDispatch::IntegrationTest
+ # test "login and browse site" do
+ # # login via https
+ # https!
+ # get "/login"
+ # assert_response :success
+ #
+ # post_via_redirect "/login", username: users(:david).username, password: users(:david).password
+ # assert_equal '/welcome', path
+ # assert_equal 'Welcome david!', flash[:notice]
+ #
+ # https!(false)
+ # get "/articles/all"
+ # assert_response :success
+ # assert assigns(:articles)
+ # end
+ # end
+ #
+ # As you can see the integration test involves multiple controllers and
+ # exercises the entire stack from database to dispatcher. In addition you can
+ # have multiple session instances open simultaneously in a test and extend
+ # those instances with assertion methods to create a very powerful testing
+ # DSL (domain-specific language) just for your application.
+ #
+ # Here's an example of multiple sessions and custom DSL in an integration test
+ #
+ # require 'test_helper'
+ #
+ # class UserFlowsTest < ActionDispatch::IntegrationTest
+ # test "login and browse site" do
+ # # User david logs in
+ # david = login(:david)
+ # # User guest logs in
+ # guest = login(:guest)
+ #
+ # # Both are now available in different sessions
+ # assert_equal 'Welcome david!', david.flash[:notice]
+ # assert_equal 'Welcome guest!', guest.flash[:notice]
+ #
+ # # User david can browse site
+ # david.browses_site
+ # # User guest can browse site as well
+ # guest.browses_site
+ #
+ # # Continue with other assertions
+ # end
+ #
+ # private
+ #
+ # module CustomDsl
+ # def browses_site
+ # get "/products/all"
+ # assert_response :success
+ # assert assigns(:products)
+ # end
+ # end
+ #
+ # def login(user)
+ # open_session do |sess|
+ # sess.extend(CustomDsl)
+ # u = users(user)
+ # sess.https!
+ # sess.post "/login", username: u.username, password: u.password
+ # assert_equal '/welcome', sess.path
+ # sess.https!(false)
+ # end
+ # end
+ # end
+ #
+ # Consult the Rails Testing Guide for more.
+
class IntegrationTest < ActiveSupport::TestCase
include Integration::Runner
include ActionController::TemplateAssertions
@@ -490,8 +659,11 @@ module ActionDispatch
end
def url_options
- reset! unless integration_session
integration_session.url_options
end
+
+ def document_root_element
+ html_document.root
+ end
end
end
diff --git a/actionpack/lib/action_dispatch/testing/test_request.rb b/actionpack/lib/action_dispatch/testing/test_request.rb
index 57c678843b..4b9a088265 100644
--- a/actionpack/lib/action_dispatch/testing/test_request.rb
+++ b/actionpack/lib/action_dispatch/testing/test_request.rb
@@ -39,7 +39,7 @@ module ActionDispatch
end
def action=(action_name)
- path_parameters["action"] = action_name.to_s
+ path_parameters[:action] = action_name.to_s
end
def if_modified_since=(last_modified)
@@ -60,7 +60,7 @@ module ActionDispatch
def accept=(mime_types)
@env.delete('action_dispatch.request.accepts')
- @env['HTTP_ACCEPT'] = Array(mime_types).collect { |mime_type| mime_type.to_s }.join(",")
+ @env['HTTP_ACCEPT'] = Array(mime_types).collect(&:to_s).join(",")
end
alias :rack_cookies :cookies
diff --git a/actionpack/lib/action_dispatch/testing/test_response.rb b/actionpack/lib/action_dispatch/testing/test_response.rb
index 82039e72e7..a9b88ac5fd 100644
--- a/actionpack/lib/action_dispatch/testing/test_response.rb
+++ b/actionpack/lib/action_dispatch/testing/test_response.rb
@@ -7,11 +7,7 @@ module ActionDispatch
# See Response for more information on controller response objects.
class TestResponse < Response
def self.from_response(response)
- new.tap do |resp|
- resp.status = response.status
- resp.headers = response.headers
- resp.body = response.body
- end
+ new response.status, response.headers, response.body, default_headers: nil
end
# Was the response successful?