diff options
Diffstat (limited to 'actionpack/lib/action_dispatch')
37 files changed, 1120 insertions, 858 deletions
diff --git a/actionpack/lib/action_dispatch/http/cache.rb b/actionpack/lib/action_dispatch/http/cache.rb index f9b278349e..63a3cbc90b 100644 --- a/actionpack/lib/action_dispatch/http/cache.rb +++ b/actionpack/lib/action_dispatch/http/cache.rb @@ -92,7 +92,7 @@ module ActionDispatch 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 2666cd4b0a..bc5410dc38 100644 --- a/actionpack/lib/action_dispatch/http/headers.rb +++ b/actionpack/lib/action_dispatch/http/headers.rb @@ -1,34 +1,63 @@ module ActionDispatch module Http + # Provides access to the request's HTTP headers from the environment. + # + # env = { "CONTENT_TYPE" => "text/plain" } + # 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 attr_reader :env - def initialize(env = {}) + def initialize(env = {}) # :nodoc: @env = env end + # Returns the value for the given key mapped to @env. def [](key) @env[env_name(key)] end + # Sets the given value for the key mapped to @env. def []=(key, value) @env[env_name(key)] = value end - def key?(key); @env.key? key; end + def key?(key) + @env.key? env_name(key) + end alias :include? :key? + # Returns the value for the given key mapped to @env. + # + # If the key is not found and an optional code block is not provided, + # raises a <tt>KeyError</tt> exception. + # + # If the code block is provided, then it will be run and + # its result returned. def fetch(key, *args, &block) @env.fetch env_name(key), *args, &block end @@ -37,12 +66,17 @@ module ActionDispatch @env.each(&block) end + # Returns a new Http::Headers instance containing the contents of + # <tt>headers_or_env</tt> and the original instance. def merge(headers_or_env) headers = Http::Headers.new(env.dup) headers.merge!(headers_or_env) headers end + # Adds the contents of <tt>headers_or_env</tt> to original instance + # entries; duplicate keys are overwritten with the values from + # <tt>headers_or_env</tt>. def merge!(headers_or_env) headers_or_env.each do |key, value| self[env_name(key)] = value @@ -50,6 +84,8 @@ module ActionDispatch end private + # Converts a HTTP header name to an environment variable name if it is + # not contained within the headers hash. def env_name(key) key = key.to_s if key =~ HTTP_HEADER diff --git a/actionpack/lib/action_dispatch/http/mime_negotiation.rb b/actionpack/lib/action_dispatch/http/mime_negotiation.rb index 0b2b60d2e4..9c8f65deac 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,8 +70,8 @@ module ActionDispatch else [Mime::HTML] end + end end - # Sets the \variant for template. def variant=(variant) if variant.is_a?(Symbol) diff --git a/actionpack/lib/action_dispatch/http/parameters.rb b/actionpack/lib/action_dispatch/http/parameters.rb index dcb299ed03..20ae48d458 100644 --- a/actionpack/lib/action_dispatch/http/parameters.rb +++ b/actionpack/lib/action_dispatch/http/parameters.rb @@ -1,13 +1,11 @@ require 'active_support/core_ext/hash/keys' require 'active_support/core_ext/hash/indifferent_access' +require 'active_support/deprecation' module ActionDispatch module Http module Parameters - def initialize(env) - super - @symbolized_path_params = nil - end + PARAMETERS_KEY = 'action_dispatch.request.path_parameters' # Returns both GET and POST \parameters in a single hash. def parameters @@ -18,55 +16,42 @@ module ActionDispatch query_parameters.dup end params.merge!(path_parameters) - params.with_indifferent_access end end alias :params :parameters def path_parameters=(parameters) #:nodoc: - @symbolized_path_params = nil - @env.delete("action_dispatch.request.parameters") - @env["action_dispatch.request.path_parameters"] = parameters + @env.delete('action_dispatch.request.parameters') + @env[PARAMETERS_KEY] = parameters end - # The same as <tt>path_parameters</tt> with explicitly symbolized keys. def symbolized_path_parameters - @symbolized_path_params ||= path_parameters.symbolize_keys + ActiveSupport::Deprecation.warn( + "`symbolized_path_parameters` is deprecated. Please use `path_parameters`" + ) + path_parameters end # Returns a hash with the \parameters used to form the \path of the request. # Returned hash keys are strings: # # {'action' => 'my_action', 'controller' => 'my_controller'} - # - # See <tt>symbolized_path_parameters</tt> for symbolized keys. def path_parameters - @env["action_dispatch.request.path_parameters"] ||= {} - end - - def reset_parameters #:nodoc: - @env.delete("action_dispatch.request.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 daa06e96e6..a519d6c1fc 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 @@ -53,6 +53,17 @@ module ActionDispatch @uuid = nil end + def check_path_parameters! + # If any of the path parameters has an invalid encoding then + # raise since it's likely to trigger errors further on. + path_parameters.each do |key, value| + next unless value.respond_to?(:valid_encoding?) + unless value.valid_encoding? + raise ActionController::BadRequest, "Invalid parameter: #{key} => #{value}" + end + end + end + def key?(key) @env.key?(key) end @@ -64,6 +75,7 @@ module ActionDispatch # Ordered Collections Protocol (WebDAV) (http://www.ietf.org/rfc/rfc3648.txt) # Web Distributed Authoring and Versioning (WebDAV) Access Control Protocol (http://www.ietf.org/rfc/rfc3744.txt) # Web Distributed Authoring and Versioning (WebDAV) SEARCH (http://www.ietf.org/rfc/rfc5323.txt) + # Calendar Extensions to WebDAV (http://www.ietf.org/rfc/rfc4791.txt) # PATCH Method for HTTP (http://www.ietf.org/rfc/rfc5789.txt) RFC2616 = %w(OPTIONS GET HEAD POST PUT DELETE TRACE CONNECT) RFC2518 = %w(PROPFIND PROPPATCH MKCOL COPY MOVE LOCK UNLOCK) @@ -71,9 +83,10 @@ module ActionDispatch RFC3648 = %w(ORDERPATCH) RFC3744 = %w(ACL) RFC5323 = %w(SEARCH) + RFC4791 = %w(MKCALENDAR) RFC5789 = %w(PATCH) - HTTP_METHODS = RFC2616 + RFC2518 + RFC3253 + RFC3648 + RFC3744 + RFC5323 + RFC5789 + HTTP_METHODS = RFC2616 + RFC2518 + RFC3253 + RFC3648 + RFC3744 + RFC5323 + RFC4791 + RFC5789 HTTP_METHOD_LOOKUP = {} @@ -196,8 +209,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 @@ -278,7 +291,7 @@ module ActionDispatch # Override Rack's GET method to support indifferent access def GET - @env["action_dispatch.request.query_parameters"] ||= Utils.deep_munge((normalize_encode_params(super) || {})) + @env["action_dispatch.request.query_parameters"] ||= Utils.deep_munge(normalize_encode_params(super || {})) rescue TypeError => e raise ActionController::BadRequest.new(:query, e) end @@ -286,7 +299,7 @@ module ActionDispatch # Override Rack's POST method to support indifferent access def POST - @env["action_dispatch.request.request_parameters"] ||= Utils.deep_munge((normalize_encode_params(super) || {})) + @env["action_dispatch.request.request_parameters"] ||= Utils.deep_munge(normalize_encode_params(super || {})) rescue TypeError => e raise ActionController::BadRequest.new(:request, e) end diff --git a/actionpack/lib/action_dispatch/http/response.rb b/actionpack/lib/action_dispatch/http/response.rb index 3d27ff2b24..2fab6be1a5 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 @@ -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,6 +262,17 @@ 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. def to_a @@ -296,6 +298,9 @@ module ActionDispatch # :nodoc: cookies end + def _status_code + @status + end private def before_committed @@ -334,6 +339,38 @@ 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 + end + def rack_response(status, header) assign_default_content_type_and_charset!(header) handle_conditional_get! @@ -344,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 6f5a52c568..473f692b05 100644 --- a/actionpack/lib/action_dispatch/http/url.rb +++ b/actionpack/lib/action_dispatch/http/url.rb @@ -5,99 +5,128 @@ 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) + 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) + 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) + def extract_subdomain(host, tld_length) extract_subdomains(host, tld_length).join('.') end - def url_for(options = {}) - options = options.dup - path = options.delete(:script_name).to_s.chomp("/") - path << options.delete(:path).to_s - - params = options[:params].is_a?(Hash) ? options[:params] : options.slice(:params) - params.reject! { |_,v| v.to_param.nil? } - - result = build_host_url(options) - if options[:trailing_slash] - if path.include?('?') - result << path.sub(/\?/, '/\&') - else - result << path.sub(/[^\/]\z|\A\z/, '\&/') - end + def url_for(options) + if options[:only_path] + path_for options else - result << path + full_url_for options end - result << "?#{params.to_query}" unless params.empty? - result << "##{Journey::Router::Utils.escape_fragment(options[:anchor].to_param.to_s)}" if options[:anchor] - result end - private + def full_url_for(options) + host = options[:host] + protocol = options[:protocol] + port = options[:port] - def build_host_url(options) - if options[:host].blank? && options[:only_path].blank? + 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 - result = "" + build_host_url(host, port, protocol, options, path_for(options)) + end + + def path_for(options) + result = options[:script_name].to_s.chomp("/") + result << options[:path].to_s + + result = add_trailing_slash(result) if options[:trailing_slash] - unless options[:only_path] - 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) - end + result = add_params options, result + add_anchor options, result + end + + private - options[:protocol] = normalize_protocol(options) - options[:host] = normalize_host(options) - options[:port] = normalize_port(options) + def add_params(options, result) + if options.key? :params + param = options[:params] + params = param.is_a?(Hash) ? param : { params: param } - result << options[:protocol] - result << rewrite_authentication(options) - result << options[:host] - result << ":#{options[:port]}" if options[:port] + params.reject! { |_,v| v.to_param.nil? } + result << "?#{params.to_query}" unless params.empty? end result end - def named_host?(host) - host && IP_HOST_REGEXP !~ host + def add_anchor(options, result) + result << "##{Journey::Router::Utils.escape_fragment(options[:anchor].to_param.to_s)}" if options[:anchor] + result + end + + def extract_domain_from(host, tld_length) + host.split('.').last(1 + tld_length).join('.') end - def same_host?(options) - (options[:subdomain] == true || !options.key?(:subdomain)) && options[:domain].nil? + def extract_subdomains_from(host, tld_length) + parts = host.split('.') + parts[0..-(tld_length + 2)] end - def rewrite_authentication(options) + def add_trailing_slash(path) + # includes querysting + if path.include?('?') + path.sub!(/\?/, '/\&') + # does not have a .format + elsif !path.include?(".") + path.sub!(/[^\/]\z|\A\z/, '\&/') + end + + path + end + + 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 + + protocol = normalize_protocol protocol + host = normalize_host(host, options) + + result = protocol.dup + if options[:user] && options[:password] - "#{Rack::Utils.escape(options[:user])}:#{Rack::Utils.escape(options[:password])}@" - else - "" + result << "#{Rack::Utils.escape(options[:user])}:#{Rack::Utils.escape(options[:password])}@" end + + result << host + normalize_port(port, protocol) { |normalized_port| + result << ":#{normalized_port}" + } + + result.concat path end - def normalize_protocol(options) - case options[:protocol] + def named_host?(host) + IP_HOST_REGEXP !~ host + end + + def normalize_protocol(protocol) + case protocol when nil "http://" when false, "//" @@ -105,36 +134,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 "//" - nil + 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 diff --git a/actionpack/lib/action_dispatch/journey/formatter.rb b/actionpack/lib/action_dispatch/journey/formatter.rb index 57f0963731..59b353b1b7 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(type, 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 @@ -30,6 +30,12 @@ module ActionDispatch parameterized_parts.key?(key) || route.defaults.key?(key) end + defaults = route.defaults + required_parts = route.required_parts + parameterized_parts.delete_if do |key, value| + value.to_s == defaults[key].to_s && !required_parts.include?(key) + end + return [route.format(parameterized_parts), params] end @@ -74,12 +80,12 @@ module ActionDispatch if named_routes.key?(name) yield named_routes[name] else - routes = non_recursive(cache, options.to_a) + routes = non_recursive(cache, options) hash = routes.group_by { |_, r| r.score(options) } hash.keys.sort.reverse_each do |score| - next if score < 0 + break if score < 0 hash[score].sort_by { |i, _| i }.each do |_, route| yield route @@ -90,14 +96,14 @@ module ActionDispatch def non_recursive(cache, options) routes = [] - stack = [cache] + queue = [cache] - while stack.any? - c = stack.shift + while queue.any? + c = queue.shift routes.concat(c[:___routes]) if c.key?(:___routes) options.each do |pair| - stack << c[pair] if c.key?(pair) + queue << c[pair] if c.key?(pair) end end @@ -126,11 +132,6 @@ module ActionDispatch } end - # Returns +true+ if no missing keys are present, otherwise +false+. - def verify_required_parts!(route, parts) - missing_keys(route, parts).empty? - end - def build_cache root = { ___routes: [] } routes.each_with_index do |route, i| diff --git a/actionpack/lib/action_dispatch/journey/nodes/node.rb b/actionpack/lib/action_dispatch/journey/nodes/node.rb index 935442ef66..bb01c087bc 100644 --- a/actionpack/lib/action_dispatch/journey/nodes/node.rb +++ b/actionpack/lib/action_dispatch/journey/nodes/node.rb @@ -93,6 +93,10 @@ module ActionDispatch class Star < Unary # :nodoc: def type; :STAR; end + + def name + left.name.tr '*:', '' + end end class Binary < Node # :nodoc: diff --git a/actionpack/lib/action_dispatch/journey/parser.rb b/actionpack/lib/action_dispatch/journey/parser.rb index 430812fafe..d129ba7e16 100644 --- a/actionpack/lib/action_dispatch/journey/parser.rb +++ b/actionpack/lib/action_dispatch/journey/parser.rb @@ -1,7 +1,7 @@ # # DO NOT MODIFY!!!! -# This file is automatically generated by Racc 1.4.9 -# from Racc grammar file "". +# This file is automatically generated by Racc 1.4.11 +# from Racc grammer file "". # require 'racc/parser.rb' @@ -9,42 +9,38 @@ require 'racc/parser.rb' require 'action_dispatch/journey/parser_extras' module ActionDispatch - module Journey # :nodoc: - class Parser < Racc::Parser # :nodoc: + module Journey + class Parser < Racc::Parser ##### State transition tables begin ### racc_action_table = [ - 17, 21, 13, 15, 14, 7, nil, 16, 8, 19, - 13, 15, 14, 7, 23, 16, 8, 19, 13, 15, - 14, 7, nil, 16, 8, 13, 15, 14, 7, nil, - 16, 8, 13, 15, 14, 7, nil, 16, 8 ] + 13, 15, 14, 7, 21, 16, 8, 19, 13, 15, + 14, 7, 17, 16, 8, 13, 15, 14, 7, 24, + 16, 8, 13, 15, 14, 7, 19, 16, 8 ] racc_action_check = [ - 1, 17, 1, 1, 1, 1, nil, 1, 1, 1, - 20, 20, 20, 20, 20, 20, 20, 20, 7, 7, - 7, 7, nil, 7, 7, 19, 19, 19, 19, nil, - 19, 19, 0, 0, 0, 0, nil, 0, 0 ] + 2, 2, 2, 2, 17, 2, 2, 2, 0, 0, + 0, 0, 1, 0, 0, 19, 19, 19, 19, 20, + 19, 19, 7, 7, 7, 7, 22, 7, 7 ] racc_action_pointer = [ - 30, 0, nil, nil, nil, nil, nil, 16, nil, nil, - nil, nil, nil, nil, nil, nil, nil, 1, nil, 23, - 8, nil, nil, nil ] + 6, 12, -2, nil, nil, nil, nil, 20, nil, nil, + nil, nil, nil, nil, nil, nil, nil, 4, nil, 13, + 13, nil, 17, nil, nil ] racc_action_default = [ - -18, -18, -2, -3, -4, -5, -6, -18, -9, -10, - -11, -12, -13, -14, -15, -16, -17, -18, -1, -18, - -18, 24, -8, -7 ] + -19, -19, -2, -3, -4, -5, -6, -19, -10, -11, + -12, -13, -14, -15, -16, -17, -18, -19, -1, -19, + -19, 25, -8, -9, -7 ] racc_goto_table = [ - 18, 1, nil, nil, nil, nil, nil, nil, 20, nil, - nil, nil, nil, nil, nil, nil, nil, nil, 22, 18 ] + 1, 22, 18, 23, nil, nil, nil, 20 ] racc_goto_check = [ - 2, 1, nil, nil, nil, nil, nil, nil, 1, nil, - nil, nil, nil, nil, nil, nil, nil, nil, 2, 2 ] + 1, 2, 1, 3, nil, nil, nil, 1 ] racc_goto_pointer = [ - nil, 1, -1, nil, nil, nil, nil, nil, nil, nil, + nil, 0, -18, -16, nil, nil, nil, nil, nil, nil, nil ] racc_goto_default = [ @@ -61,19 +57,20 @@ racc_reduce_table = [ 1, 12, :_reduce_none, 3, 15, :_reduce_7, 3, 13, :_reduce_8, - 1, 16, :_reduce_9, + 3, 13, :_reduce_9, + 1, 16, :_reduce_10, 1, 14, :_reduce_none, 1, 14, :_reduce_none, 1, 14, :_reduce_none, 1, 14, :_reduce_none, - 1, 19, :_reduce_14, - 1, 17, :_reduce_15, - 1, 18, :_reduce_16, - 1, 20, :_reduce_17 ] + 1, 19, :_reduce_15, + 1, 17, :_reduce_16, + 1, 18, :_reduce_17, + 1, 20, :_reduce_18 ] -racc_reduce_n = 18 +racc_reduce_n = 19 -racc_shift_n = 24 +racc_shift_n = 25 racc_token_table = { false => 0, @@ -137,12 +134,12 @@ Racc_debug_parser = false # reduce 0 omitted def _reduce_1(val, _values, result) - result = Cat.new(val.first, val.last) + result = Cat.new(val.first, val.last) result end def _reduce_2(val, _values, result) - result = val.first + result = val.first result end @@ -155,21 +152,24 @@ end # reduce 6 omitted def _reduce_7(val, _values, result) - result = Group.new(val[1]) + result = Group.new(val[1]) result end def _reduce_8(val, _values, result) - result = Or.new([val.first, val.last]) + result = Or.new([val.first, val.last]) result end def _reduce_9(val, _values, result) - result = Star.new(Symbol.new(val.last)) + result = Or.new([val.first, val.last]) result end -# reduce 10 omitted +def _reduce_10(val, _values, result) + result = Star.new(Symbol.new(val.last)) + result +end # reduce 11 omitted @@ -177,23 +177,25 @@ end # reduce 13 omitted -def _reduce_14(val, _values, result) - result = Slash.new('/') - result -end +# reduce 14 omitted def _reduce_15(val, _values, result) - result = Symbol.new(val.first) + result = Slash.new('/') result end def _reduce_16(val, _values, result) - result = Literal.new(val.first) + result = Symbol.new(val.first) result end def _reduce_17(val, _values, result) - result = Dot.new(val.first) + result = Literal.new(val.first) + result +end + +def _reduce_18(val, _values, result) + result = Dot.new(val.first) result end diff --git a/actionpack/lib/action_dispatch/journey/parser.y b/actionpack/lib/action_dispatch/journey/parser.y index 040f8d5922..0ead222551 100644 --- a/actionpack/lib/action_dispatch/journey/parser.y +++ b/actionpack/lib/action_dispatch/journey/parser.y @@ -4,7 +4,7 @@ token SLASH LITERAL SYMBOL LPAREN RPAREN DOT STAR OR rule expressions - : expressions expression { result = Cat.new(val.first, val.last) } + : expression expressions { result = Cat.new(val.first, val.last) } | expression { result = val.first } | or ; @@ -17,7 +17,8 @@ rule : LPAREN expressions RPAREN { result = Group.new(val[1]) } ; or - : expressions OR expression { result = Or.new([val.first, val.last]) } + : expression OR expression { result = Or.new([val.first, val.last]) } + | expression OR or { result = Or.new([val.first, val.last]) } ; star : STAR { result = Star.new(Symbol.new(val.last)) } diff --git a/actionpack/lib/action_dispatch/journey/path/pattern.rb b/actionpack/lib/action_dispatch/journey/path/pattern.rb index fb155e516f..3af940a02f 100644 --- a/actionpack/lib/action_dispatch/journey/path/pattern.rb +++ b/actionpack/lib/action_dispatch/journey/path/pattern.rb @@ -1,27 +1,20 @@ +require 'action_dispatch/journey/router/strexp' + module ActionDispatch module Journey # :nodoc: module Path # :nodoc: class Pattern # :nodoc: attr_reader :spec, :requirements, :anchored - def initialize(strexp) - parser = Journey::Parser.new - - @anchored = true + def self.from_string string + new Journey::Router::Strexp.build(string, {}, ["/.?"], true) + end - case strexp - when String - @spec = parser.parse(strexp) - @requirements = {} - @separators = "/.?" - when Router::Strexp - @spec = parser.parse(strexp.path) - @requirements = strexp.requirements - @separators = strexp.separators.join - @anchored = strexp.anchor - else - raise ArgumentError, "Bad expression: #{strexp}" - end + def initialize(strexp) + @spec = strexp.ast + @requirements = strexp.requirements + @separators = strexp.separators.join + @anchored = strexp.anchor @names = nil @optional_names = nil @@ -30,6 +23,10 @@ module ActionDispatch @offsets = nil end + def build_formatter + Visitors::FormatBuilder.new.accept(spec) + end + def ast @spec.grep(Nodes::Symbol).each do |node| re = @requirements[node.to_sym] diff --git a/actionpack/lib/action_dispatch/journey/route.rb b/actionpack/lib/action_dispatch/journey/route.rb index 2b399d3ee3..9f0a3af902 100644 --- a/actionpack/lib/action_dispatch/journey/route.rb +++ b/actionpack/lib/action_dispatch/journey/route.rb @@ -16,14 +16,6 @@ module ActionDispatch @app = app @path = path - # Unwrap any constraints so we can see what's inside for route generation. - # This allows the formatter to skip over any mounted applications or redirects - # that shouldn't be matched when using a url_for without a route name. - while app.is_a?(Routing::Mapper::Constraints) do - app = app.app - end - @dispatcher = app.is_a?(Routing::RouteSet::Dispatcher) - @constraints = constraints @defaults = defaults @required_defaults = nil @@ -31,6 +23,7 @@ module ActionDispatch @parts = nil @decorated_ast = nil @precedence = 0 + @path_formatter = @path.build_formatter end def ast @@ -72,15 +65,7 @@ module ActionDispatch alias :segment_keys :parts def format(path_options) - path_options.delete_if do |key, value| - value.to_s == defaults[key].to_s && !required_parts.include?(key) - end - - Visitors::Formatter.new(path_options).accept(path.spec) - end - - def optimized_path - Visitors::OptimizedPath.new.accept(path.spec) + @path_formatter.evaluate path_options end def optional_parts @@ -106,7 +91,7 @@ module ActionDispatch end def dispatcher? - @dispatcher + @app.dispatcher? end def matches?(request) diff --git a/actionpack/lib/action_dispatch/journey/router.rb b/actionpack/lib/action_dispatch/journey/router.rb index 36561c71a1..21817b374c 100644 --- a/actionpack/lib/action_dispatch/journey/router.rb +++ b/actionpack/lib/action_dispatch/journey/router.rb @@ -20,60 +20,32 @@ module ActionDispatch # :nodoc: VERSION = '2.0.0' - class NullReq # :nodoc: - attr_reader :env - def initialize(env) - @env = env - end - - def request_method - env['REQUEST_METHOD'] - end - - def path_info - env['PATH_INFO'] - end - - def ip - env['REMOTE_ADDR'] - end - - def [](k) - env[k] - end - end - - attr_reader :request_class, :formatter attr_accessor :routes - def initialize(routes, options) - @options = options - @params_key = options[:parameters_key] - @request_class = options[:request_class] || NullReq - @routes = routes + def initialize(routes) + @routes = routes end - def call(env) - env['PATH_INFO'] = Utils.normalize_path(env['PATH_INFO']) - - find_routes(env).each do |match, parameters, route| - script_name, path_info, set_params = env.values_at('SCRIPT_NAME', - 'PATH_INFO', - @params_key) + def serve(req) + find_routes(req).each do |match, parameters, route| + set_params = req.path_parameters + path_info = req.path_info + script_name = req.script_name unless route.path.anchored - env['SCRIPT_NAME'] = (script_name.to_s + match.to_s).chomp('/') - env['PATH_INFO'] = match.post_match + 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 - env[@params_key] = (set_params || {}).merge parameters + req.path_parameters = set_params.merge parameters - status, headers, body = route.app.call(env) + status, headers, body = route.app.serve(req) if 'pass' == headers['X-Cascade'] - env['SCRIPT_NAME'] = script_name - env['PATH_INFO'] = path_info - env[@params_key] = set_params + req.script_name = script_name + req.path_info = path_info + req.path_parameters = set_params next end @@ -83,14 +55,14 @@ module ActionDispatch return [404, {'X-Cascade' => 'pass'}, ['Not Found']] end - def recognize(req) - find_routes(req.env).each do |match, parameters, route| + def recognize(rails_req) + find_routes(rails_req).each do |match, parameters, route| unless route.path.anchored - req.env['SCRIPT_NAME'] = match.to_s - req.env['PATH_INFO'] = match.post_match.sub(/^([^\/])/, '/\1') + rails_req.script_name = match.to_s + rails_req.path_info = match.post_match.sub(/^([^\/])/, '/\1') end - yield(route, nil, parameters) + yield(route, parameters) end end @@ -124,29 +96,31 @@ module ActionDispatch simulator.memos(path) { [] } end - def find_routes env - req = request_class.new(env) - + def find_routes req routes = filter_routes(req.path_info).concat custom_routes.find_all { |r| r.path.match(req.path_info) } - routes.concat get_routes_as_head(routes) - routes.sort_by!(&:precedence).select! { |r| r.matches?(req) } + if req.env["REQUEST_METHOD"] === "HEAD" + routes.concat get_routes_as_head(routes) + end + + routes.select! { |r| r.matches?(req) } + routes.sort_by!(&:precedence) routes.map! { |r| match_data = r.path.match(req.path_info) - match_names = match_data.names.map { |n| n.to_sym } - match_values = match_data.captures.map { |v| v && Utils.unescape_uri(v) } - info = Hash[match_names.zip(match_values).find_all { |_, y| y }] - - [match_data, r.defaults.merge(info), r] + path_parameters = r.defaults.dup + match_data.names.zip(match_data.captures) { |name,val| + path_parameters[name.to_sym] = Utils.unescape_uri(val) if val + } + [match_data, path_parameters, r] } end def get_routes_as_head(routes) precedence = (routes.map(&:precedence).max || 0) + 1 - routes = routes.select { |r| + routes.select { |r| r.verb === "GET" && !(r.verb === "HEAD") }.map! { |r| Route.new(r.name, @@ -157,8 +131,6 @@ module ActionDispatch route.precedence = r.precedence + precedence end } - routes.flatten! - routes end end end diff --git a/actionpack/lib/action_dispatch/journey/router/strexp.rb b/actionpack/lib/action_dispatch/journey/router/strexp.rb index f97f1a223e..4b7738f335 100644 --- a/actionpack/lib/action_dispatch/journey/router/strexp.rb +++ b/actionpack/lib/action_dispatch/journey/router/strexp.rb @@ -6,18 +6,21 @@ module ActionDispatch alias :compile :new end - attr_reader :path, :requirements, :separators, :anchor + attr_reader :path, :requirements, :separators, :anchor, :ast - def initialize(path, requirements, separators, anchor = true) + def self.build(path, requirements, separators, anchor = true) + parser = Journey::Parser.new + ast = parser.parse path + new ast, path, requirements, separators, anchor + end + + def initialize(ast, path, requirements, separators, anchor = true) + @ast = ast @path = path @requirements = requirements @separators = separators @anchor = anchor end - - def names - @path.scan(/:\w+/).map { |s| s.tr(':', '') } - end 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/visitors.rb b/actionpack/lib/action_dispatch/journey/visitors.rb index d9f634623d..52b4c8b489 100644 --- a/actionpack/lib/action_dispatch/journey/visitors.rb +++ b/actionpack/lib/action_dispatch/journey/visitors.rb @@ -1,14 +1,57 @@ # encoding: utf-8 -require 'thread_safe' - module ActionDispatch module Journey # :nodoc: + class Format + ESCAPE_PATH = ->(value) { Router::Utils.escape_path(value) } + ESCAPE_SEGMENT = ->(value) { Router::Utils.escape_segment(value) } + + class Parameter < Struct.new(:name, :escaper) + def escape(value); escaper.call value; end + end + + def self.required_path(symbol) + Parameter.new symbol, ESCAPE_PATH + end + + def self.required_segment(symbol) + Parameter.new symbol, ESCAPE_SEGMENT + end + + def initialize(parts) + @parts = parts + @children = [] + @parameters = [] + + parts.each_with_index do |object,i| + case object + when Journey::Format + @children << i + when Parameter + @parameters << i + end + end + end + + def evaluate(hash) + parts = @parts.dup + + @parameters.each do |index| + param = parts[index] + value = hash[param.name] + return ''.freeze unless value + parts[index] = param.escape value + end + + @children.each { |index| parts[index] = parts[index].evaluate(hash) } + + parts.join + end + end + module Visitors # :nodoc: class Visitor # :nodoc: - DISPATCH_CACHE = ThreadSafe::Cache.new { |h,k| - h[k] = :"visit_#{k}" - } + DISPATCH_CACHE = {} def accept(node) visit(node) @@ -38,11 +81,41 @@ module ActionDispatch def visit_STAR(n); unary(n); end def terminal(node); end - %w{ LITERAL SYMBOL SLASH DOT }.each do |t| - class_eval %{ def visit_#{t}(n); terminal(n); end }, __FILE__, __LINE__ + def visit_LITERAL(n); terminal(n); end + def visit_SYMBOL(n); terminal(n); end + def visit_SLASH(n); terminal(n); end + def visit_DOT(n); terminal(n); end + + private_instance_methods(false).each do |pim| + next unless pim =~ /^visit_(.*)$/ + DISPATCH_CACHE[$1.to_sym] = pim end end + class FormatBuilder < Visitor # :nodoc: + def accept(node); Journey::Format.new(super); end + def terminal(node); [node.left]; end + + def binary(node) + visit(node.left) + visit(node.right) + end + + def visit_GROUP(n); [Journey::Format.new(unary(n))]; end + + def visit_STAR(n) + [Journey::Format.required_path(n.left.to_sym)] + end + + def visit_SYMBOL(n) + symbol = n.to_sym + if symbol == :controller + [Journey::Format.required_path(symbol)] + else + [Journey::Format.required_segment(symbol)] + end + end + end + # Loop through the requirements AST class Each < Visitor # :nodoc: attr_reader :block @@ -52,8 +125,8 @@ module ActionDispatch end def visit(node) - super block.call(node) + super end end @@ -77,90 +150,6 @@ module ActionDispatch end end - class OptimizedPath < Visitor # :nodoc: - def accept(node) - Array(visit(node)) - end - - private - - def visit_CAT(node) - [visit(node.left), visit(node.right)].flatten - end - - def visit_SYMBOL(node) - node.left[1..-1].to_sym - end - - def visit_STAR(node) - visit(node.left) - end - - def visit_GROUP(node) - [] - end - - %w{ LITERAL SLASH DOT }.each do |t| - class_eval %{ def visit_#{t}(n); n.left; end }, __FILE__, __LINE__ - end - end - - # Used for formatting urls (url_for) - class Formatter < Visitor # :nodoc: - attr_reader :options - - def initialize(options) - @options = options - end - - private - def escape_path(value) - Router::Utils.escape_path(value) - end - - def escape_segment(value) - Router::Utils.escape_segment(value) - end - - def visit(node, optional = false) - case node.type - when :LITERAL, :SLASH, :DOT - node.left - when :STAR - visit_STAR(node.left) - when :GROUP - visit(node.left, true) - when :CAT - visit_CAT(node, optional) - when :SYMBOL - visit_SYMBOL(node, node.to_sym) - end - end - - def visit_CAT(node, optional) - left = visit(node.left, optional) - right = visit(node.right, optional) - - if optional && !(right && left) - "" - else - [left, right].join - end - end - - def visit_STAR(node) - if value = options[node.to_sym] - escape_path(value) - end - end - - def visit_SYMBOL(node, name) - if value = options[name] - name == :controller ? escape_path(value) : escape_segment(value) - end - end - end - class Dot < Visitor # :nodoc: def initialize @nodes = [] diff --git a/actionpack/lib/action_dispatch/journey/visualizer/index.html.erb b/actionpack/lib/action_dispatch/journey/visualizer/index.html.erb index 6aff10956a..9b28a65200 100644 --- a/actionpack/lib/action_dispatch/journey/visualizer/index.html.erb +++ b/actionpack/lib/action_dispatch/journey/visualizer/index.html.erb @@ -2,13 +2,13 @@ <html> <head> <title><%= title %></title> - <link rel="stylesheet" href="https://raw.github.com/gist/1706081/af944401f75ea20515a02ddb3fb43d23ecb8c662/reset.css" type="text/css"> + <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/meyer-reset/2.0/reset.css" type="text/css"> <style> <% stylesheets.each do |style| %> <%= style %> <% end %> </style> - <script src="https://raw.github.com/gist/1706081/df464722a05c3c2bec450b7b5c8240d9c31fa52d/d3.min.js" type="text/javascript"></script> + <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.8/d3.min.js" type="text/javascript"></script> </head> <body> <div id="wrapper"> diff --git a/actionpack/lib/action_dispatch/middleware/cookies.rb b/actionpack/lib/action_dispatch/middleware/cookies.rb index 22b16b628d..ac9e5effe2 100644 --- a/actionpack/lib/action_dispatch/middleware/cookies.rb +++ b/actionpack/lib/action_dispatch/middleware/cookies.rb @@ -289,8 +289,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! @@ -468,7 +468,7 @@ module ActionDispatch options = { :value => @verifier.generate(serialize(name, 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 @@ -526,7 +526,7 @@ module ActionDispatch options[:value] = @encryptor.encrypt_and_sign(serialize(name, 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 diff --git a/actionpack/lib/action_dispatch/middleware/flash.rb b/actionpack/lib/action_dispatch/middleware/flash.rb index 4821d2a899..e90f8b9ce6 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 diff --git a/actionpack/lib/action_dispatch/middleware/public_exceptions.rb b/actionpack/lib/action_dispatch/middleware/public_exceptions.rb index cbb2d475b1..6c8944e067 100644 --- a/actionpack/lib/action_dispatch/middleware/public_exceptions.rb +++ b/actionpack/lib/action_dispatch/middleware/public_exceptions.rb @@ -32,9 +32,8 @@ module ActionDispatch end def render_html(status) - found = false - path = "#{public_path}/#{status}.#{I18n.locale}.html" if I18n.locale - path = "#{public_path}/#{status}.html" unless path && (found = File.exist?(path)) + path = "#{public_path}/#{status}.#{I18n.locale}.html" + path = "#{public_path}/#{status}.html" unless (found = File.exist?(path)) if found || File.exist?(path) render_format(status, 'text/html', File.read(path)) diff --git a/actionpack/lib/action_dispatch/middleware/remote_ip.rb b/actionpack/lib/action_dispatch/middleware/remote_ip.rb index c1df518b14..6a79b4e859 100644 --- a/actionpack/lib/action_dispatch/middleware/remote_ip.rb +++ b/actionpack/lib/action_dispatch/middleware/remote_ip.rb @@ -11,7 +11,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. @@ -31,7 +31,7 @@ module ActionDispatch TRUSTED_PROXIES = %r{ ^127\.0\.0\.1$ | # localhost IPv4 ^::1$ | # localhost IPv6 - ^fc00: | # private IPv6 range fc00 + ^[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 @@ -118,7 +118,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. 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/templates/routes/_table.html.erb b/actionpack/lib/action_dispatch/middleware/templates/routes/_table.html.erb index cce0d75af4..6ffa242da4 100644 --- a/actionpack/lib/action_dispatch/middleware/templates/routes/_table.html.erb +++ b/actionpack/lib/action_dispatch/middleware/templates/routes/_table.html.erb @@ -148,8 +148,8 @@ // On key press perform a search for matching paths searchElem.onkeyup = function(e){ var userInput = searchElem.value, - defaultExactMatch = '<tr><th colspan="4">Paths Matching (' + sanitizePath(userInput) +'):</th></tr>', - defaultFuzzyMatch = '<tr><th colspan="4">Paths Containing (' + userInput +'):</th></tr>', + defaultExactMatch = '<tr><th colspan="4">Paths Matching (' + escape(sanitizePath(userInput)) +'):</th></tr>', + defaultFuzzyMatch = '<tr><th colspan="4">Paths Containing (' + escape(userInput) +'):</th></tr>', noExactMatch = '<tr><th colspan="4">No Exact Matches Found</th></tr>', noFuzzyMatch = '<tr><th colspan="4">No Fuzzy Matches Found</th></tr>'; diff --git a/actionpack/lib/action_dispatch/routing.rb b/actionpack/lib/action_dispatch/routing.rb index 9cd884daa3..ce03164ca9 100644 --- a/actionpack/lib/action_dispatch/routing.rb +++ b/actionpack/lib/action_dispatch/routing.rb @@ -1,6 +1,7 @@ # encoding: UTF-8 require 'active_support/core_ext/object/to_param' require 'active_support/core_ext/regexp' +require 'active_support/dependencies/autoload' module ActionDispatch # The routing module provides URL rewriting in native Ruby. It's a way to diff --git a/actionpack/lib/action_dispatch/routing/endpoint.rb b/actionpack/lib/action_dispatch/routing/endpoint.rb new file mode 100644 index 0000000000..88aa13c3e8 --- /dev/null +++ b/actionpack/lib/action_dispatch/routing/endpoint.rb @@ -0,0 +1,10 @@ +module ActionDispatch + module Routing + class Endpoint # :nodoc: + def dispatcher?; false; end + def redirect?; false; end + def matches?(req); true; end + def app; self; end + end + end +end diff --git a/actionpack/lib/action_dispatch/routing/inspector.rb b/actionpack/lib/action_dispatch/routing/inspector.rb index 71a0c5e826..ea3b2f419d 100644 --- a/actionpack/lib/action_dispatch/routing/inspector.rb +++ b/actionpack/lib/action_dispatch/routing/inspector.rb @@ -5,22 +5,15 @@ module ActionDispatch module Routing class RouteWrapper < SimpleDelegator def endpoint - rack_app ? rack_app.inspect : "#{controller}##{action}" + app.dispatcher? ? "#{controller}##{action}" : rack_app.inspect end def constraints requirements.except(:controller, :action) end - def rack_app(app = self.app) - @rack_app ||= begin - class_name = app.class.name.to_s - if class_name == "ActionDispatch::Routing::Mapper::Constraints" - rack_app(app.app) - elsif ActionDispatch::Routing::Redirect === app || class_name !~ /^ActionDispatch::Routing/ - app - end - end + def rack_app + app.app end def verb @@ -73,7 +66,7 @@ module ActionDispatch end def engine? - rack_app && rack_app.respond_to?(:routes) + rack_app.respond_to?(:routes) end end diff --git a/actionpack/lib/action_dispatch/routing/mapper.rb b/actionpack/lib/action_dispatch/routing/mapper.rb index 77718a14c1..32d963ba76 100644 --- a/actionpack/lib/action_dispatch/routing/mapper.rb +++ b/actionpack/lib/action_dispatch/routing/mapper.rb @@ -6,6 +6,8 @@ require 'active_support/core_ext/array/extract_options' require 'active_support/core_ext/module/remove_method' require 'active_support/inflector' require 'action_dispatch/routing/redirection' +require 'action_dispatch/routing/endpoint' +require 'active_support/deprecation' module ActionDispatch module Routing @@ -15,121 +17,190 @@ module ActionDispatch :controller, :action, :path_names, :constraints, :shallow, :blocks, :defaults, :options] - class Constraints #:nodoc: - def self.new(app, constraints, request = Rack::Request) - if constraints.any? - super(app, constraints, request) - else - app + class Constraints < Endpoint #:nodoc: + attr_reader :app, :constraints + + def initialize(app, constraints, dispatcher_p) + # Unwrap Constraints objects. I don't actually think it's possible + # to pass a Constraints object to this constructor, but there were + # multiple places that kept testing children of this object. I + # *think* they were just being defensive, but I have no idea. + if app.is_a?(self.class) + constraints += app.constraints + app = app.app end - end - attr_reader :app, :constraints + @dispatcher = dispatcher_p - def initialize(app, constraints, request) - @app, @constraints, @request = app, constraints, request + @app, @constraints, = app, constraints end - def matches?(env) - req = @request.new(env) + def dispatcher?; @dispatcher; end + def matches?(req) @constraints.all? do |constraint| (constraint.respond_to?(:matches?) && constraint.matches?(req)) || (constraint.respond_to?(:call) && constraint.call(*constraint_args(constraint, req))) end - ensure - req.reset_parameters end - def call(env) - matches?(env) ? @app.call(env) : [ 404, {'X-Cascade' => 'pass'}, [] ] + def serve(req) + return [ 404, {'X-Cascade' => 'pass'}, [] ] unless matches?(req) + + if dispatcher? + @app.serve req + else + @app.call req.env + end end private def constraint_args(constraint, request) - constraint.arity == 1 ? [request] : [request.symbolized_path_parameters, request] + constraint.arity == 1 ? [request] : [request.path_parameters, request] end 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} - WILDCARD_PATH = %r{\*([^/\)]+)\)?$} - attr_reader :scope, :path, :options, :requirements, :conditions, :defaults + attr_reader :requirements, :conditions, :defaults + attr_reader :to, :default_controller, :default_action, :as, :anchor + + def self.build(scope, set, path, options) + options = scope[:options].merge(options) if scope[:options] - def initialize(set, scope, path, options) - @set, @scope, @path, @options = set, scope, path, options - @requirements, @conditions, @defaults = {}, {}, {} + options.delete :only + options.delete :except + options.delete :shallow_path + options.delete :shallow_prefix + options.delete :shallow - normalize_options! - normalize_path! - normalize_requirements! - normalize_conditions! - normalize_defaults! + defaults = (scope[:defaults] || {}).merge options.delete(:defaults) || {} + + new scope, set, path, defaults, options + end + + def initialize(scope, set, path, defaults, 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 = options.delete :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, 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 - def normalize_path! - raise ArgumentError, "path is required" if @path.blank? - @path = Mapper.normalize_path(@path) + def normalize_path!(path, format) + path = Mapper.normalize_path(path) - if required_format? - @path = "#{@path}.:format" - elsif optional_format? - @path = "#{@path}(.:format)" + if format == true + "#{path}.:format" + elsif optional_format?(path, format) + "#{path}(.:format)" + else + path end end - def required_format? - options[:format] == true - end - - def optional_format? - options[:format] != false && !path.include?(':format') && !path.end_with?('/') + def optional_format?(path, format) + format != false && !path.include?(':format') && !path.end_with?('/') end - def normalize_options! - @options.reverse_merge!(scope[:options]) if scope[:options] - path_without_format = path.sub(/\(\.:format\)$/, '') - + 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 path_without_format.match(WILDCARD_PATH) && @options[:format] != false - @options[$1.to_sym] ||= /.+?/ + if formatted != false + path_ast.grep(Journey::Nodes::Star) do |node| + options[node.name.to_sym] ||= /.+?/ + end end - if path_without_format.match(':controller') - raise ArgumentError, ":controller segment is not allowed within a namespace block" if scope[:module] + if path_params.include?(:controller) + 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: # GET /admin/products/show/1 # => { controller: 'admin/products', action: 'show', id: '1' } - @options[:controller] ||= /.+?/ + options[:controller] ||= /.+?/ end - @options.merge!(default_controller_and_action) + if to.respond_to? :call + options + else + 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! - constraints.each do |key, requirement| - next unless segment_keys.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 @@ -143,169 +214,143 @@ 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 - end - - if Regexp === options[:format] - @defaults[:format] = nil - elsif String === options[:format] - @defaults[:format] = options[:format] - end end - def normalize_conditions! - @conditions[:path_info] = path - - constraints.each do |key, condition| - unless segment_keys.include?(key) || key == :controller - @conditions[key] = condition - end + def verify_callable_constraint(callable_constraint) + unless callable_constraint.respond_to?(:call) || callable_constraint.respond_to?(:matches?) + raise ArgumentError, "Invalid constraint: #{callable_constraint.inspect} must respond to :call or :matches?" end + end - required_defaults = [] - options.each do |key, required_default| - unless segment_keys.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 + def add_request_method(via, conditions) + return if 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 - Constraints.new(endpoint, blocks, @set.request_class) - end + def app(blocks) + return to if Redirect === to - def default_controller_and_action if to.respond_to?(:call) - { } + Constraints.new(to, blocks, false) else - if to.is_a?(String) - controller, action = to.split('#') - elsif to.is_a?(Symbol) - action = to.to_s - end - - controller ||= default_controller - action ||= default_action - - if @scope[:module] && !controller.is_a?(Regexp) - if controller =~ %r{\A/} - controller = controller[1..-1] - else - controller = [@scope[:module], controller].compact.join("/").presence - end - end - - if controller.is_a?(String) && controller =~ %r{\A/} - raise ArgumentError, "controller name should not start with a slash" + if blocks.any? + Constraints.new(dispatcher(defaults), blocks, true) + else + dispatcher(defaults) end + end + end - controller = controller.to_s unless controller.is_a?(Regexp) - action = action.to_s unless action.is_a?(Regexp) + 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." + message << " See http://guides.rubyonrails.org/routing.html#specifying-a-controller-to-use" - if controller.blank? && segment_keys.exclude?(:controller) - message = "Missing :controller key on routes definition, please check your routes." raise ArgumentError, message - end + } + end - if action.blank? && segment_keys.exclude?(:action) - message = "Missing :action key on routes definition, please check your routes." - raise ArgumentError, message - end + check_part(:action, action, path_params, hash) { |part| + part.is_a?(Regexp) ? part : part.to_s + } + end - if controller.is_a?(String) && controller !~ /\A[a-z_0-9\/]*\z/ - message = "'#{controller}' is not a supported controller name. This can lead to potential routing problems." - message << " See http://guides.rubyonrails.org/routing.html#specifying-a-controller-to-use" + def check_part(name, part, path_params, hash) + if part + hash[name] = yield(part) + else + unless path_params.include?(name) + message = "Missing :#{name} key on routes definition, please check your routes." raise ArgumentError, message end - - hash = {} - hash[:controller] = controller unless controller.blank? - hash[:action] = action unless action.blank? - hash end - end - - def blocks - if options[:constraints].present? && !options[:constraints].is_a?(Hash) - [options[:constraints]] + hash + end + + def split_to(to) + case to + when Symbol + ActiveSupport::Deprecation.warn "defining a route where `to` is a symbol is deprecated. Please change \"to: :#{to}\" to \"action: :#{to}\"" + [nil, to.to_s] + when /#/ then to.split('#') + when String + ActiveSupport::Deprecation.warn "defining a route where `to` is a controller without an action is deprecated. Please change \"to: :#{to}\" to \"controller: :#{to}\"" + [to, nil] else - 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 add_controller_module(controller, modyoule) + if modyoule && !controller.is_a?(Regexp) + if controller =~ %r{\A/} + controller[1..-1] + else + [modyoule, controller].compact.join("/") end - - constraints.merge!(options[:constraints]) if options[:constraints].is_a?(Hash) + else + controller end end - def segment_keys - @segment_keys ||= path_pattern.names.map{ |s| s.to_sym } - end + def translate_controller(controller) + return controller if Regexp === controller + return controller.to_s if controller =~ /\A[a-z_0-9][a-z_0-9\/]*\z/ - def path_pattern - Journey::Path::Pattern.new(strexp) - end - - def strexp - Journey::Router::Strexp.compile(path, requirements, SEPARATORS) + yield end - def endpoint - to.respond_to?(:call) ? to : dispatcher + def blocks(options_constraints, scope_blocks) + if options_constraints && !options_constraints.is_a?(Hash) + verify_callable_constraint(options_constraints) + [options_constraints] + else + scope_blocks || [] + end end - def dispatcher - Routing::RouteSet::Dispatcher.new(:defaults => defaults) + 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 + end + @conditions[:required_defaults] = required_defaults + constraints end - def to - options[:to] + def path_params(ast) + ast.grep(Journey::Nodes::Symbol).map { |n| n.name.to_sym } end - def default_controller - options[:controller] || scope[:controller] + def path_ast(path) + parser = Journey::Parser.new + parser.parse path end - def default_action - options[:action] || scope[:action] + def dispatcher(defaults) + @set.dispatcher defaults end end @@ -340,18 +385,34 @@ module ActionDispatch match '/', { :as => :root, :via => :get }.merge!(options) end - # Matches a url pattern to one or more routes. Any symbols in a pattern - # are interpreted as url query parameters and thus available as +params+ - # in an action: + # Matches a url pattern to one or more routes. + # + # 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: # # # sets :controller, :action and :id in params - # match ':controller/:action/:id' + # match ':controller/:action/:id', via: [:get, :post] + # + # 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: + # + # Instead of: + # + # match ":controller/:action/:id" + # + # Do: + # + # get ":controller/:action/:id" # # Two of these symbols are special, +:controller+ maps to the controller # and +:action+ to the controller's action. A pattern can also map # wildcard segments (globs) to params: # - # match 'songs/*category/:title', to: 'songs#show' + # get 'songs/*category/:title', to: 'songs#show' # # # 'songs/rock/classic/stairway-to-heaven' sets # # params[:category] = 'rock/classic' @@ -364,17 +425,17 @@ module ActionDispatch # When a pattern points to an internal route, the route's +:action+ and # +:controller+ should be set in options or hash shorthand. Examples: # - # match 'photos/:id' => 'photos#show' - # match 'photos/:id', to: 'photos#show' - # match 'photos/:id', controller: 'photos', action: 'show' + # match 'photos/:id' => 'photos#show', via: :get + # match 'photos/:id', to: 'photos#show', via: :get + # match 'photos/:id', controller: 'photos', action: 'show', via: :get # # A pattern can also point to a +Rack+ endpoint i.e. anything that # responds to +call+: # - # match 'photos/:id', to: lambda {|hash| [200, {}, ["Coming soon"]] } - # match 'photos/:id', to: PhotoRackApp + # match 'photos/:id', to: lambda {|hash| [200, {}, ["Coming soon"]] }, via: :get + # match 'photos/:id', to: PhotoRackApp, via: :get # # Yes, controller actions are just rack endpoints - # match 'photos/:id', to: PhotosController.action(:show) + # match 'photos/:id', to: PhotosController.action(:show), via: :get # # Because requesting various HTTP verbs with a single action has security # implications, you must either specify the actions in @@ -391,13 +452,19 @@ module ActionDispatch # [:action] # The route's action. # + # [:param] + # 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>. + # # [:path] # The path prefix for the routes. # # [:module] # The namespace for :controller. # - # match 'path', to: 'c#a', module: 'sekret', controller: 'posts' + # match 'path', to: 'c#a', module: 'sekret', controller: 'posts', via: :get # # => Sekret::PostsController # # See <tt>Scoping#namespace</tt> for its scope equivalent. @@ -416,9 +483,9 @@ module ActionDispatch # Points to a +Rack+ endpoint. Can be an object that responds to # +call+ or a string representing a controller's action. # - # match 'path', to: 'controller#action' - # match 'path', to: lambda { |env| [200, {}, ["Success!"]] } - # match 'path', to: RackApp + # match 'path', to: 'controller#action', via: :get + # match 'path', to: lambda { |env| [200, {}, ["Success!"]] }, via: :get + # match 'path', to: RackApp, via: :get # # [:on] # Shorthand for wrapping routes in a specific RESTful context. Valid @@ -443,14 +510,14 @@ module ActionDispatch # other than path can also be specified with any object # that responds to <tt>===</tt> (eg. String, Array, Range, etc.). # - # match 'path/:id', constraints: { id: /[A-Z]\d{5}/ } + # match 'path/:id', constraints: { id: /[A-Z]\d{5}/ }, via: :get # - # match 'json_only', constraints: { format: 'json' } + # match 'json_only', constraints: { format: 'json' }, via: :get # # class Whitelist # def matches?(request) request.remote_ip == '1.2.3.4' end # end - # match 'path', to: 'c#a', constraints: Whitelist.new + # match 'path', to: 'c#a', constraints: Whitelist.new, via: :get # # See <tt>Scoping#constraints</tt> for more examples with its scope # equivalent. @@ -459,7 +526,7 @@ module ActionDispatch # Sets defaults for parameters # # # Sets params[:format] to 'jpg' by default - # match 'path', to: 'c#a', defaults: { format: 'jpg' } + # match 'path', to: 'c#a', defaults: { format: 'jpg' }, via: :get # # See <tt>Scoping#defaults</tt> for its scope equivalent. # @@ -468,7 +535,7 @@ module ActionDispatch # false, the pattern matches any request prefixed with the given path. # # # Matches any request starting with 'path' - # match 'path', to: 'c#a', anchor: false + # match 'path', to: 'c#a', anchor: false, via: :get # # [:format] # Allows you to specify the default value for optional +format+ @@ -510,13 +577,21 @@ module ActionDispatch raise "A rack application must be specified" unless path - options[:as] ||= app_name(app) + rails_app = rails_app? app + + if rails_app + options[:as] ||= app.railtie_name + else + # non rails apps can't have an :as + options[:as] = nil + end + 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 @@ -537,35 +612,25 @@ module ActionDispatch end private - def app_name(app) - return unless app.respond_to?(:routes) - - if app.respond_to?(:railtie_name) - app.railtie_name - else - class_name = app.class.is_a?(Class) ? app.name : app.class.name - ActiveSupport::Inflector.underscore(class_name).tr("/", "_") - end + def rails_app?(app) + app.is_a?(Class) && app < Rails::Railtie 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] _routes = @set app.routes.define_mounted_helper(name) - app.routes.singleton_class.class_eval do - redefine_method :mounted? do - true - end - - redefine_method :_generate_prefix do |options| + app.routes.extend Module.new { + def mounted?; true; 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) + end end - end + } end end @@ -652,7 +717,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 @@ -1369,7 +1434,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 } @@ -1403,7 +1468,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 @@ -1457,6 +1535,8 @@ module ActionDispatch def add_route(action, options) # :nodoc: path = path_for_action(action, options.delete(:path)) + raise ArgumentError, "path is required" if path.blank? + action = action.to_s.dup if action =~ /^[\w\-\/]+$/ @@ -1465,13 +1545,13 @@ module ActionDispatch action = nil end - if !options.fetch(:as, true) + if !options.fetch(:as, true) # if it's set to nil or false options.delete(:as) else options[:as] = name_for_action(options[:as], action) end - mapping = Mapping.new(@set, @scope, URI.parser.escape(path), options) + mapping = Mapping.build(@scope, @set, URI.parser.escape(path), options) app, conditions, requirements, defaults, as, anchor = mapping.to_route @set.add_route(app, conditions, requirements, defaults, as, anchor) end diff --git a/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb b/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb index cfd33d1f31..bd3696cda1 100644 --- a/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb +++ b/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb @@ -101,55 +101,45 @@ module ActionDispatch # polymorphic_url(Comment) # same as comments_url() # def polymorphic_url(record_or_hash_or_array, options = {}) - recipient = self - - if record_or_hash_or_array.kind_of?(Array) - record_or_hash_or_array = record_or_hash_or_array.compact - if record_or_hash_or_array.first.is_a?(ActionDispatch::Routing::RoutesProxy) - recipient = record_or_hash_or_array.shift - end - record_or_hash_or_array = record_or_hash_or_array[0] if record_or_hash_or_array.size == 1 + if Hash === record_or_hash_or_array + options = record_or_hash_or_array.merge(options) + record = options.delete :id + return polymorphic_url record, options end - record = extract_record(record_or_hash_or_array) - record = convert_to_model(record) - - args = Array === record_or_hash_or_array ? - record_or_hash_or_array.dup : - [ record_or_hash_or_array ] - - inflection = if options[:action] && options[:action].to_s == "new" - args.pop - :singular - elsif (record.respond_to?(:persisted?) && !record.persisted?) - args.pop - :plural - elsif record.is_a?(Class) - args.pop - :plural - else - :singular - end + opts = options.dup + action = opts.delete :action + type = opts.delete(:routing_type) || :url - args.delete_if {|arg| arg.is_a?(Symbol) || arg.is_a?(String)} - named_route = build_named_route_call(record_or_hash_or_array, record, inflection, options) + HelperMethodBuilder.polymorphic_method self, + record_or_hash_or_array, + action, + type, + opts - url_options = options.except(:action, :routing_type) - unless url_options.empty? - args.last.kind_of?(Hash) ? args.last.merge!(url_options) : args << url_options - end - - args.collect! { |a| convert_to_model(a) } - - recipient.send(named_route, *args) end # Returns the path component of a URL for the given record. It uses # <tt>polymorphic_url</tt> with <tt>routing_type: :path</tt>. def polymorphic_path(record_or_hash_or_array, options = {}) - polymorphic_url(record_or_hash_or_array, options.merge(:routing_type => :path)) + if Hash === record_or_hash_or_array + options = record_or_hash_or_array.merge(options) + record = options.delete :id + return polymorphic_path record, options + end + + opts = options.dup + action = opts.delete :action + type = :path + + HelperMethodBuilder.polymorphic_method self, + record_or_hash_or_array, + action, + type, + opts end + %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 = {}) @@ -167,53 +157,169 @@ module ActionDispatch end private - def action_prefix(options) - options[:action] ? "#{options[:action]}_" : '' + + class HelperMethodBuilder # :nodoc: + CACHE = { 'path' => {}, 'url' => {} } + + def self.get(action, type) + type = type.to_s + CACHE[type].fetch(action) { build action, type } end - def routing_type(options) - options[:routing_type] || :url + def self.url; CACHE['url'.freeze][nil]; end + def self.path; CACHE['path'.freeze][nil]; end + + def self.build(action, type) + prefix = action ? "#{action}_" : "" + suffix = type + if action.to_s == 'new' + HelperMethodBuilder.singular prefix, suffix + else + HelperMethodBuilder.plural prefix, suffix + end end - def build_named_route_call(records, record, inflection, options = {}) - if records.is_a?(Array) - record = records.pop - route = records.map do |parent| - if parent.is_a?(Symbol) || parent.is_a?(String) - parent - else - model_name_from_record_or_class(parent).singular_route_key - end + def self.singular(prefix, suffix) + new(->(name) { name.singular_route_key }, prefix, suffix) + end + + def self.plural(prefix, suffix) + new(->(name) { name.route_key }, prefix, suffix) + end + + def self.polymorphic_method(recipient, record_or_hash_or_array, action, type, options) + builder = get action, type + + case record_or_hash_or_array + when Array + if record_or_hash_or_array.empty? || record_or_hash_or_array.include?(nil) + raise ArgumentError, "Nil location provided. Can't build URI." end + if record_or_hash_or_array.first.is_a?(ActionDispatch::Routing::RoutesProxy) + recipient = record_or_hash_or_array.shift + end + + method, args = builder.handle_list record_or_hash_or_array + when String, Symbol + method, args = builder.handle_string record_or_hash_or_array + when Class + method, args = builder.handle_class record_or_hash_or_array + + when nil + raise ArgumentError, "Nil location provided. Can't build URI." + else + method, args = builder.handle_model record_or_hash_or_array + end + + + if options.empty? + recipient.send(method, *args) else - route = [] + recipient.send(method, *args, options) end + end + + attr_reader :suffix, :prefix - if record.is_a?(Symbol) || record.is_a?(String) - route << record - elsif record - if inflection == :singular - route << model_name_from_record_or_class(record).singular_route_key + def initialize(key_strategy, prefix, suffix) + @key_strategy = key_strategy + @prefix = prefix + @suffix = suffix + end + + def handle_string(record) + [get_method_for_string(record), []] + end + + def handle_string_call(target, str) + target.send get_method_for_string str + end + + def handle_class(klass) + [get_method_for_class(klass), []] + end + + def handle_class_call(target, klass) + target.send get_method_for_class klass + end + + def handle_model(record) + args = [] + + model = record.to_model + name = if record.persisted? + args << model + model.class.model_name.singular_route_key + else + @key_strategy.call model.class.model_name + end + + named_route = prefix + "#{name}_#{suffix}" + + [named_route, args] + end + + def handle_model_call(target, model) + method, args = handle_model model + target.send(method, *args) + end + + def handle_list(list) + record_list = list.dup + record = record_list.pop + + args = [] + + route = record_list.map { |parent| + case parent + when Symbol, String + parent.to_s + when Class + args << parent + parent.model_name.singular_route_key else - route << model_name_from_record_or_class(record).route_key + args << parent.to_model + parent.to_model.class.model_name.singular_route_key end + } + + route << + case record + when Symbol, String + record.to_s + when Class + @key_strategy.call record.model_name else - raise ArgumentError, "Nil location provided. Can't build URI." + if record.persisted? + args << record.to_model + record.to_model.class.model_name.singular_route_key + else + @key_strategy.call record.to_model.class.model_name + end end - route << routing_type(options) + route << suffix - action_prefix(options) + route.join("_") + named_route = prefix + route.join("_") + [named_route, args] end - def extract_record(record_or_hash_or_array) - case record_or_hash_or_array - when Array; record_or_hash_or_array.last - when Hash; record_or_hash_or_array[:id] - else record_or_hash_or_array - end + private + + def get_method_for_class(klass) + name = @key_strategy.call klass.model_name + prefix + "#{name}_#{suffix}" + end + + def get_method_for_string(str) + prefix + "#{str}_#{suffix}" end + + [nil, 'new', 'edit'].each do |action| + CACHE['url'][action] = build action, 'url' + CACHE['path'][action] = build action, 'path' + end + end end end end - diff --git a/actionpack/lib/action_dispatch/routing/redirection.rb b/actionpack/lib/action_dispatch/routing/redirection.rb index b08e62543b..3c1c4fadf6 100644 --- a/actionpack/lib/action_dispatch/routing/redirection.rb +++ b/actionpack/lib/action_dispatch/routing/redirection.rb @@ -3,10 +3,11 @@ require 'active_support/core_ext/uri' require 'active_support/core_ext/array/extract_options' require 'rack/utils' require 'action_controller/metal/exceptions' +require 'action_dispatch/routing/endpoint' module ActionDispatch module Routing - class Redirect # :nodoc: + class Redirect < Endpoint # :nodoc: attr_reader :status, :block def initialize(status, block) @@ -14,18 +15,15 @@ module ActionDispatch @block = block end - def call(env) - req = Request.new(env) + def redirect?; true; end - # If any of the path parameters has an invalid encoding then - # raise since it's likely to trigger errors further on. - req.symbolized_path_parameters.each do |key, value| - unless value.valid_encoding? - raise ActionController::BadRequest, "Invalid parameter: #{key} => #{value}" - end - end + def call(env) + serve Request.new env + end - uri = URI.parse(path(req.symbolized_path_parameters, req)) + def serve(req) + req.check_path_parameters! + uri = URI.parse(path(req.path_parameters, req)) unless uri.host if relative_path?(uri.path) @@ -39,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 1ec6fa674b..80c705608d 100644 --- a/actionpack/lib/action_dispatch/routing/route_set.rb +++ b/actionpack/lib/action_dispatch/routing/route_set.rb @@ -1,11 +1,14 @@ require 'action_dispatch/journey' require 'forwardable' require 'thread_safe' +require 'active_support/concern' require 'active_support/core_ext/object/to_query' require 'active_support/core_ext/hash/slice' require 'active_support/core_ext/module/remove_method' require 'active_support/core_ext/array/extract_options' require 'action_controller/metal/exceptions' +require 'action_dispatch/http/request' +require 'action_dispatch/routing/endpoint' module ActionDispatch module Routing @@ -16,27 +19,17 @@ module ActionDispatch # alias inspect to to_s. alias inspect to_s - PARAMETERS_KEY = 'action_dispatch.request.path_parameters' - - class Dispatcher #:nodoc: - def initialize(options={}) - @defaults = options[:defaults] - @glob_param = options.delete(:glob) + class Dispatcher < Routing::Endpoint #:nodoc: + def initialize(defaults) + @defaults = defaults @controller_class_names = ThreadSafe::Cache.new end - def call(env) - params = env[PARAMETERS_KEY] + def dispatcher?; true; end - # If any of the path parameters has an invalid encoding then - # raise since it's likely to trigger errors further on. - params.each do |key, value| - next unless value.respond_to?(:valid_encoding?) - - unless value.valid_encoding? - raise ActionController::BadRequest, "Invalid parameter: #{key} => #{value}" - end - end + def serve(req) + req.check_path_parameters! + params = req.path_parameters prepare_params!(params) @@ -45,13 +38,12 @@ module ActionDispatch return [404, {'X-Cascade' => 'pass'}, []] end - dispatch(controller, params[:action], env) + dispatch(controller, params[:action], req.env) end def prepare_params!(params) normalize_controller!(params) merge_default_action!(params) - split_glob_param!(params) if @glob_param end # If this is a default_controller (i.e. a controller specified by the user) @@ -87,10 +79,6 @@ module ActionDispatch def merge_default_action!(params) params[:action] ||= 'index' end - - def split_glob_param!(params) - params[@glob_param] = params[@glob_param].split('/').map { |v| URI.parser.unescape(v) } - end end # A NamedRouteCollection instance is a collection of named routes, and also @@ -146,35 +134,34 @@ module ActionDispatch end class UrlHelper # :nodoc: - def self.create(route, options) + 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 def self.optimize_helper?(route) - !route.glob? && route.requirements.except(:controller, :action).empty? + !route.glob? && route.path.requirements.empty? end + attr_reader :url_strategy, :route_name + class OptimizedUrlHelper < UrlHelper # :nodoc: attr_reader :arg_size - def initialize(route, options) + def initialize(route, options, route_name, url_strategy) super - @klass = Journey::Router::Utils @required_parts = @route.required_parts @arg_size = @required_parts.size - @optimized_path = @route.optimized_path end - def call(t, args) - if args.size == arg_size && !args.last.is_a?(Hash) && optimize_routes_generation?(t) - options = @options.dup - options.merge!(t.url_options) if t.respond_to?(:url_options) + 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 @@ -183,18 +170,14 @@ module ActionDispatch private def optimized_helper(args) - params = Hash[parameterize_args(args)] + params = parameterize_args(args) missing_keys = missing_keys(params) unless missing_keys.empty? raise_generation_error(params, missing_keys) end - @optimized_path.map{ |segment| replace_segment(params, segment) }.join - end - - def replace_segment(params, segment) - Symbol === segment ? @klass.escape_segment(params[segment]) : segment + @route.format params end def optimize_routes_generation?(t) @@ -202,7 +185,9 @@ module ActionDispatch end def parameterize_args(args) - @required_parts.zip(args.map(&:to_param)) + params = {} + @required_parts.zip(args.map(&:to_param)) { |k,v| params[k] = v } + params end def missing_keys(args) @@ -218,27 +203,36 @@ 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) - t.url_for(handle_positional_args(t, args, @options, @segment_keys)) + def call(t, args, inner_options) + controller_options = t.url_options + options = controller_options.merge @options + hash = handle_positional_args(controller_options, + inner_options || {}, + args, + options, + @segment_keys) + + t._routes.url_for(hash, route_name, url_strategy) end - def handle_positional_args(t, args, options, keys) - inner_options = args.extract_options! - result = options.dup + def handle_positional_args(controller_options, inner_options, args, result, path_params) if args.size > 0 - if args.size < keys.size - 1 # take format into account - keys -= t.url_options.keys if t.respond_to?(:url_options) - keys -= options.keys + if args.size < path_params.size - 1 # take format into account + path_params -= controller_options.keys + path_params -= result.keys end - keys -= inner_options.keys - result.merge!(Hash[keys.zip(args)]) + path_params.each { |param| + result[param] = inner_options[param] || args.shift + } end result.merge!(inner_options) @@ -259,13 +253,15 @@ module ActionDispatch # # foo_url(bar, baz, bang, sort_by: 'baz') # - def define_url_helper(route, name, options) - helper = UrlHelper.create(route, options.dup) + def define_url_helper(route, name, opts, route_key, url_strategy) + helper = UrlHelper.create(route, opts, route_key, url_strategy) @module.remove_possible_method name @module.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 @@ -273,13 +269,18 @@ module ActionDispatch 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) + define_url_helper route, :"#{name}_path", route.defaults, name, PATH + define_url_helper route, :"#{name}_url", route.defaults, name, FULL end end + # :stopdoc: + # strategy for building urls to send to the client + PATH = ->(options) { ActionDispatch::Http::URL.path_for(options) } + FULL = ->(options) { ActionDispatch::Http::URL.full_url_for(options) } + UNKNOWN = ->(options) { ActionDispatch::Http::URL.url_for(options) } + # :startdoc: + attr_accessor :formatter, :set, :named_routes, :default_scope, :router attr_accessor :disable_clear_and_finalize, :resources_path_names attr_accessor :default_url_options, :request_class @@ -302,9 +303,7 @@ module ActionDispatch @finalized = false @set = Journey::Routes.new - @router = Journey::Router.new(@set, { - :parameters_key => PARAMETERS_KEY, - :request_class => request_class}) + @router = Journey::Router.new @set @formatter = Journey::Formatter.new @set end @@ -350,6 +349,10 @@ module ActionDispatch @prepend.each { |blk| eval_block(blk) } end + def dispatcher(defaults) + Routing::RouteSet::Dispatcher.new(defaults) + end + module MountedHelpers #:nodoc: extend ActiveSupport::Concern include UrlFor @@ -393,6 +396,8 @@ module ActionDispatch @_routes = routes class << self delegate :url_for, :optimize_routes_generation?, :to => '@_routes' + attr_reader :_routes + def url_options; {}; end end # Make named_routes available in the module singleton @@ -432,7 +437,9 @@ module ActionDispatch "http://guides.rubyonrails.org/routing.html#restricting-the-routes-created" end - path = build_path(conditions.delete(:path_info), requirements, SEPARATORS, anchor) + path = conditions.delete :path_info + ast = conditions.delete :parsed_path_info + path = build_path(path, ast, requirements, anchor) conditions = build_conditions(conditions, path.names.map { |x| x.to_sym }) route = @set.add_route(app, path, conditions, defaults, name) @@ -440,8 +447,9 @@ module ActionDispatch route end - def build_path(path, requirements, separators, anchor) + def build_path(path, ast, requirements, anchor) strexp = Journey::Router::Strexp.new( + ast, path, requirements, SEPARATORS, @@ -502,8 +510,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 @@ -594,7 +602,7 @@ module ActionDispatch # Generates a path from routes, returns [path, params]. # If no route is generated the formatter will raise ActionController::UrlGenerationError def generate - @set.formatter.generate(:path_info, named_route, options, recall, PARAMETERIZE) + @set.formatter.generate(named_route, options, recall, PARAMETERIZE) end def different_controller? @@ -619,13 +627,15 @@ 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, @@ -639,41 +649,52 @@ module ActionDispatch !mounted? && default_url_options.empty? end - def _generate_prefix(options = {}) - nil + def find_script_name(options) + options.delete :script_name end - # The +options+ argument must be +nil+ or a hash whose keys are *symbols*. - def url_for(options) - options = default_url_options.merge(options || {}) + # The +options+ argument must be a hash whose keys are *symbols*. + def url_for(options, route_name = nil, url_strategy = UNKNOWN) + options = default_url_options.merge options + + user = password = nil + + if options[:user] && options[:password] + user = options.delete :user + password = options.delete :password + end - user, password = extract_authentication(options) - recall = options.delete(:_recall) + recall = options.delete(:_recall) { {} } - original_script_name = options.delete(:original_script_name).presence - script_name = options.delete(:script_name).presence || _generate_prefix(options) + original_script_name = options.delete(:original_script_name) + script_name = find_script_name options if script_name && original_script_name script_name = original_script_name + script_name end - path_options = options.except(*RESERVED_OPTIONS) - path_options = yield(path_options) if block_given? + path_options = options.dup + RESERVED_OPTIONS.each { |ro| path_options.delete ro } + + path, params = generate(route_name, path_options, recall) + + if options.key? :params + params.merge! options[:params] + end - path, params = generate(path_options, recall || {}) - params.merge!(options[:params] || {}) + options[:path] = path + options[:script_name] = script_name + options[:params] = params + options[:user] = user + options[:password] = password - ActionDispatch::Http::URL.url_for(options.merge!({ - :path => path, - :script_name => script_name, - :params => params, - :user => user, - :password => password - })) + url_strategy.call options end def call(env) - @router.call(env) + req = request_class.new(env) + req.path_info = Journey::Router::Utils.normalize_path(req.path_info) + @router.serve(req) end def recognize_path(path, environment = {}) @@ -687,8 +708,8 @@ module ActionDispatch raise ActionController::RoutingError, e.message end - req = @request_class.new(env) - @router.recognize(req) do |route, _matches, params| + req = request_class.new(env) + @router.recognize(req) do |route, params| params.merge!(extras) params.each do |key, value| if value.is_a?(String) @@ -696,14 +717,12 @@ module ActionDispatch params[key] = URI.parser.unescape(value) end end - old_params = env[::ActionDispatch::Routing::RouteSet::PARAMETERS_KEY] - env[::ActionDispatch::Routing::RouteSet::PARAMETERS_KEY] = (old_params || {}).merge(params) - dispatcher = route.app - while dispatcher.is_a?(Mapper::Constraints) && dispatcher.matches?(env) do - dispatcher = dispatcher.app - end + old_params = req.path_parameters + req.path_parameters = old_params.merge params + app = route.app + if app.matches?(req) && app.dispatcher? + dispatcher = app.app - if dispatcher.is_a?(Dispatcher) if dispatcher.controller(params, false) dispatcher.prepare_params!(params) return params @@ -715,17 +734,6 @@ module ActionDispatch raise ActionController::RoutingError, "No route matches #{path.inspect}" end - - private - - def extract_authentication(options) - if options[:user] && options[:password] - [options.delete(:user), options.delete(:password)] - else - nil - end - end - end end end diff --git a/actionpack/lib/action_dispatch/routing/url_for.rb b/actionpack/lib/action_dispatch/routing/url_for.rb index 4a0ef40873..e1c73f8f07 100644 --- a/actionpack/lib/action_dispatch/routing/url_for.rb +++ b/actionpack/lib/action_dispatch/routing/url_for.rb @@ -152,13 +152,19 @@ 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 + HelperMethodBuilder.url.handle_string_call self, options when Array polymorphic_url(options, options.extract_options!) + when Class + HelperMethodBuilder.url.handle_class_call self, options else - polymorphic_url(options) + HelperMethodBuilder.url.handle_model_call self, options end end diff --git a/actionpack/lib/action_dispatch/testing/assertions/response.rb b/actionpack/lib/action_dispatch/testing/assertions/response.rb index 68feb26936..0adc6c84ff 100644 --- a/actionpack/lib/action_dispatch/testing/assertions/response.rb +++ b/actionpack/lib/action_dispatch/testing/assertions/response.rb @@ -73,7 +73,13 @@ module ActionDispatch if Regexp === fragment fragment else - @controller._compute_redirect_to_location(fragment) + 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) end end end diff --git a/actionpack/lib/action_dispatch/testing/integration.rb b/actionpack/lib/action_dispatch/testing/integration.rb index cc6b763093..12b796b95f 100644 --- a/actionpack/lib/action_dispatch/testing/integration.rb +++ b/actionpack/lib/action_dispatch/testing/integration.rb @@ -3,7 +3,6 @@ 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: @@ -189,8 +188,8 @@ module ActionDispatch # 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) + include app.routes.url_helpers + include app.routes.mounted_helpers end end @@ -201,7 +200,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 @@ -270,12 +269,6 @@ module ActionDispatch path = location.query ? "#{location.path}?#{location.query}" : location.path end - unless ActionController::Base < ActionController::Testing - ActionController::Base.class_eval do - include ActionController::Testing - end - end - hostname, port = host.split(':') env = { @@ -300,13 +293,7 @@ module ActionDispatch # NOTE: rack-test v0.5 doesn't build a default uri correctly # Make sure requested path is always a full uri - uri = URI.parse('/') - uri.scheme ||= env['rack.url_scheme'] - uri.host ||= env['SERVER_NAME'] - uri.port ||= env['SERVER_PORT'].try(:to_i) - uri += path - - session.request(uri.to_s, env) + session.request(build_full_uri(path, env), env) @request_count += 1 @request = ActionDispatch::Request.new(session.last_request.env) @@ -319,6 +306,10 @@ module ActionDispatch return response.status end + + def build_full_uri(path, env) + "#{env['rack.url_scheme']}://#{env['SERVER_NAME']}:#{env['SERVER_PORT']}#{path}" + end end module Runner @@ -338,6 +329,7 @@ module ActionDispatch xml_http_request xhr get_via_redirect post_via_redirect).each do |method| define_method(method) do |*args| reset! unless integration_session + reset_template_assertion # reset the html_document variable, but only for new get/post calls @html_document = nil unless method == 'cookies' || method == 'assigns' integration_session.__send__(method, *args).tap do @@ -356,7 +348,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 diff --git a/actionpack/lib/action_dispatch/testing/test_request.rb b/actionpack/lib/action_dispatch/testing/test_request.rb index 57c678843b..de3dc5f924 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) |