diff options
Diffstat (limited to 'actionpack/lib')
57 files changed, 1416 insertions, 994 deletions
diff --git a/actionpack/lib/abstract_controller/base.rb b/actionpack/lib/abstract_controller/base.rb index af5de815bb..15faabf977 100644 --- a/actionpack/lib/abstract_controller/base.rb +++ b/actionpack/lib/abstract_controller/base.rb @@ -8,7 +8,8 @@ module AbstractController class Error < StandardError #:nodoc: end - class ActionNotFound < StandardError #:nodoc: + # Raised when a non-existing controller action is triggered. + class ActionNotFound < StandardError end # <tt>AbstractController::Base</tt> is a low-level API. Nobody should be @@ -120,14 +121,14 @@ module AbstractController # # The actual method that is called is determined by calling # #method_for_action. If no method can handle the action, then an - # ActionNotFound error is raised. + # AbstractController::ActionNotFound error is raised. # # ==== Returns # * <tt>self</tt> def process(action, *args) - @_action_name = action_name = action.to_s + @_action_name = action.to_s - unless action_name = method_for_action(action_name) + unless action_name = _find_action_name(@_action_name) raise ActionNotFound, "The action '#{action}' could not be found for #{self.class.name}" end @@ -160,7 +161,7 @@ module AbstractController # ==== Returns # * <tt>TrueClass</tt>, <tt>FalseClass</tt> def available_action?(action_name) - method_for_action(action_name).present? + _find_action_name(action_name).present? end private @@ -204,6 +205,24 @@ module AbstractController end # Takes an action name and returns the name of the method that will + # handle the action. + # + # It checks if the action name is valid and returns false otherwise. + # + # See method_for_action for more information. + # + # ==== Parameters + # * <tt>action_name</tt> - An action name to find a method name for + # + # ==== Returns + # * <tt>string</tt> - The name of the method that handles the action + # * false - No valid method name could be found. + # Raise AbstractController::ActionNotFound. + def _find_action_name(action_name) + _valid_action_name?(action_name) && method_for_action(action_name) + end + + # Takes an action name and returns the name of the method that will # handle the action. In normal cases, this method returns the same # name as it receives. By default, if #method_for_action receives # a name that is not an action, it will look for an #action_missing @@ -218,14 +237,14 @@ module AbstractController # the case. # # If none of these conditions are true, and method_for_action - # returns nil, an ActionNotFound exception will be raised. + # returns nil, an AbstractController::ActionNotFound exception will be raised. # # ==== Parameters # * <tt>action_name</tt> - An action name to find a method name for # # ==== Returns # * <tt>string</tt> - The name of the method that handles the action - # * <tt>nil</tt> - No method name could be found. Raise ActionNotFound. + # * <tt>nil</tt> - No method name could be found. def method_for_action(action_name) if action_method?(action_name) action_name @@ -233,5 +252,10 @@ module AbstractController "_handle_action_missing" end end + + # Checks if the action name is valid and returns false otherwise. + def _valid_action_name?(action_name) + !action_name.to_s.include? File::SEPARATOR + end end end diff --git a/actionpack/lib/abstract_controller/callbacks.rb b/actionpack/lib/abstract_controller/callbacks.rb index 69aca308d6..ca5c80cd71 100644 --- a/actionpack/lib/abstract_controller/callbacks.rb +++ b/actionpack/lib/abstract_controller/callbacks.rb @@ -42,20 +42,18 @@ module AbstractController end end - # Skip before, after, and around action callbacks matching any of the names - # Aliased as skip_filter. + # Skip before, after, and around action callbacks matching any of the names. # # ==== Parameters # * <tt>names</tt> - A list of valid names that could be used for # callbacks. Note that skipping uses Ruby equality, so it's # impossible to skip a callback defined using an anonymous proc - # using #skip_filter + # using #skip_action_callback def skip_action_callback(*names) skip_before_action(*names) skip_after_action(*names) skip_around_action(*names) end - alias_method :skip_filter, :skip_action_callback # Take callback names and an optional callback proc, normalize them, @@ -85,7 +83,6 @@ module AbstractController # :call-seq: before_action(names, block) # # Append a callback before actions. See _insert_callbacks for parameter details. - # Aliased as before_filter. ## # :method: prepend_before_action @@ -93,7 +90,6 @@ module AbstractController # :call-seq: prepend_before_action(names, block) # # Prepend a callback before actions. See _insert_callbacks for parameter details. - # Aliased as prepend_before_filter. ## # :method: skip_before_action @@ -101,7 +97,6 @@ module AbstractController # :call-seq: skip_before_action(names) # # Skip a callback before actions. See _insert_callbacks for parameter details. - # Aliased as skip_before_filter. ## # :method: append_before_action @@ -109,7 +104,6 @@ module AbstractController # :call-seq: append_before_action(names, block) # # Append a callback before actions. See _insert_callbacks for parameter details. - # Aliased as append_before_filter. ## # :method: after_action @@ -117,7 +111,6 @@ module AbstractController # :call-seq: after_action(names, block) # # Append a callback after actions. See _insert_callbacks for parameter details. - # Aliased as after_filter. ## # :method: prepend_after_action @@ -125,7 +118,6 @@ module AbstractController # :call-seq: prepend_after_action(names, block) # # Prepend a callback after actions. See _insert_callbacks for parameter details. - # Aliased as prepend_after_filter. ## # :method: skip_after_action @@ -133,7 +125,6 @@ module AbstractController # :call-seq: skip_after_action(names) # # Skip a callback after actions. See _insert_callbacks for parameter details. - # Aliased as skip_after_filter. ## # :method: append_after_action @@ -141,7 +132,6 @@ module AbstractController # :call-seq: append_after_action(names, block) # # Append a callback after actions. See _insert_callbacks for parameter details. - # Aliased as append_after_filter. ## # :method: around_action @@ -149,7 +139,6 @@ module AbstractController # :call-seq: around_action(names, block) # # Append a callback around actions. See _insert_callbacks for parameter details. - # Aliased as around_filter. ## # :method: prepend_around_action @@ -157,7 +146,6 @@ module AbstractController # :call-seq: prepend_around_action(names, block) # # Prepend a callback around actions. See _insert_callbacks for parameter details. - # Aliased as prepend_around_filter. ## # :method: skip_around_action @@ -165,7 +153,6 @@ module AbstractController # :call-seq: skip_around_action(names) # # Skip a callback around actions. See _insert_callbacks for parameter details. - # Aliased as skip_around_filter. ## # :method: append_around_action @@ -173,7 +160,6 @@ module AbstractController # :call-seq: append_around_action(names, block) # # Append a callback around actions. See _insert_callbacks for parameter details. - # Aliased as append_around_filter. # set up before_action, prepend_before_action, skip_before_action, etc. # for each of before, after, and around. @@ -183,7 +169,6 @@ module AbstractController set_callback(:process_action, callback, name, options) end end - alias_method :"#{callback}_filter", :"#{callback}_action" define_method "prepend_#{callback}_action" do |*names, &blk| @@ -191,7 +176,6 @@ module AbstractController set_callback(:process_action, callback, name, options.merge(:prepend => true)) end end - alias_method :"prepend_#{callback}_filter", :"prepend_#{callback}_action" # Skip a before, after or around callback. See _insert_callbacks @@ -201,12 +185,11 @@ module AbstractController skip_callback(:process_action, callback, name, options) end end - alias_method :"skip_#{callback}_filter", :"skip_#{callback}_action" # *_action is the same as append_*_action - alias_method :"append_#{callback}_action", :"#{callback}_action" # alias_method :append_before_action, :before_action - alias_method :"append_#{callback}_filter", :"#{callback}_action" # alias_method :append_before_filter, :before_action + alias_method :"append_#{callback}_action", :"#{callback}_action" + alias_method :"append_#{callback}_filter", :"#{callback}_action" end end end diff --git a/actionpack/lib/abstract_controller/url_for.rb b/actionpack/lib/abstract_controller/url_for.rb index 4a95e1f276..72d07b0927 100644 --- a/actionpack/lib/abstract_controller/url_for.rb +++ b/actionpack/lib/abstract_controller/url_for.rb @@ -11,7 +11,7 @@ module AbstractController def _routes raise "In order to use #url_for, you must include routing helpers explicitly. " \ - "For instance, `include Rails.application.routes.url_helpers" + "For instance, `include Rails.application.routes.url_helpers`." end module ClassMethods diff --git a/actionpack/lib/action_controller/caching/fragments.rb b/actionpack/lib/action_controller/caching/fragments.rb index 879d5fdd94..2694d4c12f 100644 --- a/actionpack/lib/action_controller/caching/fragments.rb +++ b/actionpack/lib/action_controller/caching/fragments.rb @@ -90,7 +90,13 @@ module ActionController end def instrument_fragment_cache(name, key) # :nodoc: - ActiveSupport::Notifications.instrument("#{name}.action_controller", :key => key){ yield } + payload = { + controller: controller_name, + action: action_name, + key: key + } + + ActiveSupport::Notifications.instrument("#{name}.action_controller", payload) { yield } end end end diff --git a/actionpack/lib/action_controller/log_subscriber.rb b/actionpack/lib/action_controller/log_subscriber.rb index b1acca2435..89fa75f025 100644 --- a/actionpack/lib/action_controller/log_subscriber.rb +++ b/actionpack/lib/action_controller/log_subscriber.rb @@ -16,50 +16,51 @@ module ActionController end def process_action(event) - return unless logger.info? - - payload = event.payload - additions = ActionController::Base.log_process_action(payload) - - status = payload[:status] - if status.nil? && payload[:exception].present? - exception_class_name = payload[:exception].first - status = ActionDispatch::ExceptionWrapper.status_code_for_exception(exception_class_name) + info do + payload = event.payload + additions = ActionController::Base.log_process_action(payload) + + status = payload[:status] + if status.nil? && payload[:exception].present? + exception_class_name = payload[:exception].first + status = ActionDispatch::ExceptionWrapper.status_code_for_exception(exception_class_name) + end + message = "Completed #{status} #{Rack::Utils::HTTP_STATUS_CODES[status]} in #{event.duration.round}ms" + message << " (#{additions.join(" | ")})" unless additions.blank? + message end - message = "Completed #{status} #{Rack::Utils::HTTP_STATUS_CODES[status]} in #{event.duration.round}ms" - message << " (#{additions.join(" | ")})" unless additions.blank? - - info(message) end def halted_callback(event) - info("Filter chain halted as #{event.payload[:filter].inspect} rendered or redirected") + info { "Filter chain halted as #{event.payload[:filter].inspect} rendered or redirected" } end def send_file(event) - info("Sent file #{event.payload[:path]} (#{event.duration.round(1)}ms)") + info { "Sent file #{event.payload[:path]} (#{event.duration.round(1)}ms)" } end def redirect_to(event) - info("Redirected to #{event.payload[:location]}") + info { "Redirected to #{event.payload[:location]}" } end def send_data(event) - info("Sent data #{event.payload[:filename]} (#{event.duration.round(1)}ms)") + info { "Sent data #{event.payload[:filename]} (#{event.duration.round(1)}ms)" } end def unpermitted_parameters(event) - unpermitted_keys = event.payload[:keys] - debug("Unpermitted parameter#{'s' if unpermitted_keys.size > 1}: #{unpermitted_keys.join(", ")}") + debug do + unpermitted_keys = event.payload[:keys] + "Unpermitted parameter#{'s' if unpermitted_keys.size > 1}: #{unpermitted_keys.join(", ")}" + end end def deep_munge(event) - message = "Value for params[:#{event.payload[:keys].join('][:')}] was set "\ - "to nil, because it was one of [], [null] or [null, null, ...]. "\ - "Go to http://guides.rubyonrails.org/security.html#unsafe-query-generation "\ - "for more information."\ - - debug(message) + debug do + "Value for params[:#{event.payload[:keys].join('][:')}] was set "\ + "to nil, because it was one of [], [null] or [null, null, ...]. "\ + "Go to http://guides.rubyonrails.org/security.html#unsafe-query-generation "\ + "for more information."\ + end end %w(write_fragment read_fragment exist_fragment? diff --git a/actionpack/lib/action_controller/metal.rb b/actionpack/lib/action_controller/metal.rb index 0f4cc7a8f5..9a427ebfdb 100644 --- a/actionpack/lib/action_controller/metal.rb +++ b/actionpack/lib/action_controller/metal.rb @@ -30,10 +30,8 @@ module ActionController end end - def build(action, app=nil, &block) - app ||= block + def build(action, app = Proc.new) action = action.to_s - raise "MiddlewareStack#build requires an app" unless app middlewares.reverse.inject(app) do |a, middleware| middleware.valid?(action) ? middleware.build(a) : a @@ -223,14 +221,23 @@ module ActionController # Makes the controller a Rack endpoint that runs the action in the given # +env+'s +action_dispatch.request.path_parameters+ key. def self.call(env) - action(env['action_dispatch.request.path_parameters'][:action]).call(env) + req = ActionDispatch::Request.new env + action(req.path_parameters[:action]).call(env) end # Returns a Rack endpoint for the given action name. def self.action(name, klass = ActionDispatch::Request) - middleware_stack.build(name.to_s) do |env| - new.dispatch(name, klass.new(env)) + if middleware_stack.any? + middleware_stack.build(name) do |env| + new.dispatch(name, klass.new(env)) + end + else + lambda { |env| new.dispatch(name, klass.new(env)) } end end + + def _status_code + @_status + end end end diff --git a/actionpack/lib/action_controller/metal/head.rb b/actionpack/lib/action_controller/metal/head.rb index 43407f5b78..3d2badf9c2 100644 --- a/actionpack/lib/action_controller/metal/head.rb +++ b/actionpack/lib/action_controller/metal/head.rb @@ -14,6 +14,8 @@ module ActionController # return head(:method_not_allowed) unless request.post? # return head(:bad_request) unless valid_request? # render + # + # See Rack::Utils::SYMBOL_TO_STATUS_CODE for a full list of valid +status+ symbols. def head(status, options = {}) options, status = status, nil if status.is_a?(Hash) status ||= options.delete(:status) || :ok @@ -27,7 +29,7 @@ module ActionController self.status = status self.location = url_for(location) if location - if include_content?(self.status) + if include_content?(self._status_code) self.content_type = content_type || (Mime[formats.first] if formats) self.response.charset = false if self.response self.response_body = " " diff --git a/actionpack/lib/action_controller/metal/http_authentication.rb b/actionpack/lib/action_controller/metal/http_authentication.rb index 2eb7853aa6..25c123edf7 100644 --- a/actionpack/lib/action_controller/metal/http_authentication.rb +++ b/actionpack/lib/action_controller/metal/http_authentication.rb @@ -90,17 +90,29 @@ module ActionController end def authenticate(request, &login_procedure) - unless request.authorization.blank? + if has_basic_credentials?(request) login_procedure.call(*user_name_and_password(request)) end end + def has_basic_credentials?(request) + request.authorization.present? && (auth_scheme(request) == 'Basic') + end + def user_name_and_password(request) decode_credentials(request).split(':', 2) end def decode_credentials(request) - ::Base64.decode64(request.authorization.split(' ', 2).last || '') + ::Base64.decode64(auth_param(request) || '') + end + + def auth_scheme(request) + request.authorization.split(' ', 2).first + end + + def auth_param(request) + request.authorization.split(' ', 2).second end def encode_credentials(user_name, password) @@ -109,8 +121,8 @@ module ActionController def authentication_request(controller, realm) controller.headers["WWW-Authenticate"] = %(Basic realm="#{realm.gsub(/"/, "")}") - controller.response_body = "HTTP Basic: Access denied.\n" controller.status = 401 + controller.response_body = "HTTP Basic: Access denied.\n" end end @@ -244,8 +256,8 @@ module ActionController def authentication_request(controller, realm, message = nil) message ||= "HTTP Digest: Access denied.\n" authentication_header(controller, realm) - controller.response_body = message controller.status = 401 + controller.response_body = message end def secret_token(request) @@ -437,7 +449,7 @@ module ActionController authorization_request = request.authorization.to_s if authorization_request[TOKEN_REGEX] params = token_params_from authorization_request - [params.shift.last, Hash[params].with_indifferent_access] + [params.shift[1], Hash[params].with_indifferent_access] end end @@ -452,14 +464,14 @@ module ActionController # This removes the `"` characters wrapping the value. def rewrite_param_values(array_params) - array_params.each { |param| param.last.gsub! %r/^"|"$/, '' } + array_params.each { |param| (param[1] || "").gsub! %r/^"|"$/, '' } end # This method takes an authorization body and splits up the key-value # pairs by the standardized `:`, `;`, or `\t` delimiters defined in # `AUTHN_PAIR_DELIMITERS`. def raw_params(auth) - auth.sub(TOKEN_REGEX, '').split(/"\s*#{AUTHN_PAIR_DELIMITERS}\s*/) + auth.sub(TOKEN_REGEX, '').split(/\s*#{AUTHN_PAIR_DELIMITERS}\s*/) end # Encodes the given token and options into an Authorization header value. diff --git a/actionpack/lib/action_controller/metal/live.rb b/actionpack/lib/action_controller/metal/live.rb index acf40b2e16..706ce04062 100644 --- a/actionpack/lib/action_controller/metal/live.rb +++ b/actionpack/lib/action_controller/metal/live.rb @@ -102,16 +102,30 @@ module ActionController end end - @stream.write "data: #{json}\n\n" + message = json.gsub("\n", "\ndata: ") + @stream.write "data: #{message}\n\n" end end + class ClientDisconnected < RuntimeError + end + class Buffer < ActionDispatch::Response::Buffer #:nodoc: include MonitorMixin + # Ignore that the client has disconnected. + # + # If this value is `true`, calling `write` after the client + # disconnects will result in the written content being silently + # discarded. If this value is `false` (the default), a + # ClientDisconnected exception will be raised. + attr_accessor :ignore_disconnect + def initialize(response) @error_callback = lambda { true } @cv = new_cond + @aborted = false + @ignore_disconnect = false super(response, SizedQueue.new(10)) end @@ -122,6 +136,17 @@ module ActionController end super + + unless connected? + @buf.clear + + unless @ignore_disconnect + # Raise ClientDisconnected, which is a RuntimeError (not an + # IOError), because that's more appropriate for something beyond + # the developer's control. + raise ClientDisconnected, "client disconnected" + end + end end def each @@ -132,6 +157,10 @@ module ActionController @response.sent! end + # Write a 'close' event to the buffer; the producer/writing thread + # uses this to notify us that it's finished supplying content. + # + # See also #abort. def close synchronize do super @@ -140,6 +169,26 @@ module ActionController end end + # Inform the producer/writing thread that the client has + # disconnected; the reading thread is no longer interested in + # anything that's being written. + # + # See also #close. + def abort + synchronize do + @aborted = true + @buf.clear + end + end + + # Is the client still connected and waiting for content? + # + # The result of calling `write` when this is `false` is determined + # by `ignore_disconnect`. + def connected? + !@aborted + end + def await_close synchronize do @cv.wait_until { @closed } @@ -156,7 +205,7 @@ module ActionController end class Response < ActionDispatch::Response #:nodoc: all - class Header < DelegateClass(Hash) + class Header < DelegateClass(Hash) # :nodoc: def initialize(response, header) @response = response super(header) diff --git a/actionpack/lib/action_controller/metal/mime_responds.rb b/actionpack/lib/action_controller/metal/mime_responds.rb index 1974bbf529..00e7e980f8 100644 --- a/actionpack/lib/action_controller/metal/mime_responds.rb +++ b/actionpack/lib/action_controller/metal/mime_responds.rb @@ -322,7 +322,7 @@ module ActionController #:nodoc: # end # end # - # * for a javascript request - if the template isn't found, an exception is + # * for a JavaScript request - if the template isn't found, an exception is # raised. # * for other requests - i.e. data formats such as xml, json, csv etc, if # the resource passed to +respond_with+ responds to <code>to_<format></code>, @@ -335,7 +335,7 @@ module ActionController #:nodoc: # As outlined above, the +resources+ argument passed to +respond_with+ # can play two roles. It can be used to generate the redirect url # for successful html requests (e.g. for +create+ actions when - # no template exists), while for formats other than html and javascript + # no template exists), while for formats other than html and JavaScript # it is the object that gets rendered, by being converted directly to the # required format (again assuming no template exists). # @@ -352,7 +352,7 @@ module ActionController #:nodoc: # # This would cause +respond_with+ to redirect to <code>project_task_url</code> # instead of <code>task_url</code>. For request formats other than html or - # javascript, if multiple resources are passed in this way, it is the last + # JavaScript, if multiple resources are passed in this way, it is the last # one specified that is rendered. # # === Customizing response behavior diff --git a/actionpack/lib/action_controller/metal/rack_delegation.rb b/actionpack/lib/action_controller/metal/rack_delegation.rb index bdf6e88699..6921834044 100644 --- a/actionpack/lib/action_controller/metal/rack_delegation.rb +++ b/actionpack/lib/action_controller/metal/rack_delegation.rb @@ -6,7 +6,7 @@ module ActionController extend ActiveSupport::Concern delegate :headers, :status=, :location=, :content_type=, - :status, :location, :content_type, :to => "@_response" + :status, :location, :content_type, :_status_code, :to => "@_response" def dispatch(action, request) set_response!(request) diff --git a/actionpack/lib/action_controller/metal/redirecting.rb b/actionpack/lib/action_controller/metal/redirecting.rb index 2812038938..ca8c0278d0 100644 --- a/actionpack/lib/action_controller/metal/redirecting.rb +++ b/actionpack/lib/action_controller/metal/redirecting.rb @@ -14,7 +14,7 @@ module ActionController include ActionController::RackDelegation include ActionController::UrlFor - # Redirects the browser to the target specified in +options+. This parameter can take one of three forms: + # Redirects the browser to the target specified in +options+. This parameter can be any one of: # # * <tt>Hash</tt> - The URL will be generated by calling url_for with the +options+. # * <tt>Record</tt> - The URL will be generated by calling url_for with the +options+, which will reference a named URL for that record. @@ -24,6 +24,8 @@ module ActionController # * <tt>:back</tt> - Back to the page that issued the request. Useful for forms that are triggered from multiple places. # Short-hand for <tt>redirect_to(request.env["HTTP_REFERER"])</tt> # + # === Examples: + # # redirect_to action: "show", id: 5 # redirect_to post # redirect_to "http://www.rubyonrails.org" @@ -32,7 +34,7 @@ module ActionController # redirect_to :back # redirect_to proc { edit_post_url(@post) } # - # The redirection happens as a "302 Found" header unless otherwise specified. + # The redirection happens as a "302 Found" header unless otherwise specified using the <tt>:status</tt> option: # # redirect_to post_url(@post), status: :found # redirect_to action: 'atom', status: :moved_permanently @@ -60,15 +62,18 @@ module ActionController # redirect_to post_url(@post), status: 301, flash: { updated_post_id: @post.id } # redirect_to({ action: 'atom' }, alert: "Something serious happened") # - # When using <tt>redirect_to :back</tt>, if there is no referrer, ActionController::RedirectBackError will be raised. You may specify some fallback - # behavior for this case by rescuing ActionController::RedirectBackError. + # When using <tt>redirect_to :back</tt>, if there is no referrer, + # <tt>ActionController::RedirectBackError</tt> will be raised. You + # may specify some fallback behavior for this case by rescuing + # <tt>ActionController::RedirectBackError</tt>. def redirect_to(options = {}, response_status = {}) #:doc: raise ActionControllerError.new("Cannot redirect to nil!") unless options + raise ActionControllerError.new("Cannot redirect to a parameter hash!") if options.is_a?(ActionController::Parameters) raise AbstractController::DoubleRenderError if response_body self.status = _extract_redirect_to_status(options, response_status) self.location = _compute_redirect_to_location(options) - self.response_body = "<html><body>You are being <a href=\"#{ERB::Util.h(location)}\">redirected</a>.</body></html>" + self.response_body = "<html><body>You are being <a href=\"#{ERB::Util.unwrapped_html_escape(location)}\">redirected</a>.</body></html>" end def _compute_redirect_to_location(options) #:nodoc: diff --git a/actionpack/lib/action_controller/metal/renderers.rb b/actionpack/lib/action_controller/metal/renderers.rb index 0443b73953..02c4e563f5 100644 --- a/actionpack/lib/action_controller/metal/renderers.rb +++ b/actionpack/lib/action_controller/metal/renderers.rb @@ -6,6 +6,11 @@ module ActionController Renderers.add(key, &block) end + # See <tt>Renderers.remove</tt> + def self.remove_renderer(key) + Renderers.remove(key) + end + class MissingRenderer < LoadError def initialize(format) super "No renderer defined for format: #{format}" @@ -73,7 +78,7 @@ module ActionController # respond_to do |format| # format.html # format.csv { render csv: @csvable, filename: @csvable.name } - # } + # end # end # To use renderers and their mime types in more concise ways, see # <tt>ActionController::MimeResponds::ClassMethods.respond_to</tt> and @@ -83,6 +88,17 @@ module ActionController RENDERERS << key.to_sym end + # This method is the opposite of add method. + # + # Usage: + # + # ActionController::Renderers.remove(:csv) + def self.remove(key) + RENDERERS.delete(key.to_sym) + method = "_render_option_#{key}" + remove_method(method) if method_defined?(method) + end + module All extend ActiveSupport::Concern include Renderers @@ -96,8 +112,11 @@ module ActionController json = json.to_json(options) unless json.kind_of?(String) if options[:callback].present? - self.content_type ||= Mime::JS - "#{options[:callback]}(#{json})" + if self.content_type.nil? || self.content_type == Mime::JSON + self.content_type = Mime::JS + end + + "/**/#{options[:callback]}(#{json})" else self.content_type ||= Mime::JSON json diff --git a/actionpack/lib/action_controller/metal/rendering.rb b/actionpack/lib/action_controller/metal/rendering.rb index 93e7d6954c..7bbff0450a 100644 --- a/actionpack/lib/action_controller/metal/rendering.rb +++ b/actionpack/lib/action_controller/metal/rendering.rb @@ -67,8 +67,8 @@ module ActionController options[:html] = ERB::Util.html_escape(options[:html]) end - if options.delete(:nothing) || _any_render_format_is_nil?(options) - options[:body] = " " + if options.delete(:nothing) + options[:body] = nil end if options[:status] @@ -86,10 +86,6 @@ module ActionController end end - def _any_render_format_is_nil?(options) - RENDER_FORMATS_IN_PRIORITY.any? { |format| options.key?(format) && options[format].nil? } - end - # Process controller specific options, as status, content-type and location. def _process_options(options) #:nodoc: status, content_type, location = options.values_at(:status, :content_type, :location) diff --git a/actionpack/lib/action_controller/metal/request_forgery_protection.rb b/actionpack/lib/action_controller/metal/request_forgery_protection.rb index e3b1f5ae7c..0efa0fb259 100644 --- a/actionpack/lib/action_controller/metal/request_forgery_protection.rb +++ b/actionpack/lib/action_controller/metal/request_forgery_protection.rb @@ -77,7 +77,7 @@ module ActionController #:nodoc: end module ClassMethods - # Turn on request forgery protection. Bear in mind that only non-GET, HTML/JavaScript requests are checked. + # Turn on request forgery protection. Bear in mind that GET and HEAD requests are not checked. # # class ApplicationController < ActionController::Base # protect_from_forgery @@ -247,7 +247,7 @@ module ActionController #:nodoc: # * Does the X-CSRF-Token header match the form_authenticity_token def verified_request? !protect_against_forgery? || request.get? || request.head? || - form_authenticity_token == params[request_forgery_protection_token] || + form_authenticity_token == form_authenticity_param || form_authenticity_token == request.headers['X-CSRF-Token'] end diff --git a/actionpack/lib/action_controller/metal/streaming.rb b/actionpack/lib/action_controller/metal/streaming.rb index 62d5931b45..04401cad7b 100644 --- a/actionpack/lib/action_controller/metal/streaming.rb +++ b/actionpack/lib/action_controller/metal/streaming.rb @@ -183,7 +183,7 @@ module ActionController #:nodoc: # You may also want to configure other parameters like <tt>:tcp_nodelay</tt>. # Please check its documentation for more information: http://unicorn.bogomips.org/Unicorn/Configurator.html#method-i-listen # - # If you are using Unicorn with Nginx, you may need to tweak Nginx. + # If you are using Unicorn with NGINX, you may need to tweak NGINX. # Streaming should work out of the box on Rainbows. # # ==== Passenger diff --git a/actionpack/lib/action_controller/metal/strong_parameters.rb b/actionpack/lib/action_controller/metal/strong_parameters.rb index d86d49c9dc..bc27ecaa20 100644 --- a/actionpack/lib/action_controller/metal/strong_parameters.rb +++ b/actionpack/lib/action_controller/metal/strong_parameters.rb @@ -1,5 +1,6 @@ require 'active_support/core_ext/hash/indifferent_access' require 'active_support/core_ext/array/wrap' +require 'active_support/deprecation' require 'active_support/rescuable' require 'action_dispatch/http/upload' require 'stringio' @@ -39,7 +40,7 @@ module ActionController # == Action Controller \Parameters # # Allows to choose which attributes should be whitelisted for mass updating - # and thus prevent accidentally exposing that which shouldn’t be exposed. + # and thus prevent accidentally exposing that which shouldn't be exposed. # Provides two methods for this purpose: #require and #permit. The former is # used to mark parameters as required. The latter is used to set the parameter # as permitted and limit which attributes should be allowed for mass updating. @@ -101,9 +102,23 @@ module ActionController cattr_accessor :permit_all_parameters, instance_accessor: false cattr_accessor :action_on_unpermitted_parameters, instance_accessor: false - # Never raise an UnpermittedParameters exception because of these params - # are present. They are added by Rails and it's of no concern. - NEVER_UNPERMITTED_PARAMS = %w( controller action ) + # By default, never raise an UnpermittedParameters exception if these + # params are present. The default includes both 'controller' and 'action' + # because they are added by Rails and should be of no concern. One way + # to change these is to specify `always_permitted_parameters` in your + # config. For instance: + # + # config.always_permitted_parameters = %w( controller action format ) + cattr_accessor :always_permitted_parameters + self.always_permitted_parameters = %w( controller action ) + + def self.const_missing(const_name) + super unless const_name == :NEVER_UNPERMITTED_PARAMS + ActiveSupport::Deprecation.warn "`ActionController::Parameters::NEVER_UNPERMITTED_PARAMS`"\ + " has been deprecated. Use "\ + "`ActionController::Parameters.always_permitted_parameters` instead." + self.always_permitted_parameters + end # Returns a new instance of <tt>ActionController::Parameters</tt>. # Also, sets the +permitted+ attribute to the default value of @@ -129,6 +144,10 @@ module ActionController # Attribute that keeps track of converted arrays, if any, to avoid double # looping in the common use case permit + mass-assignment. Defined in a # method to instantiate it only if needed. + # + # Testing membership still loops, but it's going to be faster than our own + # loop that converts values. Also, we are not going to build a new array + # object per fetch. def converted_arrays @converted_arrays ||= Set.new end @@ -158,8 +177,8 @@ module ActionController def permit! each_pair do |key, value| value = convert_hashes_to_parameters(key, value) - Array.wrap(value).each do |_| - _.permit! if _.respond_to? :permit! + Array.wrap(value).each do |v| + v.permit! if v.respond_to? :permit! end end @@ -180,7 +199,12 @@ module ActionController # ActionController::Parameters.new(person: {}).require(:person) # # => ActionController::ParameterMissing: param not found: person def require(key) - self[key].presence || raise(ParameterMissing.new(key)) + value = self[key] + if value.present? || value == false + value + else + raise ParameterMissing.new(key) + end end # Alias of #require. @@ -380,7 +404,7 @@ module ActionController end def unpermitted_keys(params) - self.keys - params.keys - NEVER_UNPERMITTED_PARAMS + self.keys - params.keys - self.always_permitted_parameters end # diff --git a/actionpack/lib/action_controller/metal/url_for.rb b/actionpack/lib/action_controller/metal/url_for.rb index 37d4a96ee1..07265be3fe 100644 --- a/actionpack/lib/action_controller/metal/url_for.rb +++ b/actionpack/lib/action_controller/metal/url_for.rb @@ -23,12 +23,12 @@ module ActionController include AbstractController::UrlFor def url_options - @_url_options ||= super.reverse_merge( + @_url_options ||= { :host => request.host, :port => request.optional_port, :protocol => request.protocol, - :_recall => request.symbolized_path_parameters - ).freeze + :_recall => request.path_parameters + }.merge(super).freeze if (same_origin = _routes.equal?(env["action_dispatch.routes".freeze])) || (script_name = env["ROUTES_#{_routes.object_id}_SCRIPT_NAME"]) || diff --git a/actionpack/lib/action_controller/railtie.rb b/actionpack/lib/action_controller/railtie.rb index a2fc814221..28b20052b5 100644 --- a/actionpack/lib/action_controller/railtie.rb +++ b/actionpack/lib/action_controller/railtie.rb @@ -23,6 +23,10 @@ module ActionController options = app.config.action_controller ActionController::Parameters.permit_all_parameters = options.delete(:permit_all_parameters) { false } + if app.config.action_controller[:always_permitted_parameters] + ActionController::Parameters.always_permitted_parameters = + app.config.action_controller.delete(:always_permitted_parameters) + end ActionController::Parameters.action_on_unpermitted_parameters = options.delete(:action_on_unpermitted_parameters) do (Rails.env.test? || Rails.env.development?) ? :log : false end diff --git a/actionpack/lib/action_controller/test_case.rb b/actionpack/lib/action_controller/test_case.rb index caaebc537a..71cb224f22 100644 --- a/actionpack/lib/action_controller/test_case.rb +++ b/actionpack/lib/action_controller/test_case.rb @@ -12,11 +12,13 @@ module ActionController teardown :teardown_subscriptions end + RENDER_TEMPLATE_INSTANCE_VARIABLES = %w{partials templates layouts files}.freeze + def setup_subscriptions - @_partials = Hash.new(0) - @_templates = Hash.new(0) - @_layouts = Hash.new(0) - @_files = Hash.new(0) + RENDER_TEMPLATE_INSTANCE_VARIABLES.each do |instance_variable| + instance_variable_set("@_#{instance_variable}", Hash.new(0)) + end + @_subscribers = [] @_subscribers << ActiveSupport::Notifications.subscribe("render_template.action_view") do |_name, _start, _finish, _id, payload| @@ -30,25 +32,21 @@ module ActionController end @_subscribers << ActiveSupport::Notifications.subscribe("!render_template.action_view") do |_name, _start, _finish, _id, payload| - path = payload[:virtual_path] - next unless path - partial = path =~ /^.*\/_[^\/]*$/ - - if partial - @_partials[path] += 1 - @_partials[path.split("/").last] += 1 - end - - @_templates[path] += 1 - end + if virtual_path = payload[:virtual_path] + partial = virtual_path =~ /^.*\/_[^\/]*$/ - @_subscribers << ActiveSupport::Notifications.subscribe("!render_template.action_view") do |_name, _start, _finish, _id, payload| - next if payload[:virtual_path] # files don't have virtual path + if partial + @_partials[virtual_path] += 1 + @_partials[virtual_path.split("/").last] += 1 + end - path = payload[:identifier] - if path - @_files[path] += 1 - @_files[path.split("/").last] += 1 + @_templates[virtual_path] += 1 + else + path = payload[:identifier] + if path + @_files[path] += 1 + @_files[path.split("/").last] += 1 + end end end end @@ -60,12 +58,16 @@ module ActionController end def process(*args) - @_partials = Hash.new(0) - @_templates = Hash.new(0) - @_layouts = Hash.new(0) + reset_template_assertion super end + def reset_template_assertion + RENDER_TEMPLATE_INSTANCE_VARIABLES.each do |instance_variable| + instance_variable_get("@_#{instance_variable}").clear + end + end + # Asserts that the request was rendered with the appropriate template file or partials. # # # assert that the "new" view template was rendered @@ -199,7 +201,7 @@ module ActionController value = value.dup end - if extra_keys.include?(key.to_sym) + if extra_keys.include?(key) non_path_parameters[key] = value else if value.is_a?(Array) @@ -208,7 +210,7 @@ module ActionController value = value.to_param end - path_parameters[key.to_s] = value + path_parameters[key] = value end end @@ -233,7 +235,6 @@ module ActionController @formats = nil @env.delete_if { |k, v| k =~ /^(action_dispatch|rack)\.request/ } @env.delete_if { |k, v| k =~ /^action_dispatch\.rescue/ } - @symbolized_path_params = nil @method = @request_method = nil @fullpath = @ip = @remote_ip = @protocol = nil @env['action_dispatch.request.query_parameters'] = {} @@ -550,6 +551,31 @@ module ActionController end end + # Simulate a HTTP request to +action+ by specifying request method, + # parameters and set/volley the response. + # + # - +action+: The controller action to call. + # - +http_method+: Request method used to send the http request. Possible values + # are +GET+, +POST+, +PATCH+, +PUT+, +DELETE+, +HEAD+. Defaults to +GET+. + # - +parameters+: The HTTP parameters. This may be +nil+, a hash, or a + # string that is appropriately encoded (+application/x-www-form-urlencoded+ + # or +multipart/form-data+). + # - +session+: A hash of parameters to store in the session. This may be +nil+. + # - +flash+: A hash of parameters to store in the flash. This may be +nil+. + # + # Example calling +create+ action and sending two params: + # + # process :create, 'POST', user: { name: 'Gaurish Sharma', email: 'user@example.com' } + # + # Example sending parameters, +nil+ session and setting a flash message: + # + # process :view, 'GET', { id: 7 }, nil, { notice: 'This is flash message' } + # + # To simulate +GET+, +POST+, +PATCH+, +PUT+, +DELETE+ and +HEAD+ requests + # prefer using #get, #post, #patch, #put, #delete and #head methods + # respectively which will make tests more expressive. + # + # Note that the request method is not verified. def process(action, http_method = 'GET', *args) check_required_ivars @@ -558,6 +584,7 @@ module ActionController end parameters, session, flash = args + parameters ||= {} # Ensure that numbers and symbols passed as params are converted to # proper params, as is the case when engaging rack. @@ -567,7 +594,6 @@ module ActionController unless @controller.respond_to?(:recycle!) @controller.extend(Testing::Functional) - @controller.class.class_eval { include Testing } end @request.recycle! @@ -576,7 +602,6 @@ module ActionController @request.env['REQUEST_METHOD'] = http_method - parameters ||= {} controller_class_name = @controller.class.anonymous? ? "anonymous" : @controller.class.controller_path @@ -604,8 +629,11 @@ module ActionController @response.prepare! @assigns = @controller.respond_to?(:view_assigns) ? @controller.view_assigns : {} - @request.session['flash'] = @request.flash.to_session_value - @request.session.delete('flash') if @request.session['flash'].blank? + + if flash_value = @request.flash.to_session_value + @request.session['flash'] = flash_value + end + @response end @@ -670,7 +698,7 @@ module ActionController :only_path => true, :action => action, :relative_url_root => nil, - :_recall => @request.symbolized_path_parameters) + :_recall => @request.path_parameters) url, query_string = @routes.url_for(options).split("?", 2) @@ -681,7 +709,7 @@ module ActionController end def html_format?(parameters) - return true unless parameters.is_a?(Hash) + return true unless parameters.key?(:format) Mime.fetch(parameters[:format]) { Mime['html'] }.html? end end diff --git a/actionpack/lib/action_dispatch/http/cache.rb b/actionpack/lib/action_dispatch/http/cache.rb index f9b278349e..63a3cbc90b 100644 --- a/actionpack/lib/action_dispatch/http/cache.rb +++ b/actionpack/lib/action_dispatch/http/cache.rb @@ -92,7 +92,7 @@ module ActionDispatch LAST_MODIFIED = "Last-Modified".freeze ETAG = "ETag".freeze CACHE_CONTROL = "Cache-Control".freeze - SPECIAL_KEYS = %w[extras no-cache max-age public must-revalidate] + SPECIAL_KEYS = Set.new(%w[extras no-cache max-age public must-revalidate]) def cache_control_segments if cache_control = self[CACHE_CONTROL] diff --git a/actionpack/lib/action_dispatch/http/headers.rb b/actionpack/lib/action_dispatch/http/headers.rb index 2666cd4b0a..bc5410dc38 100644 --- a/actionpack/lib/action_dispatch/http/headers.rb +++ b/actionpack/lib/action_dispatch/http/headers.rb @@ -1,34 +1,63 @@ module ActionDispatch module Http + # Provides access to the request's HTTP headers from the environment. + # + # env = { "CONTENT_TYPE" => "text/plain" } + # headers = ActionDispatch::Http::Headers.new(env) + # headers["Content-Type"] # => "text/plain" class Headers - CGI_VARIABLES = %w( - CONTENT_TYPE CONTENT_LENGTH - HTTPS AUTH_TYPE GATEWAY_INTERFACE - PATH_INFO PATH_TRANSLATED QUERY_STRING - REMOTE_ADDR REMOTE_HOST REMOTE_IDENT REMOTE_USER - REQUEST_METHOD SCRIPT_NAME - SERVER_NAME SERVER_PORT SERVER_PROTOCOL SERVER_SOFTWARE - ) + CGI_VARIABLES = Set.new(%W[ + AUTH_TYPE + CONTENT_LENGTH + CONTENT_TYPE + GATEWAY_INTERFACE + HTTPS + PATH_INFO + PATH_TRANSLATED + QUERY_STRING + REMOTE_ADDR + REMOTE_HOST + REMOTE_IDENT + REMOTE_USER + REQUEST_METHOD + SCRIPT_NAME + SERVER_NAME + SERVER_PORT + SERVER_PROTOCOL + SERVER_SOFTWARE + ]).freeze + HTTP_HEADER = /\A[A-Za-z0-9-]+\z/ include Enumerable attr_reader :env - def initialize(env = {}) + def initialize(env = {}) # :nodoc: @env = env end + # Returns the value for the given key mapped to @env. def [](key) @env[env_name(key)] end + # Sets the given value for the key mapped to @env. def []=(key, value) @env[env_name(key)] = value end - def key?(key); @env.key? key; end + def key?(key) + @env.key? env_name(key) + end alias :include? :key? + # Returns the value for the given key mapped to @env. + # + # If the key is not found and an optional code block is not provided, + # raises a <tt>KeyError</tt> exception. + # + # If the code block is provided, then it will be run and + # its result returned. def fetch(key, *args, &block) @env.fetch env_name(key), *args, &block end @@ -37,12 +66,17 @@ module ActionDispatch @env.each(&block) end + # Returns a new Http::Headers instance containing the contents of + # <tt>headers_or_env</tt> and the original instance. def merge(headers_or_env) headers = Http::Headers.new(env.dup) headers.merge!(headers_or_env) headers end + # Adds the contents of <tt>headers_or_env</tt> to original instance + # entries; duplicate keys are overwritten with the values from + # <tt>headers_or_env</tt>. def merge!(headers_or_env) headers_or_env.each do |key, value| self[env_name(key)] = value @@ -50,6 +84,8 @@ module ActionDispatch end private + # Converts a HTTP header name to an environment variable name if it is + # not contained within the headers hash. def env_name(key) key = key.to_s if key =~ HTTP_HEADER diff --git a/actionpack/lib/action_dispatch/http/mime_negotiation.rb b/actionpack/lib/action_dispatch/http/mime_negotiation.rb index 0b2b60d2e4..9c8f65deac 100644 --- a/actionpack/lib/action_dispatch/http/mime_negotiation.rb +++ b/actionpack/lib/action_dispatch/http/mime_negotiation.rb @@ -54,8 +54,14 @@ module ActionDispatch end def formats - @env["action_dispatch.request.formats"] ||= - if parameters[:format] + @env["action_dispatch.request.formats"] ||= begin + params_readable = begin + parameters[:format] + rescue ActionController::BadRequest + false + end + + if params_readable Array(Mime[parameters[:format]]) elsif use_accept_header && valid_accept_header accepts @@ -64,8 +70,8 @@ module ActionDispatch else [Mime::HTML] end + end end - # Sets the \variant for template. def variant=(variant) if variant.is_a?(Symbol) diff --git a/actionpack/lib/action_dispatch/http/parameters.rb b/actionpack/lib/action_dispatch/http/parameters.rb index dcb299ed03..20ae48d458 100644 --- a/actionpack/lib/action_dispatch/http/parameters.rb +++ b/actionpack/lib/action_dispatch/http/parameters.rb @@ -1,13 +1,11 @@ require 'active_support/core_ext/hash/keys' require 'active_support/core_ext/hash/indifferent_access' +require 'active_support/deprecation' module ActionDispatch module Http module Parameters - def initialize(env) - super - @symbolized_path_params = nil - end + PARAMETERS_KEY = 'action_dispatch.request.path_parameters' # Returns both GET and POST \parameters in a single hash. def parameters @@ -18,55 +16,42 @@ module ActionDispatch query_parameters.dup end params.merge!(path_parameters) - params.with_indifferent_access end end alias :params :parameters def path_parameters=(parameters) #:nodoc: - @symbolized_path_params = nil - @env.delete("action_dispatch.request.parameters") - @env["action_dispatch.request.path_parameters"] = parameters + @env.delete('action_dispatch.request.parameters') + @env[PARAMETERS_KEY] = parameters end - # The same as <tt>path_parameters</tt> with explicitly symbolized keys. def symbolized_path_parameters - @symbolized_path_params ||= path_parameters.symbolize_keys + ActiveSupport::Deprecation.warn( + "`symbolized_path_parameters` is deprecated. Please use `path_parameters`" + ) + path_parameters end # Returns a hash with the \parameters used to form the \path of the request. # Returned hash keys are strings: # # {'action' => 'my_action', 'controller' => 'my_controller'} - # - # See <tt>symbolized_path_parameters</tt> for symbolized keys. def path_parameters - @env["action_dispatch.request.path_parameters"] ||= {} - end - - def reset_parameters #:nodoc: - @env.delete("action_dispatch.request.parameters") + @env[PARAMETERS_KEY] ||= {} end private - # Convert nested Hash to HashWithIndifferentAccess - # and UTF-8 encode both keys and values in nested Hash. + # Convert nested Hash to HashWithIndifferentAccess. # - # TODO: Validate that the characters are UTF-8. If they aren't, - # you'll get a weird error down the road, but our form handling - # should really prevent that from happening def normalize_encode_params(params) case params - when String - params.force_encoding(Encoding::UTF_8).encode! when Hash if params.has_key?(:tempfile) UploadedFile.new(params) else params.each_with_object({}) do |(key, val), new_hash| - new_key = key.is_a?(String) ? key.dup.force_encoding(Encoding::UTF_8).encode! : key - new_hash[new_key] = if val.is_a?(Array) + new_hash[key] = if val.is_a?(Array) val.map! { |el| normalize_encode_params(el) } else normalize_encode_params(val) diff --git a/actionpack/lib/action_dispatch/http/request.rb b/actionpack/lib/action_dispatch/http/request.rb index daa06e96e6..a519d6c1fc 100644 --- a/actionpack/lib/action_dispatch/http/request.rb +++ b/actionpack/lib/action_dispatch/http/request.rb @@ -23,7 +23,7 @@ module ActionDispatch autoload :Session, 'action_dispatch/request/session' autoload :Utils, 'action_dispatch/request/utils' - LOCALHOST = Regexp.union [/^127\.0\.0\.\d{1,3}$/, /^::1$/, /^0:0:0:0:0:0:0:1(%.*)?$/] + LOCALHOST = Regexp.union [/^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/, /^::1$/, /^0:0:0:0:0:0:0:1(%.*)?$/] ENV_METHODS = %w[ AUTH_TYPE GATEWAY_INTERFACE PATH_TRANSLATED REMOTE_HOST @@ -53,6 +53,17 @@ module ActionDispatch @uuid = nil end + def check_path_parameters! + # If any of the path parameters has an invalid encoding then + # raise since it's likely to trigger errors further on. + path_parameters.each do |key, value| + next unless value.respond_to?(:valid_encoding?) + unless value.valid_encoding? + raise ActionController::BadRequest, "Invalid parameter: #{key} => #{value}" + end + end + end + def key?(key) @env.key?(key) end @@ -64,6 +75,7 @@ module ActionDispatch # Ordered Collections Protocol (WebDAV) (http://www.ietf.org/rfc/rfc3648.txt) # Web Distributed Authoring and Versioning (WebDAV) Access Control Protocol (http://www.ietf.org/rfc/rfc3744.txt) # Web Distributed Authoring and Versioning (WebDAV) SEARCH (http://www.ietf.org/rfc/rfc5323.txt) + # Calendar Extensions to WebDAV (http://www.ietf.org/rfc/rfc4791.txt) # PATCH Method for HTTP (http://www.ietf.org/rfc/rfc5789.txt) RFC2616 = %w(OPTIONS GET HEAD POST PUT DELETE TRACE CONNECT) RFC2518 = %w(PROPFIND PROPPATCH MKCOL COPY MOVE LOCK UNLOCK) @@ -71,9 +83,10 @@ module ActionDispatch RFC3648 = %w(ORDERPATCH) RFC3744 = %w(ACL) RFC5323 = %w(SEARCH) + RFC4791 = %w(MKCALENDAR) RFC5789 = %w(PATCH) - HTTP_METHODS = RFC2616 + RFC2518 + RFC3253 + RFC3648 + RFC3744 + RFC5323 + RFC5789 + HTTP_METHODS = RFC2616 + RFC2518 + RFC3253 + RFC3648 + RFC3744 + RFC5323 + RFC4791 + RFC5789 HTTP_METHOD_LOOKUP = {} @@ -196,8 +209,8 @@ module ActionDispatch end # Returns true if the "X-Requested-With" header contains "XMLHttpRequest" - # (case-insensitive). All major JavaScript libraries send this header with - # every Ajax request. + # (case-insensitive), which may need to be manually added depending on the + # choice of JavaScript libraries and frameworks. def xml_http_request? @env['HTTP_X_REQUESTED_WITH'] =~ /XMLHttpRequest/i end @@ -278,7 +291,7 @@ module ActionDispatch # Override Rack's GET method to support indifferent access def GET - @env["action_dispatch.request.query_parameters"] ||= Utils.deep_munge((normalize_encode_params(super) || {})) + @env["action_dispatch.request.query_parameters"] ||= Utils.deep_munge(normalize_encode_params(super || {})) rescue TypeError => e raise ActionController::BadRequest.new(:query, e) end @@ -286,7 +299,7 @@ module ActionDispatch # Override Rack's POST method to support indifferent access def POST - @env["action_dispatch.request.request_parameters"] ||= Utils.deep_munge((normalize_encode_params(super) || {})) + @env["action_dispatch.request.request_parameters"] ||= Utils.deep_munge(normalize_encode_params(super || {})) rescue TypeError => e raise ActionController::BadRequest.new(:request, e) end diff --git a/actionpack/lib/action_dispatch/http/response.rb b/actionpack/lib/action_dispatch/http/response.rb index 3d27ff2b24..2fab6be1a5 100644 --- a/actionpack/lib/action_dispatch/http/response.rb +++ b/actionpack/lib/action_dispatch/http/response.rb @@ -97,6 +97,9 @@ module ActionDispatch # :nodoc: x end + def abort + end + def close @response.commit! @closed = true @@ -207,18 +210,6 @@ module ActionDispatch # :nodoc: end alias_method :status_message, :message - def respond_to?(method, include_private = false) - if method.to_s == 'to_path' - stream.respond_to?(method) - else - super - end - end - - def to_path - stream.to_path - end - # Returns the content of the response as a string. This contains the contents # of any calls to <tt>render</tt>. def body @@ -271,6 +262,17 @@ module ActionDispatch # :nodoc: stream.close if stream.respond_to?(:close) end + def abort + if stream.respond_to?(:abort) + stream.abort + elsif stream.respond_to?(:close) + # `stream.close` should really be reserved for a close from the + # other direction, but we must fall back to it for + # compatibility. + stream.close + end + end + # Turns the Response into a Rack-compatible array of the status, headers, # and body. def to_a @@ -296,6 +298,9 @@ module ActionDispatch # :nodoc: cookies end + def _status_code + @status + end private def before_committed @@ -334,6 +339,38 @@ module ActionDispatch # :nodoc: !@sending_file && @charset != false end + class RackBody + def initialize(response) + @response = response + end + + def each(*args, &block) + @response.each(*args, &block) + end + + def close + # Rack "close" maps to Response#abort, and *not* Response#close + # (which is used when the controller's finished writing) + @response.abort + end + + def body + @response.body + end + + def respond_to?(method, include_private = false) + if method.to_s == 'to_path' + @response.stream.respond_to?(method) + else + super + end + end + + def to_path + @response.stream.to_path + end + end + def rack_response(status, header) assign_default_content_type_and_charset!(header) handle_conditional_get! @@ -344,7 +381,7 @@ module ActionDispatch # :nodoc: header.delete CONTENT_TYPE [status, header, []] else - [status, header, Rack::BodyProxy.new(self){}] + [status, header, RackBody.new(self)] end end end diff --git a/actionpack/lib/action_dispatch/http/upload.rb b/actionpack/lib/action_dispatch/http/upload.rb index 45bf751d09..540e11a4a0 100644 --- a/actionpack/lib/action_dispatch/http/upload.rb +++ b/actionpack/lib/action_dispatch/http/upload.rb @@ -27,7 +27,8 @@ module ActionDispatch @tempfile = hash[:tempfile] raise(ArgumentError, ':tempfile is required') unless @tempfile - @original_filename = encode_filename(hash[:filename]) + @original_filename = hash[:filename] + @original_filename &&= @original_filename.encode "UTF-8" @content_type = hash[:type] @headers = hash[:head] end @@ -66,13 +67,6 @@ module ActionDispatch def eof? @tempfile.eof? end - - private - - def encode_filename(filename) - # Encode the filename in the utf8 encoding, unless it is nil - filename.force_encoding(Encoding::UTF_8).encode! if filename - end end end end diff --git a/actionpack/lib/action_dispatch/http/url.rb b/actionpack/lib/action_dispatch/http/url.rb index 6f5a52c568..473f692b05 100644 --- a/actionpack/lib/action_dispatch/http/url.rb +++ b/actionpack/lib/action_dispatch/http/url.rb @@ -5,99 +5,128 @@ module ActionDispatch module Http module URL IP_HOST_REGEXP = /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/ - HOST_REGEXP = /(^.*:\/\/)?([^:]+)(?::(\d+$))?/ + HOST_REGEXP = /(^[^:]+:\/\/)?([^:]+)(?::(\d+$))?/ PROTOCOL_REGEXP = /^([^:]+)(:)?(\/\/)?$/ mattr_accessor :tld_length self.tld_length = 1 class << self - def extract_domain(host, tld_length = @@tld_length) - host.split('.').last(1 + tld_length).join('.') if named_host?(host) + def extract_domain(host, tld_length) + extract_domain_from(host, tld_length) if named_host?(host) end - def extract_subdomains(host, tld_length = @@tld_length) + def extract_subdomains(host, tld_length) if named_host?(host) - parts = host.split('.') - parts[0..-(tld_length + 2)] + extract_subdomains_from(host, tld_length) else [] end end - def extract_subdomain(host, tld_length = @@tld_length) + def extract_subdomain(host, tld_length) extract_subdomains(host, tld_length).join('.') end - def url_for(options = {}) - options = options.dup - path = options.delete(:script_name).to_s.chomp("/") - path << options.delete(:path).to_s - - params = options[:params].is_a?(Hash) ? options[:params] : options.slice(:params) - params.reject! { |_,v| v.to_param.nil? } - - result = build_host_url(options) - if options[:trailing_slash] - if path.include?('?') - result << path.sub(/\?/, '/\&') - else - result << path.sub(/[^\/]\z|\A\z/, '\&/') - end + def url_for(options) + if options[:only_path] + path_for options else - result << path + full_url_for options end - result << "?#{params.to_query}" unless params.empty? - result << "##{Journey::Router::Utils.escape_fragment(options[:anchor].to_param.to_s)}" if options[:anchor] - result end - private + def full_url_for(options) + host = options[:host] + protocol = options[:protocol] + port = options[:port] - def build_host_url(options) - if options[:host].blank? && options[:only_path].blank? + unless host raise ArgumentError, 'Missing host to link to! Please provide the :host parameter, set default_url_options[:host], or set :only_path to true' end - result = "" + build_host_url(host, port, protocol, options, path_for(options)) + end + + def path_for(options) + result = options[:script_name].to_s.chomp("/") + result << options[:path].to_s + + result = add_trailing_slash(result) if options[:trailing_slash] - unless options[:only_path] - if match = options[:host].match(HOST_REGEXP) - options[:protocol] ||= match[1] unless options[:protocol] == false - options[:host] = match[2] - options[:port] = match[3] unless options.key?(:port) - end + result = add_params options, result + add_anchor options, result + end + + private - options[:protocol] = normalize_protocol(options) - options[:host] = normalize_host(options) - options[:port] = normalize_port(options) + def add_params(options, result) + if options.key? :params + param = options[:params] + params = param.is_a?(Hash) ? param : { params: param } - result << options[:protocol] - result << rewrite_authentication(options) - result << options[:host] - result << ":#{options[:port]}" if options[:port] + params.reject! { |_,v| v.to_param.nil? } + result << "?#{params.to_query}" unless params.empty? end result end - def named_host?(host) - host && IP_HOST_REGEXP !~ host + def add_anchor(options, result) + result << "##{Journey::Router::Utils.escape_fragment(options[:anchor].to_param.to_s)}" if options[:anchor] + result + end + + def extract_domain_from(host, tld_length) + host.split('.').last(1 + tld_length).join('.') end - def same_host?(options) - (options[:subdomain] == true || !options.key?(:subdomain)) && options[:domain].nil? + def extract_subdomains_from(host, tld_length) + parts = host.split('.') + parts[0..-(tld_length + 2)] end - def rewrite_authentication(options) + def add_trailing_slash(path) + # includes querysting + if path.include?('?') + path.sub!(/\?/, '/\&') + # does not have a .format + elsif !path.include?(".") + path.sub!(/[^\/]\z|\A\z/, '\&/') + end + + path + end + + def build_host_url(host, port, protocol, options, path) + if match = host.match(HOST_REGEXP) + protocol ||= match[1] unless protocol == false + host = match[2] + port = match[3] unless options.key? :port + end + + protocol = normalize_protocol protocol + host = normalize_host(host, options) + + result = protocol.dup + if options[:user] && options[:password] - "#{Rack::Utils.escape(options[:user])}:#{Rack::Utils.escape(options[:password])}@" - else - "" + result << "#{Rack::Utils.escape(options[:user])}:#{Rack::Utils.escape(options[:password])}@" end + + result << host + normalize_port(port, protocol) { |normalized_port| + result << ":#{normalized_port}" + } + + result.concat path end - def normalize_protocol(options) - case options[:protocol] + def named_host?(host) + IP_HOST_REGEXP !~ host + end + + def normalize_protocol(protocol) + case protocol when nil "http://" when false, "//" @@ -105,36 +134,39 @@ module ActionDispatch when PROTOCOL_REGEXP "#{$1}://" else - raise ArgumentError, "Invalid :protocol option: #{options[:protocol].inspect}" + raise ArgumentError, "Invalid :protocol option: #{protocol.inspect}" end end - def normalize_host(options) - return options[:host] if !named_host?(options[:host]) || same_host?(options) + def normalize_host(_host, options) + return _host unless named_host?(_host) tld_length = options[:tld_length] || @@tld_length + subdomain = options.fetch :subdomain, true + domain = options[:domain] host = "" - if options[:subdomain] == true || !options.key?(:subdomain) - host << extract_subdomain(options[:host], tld_length).to_param - elsif options[:subdomain].present? - host << options[:subdomain].to_param + if subdomain == true + return _host if domain.nil? + + host << extract_subdomains_from(_host, tld_length).join('.') + elsif subdomain + host << subdomain.to_param end host << "." unless host.empty? - host << (options[:domain] || extract_domain(options[:host], tld_length)) + host << (domain || extract_domain_from(_host, tld_length)) host end - def normalize_port(options) - return nil if options[:port].nil? || options[:port] == false + def normalize_port(port, protocol) + return unless port - case options[:protocol] - when "//" - nil + case protocol + when "//" then yield port when "https://" - options[:port].to_i == 443 ? nil : options[:port] + yield port unless port.to_i == 443 else - options[:port].to_i == 80 ? nil : options[:port] + yield port unless port.to_i == 80 end end end diff --git a/actionpack/lib/action_dispatch/journey/formatter.rb b/actionpack/lib/action_dispatch/journey/formatter.rb index 57f0963731..59b353b1b7 100644 --- a/actionpack/lib/action_dispatch/journey/formatter.rb +++ b/actionpack/lib/action_dispatch/journey/formatter.rb @@ -12,12 +12,12 @@ module ActionDispatch @cache = nil end - def generate(type, name, options, recall = {}, parameterize = nil) - constraints = recall.merge(options) + def generate(name, options, path_parameters, parameterize = nil) + constraints = path_parameters.merge(options) missing_keys = [] match_route(name, constraints) do |route| - parameterized_parts = extract_parameterized_parts(route, options, recall, parameterize) + parameterized_parts = extract_parameterized_parts(route, options, path_parameters, parameterize) # Skip this route unless a name has been provided or it is a # standard Rails route since we can't determine whether an options @@ -30,6 +30,12 @@ module ActionDispatch parameterized_parts.key?(key) || route.defaults.key?(key) end + defaults = route.defaults + required_parts = route.required_parts + parameterized_parts.delete_if do |key, value| + value.to_s == defaults[key].to_s && !required_parts.include?(key) + end + return [route.format(parameterized_parts), params] end @@ -74,12 +80,12 @@ module ActionDispatch if named_routes.key?(name) yield named_routes[name] else - routes = non_recursive(cache, options.to_a) + routes = non_recursive(cache, options) hash = routes.group_by { |_, r| r.score(options) } hash.keys.sort.reverse_each do |score| - next if score < 0 + break if score < 0 hash[score].sort_by { |i, _| i }.each do |_, route| yield route @@ -90,14 +96,14 @@ module ActionDispatch def non_recursive(cache, options) routes = [] - stack = [cache] + queue = [cache] - while stack.any? - c = stack.shift + while queue.any? + c = queue.shift routes.concat(c[:___routes]) if c.key?(:___routes) options.each do |pair| - stack << c[pair] if c.key?(pair) + queue << c[pair] if c.key?(pair) end end @@ -126,11 +132,6 @@ module ActionDispatch } end - # Returns +true+ if no missing keys are present, otherwise +false+. - def verify_required_parts!(route, parts) - missing_keys(route, parts).empty? - end - def build_cache root = { ___routes: [] } routes.each_with_index do |route, i| diff --git a/actionpack/lib/action_dispatch/journey/nodes/node.rb b/actionpack/lib/action_dispatch/journey/nodes/node.rb index 935442ef66..bb01c087bc 100644 --- a/actionpack/lib/action_dispatch/journey/nodes/node.rb +++ b/actionpack/lib/action_dispatch/journey/nodes/node.rb @@ -93,6 +93,10 @@ module ActionDispatch class Star < Unary # :nodoc: def type; :STAR; end + + def name + left.name.tr '*:', '' + end end class Binary < Node # :nodoc: diff --git a/actionpack/lib/action_dispatch/journey/parser.rb b/actionpack/lib/action_dispatch/journey/parser.rb index 430812fafe..d129ba7e16 100644 --- a/actionpack/lib/action_dispatch/journey/parser.rb +++ b/actionpack/lib/action_dispatch/journey/parser.rb @@ -1,7 +1,7 @@ # # DO NOT MODIFY!!!! -# This file is automatically generated by Racc 1.4.9 -# from Racc grammar file "". +# This file is automatically generated by Racc 1.4.11 +# from Racc grammer file "". # require 'racc/parser.rb' @@ -9,42 +9,38 @@ require 'racc/parser.rb' require 'action_dispatch/journey/parser_extras' module ActionDispatch - module Journey # :nodoc: - class Parser < Racc::Parser # :nodoc: + module Journey + class Parser < Racc::Parser ##### State transition tables begin ### racc_action_table = [ - 17, 21, 13, 15, 14, 7, nil, 16, 8, 19, - 13, 15, 14, 7, 23, 16, 8, 19, 13, 15, - 14, 7, nil, 16, 8, 13, 15, 14, 7, nil, - 16, 8, 13, 15, 14, 7, nil, 16, 8 ] + 13, 15, 14, 7, 21, 16, 8, 19, 13, 15, + 14, 7, 17, 16, 8, 13, 15, 14, 7, 24, + 16, 8, 13, 15, 14, 7, 19, 16, 8 ] racc_action_check = [ - 1, 17, 1, 1, 1, 1, nil, 1, 1, 1, - 20, 20, 20, 20, 20, 20, 20, 20, 7, 7, - 7, 7, nil, 7, 7, 19, 19, 19, 19, nil, - 19, 19, 0, 0, 0, 0, nil, 0, 0 ] + 2, 2, 2, 2, 17, 2, 2, 2, 0, 0, + 0, 0, 1, 0, 0, 19, 19, 19, 19, 20, + 19, 19, 7, 7, 7, 7, 22, 7, 7 ] racc_action_pointer = [ - 30, 0, nil, nil, nil, nil, nil, 16, nil, nil, - nil, nil, nil, nil, nil, nil, nil, 1, nil, 23, - 8, nil, nil, nil ] + 6, 12, -2, nil, nil, nil, nil, 20, nil, nil, + nil, nil, nil, nil, nil, nil, nil, 4, nil, 13, + 13, nil, 17, nil, nil ] racc_action_default = [ - -18, -18, -2, -3, -4, -5, -6, -18, -9, -10, - -11, -12, -13, -14, -15, -16, -17, -18, -1, -18, - -18, 24, -8, -7 ] + -19, -19, -2, -3, -4, -5, -6, -19, -10, -11, + -12, -13, -14, -15, -16, -17, -18, -19, -1, -19, + -19, 25, -8, -9, -7 ] racc_goto_table = [ - 18, 1, nil, nil, nil, nil, nil, nil, 20, nil, - nil, nil, nil, nil, nil, nil, nil, nil, 22, 18 ] + 1, 22, 18, 23, nil, nil, nil, 20 ] racc_goto_check = [ - 2, 1, nil, nil, nil, nil, nil, nil, 1, nil, - nil, nil, nil, nil, nil, nil, nil, nil, 2, 2 ] + 1, 2, 1, 3, nil, nil, nil, 1 ] racc_goto_pointer = [ - nil, 1, -1, nil, nil, nil, nil, nil, nil, nil, + nil, 0, -18, -16, nil, nil, nil, nil, nil, nil, nil ] racc_goto_default = [ @@ -61,19 +57,20 @@ racc_reduce_table = [ 1, 12, :_reduce_none, 3, 15, :_reduce_7, 3, 13, :_reduce_8, - 1, 16, :_reduce_9, + 3, 13, :_reduce_9, + 1, 16, :_reduce_10, 1, 14, :_reduce_none, 1, 14, :_reduce_none, 1, 14, :_reduce_none, 1, 14, :_reduce_none, - 1, 19, :_reduce_14, - 1, 17, :_reduce_15, - 1, 18, :_reduce_16, - 1, 20, :_reduce_17 ] + 1, 19, :_reduce_15, + 1, 17, :_reduce_16, + 1, 18, :_reduce_17, + 1, 20, :_reduce_18 ] -racc_reduce_n = 18 +racc_reduce_n = 19 -racc_shift_n = 24 +racc_shift_n = 25 racc_token_table = { false => 0, @@ -137,12 +134,12 @@ Racc_debug_parser = false # reduce 0 omitted def _reduce_1(val, _values, result) - result = Cat.new(val.first, val.last) + result = Cat.new(val.first, val.last) result end def _reduce_2(val, _values, result) - result = val.first + result = val.first result end @@ -155,21 +152,24 @@ end # reduce 6 omitted def _reduce_7(val, _values, result) - result = Group.new(val[1]) + result = Group.new(val[1]) result end def _reduce_8(val, _values, result) - result = Or.new([val.first, val.last]) + result = Or.new([val.first, val.last]) result end def _reduce_9(val, _values, result) - result = Star.new(Symbol.new(val.last)) + result = Or.new([val.first, val.last]) result end -# reduce 10 omitted +def _reduce_10(val, _values, result) + result = Star.new(Symbol.new(val.last)) + result +end # reduce 11 omitted @@ -177,23 +177,25 @@ end # reduce 13 omitted -def _reduce_14(val, _values, result) - result = Slash.new('/') - result -end +# reduce 14 omitted def _reduce_15(val, _values, result) - result = Symbol.new(val.first) + result = Slash.new('/') result end def _reduce_16(val, _values, result) - result = Literal.new(val.first) + result = Symbol.new(val.first) result end def _reduce_17(val, _values, result) - result = Dot.new(val.first) + result = Literal.new(val.first) + result +end + +def _reduce_18(val, _values, result) + result = Dot.new(val.first) result end diff --git a/actionpack/lib/action_dispatch/journey/parser.y b/actionpack/lib/action_dispatch/journey/parser.y index 040f8d5922..0ead222551 100644 --- a/actionpack/lib/action_dispatch/journey/parser.y +++ b/actionpack/lib/action_dispatch/journey/parser.y @@ -4,7 +4,7 @@ token SLASH LITERAL SYMBOL LPAREN RPAREN DOT STAR OR rule expressions - : expressions expression { result = Cat.new(val.first, val.last) } + : expression expressions { result = Cat.new(val.first, val.last) } | expression { result = val.first } | or ; @@ -17,7 +17,8 @@ rule : LPAREN expressions RPAREN { result = Group.new(val[1]) } ; or - : expressions OR expression { result = Or.new([val.first, val.last]) } + : expression OR expression { result = Or.new([val.first, val.last]) } + | expression OR or { result = Or.new([val.first, val.last]) } ; star : STAR { result = Star.new(Symbol.new(val.last)) } diff --git a/actionpack/lib/action_dispatch/journey/path/pattern.rb b/actionpack/lib/action_dispatch/journey/path/pattern.rb index fb155e516f..3af940a02f 100644 --- a/actionpack/lib/action_dispatch/journey/path/pattern.rb +++ b/actionpack/lib/action_dispatch/journey/path/pattern.rb @@ -1,27 +1,20 @@ +require 'action_dispatch/journey/router/strexp' + module ActionDispatch module Journey # :nodoc: module Path # :nodoc: class Pattern # :nodoc: attr_reader :spec, :requirements, :anchored - def initialize(strexp) - parser = Journey::Parser.new - - @anchored = true + def self.from_string string + new Journey::Router::Strexp.build(string, {}, ["/.?"], true) + end - case strexp - when String - @spec = parser.parse(strexp) - @requirements = {} - @separators = "/.?" - when Router::Strexp - @spec = parser.parse(strexp.path) - @requirements = strexp.requirements - @separators = strexp.separators.join - @anchored = strexp.anchor - else - raise ArgumentError, "Bad expression: #{strexp}" - end + def initialize(strexp) + @spec = strexp.ast + @requirements = strexp.requirements + @separators = strexp.separators.join + @anchored = strexp.anchor @names = nil @optional_names = nil @@ -30,6 +23,10 @@ module ActionDispatch @offsets = nil end + def build_formatter + Visitors::FormatBuilder.new.accept(spec) + end + def ast @spec.grep(Nodes::Symbol).each do |node| re = @requirements[node.to_sym] diff --git a/actionpack/lib/action_dispatch/journey/route.rb b/actionpack/lib/action_dispatch/journey/route.rb index 2b399d3ee3..9f0a3af902 100644 --- a/actionpack/lib/action_dispatch/journey/route.rb +++ b/actionpack/lib/action_dispatch/journey/route.rb @@ -16,14 +16,6 @@ module ActionDispatch @app = app @path = path - # Unwrap any constraints so we can see what's inside for route generation. - # This allows the formatter to skip over any mounted applications or redirects - # that shouldn't be matched when using a url_for without a route name. - while app.is_a?(Routing::Mapper::Constraints) do - app = app.app - end - @dispatcher = app.is_a?(Routing::RouteSet::Dispatcher) - @constraints = constraints @defaults = defaults @required_defaults = nil @@ -31,6 +23,7 @@ module ActionDispatch @parts = nil @decorated_ast = nil @precedence = 0 + @path_formatter = @path.build_formatter end def ast @@ -72,15 +65,7 @@ module ActionDispatch alias :segment_keys :parts def format(path_options) - path_options.delete_if do |key, value| - value.to_s == defaults[key].to_s && !required_parts.include?(key) - end - - Visitors::Formatter.new(path_options).accept(path.spec) - end - - def optimized_path - Visitors::OptimizedPath.new.accept(path.spec) + @path_formatter.evaluate path_options end def optional_parts @@ -106,7 +91,7 @@ module ActionDispatch end def dispatcher? - @dispatcher + @app.dispatcher? end def matches?(request) diff --git a/actionpack/lib/action_dispatch/journey/router.rb b/actionpack/lib/action_dispatch/journey/router.rb index 36561c71a1..21817b374c 100644 --- a/actionpack/lib/action_dispatch/journey/router.rb +++ b/actionpack/lib/action_dispatch/journey/router.rb @@ -20,60 +20,32 @@ module ActionDispatch # :nodoc: VERSION = '2.0.0' - class NullReq # :nodoc: - attr_reader :env - def initialize(env) - @env = env - end - - def request_method - env['REQUEST_METHOD'] - end - - def path_info - env['PATH_INFO'] - end - - def ip - env['REMOTE_ADDR'] - end - - def [](k) - env[k] - end - end - - attr_reader :request_class, :formatter attr_accessor :routes - def initialize(routes, options) - @options = options - @params_key = options[:parameters_key] - @request_class = options[:request_class] || NullReq - @routes = routes + def initialize(routes) + @routes = routes end - def call(env) - env['PATH_INFO'] = Utils.normalize_path(env['PATH_INFO']) - - find_routes(env).each do |match, parameters, route| - script_name, path_info, set_params = env.values_at('SCRIPT_NAME', - 'PATH_INFO', - @params_key) + def serve(req) + find_routes(req).each do |match, parameters, route| + set_params = req.path_parameters + path_info = req.path_info + script_name = req.script_name unless route.path.anchored - env['SCRIPT_NAME'] = (script_name.to_s + match.to_s).chomp('/') - env['PATH_INFO'] = match.post_match + req.script_name = (script_name.to_s + match.to_s).chomp('/') + req.path_info = match.post_match + req.path_info = "/" + req.path_info unless req.path_info.start_with? "/" end - env[@params_key] = (set_params || {}).merge parameters + req.path_parameters = set_params.merge parameters - status, headers, body = route.app.call(env) + status, headers, body = route.app.serve(req) if 'pass' == headers['X-Cascade'] - env['SCRIPT_NAME'] = script_name - env['PATH_INFO'] = path_info - env[@params_key] = set_params + req.script_name = script_name + req.path_info = path_info + req.path_parameters = set_params next end @@ -83,14 +55,14 @@ module ActionDispatch return [404, {'X-Cascade' => 'pass'}, ['Not Found']] end - def recognize(req) - find_routes(req.env).each do |match, parameters, route| + def recognize(rails_req) + find_routes(rails_req).each do |match, parameters, route| unless route.path.anchored - req.env['SCRIPT_NAME'] = match.to_s - req.env['PATH_INFO'] = match.post_match.sub(/^([^\/])/, '/\1') + rails_req.script_name = match.to_s + rails_req.path_info = match.post_match.sub(/^([^\/])/, '/\1') end - yield(route, nil, parameters) + yield(route, parameters) end end @@ -124,29 +96,31 @@ module ActionDispatch simulator.memos(path) { [] } end - def find_routes env - req = request_class.new(env) - + def find_routes req routes = filter_routes(req.path_info).concat custom_routes.find_all { |r| r.path.match(req.path_info) } - routes.concat get_routes_as_head(routes) - routes.sort_by!(&:precedence).select! { |r| r.matches?(req) } + if req.env["REQUEST_METHOD"] === "HEAD" + routes.concat get_routes_as_head(routes) + end + + routes.select! { |r| r.matches?(req) } + routes.sort_by!(&:precedence) routes.map! { |r| match_data = r.path.match(req.path_info) - match_names = match_data.names.map { |n| n.to_sym } - match_values = match_data.captures.map { |v| v && Utils.unescape_uri(v) } - info = Hash[match_names.zip(match_values).find_all { |_, y| y }] - - [match_data, r.defaults.merge(info), r] + path_parameters = r.defaults.dup + match_data.names.zip(match_data.captures) { |name,val| + path_parameters[name.to_sym] = Utils.unescape_uri(val) if val + } + [match_data, path_parameters, r] } end def get_routes_as_head(routes) precedence = (routes.map(&:precedence).max || 0) + 1 - routes = routes.select { |r| + routes.select { |r| r.verb === "GET" && !(r.verb === "HEAD") }.map! { |r| Route.new(r.name, @@ -157,8 +131,6 @@ module ActionDispatch route.precedence = r.precedence + precedence end } - routes.flatten! - routes end end end diff --git a/actionpack/lib/action_dispatch/journey/router/strexp.rb b/actionpack/lib/action_dispatch/journey/router/strexp.rb index f97f1a223e..4b7738f335 100644 --- a/actionpack/lib/action_dispatch/journey/router/strexp.rb +++ b/actionpack/lib/action_dispatch/journey/router/strexp.rb @@ -6,18 +6,21 @@ module ActionDispatch alias :compile :new end - attr_reader :path, :requirements, :separators, :anchor + attr_reader :path, :requirements, :separators, :anchor, :ast - def initialize(path, requirements, separators, anchor = true) + def self.build(path, requirements, separators, anchor = true) + parser = Journey::Parser.new + ast = parser.parse path + new ast, path, requirements, separators, anchor + end + + def initialize(ast, path, requirements, separators, anchor = true) + @ast = ast @path = path @requirements = requirements @separators = separators @anchor = anchor end - - def names - @path.scan(/:\w+/).map { |s| s.tr(':', '') } - end end end end diff --git a/actionpack/lib/action_dispatch/journey/router/utils.rb b/actionpack/lib/action_dispatch/journey/router/utils.rb index ac4ecb1e65..2b0a6575d4 100644 --- a/actionpack/lib/action_dispatch/journey/router/utils.rb +++ b/actionpack/lib/action_dispatch/journey/router/utils.rb @@ -25,9 +25,10 @@ module ActionDispatch # http://tools.ietf.org/html/rfc3986 class UriEncoder # :nodoc: ENCODE = "%%%02X".freeze - ENCODING = Encoding::US_ASCII - EMPTY = "".force_encoding(ENCODING).freeze - DEC2HEX = (0..255).to_a.map{ |i| ENCODE % i }.map{ |s| s.force_encoding(ENCODING) } + US_ASCII = Encoding::US_ASCII + UTF_8 = Encoding::UTF_8 + EMPTY = "".force_encoding(US_ASCII).freeze + DEC2HEX = (0..255).to_a.map{ |i| ENCODE % i }.map{ |s| s.force_encoding(US_ASCII) } ALPHA = "a-zA-Z".freeze DIGIT = "0-9".freeze @@ -53,12 +54,13 @@ module ActionDispatch end def unescape_uri(uri) - uri.gsub(ESCAPED) { [$&[1, 2].hex].pack('C') }.force_encoding(uri.encoding) + encoding = uri.encoding == US_ASCII ? UTF_8 : uri.encoding + uri.gsub(ESCAPED) { [$&[1, 2].hex].pack('C') }.force_encoding(encoding) end protected def escape(component, pattern) - component.gsub(pattern){ |unsafe| percent_encode(unsafe) }.force_encoding(ENCODING) + component.gsub(pattern){ |unsafe| percent_encode(unsafe) }.force_encoding(US_ASCII) end def percent_encode(unsafe) diff --git a/actionpack/lib/action_dispatch/journey/visitors.rb b/actionpack/lib/action_dispatch/journey/visitors.rb index d9f634623d..52b4c8b489 100644 --- a/actionpack/lib/action_dispatch/journey/visitors.rb +++ b/actionpack/lib/action_dispatch/journey/visitors.rb @@ -1,14 +1,57 @@ # encoding: utf-8 -require 'thread_safe' - module ActionDispatch module Journey # :nodoc: + class Format + ESCAPE_PATH = ->(value) { Router::Utils.escape_path(value) } + ESCAPE_SEGMENT = ->(value) { Router::Utils.escape_segment(value) } + + class Parameter < Struct.new(:name, :escaper) + def escape(value); escaper.call value; end + end + + def self.required_path(symbol) + Parameter.new symbol, ESCAPE_PATH + end + + def self.required_segment(symbol) + Parameter.new symbol, ESCAPE_SEGMENT + end + + def initialize(parts) + @parts = parts + @children = [] + @parameters = [] + + parts.each_with_index do |object,i| + case object + when Journey::Format + @children << i + when Parameter + @parameters << i + end + end + end + + def evaluate(hash) + parts = @parts.dup + + @parameters.each do |index| + param = parts[index] + value = hash[param.name] + return ''.freeze unless value + parts[index] = param.escape value + end + + @children.each { |index| parts[index] = parts[index].evaluate(hash) } + + parts.join + end + end + module Visitors # :nodoc: class Visitor # :nodoc: - DISPATCH_CACHE = ThreadSafe::Cache.new { |h,k| - h[k] = :"visit_#{k}" - } + DISPATCH_CACHE = {} def accept(node) visit(node) @@ -38,11 +81,41 @@ module ActionDispatch def visit_STAR(n); unary(n); end def terminal(node); end - %w{ LITERAL SYMBOL SLASH DOT }.each do |t| - class_eval %{ def visit_#{t}(n); terminal(n); end }, __FILE__, __LINE__ + def visit_LITERAL(n); terminal(n); end + def visit_SYMBOL(n); terminal(n); end + def visit_SLASH(n); terminal(n); end + def visit_DOT(n); terminal(n); end + + private_instance_methods(false).each do |pim| + next unless pim =~ /^visit_(.*)$/ + DISPATCH_CACHE[$1.to_sym] = pim end end + class FormatBuilder < Visitor # :nodoc: + def accept(node); Journey::Format.new(super); end + def terminal(node); [node.left]; end + + def binary(node) + visit(node.left) + visit(node.right) + end + + def visit_GROUP(n); [Journey::Format.new(unary(n))]; end + + def visit_STAR(n) + [Journey::Format.required_path(n.left.to_sym)] + end + + def visit_SYMBOL(n) + symbol = n.to_sym + if symbol == :controller + [Journey::Format.required_path(symbol)] + else + [Journey::Format.required_segment(symbol)] + end + end + end + # Loop through the requirements AST class Each < Visitor # :nodoc: attr_reader :block @@ -52,8 +125,8 @@ module ActionDispatch end def visit(node) - super block.call(node) + super end end @@ -77,90 +150,6 @@ module ActionDispatch end end - class OptimizedPath < Visitor # :nodoc: - def accept(node) - Array(visit(node)) - end - - private - - def visit_CAT(node) - [visit(node.left), visit(node.right)].flatten - end - - def visit_SYMBOL(node) - node.left[1..-1].to_sym - end - - def visit_STAR(node) - visit(node.left) - end - - def visit_GROUP(node) - [] - end - - %w{ LITERAL SLASH DOT }.each do |t| - class_eval %{ def visit_#{t}(n); n.left; end }, __FILE__, __LINE__ - end - end - - # Used for formatting urls (url_for) - class Formatter < Visitor # :nodoc: - attr_reader :options - - def initialize(options) - @options = options - end - - private - def escape_path(value) - Router::Utils.escape_path(value) - end - - def escape_segment(value) - Router::Utils.escape_segment(value) - end - - def visit(node, optional = false) - case node.type - when :LITERAL, :SLASH, :DOT - node.left - when :STAR - visit_STAR(node.left) - when :GROUP - visit(node.left, true) - when :CAT - visit_CAT(node, optional) - when :SYMBOL - visit_SYMBOL(node, node.to_sym) - end - end - - def visit_CAT(node, optional) - left = visit(node.left, optional) - right = visit(node.right, optional) - - if optional && !(right && left) - "" - else - [left, right].join - end - end - - def visit_STAR(node) - if value = options[node.to_sym] - escape_path(value) - end - end - - def visit_SYMBOL(node, name) - if value = options[name] - name == :controller ? escape_path(value) : escape_segment(value) - end - end - end - class Dot < Visitor # :nodoc: def initialize @nodes = [] diff --git a/actionpack/lib/action_dispatch/journey/visualizer/index.html.erb b/actionpack/lib/action_dispatch/journey/visualizer/index.html.erb index 6aff10956a..9b28a65200 100644 --- a/actionpack/lib/action_dispatch/journey/visualizer/index.html.erb +++ b/actionpack/lib/action_dispatch/journey/visualizer/index.html.erb @@ -2,13 +2,13 @@ <html> <head> <title><%= title %></title> - <link rel="stylesheet" href="https://raw.github.com/gist/1706081/af944401f75ea20515a02ddb3fb43d23ecb8c662/reset.css" type="text/css"> + <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/meyer-reset/2.0/reset.css" type="text/css"> <style> <% stylesheets.each do |style| %> <%= style %> <% end %> </style> - <script src="https://raw.github.com/gist/1706081/df464722a05c3c2bec450b7b5c8240d9c31fa52d/d3.min.js" type="text/javascript"></script> + <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.8/d3.min.js" type="text/javascript"></script> </head> <body> <div id="wrapper"> diff --git a/actionpack/lib/action_dispatch/middleware/cookies.rb b/actionpack/lib/action_dispatch/middleware/cookies.rb index 22b16b628d..ac9e5effe2 100644 --- a/actionpack/lib/action_dispatch/middleware/cookies.rb +++ b/actionpack/lib/action_dispatch/middleware/cookies.rb @@ -289,8 +289,8 @@ module ActionDispatch end end - # Sets the cookie named +name+. The second argument may be the very cookie - # value, or a hash of options as documented above. + # Sets the cookie named +name+. The second argument may be the cookie's + # value or a hash of options as documented above. def []=(name, options) if options.is_a?(Hash) options.symbolize_keys! @@ -468,7 +468,7 @@ module ActionDispatch options = { :value => @verifier.generate(serialize(name, options)) } end - raise CookieOverflow if options[:value].size > MAX_COOKIE_SIZE + raise CookieOverflow if options[:value].bytesize > MAX_COOKIE_SIZE @parent_jar[name] = options end @@ -526,7 +526,7 @@ module ActionDispatch options[:value] = @encryptor.encrypt_and_sign(serialize(name, options[:value])) - raise CookieOverflow if options[:value].size > MAX_COOKIE_SIZE + raise CookieOverflow if options[:value].bytesize > MAX_COOKIE_SIZE @parent_jar[name] = options end diff --git a/actionpack/lib/action_dispatch/middleware/flash.rb b/actionpack/lib/action_dispatch/middleware/flash.rb index 4821d2a899..e90f8b9ce6 100644 --- a/actionpack/lib/action_dispatch/middleware/flash.rb +++ b/actionpack/lib/action_dispatch/middleware/flash.rb @@ -10,7 +10,7 @@ module ActionDispatch end end - # The flash provides a way to pass temporary objects between actions. Anything you place in the flash will be exposed + # The flash provides a way to pass temporary primitive-types (String, Array, Hash) between actions. Anything you place in the flash will be exposed # to the very next action and then cleared out. This is a great way of doing notices and alerts, such as a create # action that sets <tt>flash[:notice] = "Post successfully created"</tt> before redirecting to a display action that can # then expose the flash to its template. Actually, that exposure is automatically done. @@ -37,8 +37,11 @@ module ActionDispatch # flash.alert = "You must be logged in" # flash.notice = "Post successfully created" # - # This example just places a string in the flash, but you can put any object in there. And of course, you can put as - # many as you like at a time too. Just remember: They'll be gone by the time the next action has been performed. + # This example places a string in the flash. And of course, you can put as many as you like at a time too. If you want to pass + # non-primitive types, you will have to handle that in your application. Example: To show messages with links, you will have to + # use sanitize helper. + # + # Just remember: They'll be gone by the time the next action has been performed. # # See docs on the FlashHash class for more details about the flash. class Flash diff --git a/actionpack/lib/action_dispatch/middleware/public_exceptions.rb b/actionpack/lib/action_dispatch/middleware/public_exceptions.rb index cbb2d475b1..6c8944e067 100644 --- a/actionpack/lib/action_dispatch/middleware/public_exceptions.rb +++ b/actionpack/lib/action_dispatch/middleware/public_exceptions.rb @@ -32,9 +32,8 @@ module ActionDispatch end def render_html(status) - found = false - path = "#{public_path}/#{status}.#{I18n.locale}.html" if I18n.locale - path = "#{public_path}/#{status}.html" unless path && (found = File.exist?(path)) + path = "#{public_path}/#{status}.#{I18n.locale}.html" + path = "#{public_path}/#{status}.html" unless (found = File.exist?(path)) if found || File.exist?(path) render_format(status, 'text/html', File.read(path)) diff --git a/actionpack/lib/action_dispatch/middleware/remote_ip.rb b/actionpack/lib/action_dispatch/middleware/remote_ip.rb index c1df518b14..6a79b4e859 100644 --- a/actionpack/lib/action_dispatch/middleware/remote_ip.rb +++ b/actionpack/lib/action_dispatch/middleware/remote_ip.rb @@ -11,7 +11,7 @@ module ActionDispatch # Some Rack servers concatenate repeated headers, like {HTTP RFC 2616}[http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2] # requires. Some Rack servers simply drop preceding headers, and only report # the value that was {given in the last header}[http://andre.arko.net/2011/12/26/repeated-headers-and-ruby-web-servers]. - # If you are behind multiple proxy servers (like Nginx to HAProxy to Unicorn) + # If you are behind multiple proxy servers (like NGINX to HAProxy to Unicorn) # then you should test your Rack server to make sure your data is good. # # IF YOU DON'T USE A PROXY, THIS MAKES YOU VULNERABLE TO IP SPOOFING. @@ -31,7 +31,7 @@ module ActionDispatch TRUSTED_PROXIES = %r{ ^127\.0\.0\.1$ | # localhost IPv4 ^::1$ | # localhost IPv6 - ^fc00: | # private IPv6 range fc00 + ^[fF][cCdD] | # private IPv6 range fc00::/7 ^10\. | # private IPv4 range 10.x.x.x ^172\.(1[6-9]|2[0-9]|3[0-1])\.| # private IPv4 range 172.16.0.0 .. 172.31.255.255 ^192\.168\. # private IPv4 range 192.168.x.x @@ -118,7 +118,7 @@ module ActionDispatch # # REMOTE_ADDR will be correct if the request is made directly against the # Ruby process, on e.g. Heroku. When the request is proxied by another - # server like HAProxy or Nginx, the IP address that made the original + # server like HAProxy or NGINX, the IP address that made the original # request will be put in an X-Forwarded-For header. If there are multiple # proxies, that header may contain a list of IPs. Other proxy services # set the Client-Ip header instead, so we check that too. diff --git a/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb b/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb index 0864e7ef2a..ed25c67ae5 100644 --- a/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb +++ b/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb @@ -49,7 +49,7 @@ module ActionDispatch # reasonably sure that your upgrade is otherwise complete. Additionally, # you should take care to make sure you are not relying on the ability to # decode signed cookies generated by your app in external applications or - # Javascript before upgrading. + # JavaScript before upgrading. # # Note that changing the secret key will invalidate all existing sessions! class CookieStore < Rack::Session::Abstract::ID diff --git a/actionpack/lib/action_dispatch/middleware/show_exceptions.rb b/actionpack/lib/action_dispatch/middleware/show_exceptions.rb index 1d4f0f89a6..f0779279c1 100644 --- a/actionpack/lib/action_dispatch/middleware/show_exceptions.rb +++ b/actionpack/lib/action_dispatch/middleware/show_exceptions.rb @@ -42,6 +42,7 @@ module ActionDispatch wrapper = ExceptionWrapper.new(env, exception) status = wrapper.status_code env["action_dispatch.exception"] = wrapper.exception + env["action_dispatch.original_path"] = env["PATH_INFO"] env["PATH_INFO"] = "/#{status}" response = @exceptions_app.call(env) response[1]['X-Cascade'] == 'pass' ? pass_response(status) : response diff --git a/actionpack/lib/action_dispatch/middleware/templates/routes/_table.html.erb b/actionpack/lib/action_dispatch/middleware/templates/routes/_table.html.erb index cce0d75af4..6ffa242da4 100644 --- a/actionpack/lib/action_dispatch/middleware/templates/routes/_table.html.erb +++ b/actionpack/lib/action_dispatch/middleware/templates/routes/_table.html.erb @@ -148,8 +148,8 @@ // On key press perform a search for matching paths searchElem.onkeyup = function(e){ var userInput = searchElem.value, - defaultExactMatch = '<tr><th colspan="4">Paths Matching (' + sanitizePath(userInput) +'):</th></tr>', - defaultFuzzyMatch = '<tr><th colspan="4">Paths Containing (' + userInput +'):</th></tr>', + defaultExactMatch = '<tr><th colspan="4">Paths Matching (' + escape(sanitizePath(userInput)) +'):</th></tr>', + defaultFuzzyMatch = '<tr><th colspan="4">Paths Containing (' + escape(userInput) +'):</th></tr>', noExactMatch = '<tr><th colspan="4">No Exact Matches Found</th></tr>', noFuzzyMatch = '<tr><th colspan="4">No Fuzzy Matches Found</th></tr>'; diff --git a/actionpack/lib/action_dispatch/routing.rb b/actionpack/lib/action_dispatch/routing.rb index 9cd884daa3..ce03164ca9 100644 --- a/actionpack/lib/action_dispatch/routing.rb +++ b/actionpack/lib/action_dispatch/routing.rb @@ -1,6 +1,7 @@ # encoding: UTF-8 require 'active_support/core_ext/object/to_param' require 'active_support/core_ext/regexp' +require 'active_support/dependencies/autoload' module ActionDispatch # The routing module provides URL rewriting in native Ruby. It's a way to diff --git a/actionpack/lib/action_dispatch/routing/endpoint.rb b/actionpack/lib/action_dispatch/routing/endpoint.rb new file mode 100644 index 0000000000..88aa13c3e8 --- /dev/null +++ b/actionpack/lib/action_dispatch/routing/endpoint.rb @@ -0,0 +1,10 @@ +module ActionDispatch + module Routing + class Endpoint # :nodoc: + def dispatcher?; false; end + def redirect?; false; end + def matches?(req); true; end + def app; self; end + end + end +end diff --git a/actionpack/lib/action_dispatch/routing/inspector.rb b/actionpack/lib/action_dispatch/routing/inspector.rb index 71a0c5e826..ea3b2f419d 100644 --- a/actionpack/lib/action_dispatch/routing/inspector.rb +++ b/actionpack/lib/action_dispatch/routing/inspector.rb @@ -5,22 +5,15 @@ module ActionDispatch module Routing class RouteWrapper < SimpleDelegator def endpoint - rack_app ? rack_app.inspect : "#{controller}##{action}" + app.dispatcher? ? "#{controller}##{action}" : rack_app.inspect end def constraints requirements.except(:controller, :action) end - def rack_app(app = self.app) - @rack_app ||= begin - class_name = app.class.name.to_s - if class_name == "ActionDispatch::Routing::Mapper::Constraints" - rack_app(app.app) - elsif ActionDispatch::Routing::Redirect === app || class_name !~ /^ActionDispatch::Routing/ - app - end - end + def rack_app + app.app end def verb @@ -73,7 +66,7 @@ module ActionDispatch end def engine? - rack_app && rack_app.respond_to?(:routes) + rack_app.respond_to?(:routes) end end diff --git a/actionpack/lib/action_dispatch/routing/mapper.rb b/actionpack/lib/action_dispatch/routing/mapper.rb index 77718a14c1..32d963ba76 100644 --- a/actionpack/lib/action_dispatch/routing/mapper.rb +++ b/actionpack/lib/action_dispatch/routing/mapper.rb @@ -6,6 +6,8 @@ require 'active_support/core_ext/array/extract_options' require 'active_support/core_ext/module/remove_method' require 'active_support/inflector' require 'action_dispatch/routing/redirection' +require 'action_dispatch/routing/endpoint' +require 'active_support/deprecation' module ActionDispatch module Routing @@ -15,121 +17,190 @@ module ActionDispatch :controller, :action, :path_names, :constraints, :shallow, :blocks, :defaults, :options] - class Constraints #:nodoc: - def self.new(app, constraints, request = Rack::Request) - if constraints.any? - super(app, constraints, request) - else - app + class Constraints < Endpoint #:nodoc: + attr_reader :app, :constraints + + def initialize(app, constraints, dispatcher_p) + # Unwrap Constraints objects. I don't actually think it's possible + # to pass a Constraints object to this constructor, but there were + # multiple places that kept testing children of this object. I + # *think* they were just being defensive, but I have no idea. + if app.is_a?(self.class) + constraints += app.constraints + app = app.app end - end - attr_reader :app, :constraints + @dispatcher = dispatcher_p - def initialize(app, constraints, request) - @app, @constraints, @request = app, constraints, request + @app, @constraints, = app, constraints end - def matches?(env) - req = @request.new(env) + def dispatcher?; @dispatcher; end + def matches?(req) @constraints.all? do |constraint| (constraint.respond_to?(:matches?) && constraint.matches?(req)) || (constraint.respond_to?(:call) && constraint.call(*constraint_args(constraint, req))) end - ensure - req.reset_parameters end - def call(env) - matches?(env) ? @app.call(env) : [ 404, {'X-Cascade' => 'pass'}, [] ] + def serve(req) + return [ 404, {'X-Cascade' => 'pass'}, [] ] unless matches?(req) + + if dispatcher? + @app.serve req + else + @app.call req.env + end end private def constraint_args(constraint, request) - constraint.arity == 1 ? [request] : [request.symbolized_path_parameters, request] + constraint.arity == 1 ? [request] : [request.path_parameters, request] end end class Mapping #:nodoc: - IGNORE_OPTIONS = [:to, :as, :via, :on, :constraints, :defaults, :only, :except, :anchor, :shallow, :shallow_path, :shallow_prefix, :format] ANCHOR_CHARACTERS_REGEX = %r{\A(\\A|\^)|(\\Z|\\z|\$)\Z} - WILDCARD_PATH = %r{\*([^/\)]+)\)?$} - attr_reader :scope, :path, :options, :requirements, :conditions, :defaults + attr_reader :requirements, :conditions, :defaults + attr_reader :to, :default_controller, :default_action, :as, :anchor + + def self.build(scope, set, path, options) + options = scope[:options].merge(options) if scope[:options] - def initialize(set, scope, path, options) - @set, @scope, @path, @options = set, scope, path, options - @requirements, @conditions, @defaults = {}, {}, {} + options.delete :only + options.delete :except + options.delete :shallow_path + options.delete :shallow_prefix + options.delete :shallow - normalize_options! - normalize_path! - normalize_requirements! - normalize_conditions! - normalize_defaults! + defaults = (scope[:defaults] || {}).merge options.delete(:defaults) || {} + + new scope, set, path, defaults, options + end + + def initialize(scope, set, path, defaults, options) + @requirements, @conditions = {}, {} + @defaults = defaults + @set = set + + @to = options.delete :to + @default_controller = options.delete(:controller) || scope[:controller] + @default_action = options.delete(:action) || scope[:action] + @as = options.delete :as + @anchor = options.delete :anchor + + formatted = options.delete :format + via = Array(options.delete(:via) { [] }) + options_constraints = options.delete :constraints + + path = normalize_path! path, formatted + ast = path_ast path + path_params = path_params ast + + options = normalize_options!(options, formatted, path_params, ast, scope[:module]) + + + split_constraints(path_params, scope[:constraints]) if scope[:constraints] + constraints = constraints(options, path_params) + + split_constraints path_params, constraints + + @blocks = blocks(options_constraints, scope[:blocks]) + + if options_constraints.is_a?(Hash) + split_constraints path_params, options_constraints + options_constraints.each do |key, default| + if URL_OPTIONS.include?(key) && (String === default || Fixnum === default) + @defaults[key] ||= default + end + end + end + + normalize_format!(formatted) + + @conditions[:path_info] = path + @conditions[:parsed_path_info] = ast + + add_request_method(via, @conditions) + normalize_defaults!(options) end def to_route - [ app, conditions, requirements, defaults, options[:as], options[:anchor] ] + [ app(@blocks), conditions, requirements, defaults, as, anchor ] end private - def normalize_path! - raise ArgumentError, "path is required" if @path.blank? - @path = Mapper.normalize_path(@path) + def normalize_path!(path, format) + path = Mapper.normalize_path(path) - if required_format? - @path = "#{@path}.:format" - elsif optional_format? - @path = "#{@path}(.:format)" + if format == true + "#{path}.:format" + elsif optional_format?(path, format) + "#{path}(.:format)" + else + path end end - def required_format? - options[:format] == true - end - - def optional_format? - options[:format] != false && !path.include?(':format') && !path.end_with?('/') + def optional_format?(path, format) + format != false && !path.include?(':format') && !path.end_with?('/') end - def normalize_options! - @options.reverse_merge!(scope[:options]) if scope[:options] - path_without_format = path.sub(/\(\.:format\)$/, '') - + def normalize_options!(options, formatted, path_params, path_ast, modyoule) # Add a constraint for wildcard route to make it non-greedy and match the # optional format part of the route by default - if path_without_format.match(WILDCARD_PATH) && @options[:format] != false - @options[$1.to_sym] ||= /.+?/ + if formatted != false + path_ast.grep(Journey::Nodes::Star) do |node| + options[node.name.to_sym] ||= /.+?/ + end end - if path_without_format.match(':controller') - raise ArgumentError, ":controller segment is not allowed within a namespace block" if scope[:module] + if path_params.include?(:controller) + raise ArgumentError, ":controller segment is not allowed within a namespace block" if modyoule # Add a default constraint for :controller path segments that matches namespaced # controllers with default routes like :controller/:action/:id(.:format), e.g: # GET /admin/products/show/1 # => { controller: 'admin/products', action: 'show', id: '1' } - @options[:controller] ||= /.+?/ + options[:controller] ||= /.+?/ end - @options.merge!(default_controller_and_action) + if to.respond_to? :call + options + else + to_endpoint = split_to to + controller = to_endpoint[0] || default_controller + action = to_endpoint[1] || default_action + + controller = add_controller_module(controller, modyoule) + + options.merge! check_controller_and_action(path_params, controller, action) + end end - def normalize_requirements! - constraints.each do |key, requirement| - next unless segment_keys.include?(key) || key == :controller - verify_regexp_requirement(requirement) if requirement.is_a?(Regexp) - @requirements[key] = requirement + def split_constraints(path_params, constraints) + constraints.each_pair do |key, requirement| + if path_params.include?(key) || key == :controller + verify_regexp_requirement(requirement) if requirement.is_a?(Regexp) + @requirements[key] = requirement + else + @conditions[key] = requirement + end end + end - if options[:format] == true + def normalize_format!(formatted) + if formatted == true @requirements[:format] ||= /.+/ - elsif Regexp === options[:format] - @requirements[:format] = options[:format] - elsif String === options[:format] - @requirements[:format] = Regexp.compile(options[:format]) + elsif Regexp === formatted + @requirements[:format] = formatted + @defaults[:format] = nil + elsif String === formatted + @requirements[:format] = Regexp.compile(formatted) + @defaults[:format] = formatted end end @@ -143,169 +214,143 @@ module ActionDispatch end end - def normalize_defaults! - @defaults.merge!(scope[:defaults]) if scope[:defaults] - @defaults.merge!(options[:defaults]) if options[:defaults] - - options.each do |key, default| - unless Regexp === default || IGNORE_OPTIONS.include?(key) + def normalize_defaults!(options) + options.each_pair do |key, default| + unless Regexp === default @defaults[key] = default end end - - if options[:constraints].is_a?(Hash) - options[:constraints].each do |key, default| - if URL_OPTIONS.include?(key) && (String === default || Fixnum === default) - @defaults[key] ||= default - end - end - end - - if Regexp === options[:format] - @defaults[:format] = nil - elsif String === options[:format] - @defaults[:format] = options[:format] - end end - def normalize_conditions! - @conditions[:path_info] = path - - constraints.each do |key, condition| - unless segment_keys.include?(key) || key == :controller - @conditions[key] = condition - end + def verify_callable_constraint(callable_constraint) + unless callable_constraint.respond_to?(:call) || callable_constraint.respond_to?(:matches?) + raise ArgumentError, "Invalid constraint: #{callable_constraint.inspect} must respond to :call or :matches?" end + end - required_defaults = [] - options.each do |key, required_default| - unless segment_keys.include?(key) || IGNORE_OPTIONS.include?(key) || Regexp === required_default - required_defaults << key - end - end - @conditions[:required_defaults] = required_defaults - - via_all = options.delete(:via) if options[:via] == :all + def add_request_method(via, conditions) + return if via == [:all] - if !via_all && options[:via].blank? + if via.empty? msg = "You should not use the `match` method in your router without specifying an HTTP method.\n" \ "If you want to expose your action to both GET and POST, add `via: [:get, :post]` option.\n" \ "If you want to expose your action to GET, use `get` in the router:\n" \ " Instead of: match \"controller#action\"\n" \ " Do: get \"controller#action\"" - raise msg + raise ArgumentError, msg end - if via = options[:via] - @conditions[:request_method] = Array(via).map { |m| m.to_s.dasherize.upcase } - end + conditions[:request_method] = via.map { |m| m.to_s.dasherize.upcase } end - def app - Constraints.new(endpoint, blocks, @set.request_class) - end + def app(blocks) + return to if Redirect === to - def default_controller_and_action if to.respond_to?(:call) - { } + Constraints.new(to, blocks, false) else - if to.is_a?(String) - controller, action = to.split('#') - elsif to.is_a?(Symbol) - action = to.to_s - end - - controller ||= default_controller - action ||= default_action - - if @scope[:module] && !controller.is_a?(Regexp) - if controller =~ %r{\A/} - controller = controller[1..-1] - else - controller = [@scope[:module], controller].compact.join("/").presence - end - end - - if controller.is_a?(String) && controller =~ %r{\A/} - raise ArgumentError, "controller name should not start with a slash" + if blocks.any? + Constraints.new(dispatcher(defaults), blocks, true) + else + dispatcher(defaults) end + end + end - controller = controller.to_s unless controller.is_a?(Regexp) - action = action.to_s unless action.is_a?(Regexp) + def check_controller_and_action(path_params, controller, action) + hash = check_part(:controller, controller, path_params, {}) do |part| + translate_controller(part) { + message = "'#{part}' is not a supported controller name. This can lead to potential routing problems." + message << " See http://guides.rubyonrails.org/routing.html#specifying-a-controller-to-use" - if controller.blank? && segment_keys.exclude?(:controller) - message = "Missing :controller key on routes definition, please check your routes." raise ArgumentError, message - end + } + end - if action.blank? && segment_keys.exclude?(:action) - message = "Missing :action key on routes definition, please check your routes." - raise ArgumentError, message - end + check_part(:action, action, path_params, hash) { |part| + part.is_a?(Regexp) ? part : part.to_s + } + end - if controller.is_a?(String) && controller !~ /\A[a-z_0-9\/]*\z/ - message = "'#{controller}' is not a supported controller name. This can lead to potential routing problems." - message << " See http://guides.rubyonrails.org/routing.html#specifying-a-controller-to-use" + def check_part(name, part, path_params, hash) + if part + hash[name] = yield(part) + else + unless path_params.include?(name) + message = "Missing :#{name} key on routes definition, please check your routes." raise ArgumentError, message end - - hash = {} - hash[:controller] = controller unless controller.blank? - hash[:action] = action unless action.blank? - hash end - end - - def blocks - if options[:constraints].present? && !options[:constraints].is_a?(Hash) - [options[:constraints]] + hash + end + + def split_to(to) + case to + when Symbol + ActiveSupport::Deprecation.warn "defining a route where `to` is a symbol is deprecated. Please change \"to: :#{to}\" to \"action: :#{to}\"" + [nil, to.to_s] + when /#/ then to.split('#') + when String + ActiveSupport::Deprecation.warn "defining a route where `to` is a controller without an action is deprecated. Please change \"to: :#{to}\" to \"controller: :#{to}\"" + [to, nil] else - scope[:blocks] || [] + [] end end - def constraints - @constraints ||= {}.tap do |constraints| - constraints.merge!(scope[:constraints]) if scope[:constraints] - - options.except(*IGNORE_OPTIONS).each do |key, option| - constraints[key] = option if Regexp === option + def add_controller_module(controller, modyoule) + if modyoule && !controller.is_a?(Regexp) + if controller =~ %r{\A/} + controller[1..-1] + else + [modyoule, controller].compact.join("/") end - - constraints.merge!(options[:constraints]) if options[:constraints].is_a?(Hash) + else + controller end end - def segment_keys - @segment_keys ||= path_pattern.names.map{ |s| s.to_sym } - end + def translate_controller(controller) + return controller if Regexp === controller + return controller.to_s if controller =~ /\A[a-z_0-9][a-z_0-9\/]*\z/ - def path_pattern - Journey::Path::Pattern.new(strexp) - end - - def strexp - Journey::Router::Strexp.compile(path, requirements, SEPARATORS) + yield end - def endpoint - to.respond_to?(:call) ? to : dispatcher + def blocks(options_constraints, scope_blocks) + if options_constraints && !options_constraints.is_a?(Hash) + verify_callable_constraint(options_constraints) + [options_constraints] + else + scope_blocks || [] + end end - def dispatcher - Routing::RouteSet::Dispatcher.new(:defaults => defaults) + def constraints(options, path_params) + constraints = {} + required_defaults = [] + options.each_pair do |key, option| + if Regexp === option + constraints[key] = option + else + required_defaults << key unless path_params.include?(key) + end + end + @conditions[:required_defaults] = required_defaults + constraints end - def to - options[:to] + def path_params(ast) + ast.grep(Journey::Nodes::Symbol).map { |n| n.name.to_sym } end - def default_controller - options[:controller] || scope[:controller] + def path_ast(path) + parser = Journey::Parser.new + parser.parse path end - def default_action - options[:action] || scope[:action] + def dispatcher(defaults) + @set.dispatcher defaults end end @@ -340,18 +385,34 @@ module ActionDispatch match '/', { :as => :root, :via => :get }.merge!(options) end - # Matches a url pattern to one or more routes. Any symbols in a pattern - # are interpreted as url query parameters and thus available as +params+ - # in an action: + # Matches a url pattern to one or more routes. + # + # You should not use the `match` method in your router + # without specifying an HTTP method. + # + # If you want to expose your action to both GET and POST, use: # # # sets :controller, :action and :id in params - # match ':controller/:action/:id' + # match ':controller/:action/:id', via: [:get, :post] + # + # Note that +:controller+, +:action+ and +:id+ are interpreted as url + # query parameters and thus available through +params+ in an action. + # + # If you want to expose your action to GET, use `get` in the router: + # + # Instead of: + # + # match ":controller/:action/:id" + # + # Do: + # + # get ":controller/:action/:id" # # Two of these symbols are special, +:controller+ maps to the controller # and +:action+ to the controller's action. A pattern can also map # wildcard segments (globs) to params: # - # match 'songs/*category/:title', to: 'songs#show' + # get 'songs/*category/:title', to: 'songs#show' # # # 'songs/rock/classic/stairway-to-heaven' sets # # params[:category] = 'rock/classic' @@ -364,17 +425,17 @@ module ActionDispatch # When a pattern points to an internal route, the route's +:action+ and # +:controller+ should be set in options or hash shorthand. Examples: # - # match 'photos/:id' => 'photos#show' - # match 'photos/:id', to: 'photos#show' - # match 'photos/:id', controller: 'photos', action: 'show' + # match 'photos/:id' => 'photos#show', via: :get + # match 'photos/:id', to: 'photos#show', via: :get + # match 'photos/:id', controller: 'photos', action: 'show', via: :get # # A pattern can also point to a +Rack+ endpoint i.e. anything that # responds to +call+: # - # match 'photos/:id', to: lambda {|hash| [200, {}, ["Coming soon"]] } - # match 'photos/:id', to: PhotoRackApp + # match 'photos/:id', to: lambda {|hash| [200, {}, ["Coming soon"]] }, via: :get + # match 'photos/:id', to: PhotoRackApp, via: :get # # Yes, controller actions are just rack endpoints - # match 'photos/:id', to: PhotosController.action(:show) + # match 'photos/:id', to: PhotosController.action(:show), via: :get # # Because requesting various HTTP verbs with a single action has security # implications, you must either specify the actions in @@ -391,13 +452,19 @@ module ActionDispatch # [:action] # The route's action. # + # [:param] + # Overrides the default resource identifier `:id` (name of the + # dynamic segment used to generate the routes). + # You can access that segment from your controller using + # <tt>params[<:param>]</tt>. + # # [:path] # The path prefix for the routes. # # [:module] # The namespace for :controller. # - # match 'path', to: 'c#a', module: 'sekret', controller: 'posts' + # match 'path', to: 'c#a', module: 'sekret', controller: 'posts', via: :get # # => Sekret::PostsController # # See <tt>Scoping#namespace</tt> for its scope equivalent. @@ -416,9 +483,9 @@ module ActionDispatch # Points to a +Rack+ endpoint. Can be an object that responds to # +call+ or a string representing a controller's action. # - # match 'path', to: 'controller#action' - # match 'path', to: lambda { |env| [200, {}, ["Success!"]] } - # match 'path', to: RackApp + # match 'path', to: 'controller#action', via: :get + # match 'path', to: lambda { |env| [200, {}, ["Success!"]] }, via: :get + # match 'path', to: RackApp, via: :get # # [:on] # Shorthand for wrapping routes in a specific RESTful context. Valid @@ -443,14 +510,14 @@ module ActionDispatch # other than path can also be specified with any object # that responds to <tt>===</tt> (eg. String, Array, Range, etc.). # - # match 'path/:id', constraints: { id: /[A-Z]\d{5}/ } + # match 'path/:id', constraints: { id: /[A-Z]\d{5}/ }, via: :get # - # match 'json_only', constraints: { format: 'json' } + # match 'json_only', constraints: { format: 'json' }, via: :get # # class Whitelist # def matches?(request) request.remote_ip == '1.2.3.4' end # end - # match 'path', to: 'c#a', constraints: Whitelist.new + # match 'path', to: 'c#a', constraints: Whitelist.new, via: :get # # See <tt>Scoping#constraints</tt> for more examples with its scope # equivalent. @@ -459,7 +526,7 @@ module ActionDispatch # Sets defaults for parameters # # # Sets params[:format] to 'jpg' by default - # match 'path', to: 'c#a', defaults: { format: 'jpg' } + # match 'path', to: 'c#a', defaults: { format: 'jpg' }, via: :get # # See <tt>Scoping#defaults</tt> for its scope equivalent. # @@ -468,7 +535,7 @@ module ActionDispatch # false, the pattern matches any request prefixed with the given path. # # # Matches any request starting with 'path' - # match 'path', to: 'c#a', anchor: false + # match 'path', to: 'c#a', anchor: false, via: :get # # [:format] # Allows you to specify the default value for optional +format+ @@ -510,13 +577,21 @@ module ActionDispatch raise "A rack application must be specified" unless path - options[:as] ||= app_name(app) + rails_app = rails_app? app + + if rails_app + options[:as] ||= app.railtie_name + else + # non rails apps can't have an :as + options[:as] = nil + end + target_as = name_for_action(options[:as], path) options[:via] ||= :all match(path, options.merge(:to => app, :anchor => false, :format => false)) - define_generate_prefix(app, target_as) + define_generate_prefix(app, target_as) if rails_app self end @@ -537,35 +612,25 @@ module ActionDispatch end private - def app_name(app) - return unless app.respond_to?(:routes) - - if app.respond_to?(:railtie_name) - app.railtie_name - else - class_name = app.class.is_a?(Class) ? app.name : app.class.name - ActiveSupport::Inflector.underscore(class_name).tr("/", "_") - end + def rails_app?(app) + app.is_a?(Class) && app < Rails::Railtie end def define_generate_prefix(app, name) - return unless app.respond_to?(:routes) && app.routes.respond_to?(:define_mounted_helper) - _route = @set.named_routes.routes[name.to_sym] _routes = @set app.routes.define_mounted_helper(name) - app.routes.singleton_class.class_eval do - redefine_method :mounted? do - true - end - - redefine_method :_generate_prefix do |options| + app.routes.extend Module.new { + def mounted?; true; end + define_method :find_script_name do |options| + super(options) || begin prefix_options = options.slice(*_route.segment_keys) # we must actually delete prefix segment keys to avoid passing them to next url_for _route.segment_keys.each { |k| options.delete(k) } _routes.url_helpers.send("#{name}_path", prefix_options) + end end - end + } end end @@ -652,7 +717,7 @@ module ActionDispatch # resources :posts, module: "admin" # # If you want to route /admin/posts to +PostsController+ - # (without the Admin:: module prefix), you could use + # (without the <tt>Admin::</tt> module prefix), you could use # # scope "/admin" do # resources :posts @@ -1369,7 +1434,7 @@ module ActionDispatch end with_scope_level(:nested) do - if shallow? && shallow_nesting_depth > 1 + if shallow? && shallow_nesting_depth >= 1 shallow_scope(parent_resource.nested_scope, nested_options) { yield } else scope(parent_resource.nested_scope, nested_options) { yield } @@ -1403,7 +1468,20 @@ module ActionDispatch if rest.empty? && Hash === path options = path path, to = options.find { |name, _value| name.is_a?(String) } - options[:to] = to + + case to + when Symbol + options[:action] = to + when String + if to =~ /#/ + options[:to] = to + else + options[:controller] = to + end + else + options[:to] = to + end + options.delete(path) paths = [path] else @@ -1457,6 +1535,8 @@ module ActionDispatch def add_route(action, options) # :nodoc: path = path_for_action(action, options.delete(:path)) + raise ArgumentError, "path is required" if path.blank? + action = action.to_s.dup if action =~ /^[\w\-\/]+$/ @@ -1465,13 +1545,13 @@ module ActionDispatch action = nil end - if !options.fetch(:as, true) + if !options.fetch(:as, true) # if it's set to nil or false options.delete(:as) else options[:as] = name_for_action(options[:as], action) end - mapping = Mapping.new(@set, @scope, URI.parser.escape(path), options) + mapping = Mapping.build(@scope, @set, URI.parser.escape(path), options) app, conditions, requirements, defaults, as, anchor = mapping.to_route @set.add_route(app, conditions, requirements, defaults, as, anchor) end diff --git a/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb b/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb index cfd33d1f31..bd3696cda1 100644 --- a/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb +++ b/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb @@ -101,55 +101,45 @@ module ActionDispatch # polymorphic_url(Comment) # same as comments_url() # def polymorphic_url(record_or_hash_or_array, options = {}) - recipient = self - - if record_or_hash_or_array.kind_of?(Array) - record_or_hash_or_array = record_or_hash_or_array.compact - if record_or_hash_or_array.first.is_a?(ActionDispatch::Routing::RoutesProxy) - recipient = record_or_hash_or_array.shift - end - record_or_hash_or_array = record_or_hash_or_array[0] if record_or_hash_or_array.size == 1 + if Hash === record_or_hash_or_array + options = record_or_hash_or_array.merge(options) + record = options.delete :id + return polymorphic_url record, options end - record = extract_record(record_or_hash_or_array) - record = convert_to_model(record) - - args = Array === record_or_hash_or_array ? - record_or_hash_or_array.dup : - [ record_or_hash_or_array ] - - inflection = if options[:action] && options[:action].to_s == "new" - args.pop - :singular - elsif (record.respond_to?(:persisted?) && !record.persisted?) - args.pop - :plural - elsif record.is_a?(Class) - args.pop - :plural - else - :singular - end + opts = options.dup + action = opts.delete :action + type = opts.delete(:routing_type) || :url - args.delete_if {|arg| arg.is_a?(Symbol) || arg.is_a?(String)} - named_route = build_named_route_call(record_or_hash_or_array, record, inflection, options) + HelperMethodBuilder.polymorphic_method self, + record_or_hash_or_array, + action, + type, + opts - url_options = options.except(:action, :routing_type) - unless url_options.empty? - args.last.kind_of?(Hash) ? args.last.merge!(url_options) : args << url_options - end - - args.collect! { |a| convert_to_model(a) } - - recipient.send(named_route, *args) end # Returns the path component of a URL for the given record. It uses # <tt>polymorphic_url</tt> with <tt>routing_type: :path</tt>. def polymorphic_path(record_or_hash_or_array, options = {}) - polymorphic_url(record_or_hash_or_array, options.merge(:routing_type => :path)) + if Hash === record_or_hash_or_array + options = record_or_hash_or_array.merge(options) + record = options.delete :id + return polymorphic_path record, options + end + + opts = options.dup + action = opts.delete :action + type = :path + + HelperMethodBuilder.polymorphic_method self, + record_or_hash_or_array, + action, + type, + opts end + %w(edit new).each do |action| module_eval <<-EOT, __FILE__, __LINE__ + 1 def #{action}_polymorphic_url(record_or_hash, options = {}) # def edit_polymorphic_url(record_or_hash, options = {}) @@ -167,53 +157,169 @@ module ActionDispatch end private - def action_prefix(options) - options[:action] ? "#{options[:action]}_" : '' + + class HelperMethodBuilder # :nodoc: + CACHE = { 'path' => {}, 'url' => {} } + + def self.get(action, type) + type = type.to_s + CACHE[type].fetch(action) { build action, type } end - def routing_type(options) - options[:routing_type] || :url + def self.url; CACHE['url'.freeze][nil]; end + def self.path; CACHE['path'.freeze][nil]; end + + def self.build(action, type) + prefix = action ? "#{action}_" : "" + suffix = type + if action.to_s == 'new' + HelperMethodBuilder.singular prefix, suffix + else + HelperMethodBuilder.plural prefix, suffix + end end - def build_named_route_call(records, record, inflection, options = {}) - if records.is_a?(Array) - record = records.pop - route = records.map do |parent| - if parent.is_a?(Symbol) || parent.is_a?(String) - parent - else - model_name_from_record_or_class(parent).singular_route_key - end + def self.singular(prefix, suffix) + new(->(name) { name.singular_route_key }, prefix, suffix) + end + + def self.plural(prefix, suffix) + new(->(name) { name.route_key }, prefix, suffix) + end + + def self.polymorphic_method(recipient, record_or_hash_or_array, action, type, options) + builder = get action, type + + case record_or_hash_or_array + when Array + if record_or_hash_or_array.empty? || record_or_hash_or_array.include?(nil) + raise ArgumentError, "Nil location provided. Can't build URI." end + if record_or_hash_or_array.first.is_a?(ActionDispatch::Routing::RoutesProxy) + recipient = record_or_hash_or_array.shift + end + + method, args = builder.handle_list record_or_hash_or_array + when String, Symbol + method, args = builder.handle_string record_or_hash_or_array + when Class + method, args = builder.handle_class record_or_hash_or_array + + when nil + raise ArgumentError, "Nil location provided. Can't build URI." + else + method, args = builder.handle_model record_or_hash_or_array + end + + + if options.empty? + recipient.send(method, *args) else - route = [] + recipient.send(method, *args, options) end + end + + attr_reader :suffix, :prefix - if record.is_a?(Symbol) || record.is_a?(String) - route << record - elsif record - if inflection == :singular - route << model_name_from_record_or_class(record).singular_route_key + def initialize(key_strategy, prefix, suffix) + @key_strategy = key_strategy + @prefix = prefix + @suffix = suffix + end + + def handle_string(record) + [get_method_for_string(record), []] + end + + def handle_string_call(target, str) + target.send get_method_for_string str + end + + def handle_class(klass) + [get_method_for_class(klass), []] + end + + def handle_class_call(target, klass) + target.send get_method_for_class klass + end + + def handle_model(record) + args = [] + + model = record.to_model + name = if record.persisted? + args << model + model.class.model_name.singular_route_key + else + @key_strategy.call model.class.model_name + end + + named_route = prefix + "#{name}_#{suffix}" + + [named_route, args] + end + + def handle_model_call(target, model) + method, args = handle_model model + target.send(method, *args) + end + + def handle_list(list) + record_list = list.dup + record = record_list.pop + + args = [] + + route = record_list.map { |parent| + case parent + when Symbol, String + parent.to_s + when Class + args << parent + parent.model_name.singular_route_key else - route << model_name_from_record_or_class(record).route_key + args << parent.to_model + parent.to_model.class.model_name.singular_route_key end + } + + route << + case record + when Symbol, String + record.to_s + when Class + @key_strategy.call record.model_name else - raise ArgumentError, "Nil location provided. Can't build URI." + if record.persisted? + args << record.to_model + record.to_model.class.model_name.singular_route_key + else + @key_strategy.call record.to_model.class.model_name + end end - route << routing_type(options) + route << suffix - action_prefix(options) + route.join("_") + named_route = prefix + route.join("_") + [named_route, args] end - def extract_record(record_or_hash_or_array) - case record_or_hash_or_array - when Array; record_or_hash_or_array.last - when Hash; record_or_hash_or_array[:id] - else record_or_hash_or_array - end + private + + def get_method_for_class(klass) + name = @key_strategy.call klass.model_name + prefix + "#{name}_#{suffix}" + end + + def get_method_for_string(str) + prefix + "#{str}_#{suffix}" end + + [nil, 'new', 'edit'].each do |action| + CACHE['url'][action] = build action, 'url' + CACHE['path'][action] = build action, 'path' + end + end end end end - diff --git a/actionpack/lib/action_dispatch/routing/redirection.rb b/actionpack/lib/action_dispatch/routing/redirection.rb index b08e62543b..3c1c4fadf6 100644 --- a/actionpack/lib/action_dispatch/routing/redirection.rb +++ b/actionpack/lib/action_dispatch/routing/redirection.rb @@ -3,10 +3,11 @@ require 'active_support/core_ext/uri' require 'active_support/core_ext/array/extract_options' require 'rack/utils' require 'action_controller/metal/exceptions' +require 'action_dispatch/routing/endpoint' module ActionDispatch module Routing - class Redirect # :nodoc: + class Redirect < Endpoint # :nodoc: attr_reader :status, :block def initialize(status, block) @@ -14,18 +15,15 @@ module ActionDispatch @block = block end - def call(env) - req = Request.new(env) + def redirect?; true; end - # If any of the path parameters has an invalid encoding then - # raise since it's likely to trigger errors further on. - req.symbolized_path_parameters.each do |key, value| - unless value.valid_encoding? - raise ActionController::BadRequest, "Invalid parameter: #{key} => #{value}" - end - end + def call(env) + serve Request.new env + end - uri = URI.parse(path(req.symbolized_path_parameters, req)) + def serve(req) + req.check_path_parameters! + uri = URI.parse(path(req.path_parameters, req)) unless uri.host if relative_path?(uri.path) @@ -39,7 +37,7 @@ module ActionDispatch uri.host ||= req.host uri.port ||= req.port unless req.standard_port? - body = %(<html><body>You are being <a href="#{ERB::Util.h(uri.to_s)}">redirected</a>.</body></html>) + body = %(<html><body>You are being <a href="#{ERB::Util.unwrapped_html_escape(uri.to_s)}">redirected</a>.</body></html>) headers = { 'Location' => uri.to_s, diff --git a/actionpack/lib/action_dispatch/routing/route_set.rb b/actionpack/lib/action_dispatch/routing/route_set.rb index 1ec6fa674b..80c705608d 100644 --- a/actionpack/lib/action_dispatch/routing/route_set.rb +++ b/actionpack/lib/action_dispatch/routing/route_set.rb @@ -1,11 +1,14 @@ require 'action_dispatch/journey' require 'forwardable' require 'thread_safe' +require 'active_support/concern' require 'active_support/core_ext/object/to_query' require 'active_support/core_ext/hash/slice' require 'active_support/core_ext/module/remove_method' require 'active_support/core_ext/array/extract_options' require 'action_controller/metal/exceptions' +require 'action_dispatch/http/request' +require 'action_dispatch/routing/endpoint' module ActionDispatch module Routing @@ -16,27 +19,17 @@ module ActionDispatch # alias inspect to to_s. alias inspect to_s - PARAMETERS_KEY = 'action_dispatch.request.path_parameters' - - class Dispatcher #:nodoc: - def initialize(options={}) - @defaults = options[:defaults] - @glob_param = options.delete(:glob) + class Dispatcher < Routing::Endpoint #:nodoc: + def initialize(defaults) + @defaults = defaults @controller_class_names = ThreadSafe::Cache.new end - def call(env) - params = env[PARAMETERS_KEY] + def dispatcher?; true; end - # If any of the path parameters has an invalid encoding then - # raise since it's likely to trigger errors further on. - params.each do |key, value| - next unless value.respond_to?(:valid_encoding?) - - unless value.valid_encoding? - raise ActionController::BadRequest, "Invalid parameter: #{key} => #{value}" - end - end + def serve(req) + req.check_path_parameters! + params = req.path_parameters prepare_params!(params) @@ -45,13 +38,12 @@ module ActionDispatch return [404, {'X-Cascade' => 'pass'}, []] end - dispatch(controller, params[:action], env) + dispatch(controller, params[:action], req.env) end def prepare_params!(params) normalize_controller!(params) merge_default_action!(params) - split_glob_param!(params) if @glob_param end # If this is a default_controller (i.e. a controller specified by the user) @@ -87,10 +79,6 @@ module ActionDispatch def merge_default_action!(params) params[:action] ||= 'index' end - - def split_glob_param!(params) - params[@glob_param] = params[@glob_param].split('/').map { |v| URI.parser.unescape(v) } - end end # A NamedRouteCollection instance is a collection of named routes, and also @@ -146,35 +134,34 @@ module ActionDispatch end class UrlHelper # :nodoc: - def self.create(route, options) + def self.create(route, options, route_name, url_strategy) if optimize_helper?(route) - OptimizedUrlHelper.new(route, options) + OptimizedUrlHelper.new(route, options, route_name, url_strategy) else - new route, options + new route, options, route_name, url_strategy end end def self.optimize_helper?(route) - !route.glob? && route.requirements.except(:controller, :action).empty? + !route.glob? && route.path.requirements.empty? end + attr_reader :url_strategy, :route_name + class OptimizedUrlHelper < UrlHelper # :nodoc: attr_reader :arg_size - def initialize(route, options) + def initialize(route, options, route_name, url_strategy) super - @klass = Journey::Router::Utils @required_parts = @route.required_parts @arg_size = @required_parts.size - @optimized_path = @route.optimized_path end - def call(t, args) - if args.size == arg_size && !args.last.is_a?(Hash) && optimize_routes_generation?(t) - options = @options.dup - options.merge!(t.url_options) if t.respond_to?(:url_options) + def call(t, args, inner_options) + if args.size == arg_size && !inner_options && optimize_routes_generation?(t) + options = t.url_options.merge @options options[:path] = optimized_helper(args) - ActionDispatch::Http::URL.url_for(options) + url_strategy.call options else super end @@ -183,18 +170,14 @@ module ActionDispatch private def optimized_helper(args) - params = Hash[parameterize_args(args)] + params = parameterize_args(args) missing_keys = missing_keys(params) unless missing_keys.empty? raise_generation_error(params, missing_keys) end - @optimized_path.map{ |segment| replace_segment(params, segment) }.join - end - - def replace_segment(params, segment) - Symbol === segment ? @klass.escape_segment(params[segment]) : segment + @route.format params end def optimize_routes_generation?(t) @@ -202,7 +185,9 @@ module ActionDispatch end def parameterize_args(args) - @required_parts.zip(args.map(&:to_param)) + params = {} + @required_parts.zip(args.map(&:to_param)) { |k,v| params[k] = v } + params end def missing_keys(args) @@ -218,27 +203,36 @@ module ActionDispatch end end - def initialize(route, options) + def initialize(route, options, route_name, url_strategy) @options = options @segment_keys = route.segment_keys.uniq @route = route + @url_strategy = url_strategy + @route_name = route_name end - def call(t, args) - t.url_for(handle_positional_args(t, args, @options, @segment_keys)) + def call(t, args, inner_options) + controller_options = t.url_options + options = controller_options.merge @options + hash = handle_positional_args(controller_options, + inner_options || {}, + args, + options, + @segment_keys) + + t._routes.url_for(hash, route_name, url_strategy) end - def handle_positional_args(t, args, options, keys) - inner_options = args.extract_options! - result = options.dup + def handle_positional_args(controller_options, inner_options, args, result, path_params) if args.size > 0 - if args.size < keys.size - 1 # take format into account - keys -= t.url_options.keys if t.respond_to?(:url_options) - keys -= options.keys + if args.size < path_params.size - 1 # take format into account + path_params -= controller_options.keys + path_params -= result.keys end - keys -= inner_options.keys - result.merge!(Hash[keys.zip(args)]) + path_params.each { |param| + result[param] = inner_options[param] || args.shift + } end result.merge!(inner_options) @@ -259,13 +253,15 @@ module ActionDispatch # # foo_url(bar, baz, bang, sort_by: 'baz') # - def define_url_helper(route, name, options) - helper = UrlHelper.create(route, options.dup) + def define_url_helper(route, name, opts, route_key, url_strategy) + helper = UrlHelper.create(route, opts, route_key, url_strategy) @module.remove_possible_method name @module.module_eval do define_method(name) do |*args| - helper.call self, args + options = nil + options = args.pop if args.last.is_a? Hash + helper.call self, args, options end end @@ -273,13 +269,18 @@ module ActionDispatch end def define_named_route_methods(name, route) - define_url_helper route, :"#{name}_path", - route.defaults.merge(:use_route => name, :only_path => true) - define_url_helper route, :"#{name}_url", - route.defaults.merge(:use_route => name, :only_path => false) + define_url_helper route, :"#{name}_path", route.defaults, name, PATH + define_url_helper route, :"#{name}_url", route.defaults, name, FULL end end + # :stopdoc: + # strategy for building urls to send to the client + PATH = ->(options) { ActionDispatch::Http::URL.path_for(options) } + FULL = ->(options) { ActionDispatch::Http::URL.full_url_for(options) } + UNKNOWN = ->(options) { ActionDispatch::Http::URL.url_for(options) } + # :startdoc: + attr_accessor :formatter, :set, :named_routes, :default_scope, :router attr_accessor :disable_clear_and_finalize, :resources_path_names attr_accessor :default_url_options, :request_class @@ -302,9 +303,7 @@ module ActionDispatch @finalized = false @set = Journey::Routes.new - @router = Journey::Router.new(@set, { - :parameters_key => PARAMETERS_KEY, - :request_class => request_class}) + @router = Journey::Router.new @set @formatter = Journey::Formatter.new @set end @@ -350,6 +349,10 @@ module ActionDispatch @prepend.each { |blk| eval_block(blk) } end + def dispatcher(defaults) + Routing::RouteSet::Dispatcher.new(defaults) + end + module MountedHelpers #:nodoc: extend ActiveSupport::Concern include UrlFor @@ -393,6 +396,8 @@ module ActionDispatch @_routes = routes class << self delegate :url_for, :optimize_routes_generation?, :to => '@_routes' + attr_reader :_routes + def url_options; {}; end end # Make named_routes available in the module singleton @@ -432,7 +437,9 @@ module ActionDispatch "http://guides.rubyonrails.org/routing.html#restricting-the-routes-created" end - path = build_path(conditions.delete(:path_info), requirements, SEPARATORS, anchor) + path = conditions.delete :path_info + ast = conditions.delete :parsed_path_info + path = build_path(path, ast, requirements, anchor) conditions = build_conditions(conditions, path.names.map { |x| x.to_sym }) route = @set.add_route(app, path, conditions, defaults, name) @@ -440,8 +447,9 @@ module ActionDispatch route end - def build_path(path, requirements, separators, anchor) + def build_path(path, ast, requirements, anchor) strexp = Journey::Router::Strexp.new( + ast, path, requirements, SEPARATORS, @@ -502,8 +510,8 @@ module ActionDispatch attr_reader :options, :recall, :set, :named_route - def initialize(options, recall, set) - @named_route = options.delete(:use_route) + def initialize(named_route, options, recall, set) + @named_route = named_route @options = options.dup @recall = recall.dup @set = set @@ -594,7 +602,7 @@ module ActionDispatch # Generates a path from routes, returns [path, params]. # If no route is generated the formatter will raise ActionController::UrlGenerationError def generate - @set.formatter.generate(:path_info, named_route, options, recall, PARAMETERIZE) + @set.formatter.generate(named_route, options, recall, PARAMETERIZE) end def different_controller? @@ -619,13 +627,15 @@ module ActionDispatch end def generate_extras(options, recall={}) - path, params = generate(options, recall) + route_key = options.delete :use_route + path, params = generate(route_key, options, recall) return path, params.keys end - def generate(options, recall = {}) - Generator.new(options, recall, self).generate + def generate(route_key, options, recall = {}) + Generator.new(route_key, options, recall, self).generate end + private :generate RESERVED_OPTIONS = [:host, :protocol, :port, :subdomain, :domain, :tld_length, :trailing_slash, :anchor, :params, :only_path, :script_name, @@ -639,41 +649,52 @@ module ActionDispatch !mounted? && default_url_options.empty? end - def _generate_prefix(options = {}) - nil + def find_script_name(options) + options.delete :script_name end - # The +options+ argument must be +nil+ or a hash whose keys are *symbols*. - def url_for(options) - options = default_url_options.merge(options || {}) + # The +options+ argument must be a hash whose keys are *symbols*. + def url_for(options, route_name = nil, url_strategy = UNKNOWN) + options = default_url_options.merge options + + user = password = nil + + if options[:user] && options[:password] + user = options.delete :user + password = options.delete :password + end - user, password = extract_authentication(options) - recall = options.delete(:_recall) + recall = options.delete(:_recall) { {} } - original_script_name = options.delete(:original_script_name).presence - script_name = options.delete(:script_name).presence || _generate_prefix(options) + original_script_name = options.delete(:original_script_name) + script_name = find_script_name options if script_name && original_script_name script_name = original_script_name + script_name end - path_options = options.except(*RESERVED_OPTIONS) - path_options = yield(path_options) if block_given? + path_options = options.dup + RESERVED_OPTIONS.each { |ro| path_options.delete ro } + + path, params = generate(route_name, path_options, recall) + + if options.key? :params + params.merge! options[:params] + end - path, params = generate(path_options, recall || {}) - params.merge!(options[:params] || {}) + options[:path] = path + options[:script_name] = script_name + options[:params] = params + options[:user] = user + options[:password] = password - ActionDispatch::Http::URL.url_for(options.merge!({ - :path => path, - :script_name => script_name, - :params => params, - :user => user, - :password => password - })) + url_strategy.call options end def call(env) - @router.call(env) + req = request_class.new(env) + req.path_info = Journey::Router::Utils.normalize_path(req.path_info) + @router.serve(req) end def recognize_path(path, environment = {}) @@ -687,8 +708,8 @@ module ActionDispatch raise ActionController::RoutingError, e.message end - req = @request_class.new(env) - @router.recognize(req) do |route, _matches, params| + req = request_class.new(env) + @router.recognize(req) do |route, params| params.merge!(extras) params.each do |key, value| if value.is_a?(String) @@ -696,14 +717,12 @@ module ActionDispatch params[key] = URI.parser.unescape(value) end end - old_params = env[::ActionDispatch::Routing::RouteSet::PARAMETERS_KEY] - env[::ActionDispatch::Routing::RouteSet::PARAMETERS_KEY] = (old_params || {}).merge(params) - dispatcher = route.app - while dispatcher.is_a?(Mapper::Constraints) && dispatcher.matches?(env) do - dispatcher = dispatcher.app - end + old_params = req.path_parameters + req.path_parameters = old_params.merge params + app = route.app + if app.matches?(req) && app.dispatcher? + dispatcher = app.app - if dispatcher.is_a?(Dispatcher) if dispatcher.controller(params, false) dispatcher.prepare_params!(params) return params @@ -715,17 +734,6 @@ module ActionDispatch raise ActionController::RoutingError, "No route matches #{path.inspect}" end - - private - - def extract_authentication(options) - if options[:user] && options[:password] - [options.delete(:user), options.delete(:password)] - else - nil - end - end - end end end diff --git a/actionpack/lib/action_dispatch/routing/url_for.rb b/actionpack/lib/action_dispatch/routing/url_for.rb index 4a0ef40873..e1c73f8f07 100644 --- a/actionpack/lib/action_dispatch/routing/url_for.rb +++ b/actionpack/lib/action_dispatch/routing/url_for.rb @@ -152,13 +152,19 @@ module ActionDispatch when nil _routes.url_for(url_options.symbolize_keys) when Hash - _routes.url_for(options.symbolize_keys.reverse_merge!(url_options)) + route_name = options.delete :use_route + _routes.url_for(options.symbolize_keys.reverse_merge!(url_options), + route_name) when String options + when Symbol + HelperMethodBuilder.url.handle_string_call self, options when Array polymorphic_url(options, options.extract_options!) + when Class + HelperMethodBuilder.url.handle_class_call self, options else - polymorphic_url(options) + HelperMethodBuilder.url.handle_model_call self, options end end diff --git a/actionpack/lib/action_dispatch/testing/assertions/response.rb b/actionpack/lib/action_dispatch/testing/assertions/response.rb index 68feb26936..0adc6c84ff 100644 --- a/actionpack/lib/action_dispatch/testing/assertions/response.rb +++ b/actionpack/lib/action_dispatch/testing/assertions/response.rb @@ -73,7 +73,13 @@ module ActionDispatch if Regexp === fragment fragment else - @controller._compute_redirect_to_location(fragment) + handle = @controller || Class.new(ActionController::Metal) do + include ActionController::Redirecting + def initialize(request) + @_request = request + end + end.new(@request) + handle._compute_redirect_to_location(fragment) end end end diff --git a/actionpack/lib/action_dispatch/testing/integration.rb b/actionpack/lib/action_dispatch/testing/integration.rb index cc6b763093..12b796b95f 100644 --- a/actionpack/lib/action_dispatch/testing/integration.rb +++ b/actionpack/lib/action_dispatch/testing/integration.rb @@ -3,7 +3,6 @@ require 'uri' require 'active_support/core_ext/kernel/singleton_class' require 'active_support/core_ext/object/try' require 'rack/test' -require 'minitest' module ActionDispatch module Integration #:nodoc: @@ -189,8 +188,8 @@ module ActionDispatch # This makes app.url_for and app.foo_path available in the console if app.respond_to?(:routes) singleton_class.class_eval do - include app.routes.url_helpers if app.routes.respond_to?(:url_helpers) - include app.routes.mounted_helpers if app.routes.respond_to?(:mounted_helpers) + include app.routes.url_helpers + include app.routes.mounted_helpers end end @@ -201,7 +200,7 @@ module ActionDispatch @url_options ||= default_url_options.dup.tap do |url_options| url_options.reverse_merge!(controller.url_options) if controller - if @app.respond_to?(:routes) && @app.routes.respond_to?(:default_url_options) + if @app.respond_to?(:routes) url_options.reverse_merge!(@app.routes.default_url_options) end @@ -270,12 +269,6 @@ module ActionDispatch path = location.query ? "#{location.path}?#{location.query}" : location.path end - unless ActionController::Base < ActionController::Testing - ActionController::Base.class_eval do - include ActionController::Testing - end - end - hostname, port = host.split(':') env = { @@ -300,13 +293,7 @@ module ActionDispatch # NOTE: rack-test v0.5 doesn't build a default uri correctly # Make sure requested path is always a full uri - uri = URI.parse('/') - uri.scheme ||= env['rack.url_scheme'] - uri.host ||= env['SERVER_NAME'] - uri.port ||= env['SERVER_PORT'].try(:to_i) - uri += path - - session.request(uri.to_s, env) + session.request(build_full_uri(path, env), env) @request_count += 1 @request = ActionDispatch::Request.new(session.last_request.env) @@ -319,6 +306,10 @@ module ActionDispatch return response.status end + + def build_full_uri(path, env) + "#{env['rack.url_scheme']}://#{env['SERVER_NAME']}:#{env['SERVER_PORT']}#{path}" + end end module Runner @@ -338,6 +329,7 @@ module ActionDispatch xml_http_request xhr get_via_redirect post_via_redirect).each do |method| define_method(method) do |*args| reset! unless integration_session + reset_template_assertion # reset the html_document variable, but only for new get/post calls @html_document = nil unless method == 'cookies' || method == 'assigns' integration_session.__send__(method, *args).tap do @@ -356,7 +348,7 @@ module ActionDispatch # By default, a single session is automatically created for you, but you # can use this method to open multiple sessions that ought to be tested # simultaneously. - def open_session(app = nil) + def open_session dup.tap do |session| yield session if block_given? end diff --git a/actionpack/lib/action_dispatch/testing/test_request.rb b/actionpack/lib/action_dispatch/testing/test_request.rb index 57c678843b..de3dc5f924 100644 --- a/actionpack/lib/action_dispatch/testing/test_request.rb +++ b/actionpack/lib/action_dispatch/testing/test_request.rb @@ -39,7 +39,7 @@ module ActionDispatch end def action=(action_name) - path_parameters["action"] = action_name.to_s + path_parameters[:action] = action_name.to_s end def if_modified_since=(last_modified) |