diff options
Diffstat (limited to 'actionpack/lib/action_dispatch')
69 files changed, 1473 insertions, 716 deletions
diff --git a/actionpack/lib/action_dispatch/http/filter_parameters.rb b/actionpack/lib/action_dispatch/http/filter_parameters.rb index 289e204ac8..2b851cc28d 100644 --- a/actionpack/lib/action_dispatch/http/filter_parameters.rb +++ b/actionpack/lib/action_dispatch/http/filter_parameters.rb @@ -6,8 +6,8 @@ module ActionDispatch module Http # Allows you to specify sensitive parameters which will be replaced from # the request log by looking in the query string of the request and all - # subhashes of the params hash to filter. If a block is given, each key and - # value of the params hash and all subhashes is passed to it, the value + # sub-hashes of the params hash to filter. If a block is given, each key and + # value of the params hash and all sub-hashes is passed to it, the value # or key can be replaced using String#replace or similar method. # # env["action_dispatch.parameter_filter"] = [:password] diff --git a/actionpack/lib/action_dispatch/http/filter_redirect.rb b/actionpack/lib/action_dispatch/http/filter_redirect.rb index 900ce1c646..cd603649c3 100644 --- a/actionpack/lib/action_dispatch/http/filter_redirect.rb +++ b/actionpack/lib/action_dispatch/http/filter_redirect.rb @@ -5,7 +5,8 @@ module ActionDispatch FILTERED = '[FILTERED]'.freeze # :nodoc: def filtered_location - if !location_filter.empty? && location_filter_match? + filters = location_filter + if !filters.empty? && location_filter_match?(filters) FILTERED else location @@ -15,15 +16,15 @@ module ActionDispatch private def location_filter - if request.present? + if request request.env['action_dispatch.redirect_filter'] || [] else [] end end - def location_filter_match? - location_filter.any? do |filter| + def location_filter_match?(filters) + filters.any? do |filter| if String === filter location.include?(filter) elsif Regexp === filter diff --git a/actionpack/lib/action_dispatch/http/headers.rb b/actionpack/lib/action_dispatch/http/headers.rb index 2666cd4b0a..3e607bbde1 100644 --- a/actionpack/lib/action_dispatch/http/headers.rb +++ b/actionpack/lib/action_dispatch/http/headers.rb @@ -1,5 +1,10 @@ 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 @@ -14,21 +19,32 @@ module ActionDispatch 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 +53,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 +71,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 40bb060d52..0b2b60d2e4 100644 --- a/actionpack/lib/action_dispatch/http/mime_negotiation.rb +++ b/actionpack/lib/action_dispatch/http/mime_negotiation.rb @@ -10,6 +10,8 @@ module ActionDispatch self.ignore_accept_header = false end + attr_reader :variant + # The MIME type of the HTTP request, such as Mime::XML. # # For backward compatibility, the post \format is extracted from the @@ -48,7 +50,7 @@ module ActionDispatch # GET /posts/5 | request.format => Mime::HTML or MIME::JS, or request.accepts.first # def format(view_path = []) - formats.first + formats.first || Mime::NullType.instance end def formats @@ -64,6 +66,20 @@ module ActionDispatch end end + # Sets the \variant for template. + def variant=(variant) + if variant.is_a?(Symbol) + @variant = [variant] + elsif variant.is_a?(Array) && variant.any? && variant.all?{ |v| v.is_a?(Symbol) } + @variant = variant + else + raise ArgumentError, "request.variant must be set to a Symbol or an Array of Symbols, not a #{variant.class}. " \ + "For security reasons, never directly set the variant to a user-provided value, " \ + "like params[:variant].to_sym. Check user-provided value against a whitelist first, " \ + "then set the variant: request.variant = :tablet if params[:variant] == 'tablet'" + end + end + # Sets the \format by string extension, which can be used to force custom formats # that are not controlled by the extension. # @@ -113,7 +129,7 @@ module ActionDispatch end end - order.include?(Mime::ALL) ? formats.first : nil + order.include?(Mime::ALL) ? format : nil end protected diff --git a/actionpack/lib/action_dispatch/http/mime_type.rb b/actionpack/lib/action_dispatch/http/mime_type.rb index 61dbf2b96c..9450be838c 100644 --- a/actionpack/lib/action_dispatch/http/mime_type.rb +++ b/actionpack/lib/action_dispatch/http/mime_type.rb @@ -1,5 +1,6 @@ require 'set' -require 'active_support/core_ext/class/attribute_accessors' +require 'singleton' +require 'active_support/core_ext/module/attribute_accessors' require 'active_support/core_ext/string/starts_ends_with' module Mime @@ -27,7 +28,7 @@ module Mime class << self def [](type) return type if type.is_a?(Type) - Type.lookup_by_extension(type) || NullType.new + Type.lookup_by_extension(type) end def fetch(type) @@ -173,9 +174,9 @@ module Mime end def parse(accept_header) - if accept_header !~ /,/ + if !accept_header.include?(',') accept_header = accept_header.split(PARAMETER_SEPARATOR_REGEXP).first - parse_trailing_star(accept_header) || [Mime::Type.lookup(accept_header)] + parse_trailing_star(accept_header) || [Mime::Type.lookup(accept_header)].compact else list, index = AcceptList.new, 0 accept_header.split(',').each do |header| @@ -292,13 +293,13 @@ module Mime end class NullType + include Singleton + def nil? true end - def ref - nil - end + def ref; end def respond_to_missing?(method, include_private = false) method.to_s.ends_with? '?' diff --git a/actionpack/lib/action_dispatch/http/mime_types.rb b/actionpack/lib/action_dispatch/http/mime_types.rb index a6b3aee5e7..0e4da36038 100644 --- a/actionpack/lib/action_dispatch/http/mime_types.rb +++ b/actionpack/lib/action_dispatch/http/mime_types.rb @@ -7,6 +7,7 @@ Mime::Type.register "text/javascript", :js, %w( application/javascript applicati Mime::Type.register "text/css", :css Mime::Type.register "text/calendar", :ics Mime::Type.register "text/csv", :csv +Mime::Type.register "text/vcard", :vcf Mime::Type.register "image/png", :png, [], %w(png) Mime::Type.register "image/jpeg", :jpeg, [], %w(jpg jpeg jpe pjpeg) diff --git a/actionpack/lib/action_dispatch/http/parameters.rb b/actionpack/lib/action_dispatch/http/parameters.rb index 8e992070f1..dcb299ed03 100644 --- a/actionpack/lib/action_dispatch/http/parameters.rb +++ b/actionpack/lib/action_dispatch/http/parameters.rb @@ -57,22 +57,25 @@ module ActionDispatch # you'll get a weird error down the road, but our form handling # should really prevent that from happening def normalize_encode_params(params) - if params.is_a?(String) - return params.force_encoding(Encoding::UTF_8).encode! - elsif !params.is_a?(Hash) - return params - end - - new_hash = {} - params.each do |key, val| - new_key = key.is_a?(String) ? key.dup.force_encoding(Encoding::UTF_8).encode! : key - new_hash[new_key] = if val.is_a?(Array) - val.map! { |el| normalize_encode_params(el) } + case params + when String + params.force_encoding(Encoding::UTF_8).encode! + when Hash + if params.has_key?(:tempfile) + UploadedFile.new(params) else - normalize_encode_params(val) + 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) + val.map! { |el| normalize_encode_params(el) } + else + normalize_encode_params(val) + end + end.with_indifferent_access end + else + params end - new_hash.with_indifferent_access end end end diff --git a/actionpack/lib/action_dispatch/http/request.rb b/actionpack/lib/action_dispatch/http/request.rb index 4ca1d35489..cdb3e44b3a 100644 --- a/actionpack/lib/action_dispatch/http/request.rb +++ b/actionpack/lib/action_dispatch/http/request.rb @@ -18,7 +18,6 @@ module ActionDispatch include ActionDispatch::Http::MimeNegotiation include ActionDispatch::Http::Parameters include ActionDispatch::Http::FilterParameters - include ActionDispatch::Http::Upload include ActionDispatch::Http::URL autoload :Session, 'action_dispatch/request/session' @@ -65,6 +64,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) @@ -72,9 +72,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 = {} @@ -153,6 +154,13 @@ module ActionDispatch Http::Headers.new(@env) end + # Returns a +String+ with the last requested path including their params. + # + # # get '/foo' + # request.original_fullpath # => '/foo' + # + # # get '/foo?bar' + # request.original_fullpath # => '/foo?bar' def original_fullpath @original_fullpath ||= (env["ORIGINAL_FULLPATH"] || fullpath) end @@ -226,7 +234,7 @@ module ActionDispatch def raw_post unless @env.include? 'RAW_POST_DATA' raw_post_body = body - @env['RAW_POST_DATA'] = raw_post_body.read(@env['CONTENT_LENGTH'].to_i) + @env['RAW_POST_DATA'] = raw_post_body.read(content_length) raw_post_body.rewind if raw_post_body.respond_to?(:rewind) end @env['RAW_POST_DATA'] @@ -272,7 +280,7 @@ module ActionDispatch # Override Rack's GET method to support indifferent access def GET - @env["action_dispatch.request.query_parameters"] ||= (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 @@ -280,7 +288,7 @@ module ActionDispatch # Override Rack's POST method to support indifferent access def POST - @env["action_dispatch.request.request_parameters"] ||= (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 @@ -300,17 +308,24 @@ module ActionDispatch LOCALHOST =~ remote_addr && LOCALHOST =~ remote_ip end - protected + # Extracted into ActionDispatch::Request::Utils.deep_munge, but kept here for backwards compatibility. + def deep_munge(hash) + ActiveSupport::Deprecation.warn( + "This method has been extracted into ActionDispatch::Request::Utils.deep_munge. Please start using that instead." + ) - def parse_query(qs) - Utils.deep_munge(super) + Utils.deep_munge(hash) end - private + protected + def parse_query(qs) + Utils.deep_munge(super) + end - def check_method(name) - HTTP_METHOD_LOOKUP[name] || raise(ActionController::UnknownHttpMethod, "#{name}, accepted HTTP methods are #{HTTP_METHODS.to_sentence(:locale => :en)}") - name - end + private + def check_method(name) + HTTP_METHOD_LOOKUP[name] || raise(ActionController::UnknownHttpMethod, "#{name}, accepted HTTP methods are #{HTTP_METHODS.to_sentence(:locale => :en)}") + name + end end end diff --git a/actionpack/lib/action_dispatch/http/response.rb b/actionpack/lib/action_dispatch/http/response.rb index 5697282791..eaea93b730 100644 --- a/actionpack/lib/action_dispatch/http/response.rb +++ b/actionpack/lib/action_dispatch/http/response.rb @@ -1,4 +1,5 @@ -require 'active_support/core_ext/class/attribute_accessors' +require 'active_support/core_ext/module/attribute_accessors' +require 'action_dispatch/http/filter_redirect' require 'monitor' module ActionDispatch # :nodoc: @@ -41,7 +42,7 @@ module ActionDispatch # :nodoc: # Get and set headers for this response. attr_accessor :header - + alias_method :headers=, :header= alias_method :headers, :header @@ -90,7 +91,10 @@ module ActionDispatch # :nodoc: end def each(&block) - @buf.each(&block) + @response.sending! + x = @buf.each(&block) + @response.sent! + x end def close @@ -117,6 +121,8 @@ module ActionDispatch # :nodoc: @blank = false @cv = new_cond @committed = false + @sending = false + @sent = false @content_type = nil @charset = nil @@ -137,17 +143,37 @@ module ActionDispatch # :nodoc: end end + def await_sent + synchronize { @cv.wait_until { @sent } } + end + def commit! synchronize do + before_committed @committed = true @cv.broadcast end end - def committed? - @committed + def sending! + synchronize do + before_sending + @sending = true + @cv.broadcast + end end + def sent! + synchronize do + @sent = true + @cv.broadcast + end + end + + def sending?; synchronize { @sending }; end + def committed?; synchronize { @committed }; end + def sent?; synchronize { @sent }; end + # Sets the HTTP status code. def status=(status) @status = Rack::Utils.status_code(status) @@ -181,9 +207,9 @@ module ActionDispatch # :nodoc: end alias_method :status_message, :message - def respond_to?(method) + def respond_to?(method, include_private = false) if method.to_s == 'to_path' - stream.respond_to?(:to_path) + stream.respond_to?(method) else super end @@ -210,7 +236,9 @@ module ActionDispatch # :nodoc: if body.respond_to?(:to_path) @stream = body else - @stream = build_buffer self, munge_body_object(body) + synchronize do + @stream = build_buffer self, munge_body_object(body) + end end end @@ -268,8 +296,17 @@ module ActionDispatch # :nodoc: cookies end + def _status_code + @status + end private + def before_committed + end + + def before_sending + end + def merge_default_headers(original, default) return original unless default.respond_to?(:merge) @@ -310,7 +347,7 @@ module ActionDispatch # :nodoc: header.delete CONTENT_TYPE [status, header, []] else - [status, header, self] + [status, header, Rack::BodyProxy.new(self){}] end end end diff --git a/actionpack/lib/action_dispatch/http/upload.rb b/actionpack/lib/action_dispatch/http/upload.rb index b57c84dec8..45bf751d09 100644 --- a/actionpack/lib/action_dispatch/http/upload.rb +++ b/actionpack/lib/action_dispatch/http/upload.rb @@ -18,6 +18,7 @@ module ActionDispatch # A +Tempfile+ object with the actual uploaded file. Note that some of # its interface is available directly. attr_accessor :tempfile + alias :to_io :tempfile # A string with the headers of the multipart request. attr_accessor :headers @@ -73,18 +74,5 @@ module ActionDispatch filename.force_encoding(Encoding::UTF_8).encode! if filename end end - - module Upload # :nodoc: - # Replace file upload hash with UploadedFile objects - # when normalize and encode parameters. - def normalize_encode_params(value) - if Hash === value && value.has_key?(:tempfile) - UploadedFile.new(value) - else - super - end - end - private :normalize_encode_params - end end end diff --git a/actionpack/lib/action_dispatch/http/url.rb b/actionpack/lib/action_dispatch/http/url.rb index 6f5a52c568..a6c17f50a5 100644 --- a/actionpack/lib/action_dispatch/http/url.rb +++ b/actionpack/lib/action_dispatch/http/url.rb @@ -29,25 +29,37 @@ module ActionDispatch 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 + def url_for(options) + unless options[:host] || options[:only_path] + raise ArgumentError, 'Missing host to link to! Please provide the :host parameter, set default_url_options[:host], or set :only_path to true' + end - params = options[:params].is_a?(Hash) ? options[:params] : options.slice(:params) - params.reject! { |_,v| v.to_param.nil? } + path = options[:script_name].to_s.chomp("/") + path << options[:path].to_s - result = build_host_url(options) if options[:trailing_slash] if path.include?('?') - result << path.sub(/\?/, '/\&') + path.sub!(/\?/, '/\&') else - result << path.sub(/[^\/]\z|\A\z/, '\&/') + path.sub!(/[^\/]\z|\A\z/, '\&/') end - else - result << path end - result << "?#{params.to_query}" unless params.empty? + + result = path + + unless options[:only_path] + result.prepend build_host_url(options) + end + + if options.key? :params + params = options[:params].is_a?(Hash) ? + options[:params] : + { params: options[:params] } + + params.reject! { |_,v| v.to_param.nil? } + result << "?#{params.to_query}" unless params.empty? + end + result << "##{Journey::Router::Utils.escape_fragment(options[:anchor].to_param.to_s)}" if options[:anchor] result end @@ -55,28 +67,25 @@ module ActionDispatch private def build_host_url(options) - if options[:host].blank? && options[:only_path].blank? - raise ArgumentError, 'Missing host to link to! Please provide the :host parameter, set default_url_options[:host], or set :only_path to true' + 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 = "" + options[:protocol] = normalize_protocol(options) + options[:host] = normalize_host(options) + options[:port] = normalize_port(options) - 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 - - options[:protocol] = normalize_protocol(options) - options[:host] = normalize_host(options) - options[:port] = normalize_port(options) + result = options[:protocol] - result << options[:protocol] - result << rewrite_authentication(options) - result << options[:host] - result << ":#{options[:port]}" if options[:port] + if options[:user] && options[:password] + result << "#{Rack::Utils.escape(options[:user])}:#{Rack::Utils.escape(options[:password])}@" end + + result << options[:host] + result << ":#{options[:port]}" if options[:port] + result end @@ -88,14 +97,6 @@ module ActionDispatch (options[:subdomain] == true || !options.key?(:subdomain)) && options[:domain].nil? end - def rewrite_authentication(options) - if options[:user] && options[:password] - "#{Rack::Utils.escape(options[:user])}:#{Rack::Utils.escape(options[:password])}@" - else - "" - end - end - def normalize_protocol(options) case options[:protocol] when nil @@ -130,7 +131,7 @@ module ActionDispatch case options[:protocol] when "//" - nil + options[:port] when "https://" options[:port].to_i == 443 ? nil : options[:port] else diff --git a/actionpack/lib/action_dispatch/journey/formatter.rb b/actionpack/lib/action_dispatch/journey/formatter.rb index e288f026c7..aa8d5a4d79 100644 --- a/actionpack/lib/action_dispatch/journey/formatter.rb +++ b/actionpack/lib/action_dispatch/journey/formatter.rb @@ -12,13 +12,17 @@ module ActionDispatch @cache = nil end - def generate(type, name, options, recall = {}, parameterize = nil) + def generate(name, options, recall = {}, parameterize = nil) constraints = recall.merge(options) missing_keys = [] match_route(name, constraints) do |route| parameterized_parts = extract_parameterized_parts(route, options, recall, parameterize) - next if !name && route.requirements.empty? && route.parts.empty? + + # Skip this route unless a name has been provided or it is a + # standard Rails route since we can't determine whether an options + # hash passed to url_for matches a Rack application or a redirect. + next unless name || route.dispatcher? missing_keys = missing_keys(route, parameterized_parts) next unless missing_keys.empty? @@ -29,8 +33,8 @@ module ActionDispatch return [route.format(parameterized_parts), params] end - message = "No route matches #{constraints.inspect}" - message << " missing required keys: #{missing_keys.inspect}" if name + message = "No route matches #{Hash[constraints.sort].inspect}" + message << " missing required keys: #{missing_keys.sort.inspect}" if name raise ActionController::UrlGenerationError, message end @@ -117,9 +121,9 @@ module ActionDispatch def possibles(cache, options, depth = 0) cache.fetch(:___routes) { [] } + options.find_all { |pair| cache.key?(pair) - }.map { |pair| + }.flat_map { |pair| possibles(cache[pair], options, depth + 1) - }.flatten(1) + } end # Returns +true+ if no missing keys are present, otherwise +false+. diff --git a/actionpack/lib/action_dispatch/journey/gtg/builder.rb b/actionpack/lib/action_dispatch/journey/gtg/builder.rb index 7d2791714b..450588cda6 100644 --- a/actionpack/lib/action_dispatch/journey/gtg/builder.rb +++ b/actionpack/lib/action_dispatch/journey/gtg/builder.rb @@ -27,7 +27,7 @@ module ActionDispatch marked[s] = true # mark s s.group_by { |state| symbol(state) }.each do |sym, ps| - u = ps.map { |l| followpos(l) }.flatten + u = ps.flat_map { |l| followpos(l) } next if u.empty? if u.uniq == [DUMMY] @@ -90,7 +90,7 @@ module ActionDispatch firstpos(node.left) end when Nodes::Or - node.children.map { |c| firstpos(c) }.flatten.uniq + node.children.flat_map { |c| firstpos(c) }.uniq when Nodes::Unary firstpos(node.left) when Nodes::Terminal @@ -105,7 +105,7 @@ module ActionDispatch when Nodes::Star firstpos(node.left) when Nodes::Or - node.children.map { |c| lastpos(c) }.flatten.uniq + node.children.flat_map { |c| lastpos(c) }.uniq when Nodes::Cat if nullable?(node.right) lastpos(node.left) | lastpos(node.right) diff --git a/actionpack/lib/action_dispatch/journey/gtg/simulator.rb b/actionpack/lib/action_dispatch/journey/gtg/simulator.rb index 58ad803841..94b0a24344 100644 --- a/actionpack/lib/action_dispatch/journey/gtg/simulator.rb +++ b/actionpack/lib/action_dispatch/journey/gtg/simulator.rb @@ -19,6 +19,14 @@ module ActionDispatch end def simulate(string) + ms = memos(string) { return } + MatchData.new(ms) + end + + alias :=~ :simulate + alias :match :simulate + + def memos(string) input = StringScanner.new(string) state = [0] while sym = input.scan(%r([/.?]|[^/.?]+)) @@ -29,15 +37,10 @@ module ActionDispatch tt.accepting? s } - return if acceptance_states.empty? + return yield if acceptance_states.empty? - memos = acceptance_states.map { |x| tt.memo(x) }.flatten.compact - - MatchData.new(memos) + acceptance_states.flat_map { |x| tt.memo(x) }.compact end - - alias :=~ :simulate - alias :match :simulate end end end diff --git a/actionpack/lib/action_dispatch/journey/gtg/transition_table.rb b/actionpack/lib/action_dispatch/journey/gtg/transition_table.rb index da0cddd93c..990d2127ee 100644 --- a/actionpack/lib/action_dispatch/journey/gtg/transition_table.rb +++ b/actionpack/lib/action_dispatch/journey/gtg/transition_table.rb @@ -9,8 +9,8 @@ module ActionDispatch attr_reader :memos def initialize - @regexp_states = Hash.new { |h,k| h[k] = {} } - @string_states = Hash.new { |h,k| h[k] = {} } + @regexp_states = {} + @string_states = {} @accepting = {} @memos = Hash.new { |h,k| h[k] = [] } end @@ -40,12 +40,22 @@ module ActionDispatch end def move(t, a) - move_string(t, a).concat(move_regexp(t, a)) - end + return [] if t.empty? + + regexps = [] - def to_json - require 'json' + t.map { |s| + if states = @regexp_states[s] + regexps.concat states.map { |re, v| re === a ? v : nil } + end + if states = @string_states[s] + states[a] + end + }.compact.concat regexps + end + + def as_json(options = nil) simple_regexp = Hash.new { |h,k| h[k] = {} } @regexp_states.each do |from, hash| @@ -54,11 +64,11 @@ module ActionDispatch end end - JSON.dump({ + { regexp_states: simple_regexp, string_states: @string_states, accepting: @accepting - }) + } end def to_svg @@ -111,44 +121,35 @@ module ActionDispatch end def []=(from, to, sym) - case sym - when String - @string_states[from][sym] = to - when Regexp - @regexp_states[from][sym] = to - else - raise ArgumentError, 'unknown symbol: %s' % sym.class - end + to_mappings = states_hash_for(sym)[from] ||= {} + to_mappings[sym] = to end def states - ss = @string_states.keys + @string_states.values.map(&:values).flatten - rs = @regexp_states.keys + @regexp_states.values.map(&:values).flatten + ss = @string_states.keys + @string_states.values.flat_map(&:values) + rs = @regexp_states.keys + @regexp_states.values.flat_map(&:values) (ss + rs).uniq end def transitions - @string_states.map { |from, hash| + @string_states.flat_map { |from, hash| hash.map { |s, to| [from, s, to] } - }.flatten(1) + @regexp_states.map { |from, hash| + } + @regexp_states.flat_map { |from, hash| hash.map { |s, to| [from, s, to] } - }.flatten(1) + } end private - def move_regexp(t, a) - return [] if t.empty? - - t.map { |s| - @regexp_states[s].map { |re, v| re === a ? v : nil } - }.flatten.compact.uniq - end - - def move_string(t, a) - return [] if t.empty? - - t.map { |s| @string_states[s][a] }.compact + def states_hash_for(sym) + case sym + when String + @string_states + when Regexp + @regexp_states + else + raise ArgumentError, 'unknown symbol: %s' % sym.class + end end end end diff --git a/actionpack/lib/action_dispatch/journey/nfa/dot.rb b/actionpack/lib/action_dispatch/journey/nfa/dot.rb index 5c33a872e5..47bf76bdbf 100644 --- a/actionpack/lib/action_dispatch/journey/nfa/dot.rb +++ b/actionpack/lib/action_dispatch/journey/nfa/dot.rb @@ -16,9 +16,9 @@ module ActionDispatch # end # " #{n.object_id} [label=\"#{label}\", shape=box];" #} - #memo_edges = memos.map { |k, memos| + #memo_edges = memos.flat_map { |k, memos| # (memos || []).map { |v| " #{k} -> #{v.object_id};" } - #}.flatten.uniq + #}.uniq <<-eodot digraph nfa { diff --git a/actionpack/lib/action_dispatch/journey/nfa/simulator.rb b/actionpack/lib/action_dispatch/journey/nfa/simulator.rb index 5b40da6569..b23270db3c 100644 --- a/actionpack/lib/action_dispatch/journey/nfa/simulator.rb +++ b/actionpack/lib/action_dispatch/journey/nfa/simulator.rb @@ -34,7 +34,7 @@ module ActionDispatch return if acceptance_states.empty? - memos = acceptance_states.map { |x| tt.memo(x) }.flatten.compact + memos = acceptance_states.flat_map { |x| tt.memo(x) }.compact MatchData.new(memos) end diff --git a/actionpack/lib/action_dispatch/journey/nfa/transition_table.rb b/actionpack/lib/action_dispatch/journey/nfa/transition_table.rb index a3017aeea1..66e414213a 100644 --- a/actionpack/lib/action_dispatch/journey/nfa/transition_table.rb +++ b/actionpack/lib/action_dispatch/journey/nfa/transition_table.rb @@ -42,7 +42,7 @@ module ActionDispatch end def states - (@table.keys + @table.values.map(&:keys).flatten).uniq + (@table.keys + @table.values.flat_map(&:keys)).uniq end # Returns a generalized transition graph with reduced states. The states @@ -93,7 +93,7 @@ module ActionDispatch # Returns set of NFA states to which there is a transition on ast symbol # +a+ from some state +s+ in +t+. def following_states(t, a) - Array(t).map { |s| inverted[s][a] }.flatten.uniq + Array(t).flat_map { |s| inverted[s][a] }.uniq end # Returns set of NFA states to which there is a transition on ast symbol @@ -107,7 +107,7 @@ module ActionDispatch end def alphabet - inverted.values.map(&:keys).flatten.compact.uniq.sort_by { |x| x.to_s } + inverted.values.flat_map(&:keys).compact.uniq.sort_by { |x| x.to_s } end # Returns a set of NFA states reachable from some NFA state +s+ in set @@ -131,9 +131,9 @@ module ActionDispatch end def transitions - @table.map { |to, hash| + @table.flat_map { |to, hash| hash.map { |from, sym| [from, sym, to] } - }.flatten(1) + } end private diff --git a/actionpack/lib/action_dispatch/journey/parser.rb b/actionpack/lib/action_dispatch/journey/parser.rb index bb4cbb00e2..d129ba7e16 100644 --- a/actionpack/lib/action_dispatch/journey/parser.rb +++ b/actionpack/lib/action_dispatch/journey/parser.rb @@ -1,6 +1,6 @@ # # DO NOT MODIFY!!!! -# This file is automatically generated by Racc 1.4.9 +# This file is automatically generated by Racc 1.4.11 # from Racc grammer file "". # @@ -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 d37aa1fbe5..28c75618de 100644 --- a/actionpack/lib/action_dispatch/journey/path/pattern.rb +++ b/actionpack/lib/action_dispatch/journey/path/pattern.rb @@ -28,6 +28,11 @@ module ActionDispatch @required_names = nil @re = nil @offsets = nil + @format = Visitors::FormatBuilder.new.accept(spec) + end + + def format_path(path_options) + @format.evaluate path_options end def ast @@ -53,9 +58,9 @@ module ActionDispatch end def optional_names - @optional_names ||= spec.grep(Nodes::Group).map { |group| + @optional_names ||= spec.grep(Nodes::Group).flat_map { |group| group.grep(Nodes::Symbol) - }.flatten.map { |n| n.name }.uniq + }.map { |n| n.name }.uniq end class RegexpOffsets < Journey::Visitors::Visitor # :nodoc: diff --git a/actionpack/lib/action_dispatch/journey/route.rb b/actionpack/lib/action_dispatch/journey/route.rb index 50e1853094..d4df96314f 100644 --- a/actionpack/lib/action_dispatch/journey/route.rb +++ b/actionpack/lib/action_dispatch/journey/route.rb @@ -16,6 +16,14 @@ 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 @@ -68,7 +76,7 @@ module ActionDispatch value.to_s == defaults[key].to_s && !required_parts.include?(key) end - Visitors::Formatter.new(path_options).accept(path.spec) + path.format_path path_options end def optimized_path @@ -93,6 +101,14 @@ module ActionDispatch end end + def glob? + !path.spec.grep(Nodes::Star).empty? + end + + def dispatcher? + @dispatcher + end + def matches?(request) constraints.all? do |method, value| next true unless request.respond_to?(method) diff --git a/actionpack/lib/action_dispatch/journey/router.rb b/actionpack/lib/action_dispatch/journey/router.rb index 419e665d12..36561c71a1 100644 --- a/actionpack/lib/action_dispatch/journey/router.rb +++ b/actionpack/lib/action_dispatch/journey/router.rb @@ -121,8 +121,7 @@ module ActionDispatch def filter_routes(path) return [] unless ast - data = simulator.match(path) - data ? data.memos : [] + simulator.memos(path) { [] } end def find_routes env diff --git a/actionpack/lib/action_dispatch/journey/router/utils.rb b/actionpack/lib/action_dispatch/journey/router/utils.rb index 462f1a122d..ac4ecb1e65 100644 --- a/actionpack/lib/action_dispatch/journey/router/utils.rb +++ b/actionpack/lib/action_dispatch/journey/router/utils.rb @@ -1,5 +1,3 @@ -require 'uri' - module ActionDispatch module Journey # :nodoc: class Router # :nodoc: @@ -7,46 +5,85 @@ module ActionDispatch # Normalizes URI path. # # Strips off trailing slash and ensures there is a leading slash. + # Also converts downcase url encoded string to uppercase. # # normalize_path("/foo") # => "/foo" # normalize_path("/foo/") # => "/foo" # normalize_path("foo") # => "/foo" # normalize_path("") # => "/" + # normalize_path("/%ab") # => "/%AB" def self.normalize_path(path) path = "/#{path}" path.squeeze!('/') path.sub!(%r{/+\Z}, '') + path.gsub!(/(%[a-f0-9]{2})/) { $1.upcase } path = '/' if path == '' path end # URI path and fragment escaping # http://tools.ietf.org/html/rfc3986 - module UriEscape # :nodoc: - # Symbol captures can generate multiple path segments, so include /. - reserved_segment = '/' - reserved_fragment = '/?' - reserved_pchar = ':@&=+$,;%' - - safe_pchar = "#{URI::REGEXP::PATTERN::UNRESERVED}#{reserved_pchar}" - safe_segment = "#{safe_pchar}#{reserved_segment}" - safe_fragment = "#{safe_pchar}#{reserved_fragment}" - UNSAFE_SEGMENT = Regexp.new("[^#{safe_segment}]", false).freeze - UNSAFE_FRAGMENT = Regexp.new("[^#{safe_fragment}]", false).freeze + 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) } + + ALPHA = "a-zA-Z".freeze + DIGIT = "0-9".freeze + UNRESERVED = "#{ALPHA}#{DIGIT}\\-\\._~".freeze + SUB_DELIMS = "!\\$&'\\(\\)\\*\\+,;=".freeze + + ESCAPED = /%[a-zA-Z0-9]{2}/.freeze + + FRAGMENT = /[^#{UNRESERVED}#{SUB_DELIMS}:@\/\?]/.freeze + SEGMENT = /[^#{UNRESERVED}#{SUB_DELIMS}:@]/.freeze + PATH = /[^#{UNRESERVED}#{SUB_DELIMS}:@\/]/.freeze + + def escape_fragment(fragment) + escape(fragment, FRAGMENT) + end + + def escape_path(path) + escape(path, PATH) + end + + def escape_segment(segment) + escape(segment, SEGMENT) + end + + def unescape_uri(uri) + uri.gsub(ESCAPED) { [$&[1, 2].hex].pack('C') }.force_encoding(uri.encoding) + end + + protected + def escape(component, pattern) + component.gsub(pattern){ |unsafe| percent_encode(unsafe) }.force_encoding(ENCODING) + end + + def percent_encode(unsafe) + safe = EMPTY.dup + unsafe.each_byte { |b| safe << DEC2HEX[b] } + safe + end end - Parser = URI.const_defined?(:Parser) ? URI::Parser.new : URI + ENCODER = UriEncoder.new def self.escape_path(path) - Parser.escape(path.to_s, UriEscape::UNSAFE_SEGMENT) + ENCODER.escape_path(path.to_s) + end + + def self.escape_segment(segment) + ENCODER.escape_segment(segment.to_s) end def self.escape_fragment(fragment) - Parser.escape(fragment.to_s, UriEscape::UNSAFE_FRAGMENT) + ENCODER.escape_fragment(fragment.to_s) end def self.unescape_uri(uri) - Parser.unescape(uri) + ENCODER.unescape_uri(uri) end end end diff --git a/actionpack/lib/action_dispatch/journey/routes.rb b/actionpack/lib/action_dispatch/journey/routes.rb index a99d6d0d6a..80e3818ccd 100644 --- a/actionpack/lib/action_dispatch/journey/routes.rb +++ b/actionpack/lib/action_dispatch/journey/routes.rb @@ -30,6 +30,7 @@ module ActionDispatch def clear routes.clear + named_routes.clear end def partitioned_routes diff --git a/actionpack/lib/action_dispatch/journey/visitors.rb b/actionpack/lib/action_dispatch/journey/visitors.rb index 2964d80d9f..b635576b6a 100644 --- a/actionpack/lib/action_dispatch/journey/visitors.rb +++ b/actionpack/lib/action_dispatch/journey/visitors.rb @@ -1,11 +1,56 @@ # encoding: utf-8 + 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.fetch(param.name) { return ''.freeze } + 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 = Hash.new { |h,k| - h[k] = "visit_#{k}" - } + DISPATCH_CACHE = {} def accept(node) visit(node) @@ -35,11 +80,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 @@ -49,8 +124,8 @@ module ActionDispatch end def visit(node) - super block.call(node) + super end end @@ -74,55 +149,31 @@ module ActionDispatch end end - class OptimizedPath < String # :nodoc: - private - - def visit_GROUP(node) - "" - end - end - - # Used for formatting urls (url_for) - class Formatter < Visitor # :nodoc: - attr_reader :options, :consumed - - def initialize(options) - @options = options - @consumed = {} + class OptimizedPath < Visitor # :nodoc: + def accept(node) + Array(visit(node)) end private - def visit_GROUP(node) - if consumed == options - nil - else - route = visit(node.left) - route.include?("\0") ? nil : route - end + def visit_CAT(node) + [visit(node.left), visit(node.right)].flatten end - def terminal(node) - node.left + def visit_SYMBOL(node) + node.left[1..-1].to_sym end - def binary(node) - [visit(node.left), visit(node.right)].join + def visit_STAR(node) + visit(node.left) end - def nary(node) - node.children.map { |c| visit(c) }.join + def visit_GROUP(node) + [] end - def visit_SYMBOL(node) - key = node.to_sym - - if value = options[key] - consumed[key] = value - Router::Utils.escape_path(value) - else - "\0" - end + %w{ LITERAL SLASH DOT }.each do |t| + class_eval %{ def visit_#{t}(n); n.left; end }, __FILE__, __LINE__ end end diff --git a/actionpack/lib/action_dispatch/middleware/callbacks.rb b/actionpack/lib/action_dispatch/middleware/callbacks.rb index 852f1cf6f5..baf9d5779e 100644 --- a/actionpack/lib/action_dispatch/middleware/callbacks.rb +++ b/actionpack/lib/action_dispatch/middleware/callbacks.rb @@ -8,14 +8,14 @@ module ActionDispatch class << self delegate :to_prepare, :to_cleanup, :to => "ActionDispatch::Reloader" - end - def self.before(*args, &block) - set_callback(:call, :before, *args, &block) - end + def before(*args, &block) + set_callback(:call, :before, *args, &block) + end - def self.after(*args, &block) - set_callback(:call, :after, *args, &block) + def after(*args, &block) + set_callback(:call, :after, *args, &block) + end end def initialize(app) diff --git a/actionpack/lib/action_dispatch/middleware/cookies.rb b/actionpack/lib/action_dispatch/middleware/cookies.rb index 5b914f293d..22b16b628d 100644 --- a/actionpack/lib/action_dispatch/middleware/cookies.rb +++ b/actionpack/lib/action_dispatch/middleware/cookies.rb @@ -23,15 +23,15 @@ module ActionDispatch # # This cookie will be deleted when the user's browser is closed. # cookies[:user_name] = "david" # - # # Assign an array of values to a cookie. - # cookies[:lat_lon] = [47.68, -122.37] + # # Cookie values are String based. Other data types need to be serialized. + # cookies[:lat_lon] = JSON.generate([47.68, -122.37]) # # # Sets a cookie that expires in 1 hour. # cookies[:login] = { value: "XJ-122", expires: 1.hour.from_now } # # # Sets a signed cookie, which prevents users from tampering with its value. - # # The cookie is signed by your app's <tt>config.secret_key_base</tt> value. - # # It can be read using the signed method <tt>cookies.signed[:name]</tt> + # # The cookie is signed by your app's `secrets.secret_key_base` value. + # # It can be read using the signed method `cookies.signed[:name]` # cookies.signed[:user_id] = current_user.id # # # Sets a "permanent" cookie (which expires in 20 years from now). @@ -42,10 +42,10 @@ module ActionDispatch # # Examples of reading: # - # cookies[:user_name] # => "david" - # cookies.size # => 2 - # cookies[:lat_lon] # => [47.68, -122.37] - # cookies.signed[:login] # => "XJ-122" + # cookies[:user_name] # => "david" + # cookies.size # => 2 + # JSON.parse(cookies[:lat_lon]) # => [47.68, -122.37] + # cookies.signed[:login] # => "XJ-122" # # Example for deleting: # @@ -63,7 +63,7 @@ module ActionDispatch # # The option symbols for setting cookies are: # - # * <tt>:value</tt> - The cookie's value or list of values (as an array). + # * <tt>:value</tt> - The cookie's value. # * <tt>:path</tt> - The path for which this cookie applies. Defaults to the root # of the application. # * <tt>:domain</tt> - The domain for which this cookie applies so you can @@ -74,10 +74,10 @@ module ActionDispatch # # domain: nil # Does not sets cookie domain. (default) # domain: :all # Allow the cookie for the top most level - # domain and subdomains. + # # domain and subdomains. # # * <tt>:expires</tt> - The time at which this cookie expires, as a \Time object. - # * <tt>:secure</tt> - Whether this cookie is a only transmitted to HTTPS servers. + # * <tt>:secure</tt> - Whether this cookie is only transmitted to HTTPS servers. # Default is +false+. # * <tt>:httponly</tt> - Whether this cookie is accessible via scripting or # only HTTP. Defaults to +false+. @@ -89,6 +89,7 @@ module ActionDispatch ENCRYPTED_SIGNED_COOKIE_SALT = "action_dispatch.encrypted_signed_cookie_salt".freeze SECRET_TOKEN = "action_dispatch.secret_token".freeze SECRET_KEY_BASE = "action_dispatch.secret_key_base".freeze + COOKIES_SERIALIZER = "action_dispatch.cookies_serializer".freeze # Cookies can typically store 4096 bytes. MAX_COOKIE_SIZE = 4096 @@ -117,10 +118,10 @@ module ActionDispatch # the cookie again. This is useful for creating cookies with values that the user is not supposed to change. If a signed # cookie was tampered with by the user (or a 3rd party), nil will be returned. # - # If +config.secret_key_base+ and +config.secret_token+ (deprecated) are both set, + # If +secrets.secret_key_base+ and +config.secret_token+ (deprecated) are both set, # legacy cookies signed with the old key generator will be transparently upgraded. # - # This jar requires that you set a suitable secret for the verification on your app's +config.secret_key_base+. + # This jar requires that you set a suitable secret for the verification on your app's +secrets.secret_key_base+. # # Example: # @@ -140,10 +141,10 @@ module ActionDispatch # Returns a jar that'll automatically encrypt cookie values before sending them to the client and will decrypt them for read. # If the cookie was tampered with by the user (or a 3rd party), nil will be returned. # - # If +config.secret_key_base+ and +config.secret_token+ (deprecated) are both set, + # If +secrets.secret_key_base+ and +config.secret_token+ (deprecated) are both set, # legacy cookies signed with the old key generator will be transparently upgraded. # - # This jar requires that you set a suitable secret for the verification on your app's +config.secret_key_base+. + # This jar requires that you set a suitable secret for the verification on your app's +secrets.secret_key_base+. # # Example: # @@ -160,7 +161,7 @@ module ActionDispatch end end - # Returns the +signed+ or +encrypted jar, preferring +encrypted+ if +secret_key_base+ is set. + # Returns the +signed+ or +encrypted+ jar, preferring +encrypted+ if +secret_key_base+ is set. # Used by ActionDispatch::Session::CookieStore to avoid the need to introduce new cookie stores. def signed_or_encrypted @signed_or_encrypted ||= @@ -175,12 +176,12 @@ module ActionDispatch module VerifyAndUpgradeLegacySignedMessage def initialize(*args) super - @legacy_verifier = ActiveSupport::MessageVerifier.new(@options[:secret_token]) + @legacy_verifier = ActiveSupport::MessageVerifier.new(@options[:secret_token], serializer: NullSerializer) end def verify_and_upgrade_legacy_signed_message(name, signed_message) - @legacy_verifier.verify(signed_message).tap do |value| - self[name] = value + deserialize(name, @legacy_verifier.verify(signed_message)).tap do |value| + self[name] = { value: value } end rescue ActiveSupport::MessageVerifier::InvalidSignature nil @@ -210,7 +211,8 @@ module ActionDispatch encrypted_signed_cookie_salt: env[ENCRYPTED_SIGNED_COOKIE_SALT] || '', secret_token: env[SECRET_TOKEN], secret_key_base: env[SECRET_KEY_BASE], - upgrade_legacy_signed_cookies: env[SECRET_TOKEN].present? && env[SECRET_KEY_BASE].present? + upgrade_legacy_signed_cookies: env[SECRET_TOKEN].present? && env[SECRET_KEY_BASE].present?, + serializer: env[COOKIES_SERIALIZER] } end @@ -235,6 +237,15 @@ module ActionDispatch @secure = secure @options = options @cookies = {} + @committed = false + end + + def committed?; @committed; end + + def commit! + @committed = true + @set_cookies.freeze + @delete_cookies.freeze end def each(&block) @@ -334,8 +345,8 @@ module ActionDispatch end def recycle! #:nodoc: - @set_cookies.clear - @delete_cookies.clear + @set_cookies = {} + @delete_cookies = {} end mattr_accessor :always_write_cookie @@ -372,28 +383,89 @@ module ActionDispatch end end + class JsonSerializer + def self.load(value) + JSON.parse(value, quirks_mode: true) + end + + def self.dump(value) + JSON.generate(value, quirks_mode: true) + end + end + + # Passing the NullSerializer downstream to the Message{Encryptor,Verifier} + # allows us to handle the (de)serialization step within the cookie jar, + # which gives us the opportunity to detect and migrate legacy cookies. + class NullSerializer + def self.load(value) + value + end + + def self.dump(value) + value + end + end + + module SerializedCookieJars + MARSHAL_SIGNATURE = "\x04\x08".freeze + + protected + def needs_migration?(value) + @options[:serializer] == :hybrid && value.start_with?(MARSHAL_SIGNATURE) + end + + def serialize(name, value) + serializer.dump(value) + end + + def deserialize(name, value) + if value + if needs_migration?(value) + Marshal.load(value).tap do |v| + self[name] = { value: v } + end + else + serializer.load(value) + end + end + end + + def serializer + serializer = @options[:serializer] || :marshal + case serializer + when :marshal + Marshal + when :json, :hybrid + JsonSerializer + else + serializer + end + end + end + class SignedCookieJar #:nodoc: include ChainedCookieJars + include SerializedCookieJars def initialize(parent_jar, key_generator, options = {}) @parent_jar = parent_jar @options = options secret = key_generator.generate_key(@options[:signed_cookie_salt]) - @verifier = ActiveSupport::MessageVerifier.new(secret) + @verifier = ActiveSupport::MessageVerifier.new(secret, serializer: NullSerializer) end def [](name) if signed_message = @parent_jar[name] - verify(signed_message) + deserialize name, verify(signed_message) end end def []=(name, options) if options.is_a?(Hash) options.symbolize_keys! - options[:value] = @verifier.generate(options[:value]) + options[:value] = @verifier.generate(serialize(name, options[:value])) else - options = { :value => @verifier.generate(options) } + options = { :value => @verifier.generate(serialize(name, options)) } end raise CookieOverflow if options[:value].size > MAX_COOKIE_SIZE @@ -409,7 +481,7 @@ module ActionDispatch end # UpgradeLegacySignedCookieJar is used instead of SignedCookieJar if - # config.secret_token and config.secret_key_base are both set. It reads + # config.secret_token and secrets.secret_key_base are both set. It reads # legacy cookies signed with the old dummy key generator and re-saves # them using the new key generator to provide a smooth upgrade path. class UpgradeLegacySignedCookieJar < SignedCookieJar #:nodoc: @@ -417,17 +489,18 @@ module ActionDispatch def [](name) if signed_message = @parent_jar[name] - verify(signed_message) || verify_and_upgrade_legacy_signed_message(name, signed_message) + deserialize(name, verify(signed_message)) || verify_and_upgrade_legacy_signed_message(name, signed_message) end end end class EncryptedCookieJar #:nodoc: include ChainedCookieJars + include SerializedCookieJars def initialize(parent_jar, key_generator, options = {}) if ActiveSupport::LegacyKeyGenerator === key_generator - raise "You didn't set config.secret_key_base, which is required for this cookie jar. " + + raise "You didn't set secrets.secret_key_base, which is required for this cookie jar. " + "Read the upgrade documentation to learn more about this new config option." end @@ -435,12 +508,12 @@ module ActionDispatch @options = options secret = key_generator.generate_key(@options[:encrypted_cookie_salt]) sign_secret = key_generator.generate_key(@options[:encrypted_signed_cookie_salt]) - @encryptor = ActiveSupport::MessageEncryptor.new(secret, sign_secret) + @encryptor = ActiveSupport::MessageEncryptor.new(secret, sign_secret, serializer: NullSerializer) end def [](name) if encrypted_message = @parent_jar[name] - decrypt_and_verify(encrypted_message) + deserialize name, decrypt_and_verify(encrypted_message) end end @@ -450,7 +523,8 @@ module ActionDispatch else options = { :value => options } end - options[:value] = @encryptor.encrypt_and_sign(options[:value]) + + options[:value] = @encryptor.encrypt_and_sign(serialize(name, options[:value])) raise CookieOverflow if options[:value].size > MAX_COOKIE_SIZE @parent_jar[name] = options @@ -465,7 +539,7 @@ module ActionDispatch end # UpgradeLegacyEncryptedCookieJar is used by ActionDispatch::Session::CookieStore - # instead of EncryptedCookieJar if config.secret_token and config.secret_key_base + # instead of EncryptedCookieJar if config.secret_token and secrets.secret_key_base # are both set. It reads legacy cookies signed with the old dummy key generator and # encrypts and re-saves them using the new key generator to provide a smooth upgrade path. class UpgradeLegacyEncryptedCookieJar < EncryptedCookieJar #:nodoc: @@ -473,7 +547,7 @@ module ActionDispatch def [](name) if encrypted_or_signed_message = @parent_jar[name] - decrypt_and_verify(encrypted_or_signed_message) || verify_and_upgrade_legacy_signed_message(name, encrypted_or_signed_message) + deserialize(name, decrypt_and_verify(encrypted_or_signed_message)) || verify_and_upgrade_legacy_signed_message(name, encrypted_or_signed_message) end end end @@ -486,9 +560,11 @@ module ActionDispatch status, headers, body = @app.call(env) if cookie_jar = env['action_dispatch.cookies'] - cookie_jar.write(headers) - if headers[HTTP_HEADER].respond_to?(:join) - headers[HTTP_HEADER] = headers[HTTP_HEADER].join("\n") + unless cookie_jar.committed? + cookie_jar.write(headers) + if headers[HTTP_HEADER].respond_to?(:join) + headers[HTTP_HEADER] = headers[HTTP_HEADER].join("\n") + end end end diff --git a/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb b/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb index 64230ff1ae..0ca1a87645 100644 --- a/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb +++ b/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb @@ -34,27 +34,35 @@ module ActionDispatch log_error(env, wrapper) if env['action_dispatch.show_detailed_exceptions'] + request = Request.new(env) template = ActionView::Base.new([RESCUES_TEMPLATE_PATH], - :request => Request.new(env), - :exception => wrapper.exception, - :application_trace => wrapper.application_trace, - :framework_trace => wrapper.framework_trace, - :full_trace => wrapper.full_trace, - :routes_inspector => routes_inspector(exception), - :source_extract => wrapper.source_extract, - :line_number => wrapper.line_number, - :file => wrapper.file + request: request, + exception: wrapper.exception, + application_trace: wrapper.application_trace, + framework_trace: wrapper.framework_trace, + full_trace: wrapper.full_trace, + routes_inspector: routes_inspector(exception), + source_extract: wrapper.source_extract, + line_number: wrapper.line_number, + file: wrapper.file ) file = "rescues/#{wrapper.rescue_template}" - body = template.render(:template => file, :layout => 'rescues/layout') - render(wrapper.status_code, body) + + if request.xhr? + body = template.render(template: file, layout: false, formats: [:text]) + format = "text/plain" + else + body = template.render(template: file, layout: 'rescues/layout') + format = "text/html" + end + render(wrapper.status_code, body, format) else raise exception end end - def render(status, body) - [status, {'Content-Type' => "text/html; charset=#{Response.default_charset}", 'Content-Length' => body.bytesize.to_s}, [body]] + def render(status, body, format) + [status, {'Content-Type' => "#{format}; charset=#{Response.default_charset}", 'Content-Length' => body.bytesize.to_s}, [body]] end def log_error(env, wrapper) diff --git a/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb b/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb index 1de3d14530..2326bb043a 100644 --- a/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb +++ b/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb @@ -1,5 +1,5 @@ require 'action_controller/metal/exceptions' -require 'active_support/core_ext/class/attribute_accessors' +require 'active_support/core_ext/module/attribute_accessors' module ActionDispatch class ExceptionWrapper @@ -32,6 +32,8 @@ module ActionDispatch def initialize(env, exception) @env = env @exception = original_exception(exception) + + expand_backtrace if exception.is_a?(SyntaxError) || exception.try(:original_exception).try(:is_a?, SyntaxError) end def rescue_template @@ -96,7 +98,7 @@ module ActionDispatch def source_fragment(path, line) return unless Rails.respond_to?(:root) && Rails.root full_path = Rails.root.join(path) - if File.exists?(full_path) + if File.exist?(full_path) File.open(full_path, "r") do |file| start = [line - 3, 0].max lines = file.each_line.drop(start).take(6) @@ -104,5 +106,11 @@ module ActionDispatch end end end + + def expand_backtrace + @exception.backtrace.unshift( + @exception.to_s.split("\n") + ).flatten! + end end end diff --git a/actionpack/lib/action_dispatch/middleware/flash.rb b/actionpack/lib/action_dispatch/middleware/flash.rb index 89003e7a5e..4821d2a899 100644 --- a/actionpack/lib/action_dispatch/middleware/flash.rb +++ b/actionpack/lib/action_dispatch/middleware/flash.rb @@ -1,3 +1,5 @@ +require 'active_support/core_ext/hash/keys' + module ActionDispatch class Request < Rack::Request # Access the contents of the flash. Use <tt>flash["notice"]</tt> to @@ -50,13 +52,14 @@ module ActionDispatch end def []=(k, v) + k = k.to_s @flash[k] = v @flash.discard(k) v end def [](k) - @flash[k] + @flash[k.to_s] end # Convenience accessor for <tt>flash.now[:alert]=</tt>. @@ -92,8 +95,8 @@ module ActionDispatch end def initialize(flashes = {}, discard = []) #:nodoc: - @discard = Set.new(discard) - @flashes = flashes + @discard = Set.new(stringify_array(discard)) + @flashes = flashes.stringify_keys @now = nil end @@ -106,17 +109,18 @@ module ActionDispatch end def []=(k, v) + k = k.to_s @discard.delete k @flashes[k] = v end def [](k) - @flashes[k] + @flashes[k.to_s] end def update(h) #:nodoc: - @discard.subtract h.keys - @flashes.update h + @discard.subtract stringify_array(h.keys) + @flashes.update h.stringify_keys self end @@ -129,6 +133,7 @@ module ActionDispatch end def delete(key) + key = key.to_s @discard.delete key @flashes.delete key self @@ -155,7 +160,7 @@ module ActionDispatch def replace(h) #:nodoc: @discard.clear - @flashes.replace h + @flashes.replace h.stringify_keys self end @@ -186,6 +191,7 @@ module ActionDispatch # flash.keep # keeps the entire flash # flash.keep(:notice) # keeps only the "notice" entry, the rest of the flash is discarded def keep(k = nil) + k = k.to_s if k @discard.subtract Array(k || keys) k ? self[k] : self end @@ -195,6 +201,7 @@ module ActionDispatch # flash.discard # discard the entire flash at the end of the current action # flash.discard(:warning) # discard only the "warning" entry at the end of the current action def discard(k = nil) + k = k.to_s if k @discard.merge Array(k || keys) k ? self[k] : self end @@ -231,6 +238,12 @@ module ActionDispatch def now_is_loaded? @now end + + def stringify_array(array) + array.map do |item| + item.kind_of?(Symbol) ? item.to_s : item + end + end end def initialize(app) diff --git a/actionpack/lib/action_dispatch/middleware/params_parser.rb b/actionpack/lib/action_dispatch/middleware/params_parser.rb index fb70b60ef6..b426183488 100644 --- a/actionpack/lib/action_dispatch/middleware/params_parser.rb +++ b/actionpack/lib/action_dispatch/middleware/params_parser.rb @@ -41,7 +41,7 @@ module ActionDispatch when Proc strategy.call(request.raw_post) when :json - data = ActiveSupport::JSON.decode(request.body) + data = ActiveSupport::JSON.decode(request.raw_post) data = {:_json => data} unless data.is_a?(Hash) Request::Utils.deep_munge(data).with_indifferent_access else diff --git a/actionpack/lib/action_dispatch/middleware/reloader.rb b/actionpack/lib/action_dispatch/middleware/reloader.rb index 2f6968eb2e..15b5a48535 100644 --- a/actionpack/lib/action_dispatch/middleware/reloader.rb +++ b/actionpack/lib/action_dispatch/middleware/reloader.rb @@ -1,3 +1,5 @@ +require 'active_support/deprecation/reporting' + module ActionDispatch # ActionDispatch::Reloader provides prepare and cleanup callbacks, # intended to assist with code reloading during development. @@ -25,19 +27,26 @@ module ActionDispatch # class Reloader include ActiveSupport::Callbacks + include ActiveSupport::Deprecation::Reporting - define_callbacks :prepare, :scope => :name - define_callbacks :cleanup, :scope => :name + define_callbacks :prepare + define_callbacks :cleanup # Add a prepare callback. Prepare callbacks are run before each request, prior # to ActionDispatch::Callback's before callbacks. def self.to_prepare(*args, &block) + unless block_given? + warn "to_prepare without a block is deprecated. Please use a block" + end set_callback(:prepare, *args, &block) end # Add a cleanup callback. Cleanup callbacks are run after each request is # complete (after #close is called on the response body). def self.to_cleanup(*args, &block) + unless block_given? + warn "to_cleanup without a block is deprecated. Please use a block" + end set_callback(:cleanup, *args, &block) end diff --git a/actionpack/lib/action_dispatch/middleware/remote_ip.rb b/actionpack/lib/action_dispatch/middleware/remote_ip.rb index 8879291dbd..cbb066b092 100644 --- a/actionpack/lib/action_dispatch/middleware/remote_ip.rb +++ b/actionpack/lib/action_dispatch/middleware/remote_ip.rb @@ -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 @@ -47,12 +47,12 @@ module ActionDispatch # clients (like WAP devices), or behind proxies that set headers in an # incorrect or confusing way (like AWS ELB). # - # The +custom_trusted+ argument can take a regex, which will be used + # The +custom_proxies+ argument can take a regex, which will be used # instead of +TRUSTED_PROXIES+, or a string, which will be used in addition # to +TRUSTED_PROXIES+. Any proxy setup will put the value you want in the # middle (or at the beginning) of the X-Forwarded-For list, with your proxy # servers after it. If your proxies aren't removed, pass them in via the - # +custom_trusted+ parameter. That way, the middleware will ignore those + # +custom_proxies+ parameter. That way, the middleware will ignore those # IP addresses, and return the one that you want. def initialize(app, check_ip_spoofing = true, custom_proxies = nil) @app = app @@ -143,7 +143,7 @@ module ActionDispatch # proxies with incompatible IP header conventions, and there is no way # for us to determine which header is the right one after the fact. # Since we have no idea, we give up and explode. - should_check_ip = @check_ip && client_ips.last + should_check_ip = @check_ip && client_ips.last && forwarded_ips.last if should_check_ip && !forwarded_ips.include?(client_ips.last) # We don't know which came from the proxy, and which from the user raise IpSpoofAttackError, "IP spoofing attack?! " + diff --git a/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb b/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb index b9eb8036e9..0864e7ef2a 100644 --- a/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb +++ b/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb @@ -15,8 +15,8 @@ module ActionDispatch # best possible option given your application's configuration. # # If you only have secret_token set, your cookies will be signed, but - # not encrypted. This means a user cannot alter his +user_id+ without - # knowing your app's secret key, but can easily read his +user_id+. This + # not encrypted. This means a user cannot alter their +user_id+ without + # knowing your app's secret key, but can easily read their +user_id+. This # was the default for Rails 3 apps. # # If you have secret_key_base set, your cookies will be encrypted. This @@ -29,11 +29,12 @@ module ActionDispatch # # Configure your session store in config/initializers/session_store.rb: # - # Myapp::Application.config.session_store :cookie_store, key: '_your_app_session' + # Rails.application.config.session_store :cookie_store, key: '_your_app_session' # - # Configure your secret key in config/initializers/secret_token.rb: + # Configure your secret key in config/secrets.yml: # - # Myapp::Application.config.secret_key_base 'secret key' + # development: + # secret_key_base: 'secret key' # # To generate a secret key for an existing application, run `rake secret`. # @@ -50,7 +51,7 @@ module ActionDispatch # decode signed cookies generated by your app in external applications or # Javascript before upgrading. # - # Note that changing digest or secret invalidates all existing sessions! + # Note that changing the secret key will invalidate all existing sessions! class CookieStore < Rack::Session::Abstract::ID include Compatibility include StaleSessionCheck diff --git a/actionpack/lib/action_dispatch/middleware/show_exceptions.rb b/actionpack/lib/action_dispatch/middleware/show_exceptions.rb index fcc5bc12c4..1d4f0f89a6 100644 --- a/actionpack/lib/action_dispatch/middleware/show_exceptions.rb +++ b/actionpack/lib/action_dispatch/middleware/show_exceptions.rb @@ -29,8 +29,11 @@ module ActionDispatch def call(env) @app.call(env) rescue Exception => exception - raise exception if env['action_dispatch.show_exceptions'] == false - render_exception(env, exception) + if env['action_dispatch.show_exceptions'] == false + raise exception + else + render_exception(env, exception) + end end private diff --git a/actionpack/lib/action_dispatch/middleware/ssl.rb b/actionpack/lib/action_dispatch/middleware/ssl.rb index 9e03cbf2b7..0c7caef25d 100644 --- a/actionpack/lib/action_dispatch/middleware/ssl.rb +++ b/actionpack/lib/action_dispatch/middleware/ssl.rb @@ -32,12 +32,14 @@ module ActionDispatch private def redirect_to_https(request) - url = URI(request.url) - url.scheme = "https" - url.host = @host if @host - url.port = @port if @port - headers = hsts_headers.merge('Content-Type' => 'text/html', - 'Location' => url.to_s) + host = @host || request.host + port = @port || request.port + + location = "https://#{host}" + location << ":#{port}" if port != 80 + location << request.fullpath + + headers = { 'Content-Type' => 'text/html', 'Location' => location } [301, headers, []] end @@ -58,7 +60,7 @@ module ActionDispatch cookies = cookies.split("\n") headers['Set-Cookie'] = cookies.map { |cookie| - if cookie !~ /;\s+secure(;|$)/ + if cookie !~ /;\s*secure\s*(;|$)/i "#{cookie}; secure" else cookie diff --git a/actionpack/lib/action_dispatch/middleware/static.rb b/actionpack/lib/action_dispatch/middleware/static.rb index c6a7d9c415..2764584fe9 100644 --- a/actionpack/lib/action_dispatch/middleware/static.rb +++ b/actionpack/lib/action_dispatch/middleware/static.rb @@ -11,9 +11,10 @@ module ActionDispatch end def match?(path) - path = path.dup + path = unescape_path(path) + return false unless path.valid_encoding? - full_path = path.empty? ? @root : File.join(@root, escape_glob_chars(unescape_path(path))) + full_path = path.empty? ? @root : File.join(@root, escape_glob_chars(path)) paths = "#{full_path}#{ext}" matches = Dir[paths] @@ -40,7 +41,6 @@ module ActionDispatch end def escape_glob_chars(path) - path.force_encoding('binary') if path.respond_to? :force_encoding path.gsub(/[*?{}\[\]]/, "\\\\\\&") end end diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/_request_and_response.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/_request_and_response.html.erb index db219c8fa9..db219c8fa9 100644 --- a/actionpack/lib/action_dispatch/middleware/templates/rescues/_request_and_response.erb +++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/_request_and_response.html.erb diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/_request_and_response.text.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/_request_and_response.text.erb new file mode 100644 index 0000000000..396768ecee --- /dev/null +++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/_request_and_response.text.erb @@ -0,0 +1,23 @@ +<% + clean_params = @request.filtered_parameters.clone + clean_params.delete("action") + clean_params.delete("controller") + + request_dump = clean_params.empty? ? 'None' : clean_params.inspect.gsub(',', ",\n") + + def debug_hash(object) + object.to_hash.sort_by { |k, _| k.to_s }.map { |k, v| "#{k}: #{v.inspect rescue $!.message}" }.join("\n") + end unless self.class.method_defined?(:debug_hash) +%> + +Request parameters +<%= request_dump %> + +Session dump +<%= debug_hash @request.session %> + +Env dump +<%= debug_hash @request.env.slice(*@request.class::ENV_METHODS) %> + +Response headers +<%= defined?(@response) ? @response.headers.inspect.gsub(',', ",\n") : 'None' %> diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb index b181909bff..b181909bff 100644 --- a/actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.erb +++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.text.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.text.erb new file mode 100644 index 0000000000..d4af5c9b06 --- /dev/null +++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.text.erb @@ -0,0 +1,15 @@ +<% + traces = { "Application Trace" => @application_trace, + "Framework Trace" => @framework_trace, + "Full Trace" => @full_trace } +%> + +Rails.root: <%= defined?(Rails) && Rails.respond_to?(:root) ? Rails.root : "unset" %> + +<% traces.each do |name, trace| %> +<% if trace.any? %> +<%= name %> +<%= trace.join("\n") %> + +<% end %> +<% end %> diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/diagnostics.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb index 57a2940802..f154021ae6 100644 --- a/actionpack/lib/action_dispatch/middleware/templates/rescues/diagnostics.erb +++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb @@ -8,7 +8,7 @@ </header> <div id="container"> - <h2><%= @exception.message %></h2> + <h2><%= h @exception.message %></h2> <%= render template: "rescues/_source" %> <%= render template: "rescues/_trace" %> diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/diagnostics.text.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/diagnostics.text.erb new file mode 100644 index 0000000000..603de54b8b --- /dev/null +++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/diagnostics.text.erb @@ -0,0 +1,9 @@ +<%= @exception.class.to_s %><% + if @request.parameters['controller'] +%> in <%= @request.parameters['controller'].camelize %>Controller<% if @request.parameters['action'] %>#<%= @request.parameters['action'] %><% end %> +<% end %> + +<%= @exception.message %> +<%= render template: "rescues/_source" %> +<%= render template: "rescues/_trace" %> +<%= render template: "rescues/_request_and_response" %> diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/missing_template.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/missing_template.html.erb index ca14215946..5c016e544e 100644 --- a/actionpack/lib/action_dispatch/middleware/templates/rescues/missing_template.erb +++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/missing_template.html.erb @@ -3,5 +3,5 @@ </header> <div id="container"> - <h2><%= @exception.message %></h2> + <h2><%= h @exception.message %></h2> </div> diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/missing_template.text.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/missing_template.text.erb new file mode 100644 index 0000000000..ae62d9eb02 --- /dev/null +++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/missing_template.text.erb @@ -0,0 +1,3 @@ +Template is missing + +<%= @exception.message %> diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/routing_error.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/routing_error.html.erb index cd3daff065..7e9cedb95e 100644 --- a/actionpack/lib/action_dispatch/middleware/templates/rescues/routing_error.erb +++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/routing_error.html.erb @@ -2,7 +2,7 @@ <h1>Routing Error</h1> </header> <div id="container"> - <h2><%= @exception.message %></h2> + <h2><%= h @exception.message %></h2> <% unless @exception.failures.empty? %> <p> <h2>Failure reasons:</h2> diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/routing_error.text.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/routing_error.text.erb new file mode 100644 index 0000000000..f6e4dac1f3 --- /dev/null +++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/routing_error.text.erb @@ -0,0 +1,11 @@ +Routing Error + +<%= @exception.message %> +<% unless @exception.failures.empty? %> +Failure reasons: +<% @exception.failures.each do |route, reason| %> + - <%= route.inspect.delete('\\') %></code> failed because <%= reason.downcase %> +<% end %> +<% end %> + +<%= render template: "rescues/_trace", format: :text %> diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb index 31f46ee340..027a0f5b3e 100644 --- a/actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.erb +++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb @@ -10,7 +10,7 @@ <p> Showing <i><%= @exception.file_name %></i> where line <b>#<%= @exception.line_number %></b> raised: </p> - <pre><code><%= @exception.message %></code></pre> + <pre><code><%= h @exception.message %></code></pre> <div class="source"> <div class="info"> diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.text.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.text.erb new file mode 100644 index 0000000000..5da21d9784 --- /dev/null +++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.text.erb @@ -0,0 +1,8 @@ +<% @source_extract = @exception.source_extract(0, :html) %> +<%= @exception.original_exception.class.to_s %> in <%= @request.parameters["controller"].camelize if @request.parameters["controller"] %>#<%= @request.parameters["action"] %> + +Showing <%= @exception.file_name %> where line #<%= @exception.line_number %> raised: +<%= @exception.message %> +<%= @exception.sub_template_message %> +<%= render template: "rescues/_trace", format: :text %> +<%= render template: "rescues/_request_and_response", format: :text %> diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/unknown_action.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/unknown_action.html.erb index c1fbf67eed..259fb2bb3b 100644 --- a/actionpack/lib/action_dispatch/middleware/templates/rescues/unknown_action.erb +++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/unknown_action.html.erb @@ -2,5 +2,5 @@ <h1>Unknown action</h1> </header> <div id="container"> - <h2><%= @exception.message %></h2> + <h2><%= h @exception.message %></h2> </div> diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/unknown_action.text.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/unknown_action.text.erb new file mode 100644 index 0000000000..83973addcb --- /dev/null +++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/unknown_action.text.erb @@ -0,0 +1,3 @@ +Unknown action + +<%= @exception.message %> 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 95461fa693..cce0d75af4 100644 --- a/actionpack/lib/action_dispatch/middleware/templates/routes/_table.html.erb +++ b/actionpack/lib/action_dispatch/middleware/templates/routes/_table.html.erb @@ -4,21 +4,41 @@ border-collapse: collapse; } - #route_table td { - padding: 0 30px; + #route_table thead tr { + border-bottom: 2px solid #ddd; + } + + #route_table thead tr.bottom { + border-bottom: none; } - #route_table tr.bottom th { - padding-bottom: 10px; + #route_table thead tr.bottom th { + padding: 10px 0; line-height: 15px; } - #route_table .matched_paths { + #route_table tbody tr { + border-bottom: 1px solid #ddd; + } + + #route_table tbody tr:nth-child(odd) { + background: #f2f2f2; + } + + #route_table tbody.exact_matches, + #route_table tbody.fuzzy_matches { background-color: LightGoldenRodYellow; + border-bottom: solid 2px SlateGrey; } - #route_table .matched_paths { - border-bottom: solid 3px SlateGrey; + #route_table tbody.exact_matches tr, + #route_table tbody.fuzzy_matches tr { + background: none; + border-bottom: none; + } + + #route_table td { + padding: 4px 30px; } #path_search { @@ -45,13 +65,15 @@ <th><%# HTTP Verb %> </th> <th><%# Path %> - <%= search_field(:path, nil, id: 'path_search', placeholder: "Path Match") %> + <%= search_field(:path, nil, id: 'search', placeholder: "Path Match") %> </th> <th><%# Controller#action %> </th> </tr> </thead> - <tbody class='matched_paths' id='matched_paths'> + <tbody class='exact_matches' id='exact_matches'> + </tbody> + <tbody class='fuzzy_matches' id='fuzzy_matches'> </tbody> <tbody> <%= yield %> @@ -59,6 +81,7 @@ </table> <script type='text/javascript'> + // Iterates each element through a function function each(elems, func) { if (!elems instanceof Array) { elems = [elems]; } for (var i = 0, len = elems.length; i < len; i++) { @@ -66,77 +89,110 @@ } } - function setValOn(elems, val) { - each(elems, function(elem) { - elem.innerHTML = val; - }); + // Sets innerHTML for an element + function setContent(elem, text) { + elem.innerHTML = text; } - function onClick(elems, func) { - each(elems, function(elem) { - elem.onclick = func; - }); - } + // Enables path search functionality + function setupMatchPaths() { + // Check if the user input (sanitized as a path) matches the regexp data attribute + function checkExactMatch(section, elem, value) { + var string = sanitizePath(value), + regexp = elem.getAttribute("data-regexp"); - // Enables functionality to toggle between `_path` and `_url` helper suffixes - function setupRouteToggleHelperLinks() { - var toggleLinks = document.querySelectorAll('#route_table [data-route-helper]'); - onClick(toggleLinks, function(){ - var helperTxt = this.getAttribute("data-route-helper"), - helperElems = document.querySelectorAll('[data-route-name] span.helper'); - setValOn(helperElems, helperTxt); - }); - } + showMatch(string, regexp, section, elem); + } - // takes an array of elements with a data-regexp attribute and - // passes their their parent <tr> into the callback function - // if the regexp matchs a given path - function eachElemsForPath(elems, path, func) { - each(elems, function(e){ - var reg = e.getAttribute("data-regexp"); - if (path.match(RegExp(reg))) { - func(e.parentNode.cloneNode(true)); - } - }) - } + // Check if the route path data attribute contains the user input + function checkFuzzyMatch(section, elem, value) { + var string = elem.getAttribute("data-route-path"), + regexp = value; - // Ensure path always starts with a slash "/" and remove params or fragments - function sanitizePath(path) { - var path = path.charAt(0) == '/' ? path : "/" + path; - return path.replace(/\#.*|\?.*/, ''); - } + showMatch(string, regexp, section, elem); + } - // Enables path search functionality - function setupMatchPaths() { - var regexpElems = document.querySelectorAll('#route_table [data-regexp]'), - pathElem = document.querySelector('#path_search'), - selectedSection = document.querySelector('#matched_paths'), - noMatchText = '<tr><th colspan="4">None</th></tr>'; + // Display the parent <tr> element in the appropriate section when there's a match + function showMatch(string, regexp, section, elem) { + if(string.match(RegExp(regexp))) { + section.appendChild(elem.parentNode.cloneNode(true)); + } + } + + // Check if there are any matched results in a section + function checkNoMatch(section, defaultText, noMatchText) { + if (section.innerHTML === defaultText) { + setContent(section, defaultText + noMatchText); + } + } + // Ensure path always starts with a slash "/" and remove params or fragments + function sanitizePath(path) { + var path = path.charAt(0) == '/' ? path : "/" + path; + return path.replace(/\#.*|\?.*/, ''); + } - // Remove matches if no path is present - pathElem.onblur = function(e) { - if (pathElem.value === "") selectedSection.innerHTML = ""; + var regexpElems = document.querySelectorAll('#route_table [data-regexp]'), + searchElem = document.querySelector('#search'), + exactMatches = document.querySelector('#exact_matches'), + fuzzyMatches = document.querySelector('#fuzzy_matches'); + + // Remove matches when no search value is present + searchElem.onblur = function(e) { + if (searchElem.value === "") { + setContent(exactMatches, ""); + setContent(fuzzyMatches, ""); + } } // On key press perform a search for matching paths - pathElem.onkeyup = function(e){ - var path = sanitizePath(pathElem.value), - defaultText = '<tr><th colspan="4">Paths Matching (' + path + '):</th></tr>'; + 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>', + noExactMatch = '<tr><th colspan="4">No Exact Matches Found</th></tr>', + noFuzzyMatch = '<tr><th colspan="4">No Fuzzy Matches Found</th></tr>'; // Clear out results section - selectedSection.innerHTML= defaultText; + setContent(exactMatches, defaultExactMatch); + setContent(fuzzyMatches, defaultFuzzyMatch); + + // Display exact matches and fuzzy matches + each(regexpElems, function(elem) { + checkExactMatch(exactMatches, elem, userInput); + checkFuzzyMatch(fuzzyMatches, elem, userInput); + }) + + // Display 'No Matches' message when no matches are found + checkNoMatch(exactMatches, defaultExactMatch, noExactMatch); + checkNoMatch(fuzzyMatches, defaultFuzzyMatch, noFuzzyMatch); + } + } - // Display matches if they exist - eachElemsForPath(regexpElems, path, function(e){ - selectedSection.appendChild(e); + // Enables functionality to toggle between `_path` and `_url` helper suffixes + function setupRouteToggleHelperLinks() { + + // Sets content for each element + function setValOn(elems, val) { + each(elems, function(elem) { + setContent(elem, val); }); + } - // If no match present, tell the user - if (selectedSection.innerHTML === defaultText) { - selectedSection.innerHTML = selectedSection.innerHTML + noMatchText; - } + // Sets onClick event for each element + function onClick(elems, func) { + each(elems, function(elem) { + elem.onclick = func; + }); } + + var toggleLinks = document.querySelectorAll('#route_table [data-route-helper]'); + onClick(toggleLinks, function(){ + var helperTxt = this.getAttribute("data-route-helper"), + helperElems = document.querySelectorAll('[data-route-name] span.helper'); + + setValOn(helperElems, helperTxt); + }); } setupMatchPaths(); diff --git a/actionpack/lib/action_dispatch/railtie.rb b/actionpack/lib/action_dispatch/railtie.rb index edf37bb9a5..ddeea24bb3 100644 --- a/actionpack/lib/action_dispatch/railtie.rb +++ b/actionpack/lib/action_dispatch/railtie.rb @@ -16,12 +16,12 @@ module ActionDispatch config.action_dispatch.signed_cookie_salt = 'signed cookie' config.action_dispatch.encrypted_cookie_salt = 'encrypted cookie' config.action_dispatch.encrypted_signed_cookie_salt = 'signed encrypted cookie' + config.action_dispatch.perform_deep_munge = true config.action_dispatch.default_headers = { 'X-Frame-Options' => 'SAMEORIGIN', 'X-XSS-Protection' => '1; mode=block', - 'X-Content-Type-Options' => 'nosniff', - 'X-UA-Compatible' => 'chrome=1' + 'X-Content-Type-Options' => 'nosniff' } config.eager_load_namespaces << ActionDispatch @@ -29,6 +29,7 @@ module ActionDispatch initializer "action_dispatch.configure" do |app| ActionDispatch::Http::URL.tld_length = app.config.action_dispatch.tld_length ActionDispatch::Request.ignore_accept_header = app.config.action_dispatch.ignore_accept_header + ActionDispatch::Request::Utils.perform_deep_munge = app.config.action_dispatch.perform_deep_munge ActionDispatch::Response.default_charset = app.config.action_dispatch.default_charset || app.config.encoding ActionDispatch::Response.default_headers = app.config.action_dispatch.default_headers diff --git a/actionpack/lib/action_dispatch/request/session.rb b/actionpack/lib/action_dispatch/request/session.rb index 7bc812fd22..973627f106 100644 --- a/actionpack/lib/action_dispatch/request/session.rb +++ b/actionpack/lib/action_dispatch/request/session.rb @@ -7,6 +7,9 @@ module ActionDispatch ENV_SESSION_KEY = Rack::Session::Abstract::ENV_SESSION_KEY # :nodoc: ENV_SESSION_OPTIONS_KEY = Rack::Session::Abstract::ENV_SESSION_OPTIONS_KEY # :nodoc: + # Singleton object used to determine if an optional param wasn't specified + Unspecified = Object.new + def self.create(store, env, default_options) session_was = find env session = Request::Session.new(store, env) @@ -127,6 +130,15 @@ module ActionDispatch @delegate.delete key.to_s end + def fetch(key, default=Unspecified, &block) + load_for_read! + if default == Unspecified + @delegate.fetch(key.to_s, &block) + else + @delegate.fetch(key.to_s, default, &block) + end + end + def inspect if loaded? super diff --git a/actionpack/lib/action_dispatch/request/utils.rb b/actionpack/lib/action_dispatch/request/utils.rb index 8b43cdada8..9d4f1aa3c5 100644 --- a/actionpack/lib/action_dispatch/request/utils.rb +++ b/actionpack/lib/action_dispatch/request/utils.rb @@ -1,18 +1,29 @@ module ActionDispatch class Request < Rack::Request class Utils # :nodoc: + + mattr_accessor :perform_deep_munge + self.perform_deep_munge = true + class << self # Remove nils from the params hash - def deep_munge(hash) + def deep_munge(hash, keys = []) + return hash unless perform_deep_munge + hash.each do |k, v| + keys << k case v when Array - v.grep(Hash) { |x| deep_munge(x) } + v.grep(Hash) { |x| deep_munge(x, keys) } v.compact! - hash[k] = nil if v.empty? + if v.empty? + hash[k] = nil + ActiveSupport::Notifications.instrument("deep_munge.action_controller", keys: keys) + end when Hash - deep_munge(v) + deep_munge(v, keys) end + keys.pop end hash diff --git a/actionpack/lib/action_dispatch/routing.rb b/actionpack/lib/action_dispatch/routing.rb index a9ac2bce1d..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 @@ -11,7 +12,7 @@ module ActionDispatch # Think of creating routes as drawing a map for your requests. The map tells # them where to go based on some predefined pattern: # - # AppName::Application.routes.draw do + # Rails.application.routes.draw do # Pattern 1 tells some request to go to one place # Pattern 2 tell them to go to another # ... diff --git a/actionpack/lib/action_dispatch/routing/inspector.rb b/actionpack/lib/action_dispatch/routing/inspector.rb index cffb814e1e..71a0c5e826 100644 --- a/actionpack/lib/action_dispatch/routing/inspector.rb +++ b/actionpack/lib/action_dispatch/routing/inspector.rb @@ -69,7 +69,7 @@ module ActionDispatch end def internal? - controller.to_s =~ %r{\Arails/(info|welcome)} || path =~ %r{\A#{Rails.application.config.assets.prefix}} + controller.to_s =~ %r{\Arails/(info|mailers|welcome)} || path =~ %r{\A#{Rails.application.config.assets.prefix}\z} end def engine? @@ -179,7 +179,8 @@ module ActionDispatch private def draw_section(routes) - name_width, verb_width, path_width = widths(routes) + header_lengths = ['Prefix', 'Verb', 'URI Pattern'].map(&:length) + name_width, verb_width, path_width = widths(routes).zip(header_lengths).map(&:max) routes.map do |r| "#{r[:name].rjust(name_width)} #{r[:verb].ljust(verb_width)} #{r[:path].ljust(path_width)} #{r[:reqs]}" @@ -193,9 +194,9 @@ module ActionDispatch end def widths(routes) - [routes.map { |r| r[:name].length }.max, - routes.map { |r| r[:verb].length }.max, - routes.map { |r| r[:path].length }.max] + [routes.map { |r| r[:name].length }.max || 0, + routes.map { |r| r[:verb].length }.max || 0, + routes.map { |r| r[:path].length }.max || 0] end end diff --git a/actionpack/lib/action_dispatch/routing/mapper.rb b/actionpack/lib/action_dispatch/routing/mapper.rb index 3c58a2cfc3..58c7f5330e 100644 --- a/actionpack/lib/action_dispatch/routing/mapper.rb +++ b/actionpack/lib/action_dispatch/routing/mapper.rb @@ -3,6 +3,7 @@ require 'active_support/core_ext/hash/reverse_merge' require 'active_support/core_ext/hash/slice' require 'active_support/core_ext/enumerable' 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' @@ -11,8 +12,8 @@ module ActionDispatch class Mapper URL_OPTIONS = [:protocol, :subdomain, :domain, :host, :port] SCOPE_OPTIONS = [:path, :shallow_path, :as, :shallow_prefix, :module, - :controller, :path_names, :constraints, :defaults, - :shallow, :blocks, :options] + :controller, :action, :path_names, :constraints, + :shallow, :blocks, :defaults, :options] class Constraints #:nodoc: def self.new(app, constraints, request = Rack::Request) @@ -147,15 +148,19 @@ module ActionDispatch @defaults.merge!(options[:defaults]) if options[:defaults] options.each do |key, default| - next if Regexp === default || IGNORE_OPTIONS.include?(key) - @defaults[key] = default + unless Regexp === default || IGNORE_OPTIONS.include?(key) + @defaults[key] = default + end end if options[:constraints].is_a?(Hash) options[:constraints].each do |key, default| - next unless URL_OPTIONS.include?(key) && (String === default || Fixnum === default) - @defaults[key] ||= default + if URL_OPTIONS.include?(key) && (String === default || Fixnum === default) + @defaults[key] ||= default + end end + elsif options[:constraints] + verify_callable_constraint(options[:constraints]) end if Regexp === options[:format] @@ -165,20 +170,28 @@ module ActionDispatch end 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 + def normalize_conditions! - @conditions.merge!(:path_info => path) + @conditions[:path_info] = path constraints.each do |key, condition| - next if segment_keys.include?(key) || key == :controller - @conditions[key] = condition + unless segment_keys.include?(key) || key == :controller + @conditions[key] = condition + end end - @conditions[:required_defaults] = [] + required_defaults = [] options.each do |key, required_default| - next if segment_keys.include?(key) || IGNORE_OPTIONS.include?(key) - next if Regexp === required_default - @conditions[:required_defaults] << key + 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 @@ -192,8 +205,7 @@ module ActionDispatch end if via = options[:via] - list = Array(via).map { |m| m.to_s.dasherize.upcase } - @conditions.merge!(:request_method => list) + @conditions[:request_method] = Array(via).map { |m| m.to_s.dasherize.upcase } end end @@ -214,8 +226,12 @@ module ActionDispatch controller ||= default_controller action ||= default_action - unless controller.is_a?(Regexp) - controller = [@scope[:module], controller].compact.join("/").presence + 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/} @@ -226,11 +242,13 @@ module ActionDispatch action = action.to_s unless action.is_a?(Regexp) if controller.blank? && segment_keys.exclude?(:controller) - raise ArgumentError, "missing :controller" + message = "Missing :controller key on routes definition, please check your routes." + raise ArgumentError, message end if action.blank? && segment_keys.exclude?(:action) - raise ArgumentError, "missing :action" + message = "Missing :action key on routes definition, please check your routes." + raise ArgumentError, message end if controller.is_a?(String) && controller !~ /\A[a-z_0-9\/]*\z/ @@ -330,40 +348,61 @@ 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' # # params[:title] = 'stairway-to-heaven' # + # To match a wildcard parameter, it must have a name assigned to it. + # Without a variable name to attach the glob parameter to, the route + # can't be parsed. + # # 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 request various HTTP verbs with a single action has security - # implications, is recommendable use HttpHelpers[rdoc-ref:HttpHelpers] + # Because requesting various HTTP verbs with a single action has security + # implications, you must either specify the actions in + # the via options or use one of the HtttpHelpers[rdoc-ref:HttpHelpers] # instead +match+ # # === Options @@ -382,8 +421,8 @@ module ActionDispatch # [:module] # The namespace for :controller. # - # match 'path', to: 'c#a', module: 'sekret', controller: 'posts' - # #=> Sekret::PostsController + # match 'path', to: 'c#a', module: 'sekret', controller: 'posts', via: :get + # # => Sekret::PostsController # # See <tt>Scoping#namespace</tt> for its scope equivalent. # @@ -401,9 +440,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 @@ -428,14 +467,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 Blacklist + # class Whitelist # def matches?(request) request.remote_ip == '1.2.3.4' end # end - # match 'path', to: 'c#a', constraints: Blacklist.new + # match 'path', to: 'c#a', constraints: Whitelist.new, via: :get # # See <tt>Scoping#constraints</tt> for more examples with its scope # equivalent. @@ -444,7 +483,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. # @@ -453,7 +492,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+ @@ -496,11 +535,12 @@ module ActionDispatch raise "A rack application must be specified" unless path options[:as] ||= app_name(app) + target_as = name_for_action(options[:as], path) options[:via] ||= :all match(path, options.merge(:to => app, :anchor => false, :format => false)) - define_generate_prefix(app, options[:as]) + define_generate_prefix(app, target_as) self end @@ -538,18 +578,17 @@ module ActionDispatch _route = @set.named_routes.routes[name.to_sym] _routes = @set app.routes.define_mounted_helper(name) - app.routes.singleton_class.class_eval do - define_method :mounted? do - true - end - - define_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 @@ -695,6 +734,11 @@ module ActionDispatch options[:path] = args.flatten.join('/') if args.any? options[:constraints] ||= {} + unless nested_scope? + options[:shallow_path] ||= options[:path] if options.key?(:path) + options[:shallow_prefix] ||= options[:as] if options.key?(:as) + end + if options[:constraints].is_a?(Hash) defaults = options[:constraints].select do |k, v| URL_OPTIONS.include?(k) && (v.is_a?(String) || v.is_a?(Fixnum)) @@ -776,9 +820,16 @@ module ActionDispatch # end def namespace(path, options = {}) path = path.to_s - options = { :path => path, :as => path, :module => path, - :shallow_path => path, :shallow_prefix => path }.merge!(options) - scope(options) { yield } + + defaults = { + module: path, + path: options.fetch(:path, path), + as: options.fetch(:as, path), + shallow_path: options.fetch(:path, path), + shallow_prefix: options.fetch(:as, path) + } + + scope(defaults.merge!(options)) { yield } end # === Parameter Restriction @@ -874,6 +925,10 @@ module ActionDispatch child end + def merge_action_scope(parent, child) #:nodoc: + child + end + def merge_path_names_scope(parent, child) #:nodoc: merge_options_scope(parent, child) end @@ -963,6 +1018,7 @@ module ActionDispatch @as = options[:as] @param = (options[:param] || :id).to_sym @options = options + @shallow = false end def default_actions @@ -1023,6 +1079,13 @@ module ActionDispatch "#{path}/:#{nested_param}" end + def shallow=(value) + @shallow = value + end + + def shallow? + @shallow + end end class SingletonResource < Resource #:nodoc: @@ -1062,18 +1125,18 @@ module ActionDispatch # a singular resource to map /profile (rather than /profile/:id) to # the show action: # - # resource :geocoder + # resource :profile # # creates six different routes in your application, all mapping to - # the +GeoCoders+ controller (note that the controller is named after + # the +Profiles+ controller (note that the controller is named after # the plural): # - # GET /geocoder/new - # POST /geocoder - # GET /geocoder - # GET /geocoder/edit - # PATCH/PUT /geocoder - # DELETE /geocoder + # GET /profile/new + # POST /profile + # GET /profile + # GET /profile/edit + # PATCH/PUT /profile + # DELETE /profile # # === Options # Takes same options as +resources+. @@ -1303,8 +1366,10 @@ module ActionDispatch end with_scope_level(:member) do - scope(parent_resource.member_scope) do - yield + if shallow? + shallow_scope(parent_resource.member_scope) { yield } + else + scope(parent_resource.member_scope) { yield } end end end @@ -1327,16 +1392,8 @@ module ActionDispatch end with_scope_level(:nested) do - if shallow? - with_exclusive_scope do - if @scope[:shallow_path].blank? - scope(parent_resource.nested_scope, nested_options) { yield } - else - scope(@scope[:shallow_path], :as => @scope[:shallow_prefix]) do - scope(parent_resource.nested_scope, nested_options) { yield } - end - end - end + if shallow? && shallow_nesting_depth > 1 + shallow_scope(parent_resource.nested_scope, nested_options) { yield } else scope(parent_resource.nested_scope, nested_options) { yield } end @@ -1353,7 +1410,7 @@ module ActionDispatch end def shallow - scope(:shallow => true, :shallow_path => @scope[:path]) do + scope(:shallow => true) do yield end end @@ -1383,6 +1440,10 @@ module ActionDispatch raise ArgumentError, "Unknown scope #{on.inspect} given to :on" end + if @scope[:controller] && @scope[:action] + options[:to] ||= "#{@scope[:controller]}##{@scope[:action]}" + end + paths.each do |_path| route_options = options.dup route_options[:path] ||= _path if _path.is_a?(String) @@ -1390,6 +1451,7 @@ module ActionDispatch path_without_format = _path.to_s.sub(/\(\.:format\)$/, '') if using_match_shorthand?(path_without_format, route_options) route_options[:to] ||= path_without_format.gsub(%r{^/}, "").sub(%r{/([^/]*)$}, '#\1') + route_options[:to].tr!("-", "_") end decomposed_match(_path, route_options) @@ -1420,8 +1482,8 @@ module ActionDispatch path = path_for_action(action, options.delete(:path)) action = action.to_s.dup - if action =~ /^[\w\/]+$/ - options[:action] ||= action unless action.include?("/") + if action =~ /^[\w\-\/]+$/ + options[:action] ||= action.tr('-', '_') unless action.include?("/") else action = nil end @@ -1469,6 +1531,13 @@ module ActionDispatch return true end + if options.delete(:shallow) + shallow do + send(method, resources.pop, options, &block) + end + return true + end + if resource_scope? nested { send(method, resources.pop, options, &block) } return true @@ -1513,6 +1582,10 @@ module ActionDispatch RESOURCE_METHOD_SCOPES.include? @scope[:scope_level] end + def nested_scope? #:nodoc: + @scope[:scope_level] == :nested + end + def with_exclusive_scope begin old_name_prefix, old_path = @scope[:as], @scope[:path] @@ -1526,21 +1599,24 @@ module ActionDispatch end end - def with_scope_level(kind, resource = parent_resource) + def with_scope_level(kind) old, @scope[:scope_level] = @scope[:scope_level], kind - old_resource, @scope[:scope_level_resource] = @scope[:scope_level_resource], resource yield ensure @scope[:scope_level] = old - @scope[:scope_level_resource] = old_resource end def resource_scope(kind, resource) #:nodoc: - with_scope_level(kind, resource) do - scope(parent_resource.resource_scope) do - yield - end + resource.shallow = @scope[:shallow] + old_resource, @scope[:scope_level_resource] = @scope[:scope_level_resource], resource + @nesting.push(resource) + + with_scope_level(kind) do + scope(parent_resource.resource_scope) { yield } end + ensure + @nesting.pop + @scope[:scope_level_resource] = old_resource end def nested_options #:nodoc: @@ -1552,6 +1628,14 @@ module ActionDispatch options end + def nesting_depth #:nodoc: + @nesting.size + end + + def shallow_nesting_depth #:nodoc: + @nesting.select(&:shallow?).size + end + def param_constraint? #:nodoc: @scope[:constraints] && @scope[:constraints][parent_resource.param].is_a?(Regexp) end @@ -1564,18 +1648,20 @@ module ActionDispatch flag && resource_method_scope? && CANONICAL_ACTIONS.include?(action.to_s) end - def shallow_scoping? #:nodoc: - shallow? && @scope[:scope_level] == :member + def shallow_scope(path, options = {}) #:nodoc: + old_name_prefix, old_path = @scope[:as], @scope[:path] + @scope[:as], @scope[:path] = @scope[:shallow_prefix], @scope[:shallow_path] + + scope(path, options) { yield } + ensure + @scope[:as], @scope[:path] = old_name_prefix, old_path end def path_for_action(action, path) #:nodoc: - prefix = shallow_scoping? ? - "#{@scope[:shallow_path]}/#{parent_resource.shallow_scope}" : @scope[:path] - if canonical_action?(action, path.blank?) - prefix.to_s + @scope[:path].to_s else - "#{prefix}/#{action_path(action, path)}" + "#{@scope[:path]}/#{action_path(action, path)}" end end @@ -1586,10 +1672,11 @@ module ActionDispatch def prefix_name_for_action(as, action) #:nodoc: if as - as.to_s + prefix = as elsif !canonical_action?(action, @scope[:scope_level]) - action.to_s + prefix = action end + prefix.to_s.tr('-', '_') if prefix end def name_for_action(as, action) #:nodoc: @@ -1612,7 +1699,7 @@ module ActionDispatch when :new [prefix, :new, name_prefix, member_name] when :member - [prefix, shallow_scoping? ? @scope[:shallow_prefix] : name_prefix, member_name] + [prefix, name_prefix, member_name] when :root [name_prefix, collection_name, prefix] else @@ -1753,6 +1840,7 @@ module ActionDispatch @set = set @scope = { :path_names => @set.resources_path_names } @concerns = {} + @nesting = [] end include Base diff --git a/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb b/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb index 6d3f8da932..bd3696cda1 100644 --- a/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb +++ b/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb @@ -74,6 +74,19 @@ module ActionDispatch # * <tt>:routing_type</tt> - Allowed values are <tt>:path</tt> or <tt>:url</tt>. # Default is <tt>:url</tt>. # + # Also includes all the options from <tt>url_for</tt>. These include such + # things as <tt>:anchor</tt> or <tt>:trailing_slash</tt>. Example usage + # is given below: + # + # polymorphic_url([blog, post], anchor: 'my_anchor') + # # => "http://example.com/blogs/1/posts/1#my_anchor" + # polymorphic_url([blog, post], anchor: 'my_anchor', script_name: "/my_app") + # # => "http://example.com/my_app/blogs/1/posts/1#my_anchor" + # + # For all of these options, see the documentation for <tt>url_for</tt>. + # + # ==== Functionality + # # # an Article record # polymorphic_url(record) # same as article_url(record) # @@ -88,53 +101,45 @@ module ActionDispatch # polymorphic_url(Comment) # same as comments_url() # def polymorphic_url(record_or_hash_or_array, options = {}) - 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) - proxy = 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) + opts = options.dup + action = opts.delete :action + type = opts.delete(:routing_type) || :url - args = Array === record_or_hash_or_array ? - record_or_hash_or_array.dup : - [ record_or_hash_or_array ] + HelperMethodBuilder.polymorphic_method self, + record_or_hash_or_array, + action, + type, + opts - 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 - - args.delete_if {|arg| arg.is_a?(Symbol) || arg.is_a?(String)} - named_route = build_named_route_call(record_or_hash_or_array, inflection, options) - - 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) } - - (proxy || self).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 = {}) @@ -152,54 +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 self.singular(prefix, suffix) + new(->(name) { name.singular_route_key }, prefix, suffix) end - def build_named_route_call(records, 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.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 - record = extract_record(records) - route = [] + recipient.send(method, *args, options) end + end + + attr_reader :suffix, :prefix + + 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 - 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 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 d751e04e6a..b08e62543b 100644 --- a/actionpack/lib/action_dispatch/routing/redirection.rb +++ b/actionpack/lib/action_dispatch/routing/redirection.rb @@ -17,7 +17,7 @@ module ActionDispatch def call(env) req = Request.new(env) - # If any of the path parameters has a invalid encoding then + # 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? @@ -26,6 +26,15 @@ module ActionDispatch end uri = URI.parse(path(req.symbolized_path_parameters, req)) + + unless uri.host + if relative_path?(uri.path) + uri.path = "#{req.script_name}/#{uri.path}" + elsif uri.path.empty? + uri.path = req.script_name.empty? ? "/" : req.script_name + end + end + uri.scheme ||= req.scheme uri.host ||= req.host uri.port ||= req.port unless req.standard_port? @@ -48,11 +57,38 @@ module ActionDispatch def inspect "redirect(#{status})" end + + private + def relative_path?(path) + path && !path.empty? && path[0] != '/' + end + + def escape(params) + Hash[params.map{ |k,v| [k, Rack::Utils.escape(v)] }] + end + + def escape_fragment(params) + Hash[params.map{ |k,v| [k, Journey::Router::Utils.escape_fragment(v)] }] + end + + def escape_path(params) + Hash[params.map{ |k,v| [k, Journey::Router::Utils.escape_path(v)] }] + end end class PathRedirect < Redirect + URL_PARTS = /\A([^?]+)?(\?[^#]+)?(#.+)?\z/ + def path(params, request) - (params.empty? || !block.match(/%\{\w*\}/)) ? block : (block % escape(params)) + if block.match(URL_PARTS) + path = interpolation_required?($1, params) ? $1 % escape_path(params) : $1 + query = interpolation_required?($2, params) ? $2 % escape(params) : $2 + fragment = interpolation_required?($3, params) ? $3 % escape_fragment(params) : $3 + + "#{path}#{query}#{fragment}" + else + interpolation_required?(block, params) ? block % escape(params) : block + end end def inspect @@ -60,8 +96,8 @@ module ActionDispatch end private - def escape(params) - Hash[params.map{ |k,v| [k, Rack::Utils.escape(v)] }] + def interpolation_required?(string, params) + !params.empty? && string && string.match(/%\{\w*\}/) end end @@ -81,17 +117,22 @@ module ActionDispatch url_options[:path] = (url_options[:path] % escape_path(params)) end + unless options[:host] || options[:domain] + if relative_path?(url_options[:path]) + url_options[:path] = "/#{url_options[:path]}" + url_options[:script_name] = request.script_name + elsif url_options[:path].empty? + url_options[:path] = request.script_name.empty? ? "/" : "" + url_options[:script_name] = request.script_name + end + end + ActionDispatch::Http::URL.url_for url_options end def inspect "redirect(#{status}, #{options.map{ |k,v| "#{k}: #{v}" }.join(', ')})" end - - private - def escape_path(params) - Hash[params.map{ |k,v| [k, URI.parser.escape(v)] }] - end end module Redirection @@ -104,6 +145,10 @@ module ActionDispatch # # get 'docs/:article', to: redirect('/wiki/%{article}') # + # Note that if you return a path without a leading slash then the url is prefixed with the + # current SCRIPT_NAME environment variable. This is typically '/' but may be different in + # a mounted engine or where the application is deployed to a subdirectory of a website. + # # Alternatively you can use one of the other syntaxes: # # The block version of redirect allows for the easy encapsulation of any logic associated with diff --git a/actionpack/lib/action_dispatch/routing/route_set.rb b/actionpack/lib/action_dispatch/routing/route_set.rb index 3ae9f92c0b..01378ae0ec 100644 --- a/actionpack/lib/action_dispatch/routing/route_set.rb +++ b/actionpack/lib/action_dispatch/routing/route_set.rb @@ -1,11 +1,13 @@ 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' module ActionDispatch module Routing @@ -28,7 +30,7 @@ module ActionDispatch def call(env) params = env[PARAMETERS_KEY] - # If any of the path parameters has a invalid encoding then + # 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?) @@ -155,7 +157,7 @@ module ActionDispatch end def self.optimize_helper?(route) - route.requirements.except(:controller, :action).empty? + !route.glob? && route.path.requirements.empty? end class OptimizedUrlHelper < UrlHelper # :nodoc: @@ -163,15 +165,15 @@ module ActionDispatch def initialize(route, options) super - @path_parts = @route.required_parts - @arg_size = @path_parts.size - @string_route = @route.optimized_path + @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) + options = t.url_options.merge @options options[:path] = optimized_helper(args) ActionDispatch::Http::URL.url_for(options) else @@ -182,44 +184,65 @@ module ActionDispatch private def optimized_helper(args) - path = @string_route.dup - klass = Journey::Router::Utils - - @path_parts.zip(args) do |part, arg| - # Replace each route parameter - # e.g. :id for regular parameter or *path for globbing - # with ruby string interpolation code - path.gsub!(/(\*|:)#{part}/, klass.escape_fragment(arg.to_param)) + params = Hash[parameterize_args(args)] + missing_keys = missing_keys(params) + + unless missing_keys.empty? + raise_generation_error(params, missing_keys) end - path + + @optimized_path.map{ |segment| replace_segment(params, segment) }.join + end + + def replace_segment(params, segment) + Symbol === segment ? @klass.escape_segment(params[segment]) : segment end def optimize_routes_generation?(t) t.send(:optimize_routes_generation?) end + + def parameterize_args(args) + @required_parts.zip(args.map(&:to_param)) + end + + def missing_keys(args) + args.select{ |part, arg| arg.nil? || arg.empty? }.keys + end + + def raise_generation_error(args, missing_keys) + constraints = Hash[@route.requirements.merge(args).sort] + message = "No route matches #{constraints.inspect}" + message << " missing required keys: #{missing_keys.sort.inspect}" + + raise ActionController::UrlGenerationError, message + end end def initialize(route, options) @options = options - @segment_keys = route.segment_keys + @segment_keys = route.segment_keys.uniq @route = route end def call(t, args) - t.url_for(handle_positional_args(t, args, @options, @segment_keys)) + controller_options = t.url_options + options = controller_options.merge @options + hash = handle_positional_args(controller_options, args, options, @segment_keys) + t._routes.url_for(hash) end - def handle_positional_args(t, args, options, keys) + def handle_positional_args(controller_options, args, result, path_params) inner_options = args.extract_options! - result = options.dup 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) @@ -336,7 +359,7 @@ module ActionDispatch include UrlFor end - # Contains all the mounted helpers accross different + # Contains all the mounted helpers across different # engines and the `main_app` helper for the application. # You can include this in your classes if you want to # access routes for other engines. @@ -374,6 +397,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 @@ -489,11 +514,12 @@ module ActionDispatch @recall = recall.dup @set = set + normalize_recall! normalize_options! normalize_controller_action_id! use_relative_controller! normalize_controller! - handle_nil_action! + normalize_action! end def controller @@ -512,6 +538,11 @@ module ActionDispatch end end + # Set 'index' as default action for recall + def normalize_recall! + @recall[:action] ||= 'index' + end + def normalize_options! # If an explicit :controller was given, always make :action explicit # too, so that action expiry works as expected for things like @@ -527,8 +558,8 @@ module ActionDispatch options[:controller] = options[:controller].to_s end - if options[:action] - options[:action] = options[:action].to_s + if options.key?(:action) + options[:action] = (options[:action] || 'index').to_s end end @@ -538,8 +569,6 @@ module ActionDispatch # :controller, :action or :id is not found, don't pull any # more keys from the recall. def normalize_controller_action_id! - @recall[:action] ||= 'index' if current_controller - use_recall_for(:controller) or return use_recall_for(:action) or return use_recall_for(:id) @@ -561,19 +590,17 @@ module ActionDispatch @options[:controller] = controller.sub(%r{^/}, '') if controller end - # This handles the case of action: nil being explicitly passed. - # It is identical to action: "index" - def handle_nil_action! - if options.has_key?(:action) && options[:action].nil? - options[:action] = 'index' + # Move 'index' action from options to recall + def normalize_action! + if @options[:action] == 'index' + @recall[:action] = @options.delete(:action) end - recall[:action] = options.delete(:action) if options[:action] == 'index' end # 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? @@ -618,37 +645,46 @@ 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*. + # The +options+ argument must be a hash whose keys are *symbols*. def url_for(options) - options = default_url_options.merge(options || {}) + 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(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 - })) + ActionDispatch::Http::URL.url_for(options) end def call(env) @@ -694,17 +730,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 8e19025722..e624fe3c4a 100644 --- a/actionpack/lib/action_dispatch/routing/url_for.rb +++ b/actionpack/lib/action_dispatch/routing/url_for.rb @@ -20,7 +20,7 @@ module ActionDispatch # # <%= link_to('Click here', controller: 'users', # action: 'new', message: 'Welcome!') %> - # # => "/users/new?message=Welcome%21" + # # => <a href="/users/new?message=Welcome%21">Click here</a> # # link_to, and all other functions that require URL generation functionality, # actually use ActionController::UrlFor under the hood. And in particular, @@ -155,8 +155,14 @@ module ActionDispatch _routes.url_for(options.symbolize_keys.reverse_merge!(url_options)) 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/dom.rb b/actionpack/lib/action_dispatch/testing/assertions/dom.rb index 8f90a1223e..241a39393a 100644 --- a/actionpack/lib/action_dispatch/testing/assertions/dom.rb +++ b/actionpack/lib/action_dispatch/testing/assertions/dom.rb @@ -7,20 +7,20 @@ module ActionDispatch # # # assert that the referenced method generates the appropriate HTML string # assert_dom_equal '<a href="http://www.example.com">Apples</a>', link_to("Apples", "http://www.example.com") - def assert_dom_equal(expected, actual, message = "") + def assert_dom_equal(expected, actual, message = nil) expected_dom = HTML::Document.new(expected).root actual_dom = HTML::Document.new(actual).root - assert_equal expected_dom, actual_dom + assert_equal expected_dom, actual_dom, message end # The negated form of +assert_dom_equivalent+. # # # assert that the referenced method does not generate the specified HTML string # assert_dom_not_equal '<a href="http://www.example.com">Apples</a>', link_to("Oranges", "http://www.example.com") - def assert_dom_not_equal(expected, actual, message = "") + def assert_dom_not_equal(expected, actual, message = nil) expected_dom = HTML::Document.new(expected).root actual_dom = HTML::Document.new(actual).root - assert_not_equal expected_dom, actual_dom + assert_not_equal expected_dom, actual_dom, message end end end diff --git a/actionpack/lib/action_dispatch/testing/assertions/response.rb b/actionpack/lib/action_dispatch/testing/assertions/response.rb index 44ed0ac1f3..0adc6c84ff 100644 --- a/actionpack/lib/action_dispatch/testing/assertions/response.rb +++ b/actionpack/lib/action_dispatch/testing/assertions/response.rb @@ -27,6 +27,9 @@ module ActionDispatch assert @response.send("#{type}?"), message else code = Rack::Utils::SYMBOL_TO_STATUS_CODE[type] + if code.nil? + raise ArgumentError, "Invalid response type :#{type}" + end assert_equal code, @response.response_code, message end else @@ -67,21 +70,17 @@ module ActionDispatch end def normalize_argument_to_redirection(fragment) - normalized = case fragment - when Regexp - fragment - when %r{^\w[A-Za-z\d+.-]*:.*} - fragment - when String - @request.protocol + @request.host_with_port + fragment - when :back - raise RedirectBackError unless refer = @request.headers["Referer"] - refer - else - @controller.url_for(fragment) - end - - normalized.respond_to?(:delete) ? normalized.delete("\0\r\n") : normalized + if Regexp === fragment + fragment + else + handle = @controller || Class.new(ActionController::Metal) do + include ActionController::Redirecting + def initialize(request) + @_request = request + end + end.new(@request) + handle._compute_redirect_to_location(fragment) + end end end end diff --git a/actionpack/lib/action_dispatch/testing/assertions/routing.rb b/actionpack/lib/action_dispatch/testing/assertions/routing.rb index 496682e8bd..f1f998d932 100644 --- a/actionpack/lib/action_dispatch/testing/assertions/routing.rb +++ b/actionpack/lib/action_dispatch/testing/assertions/routing.rb @@ -211,7 +211,7 @@ module ActionDispatch def fail_on(exception_class) yield rescue exception_class => e - raise MiniTest::Assertion, e.message + raise Minitest::Assertion, e.message end end end diff --git a/actionpack/lib/action_dispatch/testing/assertions/selector.rb b/actionpack/lib/action_dispatch/testing/assertions/selector.rb index 3253a3d424..12023e6f77 100644 --- a/actionpack/lib/action_dispatch/testing/assertions/selector.rb +++ b/actionpack/lib/action_dispatch/testing/assertions/selector.rb @@ -267,7 +267,7 @@ module ActionDispatch text.strip! unless NO_STRIP.include?(match.name) text.sub!(/\A\n/, '') if match.name == "textarea" unless match_with.is_a?(Regexp) ? (text =~ match_with) : (text == match_with.to_s) - content_mismatch ||= sprintf("<%s> expected but was\n<%s>.", match_with, text) + content_mismatch ||= sprintf("<%s> expected but was\n<%s>", match_with, text) true end end @@ -276,7 +276,7 @@ module ActionDispatch html = match.children.map(&:to_s).join html.strip! unless NO_STRIP.include?(match.name) unless match_with.is_a?(Regexp) ? (html =~ match_with) : (html == match_with.to_s) - content_mismatch ||= sprintf("<%s> expected but was\n<%s>.", match_with, html) + content_mismatch ||= sprintf("<%s> expected but was\n<%s>", match_with, html) true end end @@ -289,9 +289,9 @@ module ActionDispatch # FIXME: minitest provides messaging when we use assert_operator, # so is this custom message really needed? - message = message || %(Expected #{count_description(min, max, count)} matching "#{selector.to_s}", found #{matches.size}.) + message = message || %(Expected #{count_description(min, max, count)} matching "#{selector.to_s}", found #{matches.size}) if count - assert_equal matches.size, count, message + assert_equal count, matches.size, message else assert_operator matches.size, :>=, min, message if min assert_operator matches.size, :<=, max, message if max diff --git a/actionpack/lib/action_dispatch/testing/integration.rb b/actionpack/lib/action_dispatch/testing/integration.rb index 1f899a434c..cc6b763093 100644 --- a/actionpack/lib/action_dispatch/testing/integration.rb +++ b/actionpack/lib/action_dispatch/testing/integration.rb @@ -137,7 +137,7 @@ module ActionDispatch class Session DEFAULT_HOST = "www.example.com" - include MiniTest::Assertions + include Minitest::Assertions include TestProcess, RequestHelpers, Assertions %w( status status_message headers body redirect? ).each do |method| @@ -242,7 +242,7 @@ module ActionDispatch @https = flag end - # Return +true+ if the session is mimicking a secure HTTPS request. + # Returns +true+ if the session is mimicking a secure HTTPS request. # # if session.https? # ... @@ -298,8 +298,6 @@ module ActionDispatch session = Rack::Test::Session.new(_mock_session) - env.merge!(env) - # NOTE: rack-test v0.5 doesn't build a default uri correctly # Make sure requested path is always a full uri uri = URI.parse('/') @@ -489,10 +487,6 @@ module ActionDispatch @@app = nil def self.app - if !@@app && !ActionDispatch.test_app - ActiveSupport::Deprecation.warn "Rails application fallback is deprecated and no longer works, please set ActionDispatch.test_app" - end - @@app || ActionDispatch.test_app end diff --git a/actionpack/lib/action_dispatch/testing/test_request.rb b/actionpack/lib/action_dispatch/testing/test_request.rb index c63778f870..57c678843b 100644 --- a/actionpack/lib/action_dispatch/testing/test_request.rb +++ b/actionpack/lib/action_dispatch/testing/test_request.rb @@ -3,7 +3,11 @@ require 'rack/utils' module ActionDispatch class TestRequest < Request - DEFAULT_ENV = Rack::MockRequest.env_for('/') + DEFAULT_ENV = Rack::MockRequest.env_for('/', + 'HTTP_HOST' => 'test.host', + 'REMOTE_ADDR' => '0.0.0.0', + 'HTTP_USER_AGENT' => 'Rails Testing' + ) def self.new(env = {}) super @@ -12,10 +16,6 @@ module ActionDispatch def initialize(env = {}) env = Rails.application.env_config.merge(env) if defined?(Rails.application) && Rails.application super(default_env.merge(env)) - - self.host = 'test.host' - self.remote_addr = '0.0.0.0' - self.user_agent = 'Rails Testing' end def request_method=(method) |