diff options
Diffstat (limited to 'actionpack/lib/action_dispatch')
22 files changed, 434 insertions, 305 deletions
diff --git a/actionpack/lib/action_dispatch/http/request.rb b/actionpack/lib/action_dispatch/http/request.rb index a519d6c1fc..8c035c3c6c 100644 --- a/actionpack/lib/action_dispatch/http/request.rb +++ b/actionpack/lib/action_dispatch/http/request.rb @@ -225,7 +225,7 @@ module ActionDispatch @remote_ip ||= (@env["action_dispatch.remote_ip"] || ip).to_s end - # Returns the unique request id, which is based off either the X-Request-Id header that can + # Returns the unique request id, which is based on either the X-Request-Id header that can # be generated by a firewall, load balancer, or web server or by the RequestId middleware # (which sets the action_dispatch.request_id environment variable). # diff --git a/actionpack/lib/action_dispatch/http/url.rb b/actionpack/lib/action_dispatch/http/url.rb index 473f692b05..6b8dcaf497 100644 --- a/actionpack/lib/action_dispatch/http/url.rb +++ b/actionpack/lib/action_dispatch/http/url.rb @@ -49,31 +49,26 @@ module ActionDispatch end def path_for(options) - result = options[:script_name].to_s.chomp("/") - result << options[:path].to_s + path = options[:script_name].to_s.chomp("/") + path << options[:path] if options.key?(:path) - result = add_trailing_slash(result) if options[:trailing_slash] + add_trailing_slash(path) if options[:trailing_slash] + add_params(path, options[:params]) if options.key?(:params) + add_anchor(path, options[:anchor]) if options.key?(:anchor) - result = add_params options, result - add_anchor options, result + path end private - def add_params(options, result) - if options.key? :params - param = options[:params] - params = param.is_a?(Hash) ? param : { params: param } - - params.reject! { |_,v| v.to_param.nil? } - result << "?#{params.to_query}" unless params.empty? - end - result + def add_params(path, params) + params = { params: params } unless params.is_a?(Hash) + params.reject! { |_,v| v.to_param.nil? } + path << "?#{params.to_query}" unless params.empty? end - def add_anchor(options, result) - result << "##{Journey::Router::Utils.escape_fragment(options[:anchor].to_param.to_s)}" if options[:anchor] - result + def add_anchor(path, anchor) + path << "##{Journey::Router::Utils.escape_fragment(anchor.to_param.to_s)}" end def extract_domain_from(host, tld_length) @@ -93,19 +88,17 @@ module ActionDispatch elsif !path.include?(".") path.sub!(/[^\/]\z|\A\z/, '\&/') end - - path end def build_host_url(host, port, protocol, options, path) if match = host.match(HOST_REGEXP) - protocol ||= match[1] unless protocol == false - host = match[2] - port = match[3] unless options.key? :port + protocol ||= match[1] unless protocol == false + host = match[2] + port = match[3] unless options.key? :port end - protocol = normalize_protocol protocol - host = normalize_host(host, options) + protocol = normalize_protocol protocol + host = normalize_host(host, options) result = protocol.dup diff --git a/actionpack/lib/action_dispatch/journey/parser.rb b/actionpack/lib/action_dispatch/journey/parser.rb index d129ba7e16..9012297400 100644 --- a/actionpack/lib/action_dispatch/journey/parser.rb +++ b/actionpack/lib/action_dispatch/journey/parser.rb @@ -86,7 +86,7 @@ racc_token_table = { racc_nt_base = 10 -racc_use_result_var = true +racc_use_result_var = false Racc_arg = [ racc_action_table, @@ -133,14 +133,12 @@ Racc_debug_parser = false # reduce 0 omitted -def _reduce_1(val, _values, result) - result = Cat.new(val.first, val.last) - result +def _reduce_1(val, _values) + Cat.new(val.first, val.last) end -def _reduce_2(val, _values, result) - result = val.first - result +def _reduce_2(val, _values) + val.first end # reduce 3 omitted @@ -151,24 +149,20 @@ end # reduce 6 omitted -def _reduce_7(val, _values, result) - result = Group.new(val[1]) - result +def _reduce_7(val, _values) + Group.new(val[1]) end -def _reduce_8(val, _values, result) - result = Or.new([val.first, val.last]) - result +def _reduce_8(val, _values) + Or.new([val.first, val.last]) end -def _reduce_9(val, _values, result) - result = Or.new([val.first, val.last]) - result +def _reduce_9(val, _values) + Or.new([val.first, val.last]) end -def _reduce_10(val, _values, result) - result = Star.new(Symbol.new(val.last)) - result +def _reduce_10(val, _values) + Star.new(Symbol.new(val.last)) end # reduce 11 omitted @@ -179,27 +173,23 @@ end # reduce 14 omitted -def _reduce_15(val, _values, result) - result = Slash.new('/') - result +def _reduce_15(val, _values) + Slash.new('/') end -def _reduce_16(val, _values, result) - result = Symbol.new(val.first) - result +def _reduce_16(val, _values) + Symbol.new(val.first) end -def _reduce_17(val, _values, result) - result = Literal.new(val.first) - result +def _reduce_17(val, _values) + Literal.new(val.first) end -def _reduce_18(val, _values, result) - result = Dot.new(val.first) - result +def _reduce_18(val, _values) + Dot.new(val.first) end -def _reduce_none(val, _values, result) +def _reduce_none(val, _values) val[0] end diff --git a/actionpack/lib/action_dispatch/journey/parser.y b/actionpack/lib/action_dispatch/journey/parser.y index 0ead222551..d3f7c4d765 100644 --- a/actionpack/lib/action_dispatch/journey/parser.y +++ b/actionpack/lib/action_dispatch/journey/parser.y @@ -1,11 +1,11 @@ class ActionDispatch::Journey::Parser - + options no_result_var token SLASH LITERAL SYMBOL LPAREN RPAREN DOT STAR OR rule expressions - : expression expressions { result = Cat.new(val.first, val.last) } - | expression { result = val.first } + : expression expressions { Cat.new(val.first, val.last) } + | expression { val.first } | or ; expression @@ -14,14 +14,14 @@ rule | star ; group - : LPAREN expressions RPAREN { result = Group.new(val[1]) } + : LPAREN expressions RPAREN { Group.new(val[1]) } ; or - : expression OR expression { result = Or.new([val.first, val.last]) } - | expression OR or { result = Or.new([val.first, val.last]) } + : expression OR expression { Or.new([val.first, val.last]) } + | expression OR or { Or.new([val.first, val.last]) } ; star - : STAR { result = Star.new(Symbol.new(val.last)) } + : STAR { Star.new(Symbol.new(val.last)) } ; terminal : symbol @@ -30,16 +30,16 @@ rule | dot ; slash - : SLASH { result = Slash.new('/') } + : SLASH { Slash.new('/') } ; symbol - : SYMBOL { result = Symbol.new(val.first) } + : SYMBOL { Symbol.new(val.first) } ; literal - : LITERAL { result = Literal.new(val.first) } + : LITERAL { Literal.new(val.first) } ; dot - : DOT { result = Dot.new(val.first) } + : DOT { Dot.new(val.first) } ; end diff --git a/actionpack/lib/action_dispatch/middleware/cookies.rb b/actionpack/lib/action_dispatch/middleware/cookies.rb index ac9e5effe2..83ac62a83d 100644 --- a/actionpack/lib/action_dispatch/middleware/cookies.rb +++ b/actionpack/lib/action_dispatch/middleware/cookies.rb @@ -3,6 +3,7 @@ require 'active_support/core_ext/module/attribute_accessors' require 'active_support/core_ext/object/blank' require 'active_support/key_generator' require 'active_support/message_verifier' +require 'active_support/json' module ActionDispatch class Request < Rack::Request @@ -90,6 +91,7 @@ module ActionDispatch SECRET_TOKEN = "action_dispatch.secret_token".freeze SECRET_KEY_BASE = "action_dispatch.secret_key_base".freeze COOKIES_SERIALIZER = "action_dispatch.cookies_serializer".freeze + COOKIES_DIGEST = "action_dispatch.cookies_digest".freeze # Cookies can typically store 4096 bytes. MAX_COOKIE_SIZE = 4096 @@ -173,10 +175,14 @@ module ActionDispatch end end + # Passing the ActiveSupport::MessageEncryptor::NullSerializer downstream + # to the Message{Encryptor,Verifier} allows us to handle the + # (de)serialization step within the cookie jar, which gives us the + # opportunity to detect and migrate legacy cookies. module VerifyAndUpgradeLegacySignedMessage def initialize(*args) super - @legacy_verifier = ActiveSupport::MessageVerifier.new(@options[:secret_token], serializer: NullSerializer) + @legacy_verifier = ActiveSupport::MessageVerifier.new(@options[:secret_token], serializer: ActiveSupport::MessageEncryptor::NullSerializer) end def verify_and_upgrade_legacy_signed_message(name, signed_message) @@ -212,7 +218,8 @@ module ActionDispatch secret_token: env[SECRET_TOKEN], secret_key_base: env[SECRET_KEY_BASE], upgrade_legacy_signed_cookies: env[SECRET_TOKEN].present? && env[SECRET_KEY_BASE].present?, - serializer: env[COOKIES_SERIALIZER] + serializer: env[COOKIES_SERIALIZER], + digest: env[COOKIES_DIGEST] } end @@ -385,24 +392,11 @@ module ActionDispatch class JsonSerializer def self.load(value) - JSON.parse(value, quirks_mode: true) + ActiveSupport::JSON.decode(value) end def self.dump(value) - JSON.generate(value, quirks_mode: true) - end - end - - # Passing the NullSerializer downstream to the Message{Encryptor,Verifier} - # allows us to handle the (de)serialization step within the cookie jar, - # which gives us the opportunity to detect and migrate legacy cookies. - class NullSerializer - def self.load(value) - value - end - - def self.dump(value) - value + ActiveSupport::JSON.encode(value) end end @@ -441,6 +435,10 @@ module ActionDispatch serializer end end + + def digest + @options[:digest] || 'SHA1' + end end class SignedCookieJar #:nodoc: @@ -451,7 +449,7 @@ module ActionDispatch @parent_jar = parent_jar @options = options secret = key_generator.generate_key(@options[:signed_cookie_salt]) - @verifier = ActiveSupport::MessageVerifier.new(secret, serializer: NullSerializer) + @verifier = ActiveSupport::MessageVerifier.new(secret, digest: digest, serializer: ActiveSupport::MessageEncryptor::NullSerializer) end def [](name) @@ -508,7 +506,7 @@ module ActionDispatch @options = options secret = key_generator.generate_key(@options[:encrypted_cookie_salt]) sign_secret = key_generator.generate_key(@options[:encrypted_signed_cookie_salt]) - @encryptor = ActiveSupport::MessageEncryptor.new(secret, sign_secret, serializer: NullSerializer) + @encryptor = ActiveSupport::MessageEncryptor.new(secret, sign_secret, digest: digest, serializer: ActiveSupport::MessageEncryptor::NullSerializer) end def [](name) diff --git a/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb b/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb index 0ca1a87645..274f6f2f22 100644 --- a/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb +++ b/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb @@ -38,9 +38,7 @@ module ActionDispatch template = ActionView::Base.new([RESCUES_TEMPLATE_PATH], request: request, exception: wrapper.exception, - application_trace: wrapper.application_trace, - framework_trace: wrapper.framework_trace, - full_trace: wrapper.full_trace, + traces: traces_from_wrapper(wrapper), routes_inspector: routes_inspector(exception), source_extract: wrapper.source_extract, line_number: wrapper.line_number, @@ -95,5 +93,36 @@ module ActionDispatch ActionDispatch::Routing::RoutesInspector.new(@routes_app.routes.routes) end end + + # Augment the exception traces by providing ids for all unique stack frame + def traces_from_wrapper(wrapper) + application_trace = wrapper.application_trace + framework_trace = wrapper.framework_trace + full_trace = wrapper.full_trace + + if application_trace && framework_trace + id_counter = 0 + + application_trace = application_trace.map do |trace| + prev = id_counter + id_counter += 1 + { id: prev, trace: trace } + end + + framework_trace = framework_trace.map do |trace| + prev = id_counter + id_counter += 1 + { id: prev, trace: trace } + end + + full_trace = application_trace + framework_trace + end + + { + "Application Trace" => application_trace, + "Framework Trace" => framework_trace, + "Full Trace" => full_trace + } + end end end diff --git a/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb b/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb index 2326bb043a..b98b553c38 100644 --- a/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb +++ b/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb @@ -61,12 +61,15 @@ module ActionDispatch end def source_extract - if application_trace && trace = application_trace.first - file, line, _ = trace.split(":") - @file = file - @line_number = line.to_i - source_fragment(@file, @line_number) - end + exception.backtrace.map do |trace| + file, line = trace.split(":") + line_number = line.to_i + { + code: source_fragment(file, line_number), + file: file, + line_number: line_number + } + end if exception.backtrace end private @@ -110,7 +113,7 @@ module ActionDispatch def expand_backtrace @exception.backtrace.unshift( @exception.to_s.split("\n") - ).flatten! + ).flatten! end end end diff --git a/actionpack/lib/action_dispatch/middleware/request_id.rb b/actionpack/lib/action_dispatch/middleware/request_id.rb index 5d1740d0d4..25658bac3d 100644 --- a/actionpack/lib/action_dispatch/middleware/request_id.rb +++ b/actionpack/lib/action_dispatch/middleware/request_id.rb @@ -5,7 +5,7 @@ module ActionDispatch # Makes a unique request id available to the action_dispatch.request_id env variable (which is then accessible through # ActionDispatch::Request#uuid) and sends the same id to the client via the X-Request-Id header. # - # The unique request id is either based off the X-Request-Id header in the request, which would typically be generated + # The unique request id is either based on the X-Request-Id header in the request, which would typically be generated # by a firewall, load balancer, or the web server, or, if this header is not available, a random uuid. If the # header is accepted from the outside world, we sanitize it to a max of 255 chars and alphanumeric and dashes only. # diff --git a/actionpack/lib/action_dispatch/middleware/session/cache_store.rb b/actionpack/lib/action_dispatch/middleware/session/cache_store.rb index 1db6194271..625050dc4b 100644 --- a/actionpack/lib/action_dispatch/middleware/session/cache_store.rb +++ b/actionpack/lib/action_dispatch/middleware/session/cache_store.rb @@ -16,9 +16,9 @@ module ActionDispatch # Get a session from the cache. def get_session(env, sid) - sid ||= generate_sid - session = @cache.read(cache_key(sid)) - session ||= {} + unless sid and session = @cache.read(cache_key(sid)) + sid, session = generate_sid, {} + end [sid, session] end diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/_source.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/_source.erb index 38429cb78e..51660a619b 100644 --- a/actionpack/lib/action_dispatch/middleware/templates/rescues/_source.erb +++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/_source.erb @@ -1,25 +1,29 @@ <% if @source_extract %> -<div class="source"> -<div class="info"> - Extracted source (around line <strong>#<%= @line_number %></strong>): -</div> -<div class="data"> - <table cellpadding="0" cellspacing="0" class="lines"> - <tr> - <td> - <pre class="line_numbers"> - <% @source_extract.keys.each do |line_number| %> + <% @source_extract.each_with_index do |extract_source, index| %> + <% if extract_source[:code] %> + <div class="source <%="hidden" if index != 0%>" id="frame-source-<%=index%>"> + <div class="info"> + Extracted source (around line <strong>#<%= extract_source[:line_number] %></strong>): + </div> + <div class="data"> + <table cellpadding="0" cellspacing="0" class="lines"> + <tr> + <td> + <pre class="line_numbers"> + <% extract_source[:code].keys.each do |line_number| %> <span><%= line_number -%></span> - <% end %> - </pre> - </td> + <% end %> + </pre> + </td> <td width="100%"> <pre> -<% @source_extract.each do |line, source| -%><div class="line<%= " active" if line == @line_number -%>"><%= source -%></div><% end -%> +<% extract_source[:code].each do |line, source| -%><div class="line<%= " active" if line == extract_source[:line_number] -%>"><%= source -%></div><% end -%> </pre> </td> - </tr> - </table> -</div> -</div> + </tr> + </table> + </div> + </div> + <% end %> + <% end %> <% end %> diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb index b181909bff..f62caf51d7 100644 --- a/actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb +++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb @@ -1,9 +1,4 @@ -<% - traces = { "Application Trace" => @application_trace, - "Framework Trace" => @framework_trace, - "Full Trace" => @full_trace } - names = traces.keys -%> +<% names = @traces.keys %> <p><code>Rails.root: <%= defined?(Rails) && Rails.respond_to?(:root) ? Rails.root : "unset" %></code></p> @@ -16,9 +11,42 @@ <a href="#" onclick="<%= hide.join %><%= show %>; return false;"><%= name %></a> <%= '|' unless names.last == name %> <% end %> - <% traces.each do |name, trace| %> + <% @traces.each do |name, trace| %> <div id="<%= name.gsub(/\s/, '-') %>" style="display: <%= (name == "Application Trace") ? 'block' : 'none' %>;"> - <pre><code><%= trace.join "\n" %></code></pre> + <pre><code><% trace.each do |frame| %><a class="trace-frames" data-frame-id="<%= frame[:id] %>" href="#"><%= frame[:trace] %></a><br><% end %></code></pre> </div> <% end %> + + <script type="text/javascript"> + var traceFrames = document.getElementsByClassName('trace-frames'); + var selectedFrame, currentSource = document.getElementById('frame-source-0'); + + // Add click listeners for all stack frames + for (var i = 0; i < traceFrames.length; i++) { + traceFrames[i].addEventListener('click', function(e) { + e.preventDefault(); + var target = e.target; + var frame_id = target.dataset.frameId; + + if (selectedFrame) { + selectedFrame.className = selectedFrame.className.replace("selected", ""); + } + + target.className += " selected"; + selectedFrame = target; + + // Change the extracted source code + changeSourceExtract(frame_id); + }); + + function changeSourceExtract(frame_id) { + var el = document.getElementById('frame-source-' + frame_id); + if (currentSource && el) { + currentSource.className += " hidden"; + el.className = el.className.replace(" hidden", ""); + currentSource = el; + } + } + } + </script> </div> diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.text.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.text.erb index d4af5c9b06..c0b53068f7 100644 --- a/actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.text.erb +++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.text.erb @@ -1,15 +1,9 @@ -<% - traces = { "Application Trace" => @application_trace, - "Framework Trace" => @framework_trace, - "Full Trace" => @full_trace } -%> - Rails.root: <%= defined?(Rails) && Rails.respond_to?(:root) ? Rails.root : "unset" %> -<% traces.each do |name, trace| %> +<% @traces.each do |name, trace| %> <% if trace.any? %> <%= name %> -<%= trace.join("\n") %> +<%= trace.map { |t| t[:trace] }.join("\n") %> <% end %> <% end %> diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/layout.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/layout.erb index bc5d03dc10..e0509f56f4 100644 --- a/actionpack/lib/action_dispatch/middleware/templates/rescues/layout.erb +++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/layout.erb @@ -116,9 +116,15 @@ background-color: #FFCCCC; } + .hidden { + display: none; + } + a { color: #980905; } a:visited { color: #666; } + a.trace-frames { color: #666; } a:hover { color: #C52F24; } + a.trace-frames.selected { color: #C52F24 } <%= yield :style %> </style> diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb index 027a0f5b3e..c1e8b6cae3 100644 --- a/actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb +++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb @@ -1,4 +1,3 @@ -<% @source_extract = @exception.source_extract(0, :html) %> <header> <h1> <%= @exception.original_exception.class.to_s %> in @@ -12,29 +11,7 @@ </p> <pre><code><%= h @exception.message %></code></pre> - <div class="source"> - <div class="info"> - <p>Extracted source (around line <strong>#<%= @exception.line_number %></strong>):</p> - </div> - <div class="data"> - <table cellpadding="0" cellspacing="0" class="lines"> - <tr> - <td> - <pre class="line_numbers"> - <% @source_extract.keys.each do |line_number| %> -<span><%= line_number -%></span> - <% end %> - </pre> - </td> -<td width="100%"> -<pre> -<% @source_extract.each do |line, source| -%><div class="line<%= " active" if line == @exception.line_number -%>"><%= source -%></div><% end -%> -</pre> -</td> - </tr> - </table> -</div> -</div> + <%= render template: "rescues/_source" %> <p><%= @exception.sub_template_message %></p> diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.text.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.text.erb index 5da21d9784..77bcd26726 100644 --- a/actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.text.erb +++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.text.erb @@ -1,4 +1,3 @@ -<% @source_extract = @exception.source_extract(0, :html) %> <%= @exception.original_exception.class.to_s %> in <%= @request.parameters["controller"].camelize if @request.parameters["controller"] %>#<%= @request.parameters["action"] %> Showing <%= @exception.file_name %> where line #<%= @exception.line_number %> raised: diff --git a/actionpack/lib/action_dispatch/routing/mapper.rb b/actionpack/lib/action_dispatch/routing/mapper.rb index 32d963ba76..e92baa5aa7 100644 --- a/actionpack/lib/action_dispatch/routing/mapper.rb +++ b/actionpack/lib/action_dispatch/routing/mapper.rb @@ -13,9 +13,6 @@ module ActionDispatch module Routing class Mapper URL_OPTIONS = [:protocol, :subdomain, :domain, :host, :port] - SCOPE_OPTIONS = [:path, :shallow_path, :as, :shallow_prefix, :module, - :controller, :action, :path_names, :constraints, - :shallow, :blocks, :defaults, :options] class Constraints < Endpoint #:nodoc: attr_reader :app, :constraints @@ -66,7 +63,7 @@ module ActionDispatch attr_reader :requirements, :conditions, :defaults attr_reader :to, :default_controller, :default_action, :as, :anchor - def self.build(scope, set, path, options) + def self.build(scope, set, path, as, options) options = scope[:options].merge(options) if scope[:options] options.delete :only @@ -77,10 +74,10 @@ module ActionDispatch defaults = (scope[:defaults] || {}).merge options.delete(:defaults) || {} - new scope, set, path, defaults, options + new scope, set, path, defaults, as, options end - def initialize(scope, set, path, defaults, options) + def initialize(scope, set, path, defaults, as, options) @requirements, @conditions = {}, {} @defaults = defaults @set = set @@ -88,7 +85,7 @@ module ActionDispatch @to = options.delete :to @default_controller = options.delete(:controller) || scope[:controller] @default_action = options.delete(:action) || scope[:action] - @as = options.delete :as + @as = as @anchor = options.delete :anchor formatted = options.delete :format @@ -617,17 +614,19 @@ module ActionDispatch end def define_generate_prefix(app, name) - _route = @set.named_routes.routes[name.to_sym] + _route = @set.named_routes.get name _routes = @set app.routes.define_mounted_helper(name) app.routes.extend Module.new { - def mounted?; true; end + def optimize_routes_generation?; false; end define_method :find_script_name do |options| - super(options) || begin - prefix_options = options.slice(*_route.segment_keys) - # we must actually delete prefix segment keys to avoid passing them to next url_for - _route.segment_keys.each { |k| options.delete(k) } - _routes.url_helpers.send("#{name}_path", prefix_options) + if options.key? :script_name + super(options) + else + prefix_options = options.slice(*_route.segment_keys) + # we must actually delete prefix segment keys to avoid passing them to next url_for + _route.segment_keys.each { |k| options.delete(k) } + _routes.url_helpers.send("#{name}_path", prefix_options) end end } @@ -771,7 +770,7 @@ module ActionDispatch # end def scope(*args) options = args.extract_options!.dup - recover = {} + scope = {} options[:path] = args.flatten.join('/') if args.any? options[:constraints] ||= {} @@ -791,7 +790,7 @@ module ActionDispatch block, options[:constraints] = options[:constraints], {} end - SCOPE_OPTIONS.each do |option| + @scope.options.each do |option| if option == :blocks value = block elsif option == :options @@ -801,15 +800,15 @@ module ActionDispatch end if value - recover[option] = @scope[option] - @scope[option] = send("merge_#{option}_scope", @scope[option], value) + scope[option] = send("merge_#{option}_scope", @scope[option], value) end end + @scope = @scope.new scope yield self ensure - @scope.merge!(recover) + @scope = @scope.parent end # Scopes routes to a specific controller @@ -1047,8 +1046,6 @@ module ActionDispatch VALID_ON_OPTIONS = [:new, :collection, :member] RESOURCE_OPTIONS = [:as, :controller, :path, :only, :except, :param, :concerns] CANONICAL_ACTIONS = %w(index create new show update destroy) - RESOURCE_METHOD_SCOPES = [:collection, :member, :new] - RESOURCE_SCOPES = [:resource, :resources] class Resource #:nodoc: attr_reader :controller, :path, :options, :param @@ -1522,7 +1519,7 @@ module ActionDispatch if on = options.delete(:on) send(on) { decomposed_match(path, options) } else - case @scope[:scope_level] + case @scope.scope_level when :resources nested { decomposed_match(path, options) } when :resource @@ -1545,13 +1542,13 @@ module ActionDispatch action = nil end - if !options.fetch(:as, true) # if it's set to nil or false - options.delete(:as) - else - options[:as] = name_for_action(options[:as], action) - end + as = if !options.fetch(:as, true) # if it's set to nil or false + options.delete(:as) + else + name_for_action(options.delete(:as), action) + end - mapping = Mapping.build(@scope, @set, URI.parser.escape(path), options) + mapping = Mapping.build(@scope, @set, URI.parser.escape(path), as, options) app, conditions, requirements, defaults, as, anchor = mapping.to_route @set.add_route(app, conditions, requirements, defaults, as, anchor) end @@ -1565,7 +1562,7 @@ module ActionDispatch raise ArgumentError, "must be called with a path and/or options" end - if @scope[:scope_level] == :resources + if @scope.resources? with_scope_level(:root) do scope(parent_resource.path) do super(options) @@ -1632,40 +1629,39 @@ module ActionDispatch end def resource_scope? #:nodoc: - RESOURCE_SCOPES.include? @scope[:scope_level] + @scope.resource_scope? end def resource_method_scope? #:nodoc: - RESOURCE_METHOD_SCOPES.include? @scope[:scope_level] + @scope.resource_method_scope? end def nested_scope? #:nodoc: - @scope[:scope_level] == :nested + @scope.nested? end def with_exclusive_scope begin - old_name_prefix, old_path = @scope[:as], @scope[:path] - @scope[:as], @scope[:path] = nil, nil + @scope = @scope.new(:as => nil, :path => nil) with_scope_level(:exclusive) do yield end ensure - @scope[:as], @scope[:path] = old_name_prefix, old_path + @scope = @scope.parent end end def with_scope_level(kind) - old, @scope[:scope_level] = @scope[:scope_level], kind + @scope = @scope.new_level(kind) yield ensure - @scope[:scope_level] = old + @scope = @scope.parent end def resource_scope(kind, resource) #:nodoc: resource.shallow = @scope[:shallow] - old_resource, @scope[:scope_level_resource] = @scope[:scope_level_resource], resource + @scope = @scope.new(:scope_level_resource => resource) @nesting.push(resource) with_scope_level(kind) do @@ -1673,7 +1669,7 @@ module ActionDispatch end ensure @nesting.pop - @scope[:scope_level_resource] = old_resource + @scope = @scope.parent end def nested_options #:nodoc: @@ -1701,21 +1697,22 @@ module ActionDispatch @scope[:constraints][parent_resource.param] end - def canonical_action?(action, flag) #:nodoc: - flag && resource_method_scope? && CANONICAL_ACTIONS.include?(action.to_s) + def canonical_action?(action) #:nodoc: + resource_method_scope? && CANONICAL_ACTIONS.include?(action.to_s) end def shallow_scope(path, options = {}) #:nodoc: - old_name_prefix, old_path = @scope[:as], @scope[:path] - @scope[:as], @scope[:path] = @scope[:shallow_prefix], @scope[:shallow_path] + scope = { :as => @scope[:shallow_prefix], + :path => @scope[:shallow_path] } + @scope = @scope.new scope scope(path, options) { yield } ensure - @scope[:as], @scope[:path] = old_name_prefix, old_path + @scope = @scope.parent end def path_for_action(action, path) #:nodoc: - if canonical_action?(action, path.blank?) + if path.blank? && canonical_action?(action) @scope[:path].to_s else "#{@scope[:path]}/#{action_path(action, path)}" @@ -1730,15 +1727,17 @@ module ActionDispatch def prefix_name_for_action(as, action) #:nodoc: if as prefix = as - elsif !canonical_action?(action, @scope[:scope_level]) + elsif !canonical_action?(action) prefix = action end - prefix.to_s.tr('-', '_') if prefix + + if prefix && prefix != '/' && !prefix.empty? + Mapper.normalize_name prefix.to_s.tr('-', '_') + end end def name_for_action(as, action) #:nodoc: prefix = prefix_name_for_action(as, action) - prefix = Mapper.normalize_name(prefix) if prefix name_prefix = @scope[:as] if parent_resource @@ -1748,27 +1747,14 @@ module ActionDispatch member_name = parent_resource.member_name end - name = case @scope[:scope_level] - when :nested - [name_prefix, prefix] - when :collection - [prefix, name_prefix, collection_name] - when :new - [prefix, :new, name_prefix, member_name] - when :member - [prefix, name_prefix, member_name] - when :root - [name_prefix, collection_name, prefix] - else - [name_prefix, member_name, prefix] - end + name = @scope.action_name(name_prefix, prefix, collection_name, member_name) - if candidate = name.select(&:present?).join("_").presence + if candidate = name.compact.join("_").presence # If a name was not explicitly given, we check if it is valid # and return nil in case it isn't. Otherwise, we pass the invalid name # forward so the underlying router engine treats it and raises an exception. if as.nil? - candidate unless @set.routes.find { |r| r.name == candidate } || candidate !~ /\A[_a-z]/i + candidate unless candidate !~ /\A[_a-z]/i || @set.named_routes.key?(candidate) else candidate end @@ -1893,9 +1879,83 @@ module ActionDispatch end end + class Scope # :nodoc: + OPTIONS = [:path, :shallow_path, :as, :shallow_prefix, :module, + :controller, :action, :path_names, :constraints, + :shallow, :blocks, :defaults, :options] + + RESOURCE_SCOPES = [:resource, :resources] + RESOURCE_METHOD_SCOPES = [:collection, :member, :new] + + attr_reader :parent, :scope_level + + def initialize(hash, parent = {}, scope_level = nil) + @hash = hash + @parent = parent + @scope_level = scope_level + end + + def nested? + scope_level == :nested + end + + def resources? + scope_level == :resources + end + + def resource_method_scope? + RESOURCE_METHOD_SCOPES.include? scope_level + end + + def action_name(name_prefix, prefix, collection_name, member_name) + case scope_level + when :nested + [name_prefix, prefix] + when :collection + [prefix, name_prefix, collection_name] + when :new + [prefix, :new, name_prefix, member_name] + when :member + [prefix, name_prefix, member_name] + when :root + [name_prefix, collection_name, prefix] + else + [name_prefix, member_name, prefix] + end + end + + def resource_scope? + RESOURCE_SCOPES.include? scope_level + end + + def options + OPTIONS + end + + def new(hash) + self.class.new hash, self, scope_level + end + + def new_level(level) + self.class.new(self, self, level) + end + + def fetch(key, &block) + @hash.fetch(key, &block) + end + + def [](key) + @hash.fetch(key) { @parent[key] } + end + + def []=(k,v) + @hash[k] = v + end + end + def initialize(set) #:nodoc: @set = set - @scope = { :path_names => @set.resources_path_names } + @scope = Scope.new({ :path_names => @set.resources_path_names }) @concerns = {} @nesting = [] end diff --git a/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb b/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb index bd3696cda1..cd8b1ab066 100644 --- a/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb +++ b/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb @@ -142,22 +142,27 @@ module ActionDispatch %w(edit new).each do |action| module_eval <<-EOT, __FILE__, __LINE__ + 1 - def #{action}_polymorphic_url(record_or_hash, options = {}) # def edit_polymorphic_url(record_or_hash, options = {}) - polymorphic_url( # polymorphic_url( - record_or_hash, # record_or_hash, - options.merge(:action => "#{action}")) # options.merge(:action => "edit")) - end # end - # - def #{action}_polymorphic_path(record_or_hash, options = {}) # def edit_polymorphic_path(record_or_hash, options = {}) - polymorphic_url( # polymorphic_url( - record_or_hash, # record_or_hash, - options.merge(:action => "#{action}", :routing_type => :path)) # options.merge(:action => "edit", :routing_type => :path)) - end # end + def #{action}_polymorphic_url(record_or_hash, options = {}) + polymorphic_url_for_action("#{action}", record_or_hash, options) + end + + def #{action}_polymorphic_path(record_or_hash, options = {}) + polymorphic_path_for_action("#{action}", record_or_hash, options) + end EOT end private + def polymorphic_url_for_action(action, record_or_hash, options) + polymorphic_url(record_or_hash, options.merge(:action => action)) + end + + def polymorphic_path_for_action(action, record_or_hash, options) + options = options.merge(:action => action, :routing_type => :path) + polymorphic_path(record_or_hash, options) + end + class HelperMethodBuilder # :nodoc: CACHE = { 'path' => {}, 'url' => {} } diff --git a/actionpack/lib/action_dispatch/routing/route_set.rb b/actionpack/lib/action_dispatch/routing/route_set.rb index 80c705608d..5b3651aaee 100644 --- a/actionpack/lib/action_dispatch/routing/route_set.rb +++ b/actionpack/lib/action_dispatch/routing/route_set.rb @@ -86,36 +86,64 @@ module ActionDispatch # named routes. class NamedRouteCollection #:nodoc: include Enumerable - attr_reader :routes, :helpers, :module + attr_reader :routes, :url_helpers_module def initialize @routes = {} - @helpers = [] - @module = Module.new + @path_helpers = Set.new + @url_helpers = Set.new + @url_helpers_module = Module.new + @path_helpers_module = Module.new + end + + def route_defined?(name) + key = name.to_sym + @path_helpers.include?(key) || @url_helpers.include?(key) end def helper_names - @helpers.map(&:to_s) + @path_helpers.map(&:to_s) + @url_helpers.map(&:to_s) end def clear! - @helpers.each do |helper| - @module.remove_possible_method helper + @path_helpers.each do |helper| + @path_helpers_module.send :undef_method, helper + end + + @url_helpers.each do |helper| + @url_helpers_module.send :undef_method, helper end @routes.clear - @helpers.clear + @path_helpers.clear + @url_helpers.clear end def add(name, route) - routes[name.to_sym] = route - define_named_route_methods(name, route) + key = name.to_sym + path_name = :"#{name}_path" + url_name = :"#{name}_url" + + if routes.key? key + @path_helpers_module.send :undef_method, path_name + @url_helpers_module.send :undef_method, url_name + end + routes[key] = route + define_url_helper @path_helpers_module, route, path_name, route.defaults, name, PATH + define_url_helper @url_helpers_module, route, url_name, route.defaults, name, FULL + + @path_helpers << path_name + @url_helpers << url_name end def get(name) routes[name.to_sym] end + def key?(name) + routes.key? name.to_sym + end + alias []= add alias [] get alias clear clear! @@ -133,6 +161,25 @@ module ActionDispatch routes.length end + def path_helpers_module(warn = false) + if warn + mod = @path_helpers_module + helpers = @path_helpers + Module.new do + include mod + + helpers.each do |meth| + define_method(meth) do |*args, &block| + ActiveSupport::Deprecation.warn("The method `#{meth}` cannot be used here as a full URL is required. Use `#{meth.to_s.sub(/_path$/, '_url')}` instead") + super(*args, &block) + end + end + end + else + @path_helpers_module + end + end + class UrlHelper # :nodoc: def self.create(route, options, route_name, url_strategy) if optimize_helper?(route) @@ -253,24 +300,15 @@ module ActionDispatch # # foo_url(bar, baz, bang, sort_by: 'baz') # - def define_url_helper(route, name, opts, route_key, url_strategy) + def define_url_helper(mod, route, name, opts, route_key, url_strategy) helper = UrlHelper.create(route, opts, route_key, url_strategy) - - @module.remove_possible_method name - @module.module_eval do + mod.module_eval do define_method(name) do |*args| options = nil options = args.pop if args.last.is_a? Hash helper.call self, args, options end end - - helpers << name - end - - def define_named_route_methods(name, route) - define_url_helper route, :"#{name}_path", route.defaults, name, PATH - define_url_helper route, :"#{name}_url", route.defaults, name, FULL end end @@ -293,7 +331,7 @@ module ActionDispatch def initialize(request_class = ActionDispatch::Request) self.named_routes = NamedRouteCollection.new - self.resources_path_names = self.class.default_resources_path_names.dup + self.resources_path_names = self.class.default_resources_path_names self.default_url_options = {} self.request_class = request_class @@ -334,6 +372,7 @@ module ActionDispatch mapper.instance_exec(&block) end end + private :eval_block def finalize! return if @finalized @@ -383,42 +422,51 @@ module ActionDispatch RUBY end - def url_helpers - @url_helpers ||= begin - routes = self - - Module.new do - extend ActiveSupport::Concern - include UrlFor - - # Define url_for in the singleton level so one can do: - # Rails.application.routes.url_helpers.url_for(args) - @_routes = routes - class << self - delegate :url_for, :optimize_routes_generation?, :to => '@_routes' - attr_reader :_routes - def url_options; {}; end - end + def url_helpers(include_path_helpers = true) + routes = self + + Module.new do + extend ActiveSupport::Concern + include UrlFor + + # Define url_for in the singleton level so one can do: + # Rails.application.routes.url_helpers.url_for(args) + @_routes = routes + class << self + delegate :url_for, :optimize_routes_generation?, to: '@_routes' + attr_reader :_routes + def url_options; {}; end + end - # Make named_routes available in the module singleton - # as well, so one can do: - # Rails.application.routes.url_helpers.posts_path - extend routes.named_routes.module + url_helpers = routes.named_routes.url_helpers_module - # Any class that includes this module will get all - # named routes... - include routes.named_routes.module + # Make named_routes available in the module singleton + # as well, so one can do: + # Rails.application.routes.url_helpers.posts_path + extend url_helpers - # plus a singleton class method called _routes ... - included do - singleton_class.send(:redefine_method, :_routes) { routes } - end + # Any class that includes this module will get all + # named routes... + include url_helpers - # And an instance method _routes. Note that - # UrlFor (included in this module) add extra - # conveniences for working with @_routes. - define_method(:_routes) { @_routes || routes } + if include_path_helpers + path_helpers = routes.named_routes.path_helpers_module + else + path_helpers = routes.named_routes.path_helpers_module(true) end + + include path_helpers + extend path_helpers + + # plus a singleton class method called _routes ... + included do + singleton_class.send(:redefine_method, :_routes) { routes } + end + + # And an instance method _routes. Note that + # UrlFor (included in this module) add extra + # conveniences for working with @_routes. + define_method(:_routes) { @_routes || routes } end end @@ -641,16 +689,16 @@ module ActionDispatch :trailing_slash, :anchor, :params, :only_path, :script_name, :original_script_name] - def mounted? - false - end - def optimize_routes_generation? - !mounted? && default_url_options.empty? + default_url_options.empty? end def find_script_name(options) - options.delete :script_name + options.delete(:script_name) { '' } + end + + def path_for(options, route_name = nil) # :nodoc: + url_for(options, route_name, PATH) end # The +options+ argument must be a hash whose keys are *symbols*. @@ -669,7 +717,7 @@ module ActionDispatch original_script_name = options.delete(:original_script_name) script_name = find_script_name options - if script_name && original_script_name + if original_script_name script_name = original_script_name + script_name end diff --git a/actionpack/lib/action_dispatch/routing/url_for.rb b/actionpack/lib/action_dispatch/routing/url_for.rb index e1c73f8f07..eb554ec383 100644 --- a/actionpack/lib/action_dispatch/routing/url_for.rb +++ b/actionpack/lib/action_dispatch/routing/url_for.rb @@ -171,8 +171,7 @@ module ActionDispatch protected def optimize_routes_generation? - return @_optimized_routes if defined?(@_optimized_routes) - @_optimized_routes = _routes.optimize_routes_generation? && default_url_options.empty? + _routes.optimize_routes_generation? && default_url_options.empty? end def _with_routes(routes) diff --git a/actionpack/lib/action_dispatch/testing/assertions/response.rb b/actionpack/lib/action_dispatch/testing/assertions/response.rb index 0adc6c84ff..13a72220b3 100644 --- a/actionpack/lib/action_dispatch/testing/assertions/response.rb +++ b/actionpack/lib/action_dispatch/testing/assertions/response.rb @@ -73,13 +73,8 @@ module ActionDispatch if Regexp === fragment fragment else - handle = @controller || Class.new(ActionController::Metal) do - include ActionController::Redirecting - def initialize(request) - @_request = request - end - end.new(@request) - handle._compute_redirect_to_location(fragment) + handle = @controller || ActionController::Redirecting + handle._compute_redirect_to_location(@request, fragment) end end end diff --git a/actionpack/lib/action_dispatch/testing/assertions/routing.rb b/actionpack/lib/action_dispatch/testing/assertions/routing.rb index f1f998d932..2cf38a9c2d 100644 --- a/actionpack/lib/action_dispatch/testing/assertions/routing.rb +++ b/actionpack/lib/action_dispatch/testing/assertions/routing.rb @@ -165,7 +165,7 @@ module ActionDispatch # ROUTES TODO: These assertions should really work in an integration context def method_missing(selector, *args, &block) - if defined?(@controller) && @controller && @routes && @routes.named_routes.helpers.include?(selector) + if defined?(@controller) && @controller && @routes && @routes.named_routes.route_defined?(selector) @controller.send(selector, *args, &block) else super diff --git a/actionpack/lib/action_dispatch/testing/integration.rb b/actionpack/lib/action_dispatch/testing/integration.rb index 12b796b95f..192ccdb9d5 100644 --- a/actionpack/lib/action_dispatch/testing/integration.rb +++ b/actionpack/lib/action_dispatch/testing/integration.rb @@ -3,6 +3,7 @@ require 'uri' require 'active_support/core_ext/kernel/singleton_class' require 'active_support/core_ext/object/try' require 'rack/test' +require 'minitest' module ActionDispatch module Integration #:nodoc: |