diff options
Diffstat (limited to 'actionpack/lib/action_dispatch')
45 files changed, 688 insertions, 435 deletions
diff --git a/actionpack/lib/action_dispatch/http/cache.rb b/actionpack/lib/action_dispatch/http/cache.rb index 63a3cbc90b..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,6 +89,7 @@ module ActionDispatch private + DATE = 'Date'.freeze LAST_MODIFIED = "Last-Modified".freeze ETAG = "ETag".freeze CACHE_CONTROL = "Cache-Control".freeze diff --git a/actionpack/lib/action_dispatch/http/mime_negotiation.rb b/actionpack/lib/action_dispatch/http/mime_negotiation.rb index 9c8f65deac..53a98c5d0a 100644 --- a/actionpack/lib/action_dispatch/http/mime_negotiation.rb +++ b/actionpack/lib/action_dispatch/http/mime_negotiation.rb @@ -72,11 +72,12 @@ module ActionDispatch 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 b9d5009683..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,7 +45,7 @@ module Mime # # respond_to do |format| # format.html - # format.ics { render text: @post.to_ics, mime_type: Mime::Type["text/calendar"] } + # format.ics { render text: @post.to_ics, mime_type: Mime::Type.lookup("text/calendar") } # format.xml { render xml: @post } # 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 20ae48d458..c2f05ecc86 100644 --- a/actionpack/lib/action_dispatch/http/parameters.rb +++ b/actionpack/lib/action_dispatch/http/parameters.rb @@ -1,6 +1,5 @@ require 'active_support/core_ext/hash/keys' require 'active_support/core_ext/hash/indifferent_access' -require 'active_support/deprecation' module ActionDispatch module Http @@ -25,13 +24,6 @@ module ActionDispatch @env[PARAMETERS_KEY] = parameters end - def symbolized_path_parameters - ActiveSupport::Deprecation.warn( - "`symbolized_path_parameters` is deprecated. Please use `path_parameters`" - ) - path_parameters - end - # Returns a hash with the \parameters used to form the \path of the request. # Returned hash keys are strings: # diff --git a/actionpack/lib/action_dispatch/http/request.rb b/actionpack/lib/action_dispatch/http/request.rb index a8785ee6bf..07b3814ca4 100644 --- a/actionpack/lib/action_dispatch/http/request.rb +++ b/actionpack/lib/action_dispatch/http/request.rb @@ -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,18 @@ 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 @@ -237,10 +249,12 @@ module ActionDispatch # # 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 @@ -325,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) diff --git a/actionpack/lib/action_dispatch/http/response.rb b/actionpack/lib/action_dispatch/http/response.rb index c5e18048da..a895d1ab18 100644 --- a/actionpack/lib/action_dispatch/http/response.rb +++ b/actionpack/lib/action_dispatch/http/response.rb @@ -1,5 +1,4 @@ require 'active_support/core_ext/module/attribute_accessors' -require 'active_support/deprecation' require 'action_dispatch/http/filter_redirect' require 'monitor' @@ -114,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 @@ -283,15 +282,6 @@ module ActionDispatch # :nodoc: end alias prepare! to_a - # Be super clear that a response object is not an Array. Defining this - # would make implicit splatting work, but it also makes adding responses - # as arrays work, and "flattening" responses, cascading to the rack body! - # Not sensible behavior. - def to_ary - ActiveSupport::Deprecation.warn 'ActionDispatch::Response#to_ary no longer performs implicit conversion to an Array. Please use response.to_a instead, or a splat like `status, headers, body = *response`' - to_a - end - # Returns the response cookies, converted to a Hash of (name => value) pairs # # assert_equal 'AuthorOfNewPage', r.cookies['author'] @@ -309,9 +299,6 @@ module ActionDispatch # :nodoc: cookies end - def _status_code - @status - end private def before_committed @@ -321,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) diff --git a/actionpack/lib/action_dispatch/http/url.rb b/actionpack/lib/action_dispatch/http/url.rb index 6b8dcaf497..001b14ec97 100644 --- a/actionpack/lib/action_dispatch/http/url.rb +++ b/actionpack/lib/action_dispatch/http/url.rb @@ -12,10 +12,22 @@ module ActionDispatch self.tld_length = 1 class << self + # 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 + # 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) extract_subdomains_from(host, tld_length) @@ -24,6 +36,12 @@ module ActionDispatch end end + # 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 @@ -68,7 +86,9 @@ module ActionDispatch end def add_anchor(path, anchor) - path << "##{Journey::Router::Utils.escape_fragment(anchor.to_param.to_s)}" + if anchor + path << "##{Journey::Router::Utils.escape_fragment(anchor.to_param)}" + end end def extract_domain_from(host, tld_length) @@ -171,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 @@ -190,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+)$/ @@ -212,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 @@ -220,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 59b353b1b7..992c1a9efe 100644 --- a/actionpack/lib/action_dispatch/journey/formatter.rb +++ b/actionpack/lib/action_dispatch/journey/formatter.rb @@ -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 1b914f0637..d7ce6042c2 100644 --- a/actionpack/lib/action_dispatch/journey/gtg/transition_table.rb +++ b/actionpack/lib/action_dispatch/journey/gtg/transition_table.rb @@ -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/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 9131b65380..e9df984c86 100644 --- a/actionpack/lib/action_dispatch/journey/router.rb +++ b/actionpack/lib/action_dispatch/journey/router.rb @@ -68,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 @@ -121,6 +121,7 @@ module ActionDispatch end def match_head_routes(routes, req) + routes.delete_if { |route| route.verb == // } head_routes = match_routes(routes, req) if head_routes.empty? diff --git a/actionpack/lib/action_dispatch/journey/scanner.rb b/actionpack/lib/action_dispatch/journey/scanner.rb index ad1cd0f5e8..19e0bc03d6 100644 --- a/actionpack/lib/action_dispatch/journey/scanner.rb +++ b/actionpack/lib/action_dispatch/journey/scanner.rb @@ -50,7 +50,7 @@ module ActionDispatch when text = @ss.scan(/(?<!\\):\w+/) [:SYMBOL, text] when text = @ss.scan(/(?:[\w%\-~!$&'*+,;=@]|\\:|\\\(|\\\))+/) - [:LITERAL, text.gsub('\\', '')] + [: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 83ac62a83d..b7687ca100 100644 --- a/actionpack/lib/action_dispatch/middleware/cookies.rb +++ b/actionpack/lib/action_dispatch/middleware/cookies.rb @@ -71,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. @@ -120,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+. @@ -143,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+. @@ -281,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 @@ -408,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 @@ -461,9 +463,9 @@ 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].bytesize > MAX_COOKIE_SIZE @@ -479,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: @@ -522,7 +524,7 @@ 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].bytesize > MAX_COOKIE_SIZE @parent_jar[name] = options @@ -537,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 274f6f2f22..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,12 +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, - traces: traces_from_wrapper(wrapper), + 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 ) @@ -93,36 +136,5 @@ module ActionDispatch ActionDispatch::Routing::RoutesInspector.new(@routes_app.routes.routes) end end - - # Augment the exception traces by providing ids for all unique stack frame - def traces_from_wrapper(wrapper) - application_trace = wrapper.application_trace - framework_trace = wrapper.framework_trace - full_trace = wrapper.full_trace - - if application_trace && framework_trace - id_counter = 0 - - application_trace = application_trace.map do |trace| - prev = id_counter - id_counter += 1 - { id: prev, trace: trace } - end - - framework_trace = framework_trace.map do |trace| - prev = id_counter - id_counter += 1 - { id: prev, trace: trace } - end - - full_trace = application_trace + framework_trace - end - - { - "Application Trace" => application_trace, - "Framework Trace" => framework_trace, - "Full Trace" => full_trace - } - end end end diff --git a/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb b/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb index b98b553c38..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,24 +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 - exception.backtrace.map do |trace| - file, line = trace.split(":") - line_number = line.to_i + def source_extracts + backtrace.map do |trace| + file, line_number = extract_file_and_line_number(trace) + { code: source_fragment(file, line_number), - file: file, line_number: line_number } - end if exception.backtrace + end end private + def backtrace + Array(@exception.backtrace) + end + def original_exception(exception) if registered_original_exception?(exception) exception.original_exception @@ -88,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 @@ -110,6 +139,13 @@ 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") diff --git a/actionpack/lib/action_dispatch/middleware/flash.rb b/actionpack/lib/action_dispatch/middleware/flash.rb index e90f8b9ce6..59639a010e 100644 --- a/actionpack/lib/action_dispatch/middleware/flash.rb +++ b/actionpack/lib/action_dispatch/middleware/flash.rb @@ -79,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: @@ -132,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/request_id.rb b/actionpack/lib/action_dispatch/middleware/request_id.rb index 25658bac3d..b9ca524309 100644 --- a/actionpack/lib/action_dispatch/middleware/request_id.rb +++ b/actionpack/lib/action_dispatch/middleware/request_id.rb @@ -3,7 +3,7 @@ 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 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 @@ -12,19 +12,23 @@ module ActionDispatch # 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/static.rb b/actionpack/lib/action_dispatch/middleware/static.rb index e66c21ef85..2e1bd45c3d 100644 --- a/actionpack/lib/action_dispatch/middleware/static.rb +++ b/actionpack/lib/action_dispatch/middleware/static.rb @@ -23,10 +23,19 @@ module ActionDispatch def match?(path) path = URI.parser.unescape(path) return false unless path.valid_encoding? + path = Rack::Utils.clean_path_info path paths = [path, "#{path}#{ext}", "#{path}/index#{ext}"] - if match = paths.detect {|p| File.file?(File.join(@root, p)) } + 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 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 ba29e26930..e7b913bbe4 100644 --- a/actionpack/lib/action_dispatch/middleware/templates/rescues/_source.erb +++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/_source.erb @@ -1,29 +1,27 @@ -<% if @source_extract %> - <% @source_extract.each_with_index do |extract_source, index| %> - <% if extract_source[:code] %> - <div class="source <%="hidden" if index != 0%>" id="frame-source-<%=index%>"> - <div class="info"> - Extracted source (around line <strong>#<%= extract_source[:line_number] %></strong>): - </div> - <div class="data"> - <table cellpadding="0" cellspacing="0" class="lines"> - <tr> - <td> - <pre class="line_numbers"> - <% extract_source[:code].each_key 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> -<% extract_source[:code].each do |line, source| -%><div class="line<%= " active" if line == extract_source[: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> + </tr> + </table> </div> - <% end %> + </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 f62caf51d7..ab57b11c7d 100644 --- a/actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb +++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb @@ -12,7 +12,7 @@ <% end %> <% @traces.each do |name, trace| %> - <div id="<%= name.gsub(/\s/, '-') %>" style="display: <%= (name == "Application Trace") ? 'block' : 'none' %>;"> + <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 %> 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/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 fc28740828..34b5b48f3a 100644 --- a/actionpack/lib/action_dispatch/routing/mapper.rb +++ b/actionpack/lib/action_dispatch/routing/mapper.rb @@ -7,7 +7,6 @@ require 'active_support/core_ext/module/remove_method' require 'active_support/inflector' require 'action_dispatch/routing/redirection' require 'action_dispatch/routing/endpoint' -require 'active_support/deprecation' module ActionDispatch module Routing @@ -243,12 +242,10 @@ module ActionDispatch 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(defaults), blocks, true) - else - dispatcher(defaults) - end + dispatcher(defaults) end end @@ -280,14 +277,8 @@ module ActionDispatch end def split_to(to) - case to - when Symbol - ActiveSupport::Deprecation.warn "defining a route where `to` is a symbol is deprecated. Please change \"to: :#{to}\" to \"action: :#{to}\"" - [nil, to.to_s] - when /#/ then to.split('#') - when String - ActiveSupport::Deprecation.warn "defining a route where `to` is a controller without an action is deprecated. Please change \"to: :#{to}\" to \"controller: :#{to}\"" - [to, nil] + if to =~ /#/ + to.split('#') else [] end @@ -382,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: @@ -393,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: # @@ -434,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 @@ -448,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>. @@ -573,13 +564,7 @@ module ActionDispatch raise "A rack application must be specified" unless path rails_app = rails_app? app - - if rails_app - options[:as] ||= app.railtie_name - else - # non rails apps can't have an :as - options[:as] = nil - end + options[:as] ||= app_name(app, rails_app) target_as = name_for_action(options[:as], path) options[:via] ||= :all @@ -611,6 +596,15 @@ module ActionDispatch app.is_a?(Class) && app < Rails::Railtie end + def app_name(app, rails_app) + if rails_app + app.railtie_name + elsif app.is_a?(Class) + class_name = app.name + ActiveSupport::Inflector.underscore(class_name).tr("/", "_") + end + end + def define_generate_prefix(app, name) _route = @set.named_routes.get name _routes = @set @@ -1510,7 +1504,7 @@ 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: @@ -1745,9 +1739,10 @@ module ActionDispatch member_name = parent_resource.member_name end - name = @scope.action_name(name_prefix, prefix, collection_name, member_name) + action_name = @scope.action_name(name_prefix, prefix, collection_name, member_name) + candidate = action_name.select(&:present?).join('_') - if candidate = name.compact.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. diff --git a/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb b/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb index 0847842fa2..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: # @@ -251,7 +247,7 @@ module ActionDispatch args = [] model = record.to_model - name = if record.persisted? + name = if model.persisted? args << model model.model_name.singular_route_key else @@ -294,11 +290,12 @@ module ActionDispatch when Class @key_strategy.call record.model_name else - if record.persisted? - args << record.to_model - record.to_model.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.model_name + @key_strategy.call model.model_name end end diff --git a/actionpack/lib/action_dispatch/routing/route_set.rb b/actionpack/lib/action_dispatch/routing/route_set.rb index f51bee3b31..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,9 +85,9 @@ 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, :url_helpers_module + attr_reader :routes, :url_helpers_module, :path_helpers_module def initialize @routes = {} @@ -101,11 +102,6 @@ module ActionDispatch @path_helpers.include?(key) || @url_helpers.include?(key) end - def helpers - ActiveSupport::Deprecation.warn("`named_routes.helpers` is deprecated, please use `route_defined?(route_name)` to see if a named route was defined.") - @path_helpers + @url_helpers - end - def helper_names @path_helpers.map(&:to_s) + @url_helpers.map(&:to_s) end @@ -135,7 +131,7 @@ module ActionDispatch 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, FULL + define_url_helper @url_helpers_module, route, url_name, route.defaults, name, UNKNOWN @path_helpers << path_name @url_helpers << url_name @@ -166,26 +162,7 @@ module ActionDispatch routes.length end - def path_helpers_module(warn = false) - if warn - mod = @path_helpers_module - helpers = @path_helpers - Module.new do - include mod - - helpers.each do |meth| - define_method(meth) do |*args, &block| - ActiveSupport::Deprecation.warn("The method `#{meth}` cannot be used here as a full URL is required. Use `#{meth.to_s.sub(/_path$/, '_url')}` instead") - super(*args, &block) - end - end - end - else - @path_helpers_module - end - end - - class UrlHelper # :nodoc: + class UrlHelper def self.create(route, options, route_name, url_strategy) if optimize_helper?(route) OptimizedUrlHelper.new(route, options, route_name, url_strategy) @@ -200,7 +177,7 @@ module ActionDispatch attr_reader :url_strategy, :route_name - class OptimizedUrlHelper < UrlHelper # :nodoc: + class OptimizedUrlHelper < UrlHelper attr_reader :arg_size def initialize(route, options, route_name, url_strategy) @@ -276,15 +253,22 @@ module ActionDispatch 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) @@ -317,12 +301,9 @@ module ActionDispatch end end - # :stopdoc: # strategy for building urls to send to the client PATH = ->(options) { ActionDispatch::Http::URL.path_for(options) } - FULL = ->(options) { ActionDispatch::Http::URL.full_url_for(options) } UNKNOWN = ->(options) { ActionDispatch::Http::URL.url_for(options) } - # :startdoc: attr_accessor :formatter, :set, :named_routes, :default_scope, :router attr_accessor :disable_clear_and_finalize, :resources_path_names @@ -397,7 +378,7 @@ module ActionDispatch Routing::RouteSet::Dispatcher.new(defaults) end - module MountedHelpers #:nodoc: + module MountedHelpers extend ActiveSupport::Concern include UrlFor end @@ -414,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 @@ -427,7 +410,7 @@ module ActionDispatch RUBY end - def url_helpers(include_path_helpers = true) + def url_helpers(supports_path = true) routes = self Module.new do @@ -438,7 +421,14 @@ module ActionDispatch # Rails.application.routes.url_helpers.url_for(args) @_routes = routes class << self - delegate :url_for, :optimize_routes_generation?, to: '@_routes' + def url_for(options) + @_routes.url_for(options) + end + + def optimize_routes_generation? + @_routes.optimize_routes_generation? + end + attr_reader :_routes def url_options; {}; end end @@ -454,14 +444,12 @@ module ActionDispatch # named routes... include url_helpers - if include_path_helpers + if supports_path path_helpers = routes.named_routes.path_helpers_module - else - path_helpers = routes.named_routes.path_helpers_module(true) - end - include path_helpers - extend path_helpers + include path_helpers + extend path_helpers + end # plus a singleton class method called _routes ... included do @@ -472,6 +460,12 @@ module ActionDispatch # 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 @@ -493,7 +487,7 @@ module ActionDispatch path = conditions.delete :path_info ast = conditions.delete :parsed_path_info path = build_path(path, ast, requirements, anchor) - conditions = build_conditions(conditions, path.names.map { |x| x.to_sym }) + conditions = build_conditions(conditions, path.names.map(&:to_sym)) route = @set.add_route(app, path, conditions, defaults, name) named_routes[name] = route if name @@ -550,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 @@ -699,10 +693,10 @@ module ActionDispatch end def find_script_name(options) - options.delete(:script_name) { '' } + options.delete(:script_name) || '' end - def path_for(options, route_name = nil) # :nodoc: + def path_for(options, route_name = nil) url_for(options, route_name, PATH) end @@ -788,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 eb554ec383..dca86858cc 100644 --- a/actionpack/lib/action_dispatch/routing/url_for.rb +++ b/actionpack/lib/action_dispatch/routing/url_for.rb @@ -184,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 41d00b5e2b..f325c35b57 100644 --- a/actionpack/lib/action_dispatch/testing/assertions.rb +++ b/actionpack/lib/action_dispatch/testing/assertions.rb @@ -15,7 +15,7 @@ module ActionDispatch @html_document ||= if @response.content_type =~ /xml$/ Nokogiri::XML::Document.parse(@response.body) else - Nokogiri::HTML::DocumentFragment.parse(@response.body) + Nokogiri::HTML::Document.parse(@response.body) 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 fb579b52fe..0000000000 --- a/actionpack/lib/action_dispatch/testing/assertions/dom.rb +++ /dev/null @@ -1,3 +0,0 @@ -require 'active_support/deprecation' - -ActiveSupport::Deprecation.warn("ActionDispatch::Assertions::DomAssertions has been extracted to the rails-dom-testing gem.")
\ No newline at end of file diff --git a/actionpack/lib/action_dispatch/testing/assertions/routing.rb b/actionpack/lib/action_dispatch/testing/assertions/routing.rb index e06f7037c6..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, msg) + 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+. @@ -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 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 7361e6c44b..0000000000 --- a/actionpack/lib/action_dispatch/testing/assertions/selector.rb +++ /dev/null @@ -1,3 +0,0 @@ -require 'active_support/deprecation' - -ActiveSupport::Deprecation.warn("ActionDispatch::Assertions::SelectorAssertions has been extracted to the rails-dom-testing gem.") 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 5c2905d1ac..0000000000 --- a/actionpack/lib/action_dispatch/testing/assertions/tag.rb +++ /dev/null @@ -1,3 +0,0 @@ -require 'active_support/deprecation' - -ActiveSupport::Deprecation.warn("ActionDispatch::Assertions::TagAssertions has been extracted to the rails-dom-testing gem.") diff --git a/actionpack/lib/action_dispatch/testing/integration.rb b/actionpack/lib/action_dispatch/testing/integration.rb index c300a4ea0d..2fe37c5bd4 100644 --- a/actionpack/lib/action_dispatch/testing/integration.rb +++ b/actionpack/lib/action_dispatch/testing/integration.rb @@ -12,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 @@ -28,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 @@ -68,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 @@ -89,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 @@ -185,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 - include app.routes.mounted_helpers - end - end - reset! end @@ -261,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 @@ -272,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"), @@ -287,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) @@ -316,23 +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_template_assertion - # 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 @@ -358,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 @@ -380,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! @@ -389,11 +483,6 @@ module ActionDispatch super end end - - private - def integration_session - @integration_session ||= nil - end end end @@ -472,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 @@ -492,12 +659,11 @@ module ActionDispatch end def url_options - reset! unless integration_session integration_session.url_options end def document_root_element - html_document + 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 de3dc5f924..4b9a088265 100644 --- a/actionpack/lib/action_dispatch/testing/test_request.rb +++ b/actionpack/lib/action_dispatch/testing/test_request.rb @@ -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? |