diff options
Diffstat (limited to 'actionpack/lib/action_dispatch')
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? |